{T}

纹理与光照

纹理映射

纹理映射是将图像贴到 3D 模型表面的技术,使模型更加真实。

纹理坐标系

WebGL 使用归一化的纹理坐标系统:

code
(0, 1) ──────── (1, 1)
  │                │
  │    纹理图像    │
  │                │
(0, 0) ──────── (1, 0)

纹理坐标 s 对应水平方向,t 对应垂直方向(在 GLSL 中也称为 x/yu/v)。

创建纹理

javascript
// 创建纹理对象
const texture = gl.createTexture();

// 绑定纹理
gl.bindTexture(gl.TEXTURE_2D, texture);

// 加载图像
const image = new Image();
image.onload = function() {
  // 配置纹理
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
  
  // 设置纹理参数
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
};
image.src = 'texture.png';

纹理参数

纹理环绕方式

参数说明
gl.REPEAT重复纹理
gl.CLAMP_TO_EDGE边缘拉伸
gl.MIRRORED_REPEAT镜像重复
javascript
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

纹理过滤方式

参数说明性能
gl.NEAREST最近邻过滤
gl.LINEAR线性过滤
gl.NEAREST_MIPMAP_NEAREST最近邻 Mipmap
gl.LINEAR_MIPMAP_NEAREST线性 Mipmap
gl.NEAREST_MIPMAP_LINEAR最近邻线性 Mipmap
gl.LINEAR_MIPMAP_LINEAR三线性过滤最慢
javascript
// 缩小过滤
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);

// 放大过滤
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);

// 生成 Mipmap
gl.generateMipmap(gl.TEXTURE_2D);

使用纹理

顶点着色器

glsl
attribute vec3 a_Position;
attribute vec2 a_TexCoord;

varying vec2 v_TexCoord;

void main() {
  gl_Position = vec4(a_Position, 1.0);
  v_TexCoord = a_TexCoord;
}

片段着色器

glsl
precision mediump float;

uniform sampler2D u_Texture;
varying vec2 v_TexCoord;

void main() {
  gl_FragColor = texture2D(u_Texture, v_TexCoord);
}

JavaScript 代码

javascript
// 顶点数据(位置 + 纹理坐标)
const vertices = new Float32Array([
  // x, y, z, s, t
 -0.5,  0.5, 0.0, 0.0, 1.0,  // 左上
 -0.5, -0.5, 0.0, 0.0, 0.0,  // 左下
  0.5,  0.5, 0.0, 1.0, 1.0,  // 右上
  0.5, -0.5, 0.0, 1.0, 0.0   // 右下
]);

// 设置纹理单元
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);

// 将纹理单元传递给着色器
const u_Texture = gl.getUniformLocation(program, 'u_Texture');
gl.uniform1i(u_Texture, 0);  // 纹理单元 0

多重纹理

WebGL 支持同时使用多个纹理。

顶点着色器

glsl
attribute vec3 a_Position;
attribute vec2 a_TexCoord;

varying vec2 v_TexCoord;

void main() {
  gl_Position = vec4(a_Position, 1.0);
  v_TexCoord = a_TexCoord;
}

片段着色器

glsl
precision mediump float;

uniform sampler2D u_Texture0;
uniform sampler2D u_Texture1;
varying vec2 v_TexCoord;

void main() {
  vec4 color0 = texture2D(u_Texture0, v_TexCoord);
  vec4 color1 = texture2D(u_Texture1, v_TexCoord);
  
  // 混合两个纹理
  gl_FragColor = mix(color0, color1, 0.5);
}

JavaScript 代码

javascript
// 创建并配置纹理 0
const texture0 = gl.createTexture();
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture0);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image0);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);

// 创建并配置纹理 1
const texture1 = gl.createTexture();
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, texture1);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image1);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);

// 设置纹理单元
gl.useProgram(program);
gl.uniform1i(gl.getUniformLocation(program, 'u_Texture0'), 0);
gl.uniform1i(gl.getUniformLocation(program, 'u_Texture1'), 1);

光照基础

光照是 3D 图形中最核心的技术之一,使物体呈现立体感。

光照模型

环境光(Ambient Light)

均匀照亮场景的基础光照:

glsl
uniform vec3 u_AmbientLight;  // 环境光颜色
uniform vec3 u_AmbientMaterial;  // 材质环境光反射系数

void main() {
  vec3 ambient = u_AmbientLight * u_AmbientMaterial;
}

漫反射光(Diffuse Light)

Lambert 漫反射模型:

glsl
uniform vec3 u_LightPosition;  // 光源位置
uniform vec3 u_DiffuseLight;   // 漫反射光颜色
uniform vec3 u_DiffuseMaterial;  // 材质漫反射系数

varying vec3 v_Normal;  // 法线
varying vec3 v_Position;  // 顶点位置

void main() {
  // 光线方向
  vec3 lightDir = normalize(u_LightPosition - v_Position);
  
  // 法线方向
  vec3 normal = normalize(v_Normal);
  
  // 漫反射强度
  float diff = max(dot(normal, lightDir), 0.0);
  
  // 漫反射分量
  vec3 diffuse = diff * u_DiffuseLight * u_DiffuseMaterial;
}

镜面反射光(Specular Light)

Phong 镜面反射模型:

glsl
uniform vec3 u_LightPosition;
uniform vec3 u_CameraPosition;
uniform vec3 u_SpecularLight;
uniform vec3 u_SpecularMaterial;
uniform float u_Shininess;  // 高光指数

void main() {
  vec3 normal = normalize(v_Normal);
  vec3 lightDir = normalize(u_LightPosition - v_Position);
  
  // 反射方向
  vec3 reflectDir = reflect(-lightDir, normal);
  
  // 视线方向
  vec3 viewDir = normalize(u_CameraPosition - v_Position);
  
  // 镜面反射强度
  float spec = pow(max(dot(viewDir, reflectDir), 0.0), u_Shininess);
  
  // 镜面反射分量
  vec3 specular = spec * u_SpecularLight * u_SpecularMaterial;
}

Blinn-Phong 模型

比 Phong 更高效的镜面反射计算:

glsl
void main() {
  vec3 normal = normalize(v_Normal);
  vec3 lightDir = normalize(u_LightPosition - v_Position);
  vec3 viewDir = normalize(u_CameraPosition - v_Position);
  
  // 半角向量
  vec3 halfDir = normalize(lightDir + viewDir);
  
  // 镜面反射强度
  float spec = pow(max(dot(normal, halfDir), 0.0), u_Shininess);
  
  vec3 specular = spec * u_SpecularLight * u_SpecularMaterial;
}

Phong 光照模型

完整的 Phong 光照模型组合了上述三种光照:

glsl
// 顶点着色器
attribute vec3 a_Position;
attribute vec3 a_Normal;

uniform mat4 u_ModelMatrix;
uniform mat4 u_ViewMatrix;
uniform mat4 u_ProjectionMatrix;

varying vec3 v_Position;
varying vec3 v_Normal;

void main() {
  vec4 worldPosition = u_ModelMatrix * vec4(a_Position, 1.0);
  v_Position = worldPosition.xyz;
  
  // 法线变换
  mat3 normalMatrix = mat3(transpose(inverse(u_ModelMatrix)));
  v_Normal = normalMatrix * a_Normal;
  
  gl_Position = u_ProjectionMatrix * u_ViewMatrix * worldPosition;
}
glsl
// 片段着色器
precision mediump float;

uniform vec3 u_LightPosition;
uniform vec3 u_CameraPosition;

// 光源属性
uniform vec3 u_AmbientLight;
uniform vec3 u_DiffuseLight;
uniform vec3 u_SpecularLight;

// 材质属性
uniform vec3 u_AmbientMaterial;
uniform vec3 u_DiffuseMaterial;
uniform vec3 u_SpecularMaterial;
uniform float u_Shininess;

varying vec3 v_Position;
varying vec3 v_Normal;

void main() {
  vec3 normal = normalize(v_Normal);
  vec3 lightDir = normalize(u_LightPosition - v_Position);
  vec3 viewDir = normalize(u_CameraPosition - v_Position);
  
  // 环境光
  vec3 ambient = u_AmbientLight * u_AmbientMaterial;
  
  // 漫反射
  float diff = max(dot(normal, lightDir), 0.0);
  vec3 diffuse = diff * u_DiffuseLight * u_DiffuseMaterial;
  
  // 镜面反射
  vec3 reflectDir = reflect(-lightDir, normal);
  float spec = pow(max(dot(viewDir, reflectDir), 0.0), u_Shininess);
  vec3 specular = spec * u_SpecularLight * u_SpecularMaterial;
  
  // 最终颜色
  vec3 color = ambient + diffuse + specular;
  gl_FragColor = vec4(color, 1.0);
}

光照类型

点光源

从一点向所有方向发射光线:

glsl
uniform vec3 u_LightPosition;
uniform vec3 u_LightColor;
uniform float u_LightIntensity;

// 光照衰减
float distance = length(u_LightPosition - v_Position);
float attenuation = 1.0 / (1.0 + 0.09 * distance + 0.032 * distance * distance);

vec3 light = u_LightColor * u_LightIntensity * attenuation;

方向光

平行光,如太阳光:

glsl
uniform vec3 u_LightDirection;  // 光线方向

void main() {
  vec3 lightDir = normalize(-u_LightDirection);
  float diff = max(dot(normal, lightDir), 0.0);
  vec3 diffuse = diff * u_DiffuseLight * u_DiffuseMaterial;
}

聚光灯

锥形光源:

glsl
uniform vec3 u_LightPosition;
uniform vec3 u_LightDirection;
uniform float u_CutOff;  // 内切角
uniform float u_OuterCutOff;  // 外切角

void main() {
  vec3 lightDir = normalize(u_LightPosition - v_Position);
  vec3 spotDir = normalize(-u_LightDirection);
  
  // 计算角度
  float theta = dot(lightDir, spotDir);
  float epsilon = u_CutOff - u_OuterCutOff;
  float intensity = clamp((theta - u_OuterCutOff) / epsilon, 0.0, 1.0);
  
  // 应用光照
  float diff = max(dot(normal, lightDir), 0.0);
  vec3 diffuse = diff * u_DiffuseLight * u_DiffuseMaterial * intensity;
}

材质系统

材质结构体

glsl
struct Material {
  vec3 ambient;
  vec3 diffuse;
  vec3 specular;
  float shininess;
  sampler2D diffuseMap;
  sampler2D specularMap;
  sampler2D normalMap;
};

uniform Material u_Material;

光源结构体

glsl
struct Light {
  vec3 position;
  vec3 direction;
  vec3 ambient;
  vec3 diffuse;
  vec3 specular;
  float cutOff;
  float outerCutOff;
  float constant;
  float linear;
  float quadratic;
};

uniform Light u_Light;

完整光照计算函数

glsl
vec3 calculateLight(Light light, Material material, vec3 normal, vec3 fragPos, vec3 viewDir) {
  // 环境光
  vec3 ambient = light.ambient * vec3(texture(material.diffuseMap, v_TexCoord));
  
  // 光线方向
  vec3 lightDir;
  float attenuation = 1.0;
  
  if (light.position.w == 0.0) {
    // 方向光
    lightDir = normalize(-light.direction);
  } else {
    // 点光源
    lightDir = normalize(light.position.xyz - fragPos);
    float distance = length(light.position.xyz - fragPos);
    attenuation = 1.0 / (light.constant + light.linear * distance + light.quadratic * distance * distance);
  }
  
  // 漫反射
  float diff = max(dot(normal, lightDir), 0.0);
  vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuseMap, v_TexCoord));
  
  // 镜面反射
  vec3 reflectDir = reflect(-lightDir, normal);
  float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
  vec3 specular = light.specular * spec * vec3(texture(material.specularMap, v_TexCoord));
  
  return (ambient + diffuse + specular) * attenuation;
}

PBR 材质(基于物理的渲染)

glsl
struct PBRMaterial {
  vec3 albedo;          // 基础颜色
  float metallic;       // 金属度
  float roughness;      // 粗糙度
  float ao;             // 环境遮蔽
  sampler2D albedoMap;
  sampler2D normalMap;
  sampler2D metallicMap;
  sampler2D roughnessMap;
  sampler2D aoMap;
};

// PBR 光照计算
vec3 calculatePBRLight(vec3 L, vec3 V, vec3 N, PBRMaterial material) {
  vec3 H = normalize(V + L);
  
  // 计算各向同性 BRDF
  float NDF = DistributionGGX(N, H, material.roughness);
  float G = GeometrySmith(N, V, L, material.roughness);
  vec3 F = fresnelSchlick(max(dot(H, V), 0.0), material.albedo, material.metallic);
  
  vec3 numerator = NDF * G * F;
  float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0) + 0.0001;
  vec3 specular = numerator / denominator;
  
  vec3 kS = F;
  vec3 kD = vec3(1.0) - kS;
  kD *= 1.0 - material.metallic;
  
  float NdotL = max(dot(N, L), 0.0);
  return (kD * material.albedo / PI + specular) * NdotL;
}

阴影技术

阴影贴图原理

阴影贴图通过从光源视角渲染深度图来实现阴影效果。

基本步骤

  1. 从光源视角渲染深度图
  2. 在正常渲染时比较深度值

创建深度贴图帧缓冲

javascript
function createShadowMap(gl, width, height) {
  // 创建帧缓冲
  const framebuffer = gl.createFramebuffer();
  gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
  
  // 创建深度纹理
  const depthTexture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, depthTexture);
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT24, width, height, 0, gl.DEPTH_COMPONENT, gl.UNSIGNED_INT, null);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
  
  // 附加到帧缓冲
  gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, depthTexture, 0);
  
  // 检查状态
  if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) {
    console.error('阴影贴图帧缓冲不完整');
  }
  
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
  
  return { framebuffer, depthTexture };
}

深度渲染着色器

glsl
// 深度顶点着色器
uniform mat4 u_LightSpaceMatrix;

void main() {
  gl_Position = u_LightSpaceMatrix * vec4(a_Position, 1.0);
}

// 深度片段着色器(空着色器,仅记录深度)
void main() {
  // 深度自动写入
}

阴影计算着色器

glsl
uniform sampler2D u_ShadowMap;
uniform mat4 u_LightSpaceMatrix;

float calculateShadow(vec4 fragPosLightSpace, vec3 normal, vec3 lightDir) {
  // 执行透视除法
  vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
  projCoords = projCoords * 0.5 + 0.5;
  
  // 获取最近深度
  float closestDepth = texture(u_ShadowMap, projCoords.xy).r;
  
  // 当前片段深度
  float currentDepth = projCoords.z;
  
  // 偏移(解决阴影失真)
  float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);
  
  // PCF 软阴影
  float shadow = 0.0;
  vec2 texelSize = 1.0 / vec2(textureSize(u_ShadowMap, 0));
  for (int x = -1; x <= 1; ++x) {
    for (int y = -1; y <= 1; ++y) {
      float pcfDepth = texture(u_ShadowMap, projCoords.xy + vec2(x, y) * texelSize).r;
      shadow += currentDepth - bias > pcfDepth ? 1.0 : 0.0;
    }
  }
  shadow /= 9.0;
  
  // 超出视锥体范围
  if (projCoords.z > 1.0) {
    shadow = 0.0;
  }
  
  return shadow;
}

级联阴影(CSM)

用于大场景的阴影技术:

javascript
// 计算级联分割
function calculateCascadeSplits(near, far, cascadeCount) {
  const splits = [];
  for (let i = 0; i < cascadeCount; i++) {
    const p = (i + 1) / cascadeCount;
    const uniform = near + (far - near) * p;
    const log = near * Math.pow(far / near, p);
    splits.push(0.5 * (uniform + log));
  }
  return splits;
}

法线贴图

法线贴图用于在低模上模拟高模的细节。

切线空间

glsl
attribute vec3 a_Tangent;
attribute vec3 a_Bitangent;

varying mat3 v_TBN;

void main() {
  vec3 T = normalize(mat3(u_ModelMatrix) * a_Tangent);
  vec3 B = normalize(mat3(u_ModelMatrix) * a_Bitangent);
  vec3 N = normalize(mat3(u_ModelMatrix) * a_Normal);
  
  v_TBN = mat3(T, B, N);
}

使用法线贴图

glsl
uniform sampler2D u_NormalMap;

void main() {
  // 从法线贴图采样
  vec3 normal = texture2D(u_NormalMap, v_TexCoord).rgb;
  normal = normalize(normal * 2.0 - 1.0);  // 转换到 [-1, 1]
  
  // 转换到世界空间
  normal = normalize(v_TBN * normal);
  
  // 应用光照
  // ...
}

实例:纹理光照立方体

完整示例:

html
<!DOCTYPE html>
<html>
<head>
  <title>纹理光照立方体</title>
</head>
<body>
  <canvas id="glCanvas" width="600" height="400"></canvas>
  
  <script>
    const vertexShaderSource = `
      attribute vec3 a_Position;
      attribute vec3 a_Normal;
      attribute vec2 a_TexCoord;
      
      uniform mat4 u_ModelMatrix;
      uniform mat4 u_ViewMatrix;
      uniform mat4 u_ProjectionMatrix;
      
      varying vec3 v_Position;
      varying vec3 v_Normal;
      varying vec2 v_TexCoord;
      
      void main() {
        vec4 worldPos = u_ModelMatrix * vec4(a_Position, 1.0);
        v_Position = worldPos.xyz;
        v_Normal = mat3(u_ModelMatrix) * a_Normal;
        v_TexCoord = a_TexCoord;
        
        gl_Position = u_ProjectionMatrix * u_ViewMatrix * worldPos;
      }
    `;
    
    const fragmentShaderSource = `
      precision mediump float;
      
      uniform sampler2D u_Texture;
      uniform vec3 u_LightPosition;
      uniform vec3 u_CameraPosition;
      
      varying vec3 v_Position;
      varying vec3 v_Normal;
      varying vec2 v_TexCoord;
      
      void main() {
        vec3 normal = normalize(v_Normal);
        vec3 lightDir = normalize(u_LightPosition - v_Position);
        vec3 viewDir = normalize(u_CameraPosition - v_Position);
        
        // 环境光
        float ambient = 0.3;
        
        // 漫反射
        float diff = max(dot(normal, lightDir), 0.0);
        
        // 镜面反射
        vec3 reflectDir = reflect(-lightDir, normal);
        float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0);
        
        // 纹理颜色
        vec4 texColor = texture2D(u_Texture, v_TexCoord);
        
        // 最终颜色
        float light = ambient + diff * 0.7 + spec * 0.3;
        gl_FragColor = vec4(texColor.rgb * light, texColor.a);
      }
    `;
    
    // ... 其他初始化代码 ...
  </script>
</body>
</html>

小结

核心概念

  • 纹理映射:将图像贴到模型表面
  • 纹理参数:环绕方式和过滤方式
  • 光照模型:环境光、漫反射、镜面反射
  • 材质系统:物体的光学属性
  • 阴影技术:阴影贴图、级联阴影
  • 法线贴图:模拟表面细节

性能建议

  1. 纹理压缩:使用压缩纹理格式
  2. Mipmap:为纹理生成多级渐远纹理
  3. 光照优化:减少光源数量,使用简化模型
  4. 批量渲染:合并相同材质的物体

下一步

继续学习矩阵变换,实现 3D 场景的交互。