第10章:纹理映射
10.1 纹理基础
10.1.1 纹理的概念
纹理映射(Texture Mapping)是将 2D 图像"贴"到 3D 模型表面的技术,用于增加细节而不增加几何复杂度。
纹理映射的基本原理
3D 模型上的每个顶点都有一个纹理坐标 (u, v):
纹理图像(2D) 3D 模型表面
┌─────────────────┐ ╱╲
│(0,1) (1,1)│ ╱ ╲
│ │ ╱ 纹 ╲
│ 砖墙纹理 │ ──────► ╱ 理 ╲
│ │ ╲ 贴 ╱
│ │ ╲ 图 ╱
│(0,0) (1,0)│ ╲ ╱
└─────────────────┘ ╲╱
纹理坐标系统(UV 坐标):
v
▲
│
1 ┼───────────●
│ │
│ 纹理 │
│ │
0 ●───────────┼───► u
0 1
- u 对应水平方向(0 到 1)
- v 对应垂直方向(0 到 1)
- 可以超出 [0,1] 范围(平铺/钳制)10.1.2 纹理坐标
顶点与纹理坐标
每个顶点除了位置,还存储纹理坐标:
struct Vertex {
vec3 position; // 3D 位置
vec3 normal; // 法向量
vec2 uv; // 纹理坐标
}
示例:一个正方形的纹理映射
V3──────────V2
│(0,1) (1,1)│
│ │
│ 纹理 │
│ │
│(0,0) (1,0)│
V0──────────V1
顶点数据:
V0: position=(-1,-1,0), uv=(0,0)
V1: position=(1,-1,0), uv=(1,0)
V2: position=(1,1,0), uv=(1,1)
V3: position=(-1,1,0), uv=(0,1)10.1.3 纹理采样
javascript
/**
* 纹理类
*/
class Texture {
constructor(image) {
this.width = image.width;
this.height = image.height;
// 获取像素数据
const canvas = document.createElement('canvas');
canvas.width = image.width;
canvas.height = image.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0);
this.data = ctx.getImageData(0, 0, image.width, image.height).data;
// 采样模式
this.wrapS = 'repeat'; // u 方向
this.wrapT = 'repeat'; // v 方向
this.filter = 'linear'; // 'nearest' 或 'linear'
}
/**
* 采样纹理
* @param u 纹理坐标 u
* @param v 纹理坐标 v
* @returns {r, g, b, a} 颜色值 [0,1]
*/
sample(u, v) {
// 应用寻址模式
u = this.applyWrap(u, this.wrapS);
v = this.applyWrap(v, this.wrapT);
if (this.filter === 'nearest') {
return this.sampleNearest(u, v);
} else {
return this.sampleBilinear(u, v);
}
}
/**
* 最近邻采样
*/
sampleNearest(u, v) {
const x = Math.floor(u * this.width);
const y = Math.floor((1 - v) * this.height); // y 翻转
const clampedX = Math.max(0, Math.min(this.width - 1, x));
const clampedY = Math.max(0, Math.min(this.height - 1, y));
const i = (clampedY * this.width + clampedX) * 4;
return {
r: this.data[i] / 255,
g: this.data[i + 1] / 255,
b: this.data[i + 2] / 255,
a: this.data[i + 3] / 255
};
}
/**
* 双线性插值采样
*/
sampleBilinear(u, v) {
const x = u * this.width - 0.5;
const y = (1 - v) * this.height - 0.5;
const x0 = Math.floor(x);
const y0 = Math.floor(y);
const x1 = x0 + 1;
const y1 = y0 + 1;
const tx = x - x0;
const ty = y - y0;
// 四个相邻像素
const c00 = this.getPixel(x0, y0);
const c10 = this.getPixel(x1, y0);
const c01 = this.getPixel(x0, y1);
const c11 = this.getPixel(x1, y1);
// 双线性插值
return {
r: this.lerp2D(c00.r, c10.r, c01.r, c11.r, tx, ty),
g: this.lerp2D(c00.g, c10.g, c01.g, c11.g, tx, ty),
b: this.lerp2D(c00.b, c10.b, c01.b, c11.b, tx, ty),
a: this.lerp2D(c00.a, c10.a, c01.a, c11.a, tx, ty)
};
}
getPixel(x, y) {
x = Math.max(0, Math.min(this.width - 1, x));
y = Math.max(0, Math.min(this.height - 1, y));
const i = (y * this.width + x) * 4;
return {
r: this.data[i] / 255,
g: this.data[i + 1] / 255,
b: this.data[i + 2] / 255,
a: this.data[i + 3] / 255
};
}
lerp2D(c00, c10, c01, c11, tx, ty) {
const c0 = c00 * (1 - tx) + c10 * tx;
const c1 = c01 * (1 - tx) + c11 * tx;
return c0 * (1 - ty) + c1 * ty;
}
applyWrap(coord, mode) {
switch (mode) {
case 'repeat':
return coord - Math.floor(coord);
case 'clamp':
return Math.max(0, Math.min(1, coord));
case 'mirror':
const t = coord - Math.floor(coord);
return Math.floor(coord) % 2 === 0 ? t : 1 - t;
default:
return coord;
}
}
}10.2 纹理过滤
10.2.1 采样问题
纹理采样的问题
当纹理像素(texel)与屏幕像素(pixel)大小不匹配时:
1. 放大(Magnification)
一个 texel 覆盖多个 pixel
纹理(小): 屏幕(大):
┌───┬───┐ ┌─┬─┬─┬─┬─┬─┐
│ A │ B │ │A│A│A│B│B│B│
├───┼───┤ ──────► ├─┼─┼─┼─┼─┼─┤
│ C │ D │ │A│A│A│B│B│B│
└───┴───┘ ├─┼─┼─┼─┼─┼─┤
│C│C│C│D│D│D│
问题:块状、锯齿 └─┴─┴─┴─┴─┴─┘
2. 缩小(Minification)
多个 texel 映射到一个 pixel
纹理(大): 屏幕(小):
┌─┬─┬─┬─┬─┬─┐ ┌───┬───┐
│a│b│c│d│e│f│ │ ? │ ? │
├─┼─┼─┼─┼─┼─┤ ├───┼───┤
│g│h│i│j│k│l│ ──────► │ ? │ ? │
├─┼─┼─┼─┼─┼─┤ └───┴───┘
│m│n│o│p│q│r│
└─┴─┴─┴─┴─┴─┘ 问题:闪烁、摩尔纹10.2.2 最近邻过滤
最近邻过滤(Nearest Neighbor)
选择最近的一个 texel 的颜色。
纹理坐标 (0.3, 0.7)
↓
┌───┬───┬───┐
│ │ │ │
├───┼───┼───┤
│ │ ● │ ← │ 选择这个 texel
├───┼───┼───┤
│ │ │ │
└───┴───┴───┘
优点:
- 计算简单
- 保持锐利边缘
缺点:
- 放大时有块状感
- 缩小时有闪烁
WebGL 设置:
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);10.2.3 双线性过滤
双线性过滤(Bilinear Filtering)
对四个相邻 texel 进行插值。
采样点 P
↓
┌───┬───┐
│c00│c10│
├───┼─●─┤ P 在四个 texel 之间
│c01│c11│
└───┴───┘
计算:
tx = P.x 的小数部分
ty = P.y 的小数部分
c_top = lerp(c00, c10, tx)
c_bottom = lerp(c01, c11, tx)
result = lerp(c_top, c_bottom, ty)
优点:
- 平滑过渡
- 减少块状感
缺点:
- 缩小时仍有问题
- 轻微模糊
WebGL 设置:
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);10.3 Mipmap
10.3.1 Mipmap 原理
Mipmap(多级纹理)
预先生成不同分辨率的纹理,根据距离选择合适的级别。
Level 0 (原始): Level 1: Level 2: Level 3:
┌───────────────┐ ┌───────┐ ┌───┐ ┌─┐
│ │ │ │ │ │ │ │
│ 1024×1024 │ │512×512│ │256│ │1│
│ │ │ │ │ │ └─┘
│ │ └───────┘ └───┘
│ │
└───────────────┘
选择级别的依据:
- 屏幕像素对应的纹理区域大小
- 如果一个像素覆盖 4×4 texels,选择 level 2
远处(使用小纹理)
╱─────────────────────╲
╱ ╲
╱ ╲
╱ ╲
╱─────────────────────────────╲
╱ ╲
╱ ╲
相机 近处(使用大纹理)
内存占用:
Mipmap 增加约 1/3 的内存
总内存 = 1 + 1/4 + 1/16 + 1/64 + ... ≈ 4/310.3.2 Mipmap 生成
javascript
/**
* 生成 Mipmap
*/
class MipmapGenerator {
/**
* 生成完整的 mipmap 链
*/
static generate(imageData) {
const levels = [imageData];
let width = imageData.width;
let height = imageData.height;
let data = imageData.data;
while (width > 1 || height > 1) {
const newWidth = Math.max(1, Math.floor(width / 2));
const newHeight = Math.max(1, Math.floor(height / 2));
const newData = new Uint8ClampedArray(newWidth * newHeight * 4);
// 下采样:2×2 像素取平均
for (let y = 0; y < newHeight; y++) {
for (let x = 0; x < newWidth; x++) {
const srcX = x * 2;
const srcY = y * 2;
// 收集 2×2 像素
const samples = [];
for (let dy = 0; dy < 2 && srcY + dy < height; dy++) {
for (let dx = 0; dx < 2 && srcX + dx < width; dx++) {
const i = ((srcY + dy) * width + (srcX + dx)) * 4;
samples.push({
r: data[i],
g: data[i + 1],
b: data[i + 2],
a: data[i + 3]
});
}
}
// 计算平均值
const avg = {
r: 0, g: 0, b: 0, a: 0
};
for (const s of samples) {
avg.r += s.r;
avg.g += s.g;
avg.b += s.b;
avg.a += s.a;
}
const count = samples.length;
const dstI = (y * newWidth + x) * 4;
newData[dstI] = avg.r / count;
newData[dstI + 1] = avg.g / count;
newData[dstI + 2] = avg.b / count;
newData[dstI + 3] = avg.a / count;
}
}
levels.push({
width: newWidth,
height: newHeight,
data: newData
});
width = newWidth;
height = newHeight;
data = newData;
}
return levels;
}
/**
* 计算应该使用的 mipmap 级别
* @param dUdx, dUdy, dVdx, dVdy 纹理坐标的屏幕空间导数
*/
static computeMipLevel(dUdx, dUdy, dVdx, dVdy, texWidth, texHeight) {
// 计算一个像素在纹理空间中覆盖的面积
const dudx = dUdx * texWidth;
const dvdx = dVdx * texHeight;
const dudy = dUdy * texWidth;
const dvdy = dVdy * texHeight;
// 取最大伸展
const maxU = Math.max(Math.abs(dudx), Math.abs(dudy));
const maxV = Math.max(Math.abs(dvdx), Math.abs(dvdy));
const rho = Math.max(maxU, maxV);
// 转换为 mipmap 级别
return Math.log2(rho);
}
}
// WebGL 中创建 mipmap
// gl.generateMipmap(gl.TEXTURE_2D);
// 设置 mipmap 过滤
// gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);10.3.3 三线性过滤
三线性过滤(Trilinear Filtering)
在两个 mipmap 级别之间进行插值。
计算步骤:
1. 计算 mipmap 级别 λ
2. 在 level ⌊λ⌋ 进行双线性采样 → c1
3. 在 level ⌈λ⌉ 进行双线性采样 → c2
4. 在 c1 和 c2 之间插值
Level n Level n+1
┌───┬───┐ ┌─┬─┐
│ │ │ │ │ │
├───┼─●─┤ ├─●─┤
│ │ │ └─┴─┘
└───┴───┘
c1 c2
result = lerp(c1, c2, frac(λ))
优点:
- 消除 mipmap 级别之间的跳变
- 非常平滑
缺点:
- 8 次纹理采样
- 略有性能开销10.4 纹理寻址
10.4.1 寻址模式
纹理寻址模式(Wrap Mode)
当纹理坐标超出 [0,1] 范围时的处理方式。
1. Repeat(平铺)
uv = frac(uv)
┌───┬───┬───┐
│ ▲ │ ▲ │ ▲ │
├───┼───┼───┤
│ ▲ │ ▲ │ ▲ │
└───┴───┴───┘
2. Clamp(钳制)
uv = clamp(uv, 0, 1)
┌───┬───────────────┐
│ ▲ │▲ ▲ ▲ ▲ ▲ │ ← 边缘像素延伸
├───┤ │
│ ▲ │▲ ▲ ▲ ▲ ▲ │
└───┴───────────────┘
3. Mirror(镜像)
交替翻转
┌───┬───┬───┐
│ ▲ │ ▼ │ ▲ │
├───┼───┼───┤
│ ▼ │ ▲ │ ▼ │
└───┴───┴───┘
4. Border(边框)
使用固定颜色
┌───┬───────────────┐
│ ▲ │■ ■ ■ ■ ■ │ ← 边框颜色
├───┤■ ■ ■ ■ ■ │
│ ▲ │■ ■ ■ ■ ■ │
└───┴───────────────┘10.4.2 WebGL 纹理设置
javascript
/**
* WebGL 纹理配置
*/
class TextureConfig {
static createTexture(gl, image, options = {}) {
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
// 上传图像数据
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
gl.RGBA,
gl.UNSIGNED_BYTE,
image
);
// 设置过滤模式
const magFilter = options.magFilter || gl.LINEAR;
const minFilter = options.minFilter || gl.LINEAR_MIPMAP_LINEAR;
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, magFilter);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, minFilter);
// 设置寻址模式
const wrapS = options.wrapS || gl.REPEAT;
const wrapT = options.wrapT || gl.REPEAT;
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrapS);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrapT);
// 生成 mipmap(如果需要)
if (this.isPowerOfTwo(image.width) && this.isPowerOfTwo(image.height)) {
gl.generateMipmap(gl.TEXTURE_2D);
} else {
// 非 2 的幂次纹理不支持 mipmap
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_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);
}
// 各向异性过滤(扩展)
const ext = gl.getExtension('EXT_texture_filter_anisotropic');
if (ext && options.anisotropy) {
const maxAnisotropy = gl.getParameter(ext.MAX_TEXTURE_MAX_ANISOTROPY_EXT);
gl.texParameterf(
gl.TEXTURE_2D,
ext.TEXTURE_MAX_ANISOTROPY_EXT,
Math.min(options.anisotropy, maxAnisotropy)
);
}
return texture;
}
static isPowerOfTwo(value) {
return (value & (value - 1)) === 0;
}
}10.5 凹凸贴图
10.5.1 法线贴图
法线贴图(Normal Mapping)
用纹理存储表面法向量,模拟细节几何。
法线贴图中的颜色表示法向量:
- R = (Nx + 1) / 2
- G = (Ny + 1) / 2
- B = (Nz + 1) / 2
典型的法线贴图呈蓝紫色(因为大部分法线指向 Z 轴)
对比:
实际几何: 使用法线贴图:
┌───────────────┐ ┌───────────────┐
│╱╲╱╲╱╲╱╲╱╲╱╲╱╲│ │ │
│╱╲╱╲╱╲╱╲╱╲╱╲╱╲│ │ 同样效果 │
│╱╲╱╲╱╲╱╲╱╲╱╲╱╲│ │ 更少多边形 │
└───────────────┘ └───────────────┘
100,000 面 2 面 + 法线贴图10.5.2 切线空间
切线空间(Tangent Space)
法线贴图中的法向量是在切线空间中定义的。
切线空间坐标系:
- T(Tangent):沿纹理 U 方向
- B(Bitangent):沿纹理 V 方向
- N(Normal):表面法向量
N
│
│
─────┼─────► T
╱│
╱ │
▼
B
TBN 矩阵:
将切线空间法向量转换到世界空间
worldNormal = TBN × tangentNormal
其中 TBN = [T, B, N]10.5.3 法线贴图实现
javascript
/**
* 法线贴图着色
*/
// 顶点着色器
const normalMapVertexShader = `
attribute vec3 aPosition;
attribute vec3 aNormal;
attribute vec2 aTexCoord;
attribute vec3 aTangent;
uniform mat4 uModelMatrix;
uniform mat4 uViewMatrix;
uniform mat4 uProjectionMatrix;
uniform mat3 uNormalMatrix;
varying vec2 vTexCoord;
varying vec3 vPosition;
varying mat3 vTBN;
void main() {
vec4 worldPosition = uModelMatrix * vec4(aPosition, 1.0);
vPosition = worldPosition.xyz;
vTexCoord = aTexCoord;
// 计算 TBN 矩阵
vec3 T = normalize(uNormalMatrix * aTangent);
vec3 N = normalize(uNormalMatrix * aNormal);
// 重新正交化
T = normalize(T - dot(T, N) * N);
vec3 B = cross(N, T);
vTBN = mat3(T, B, N);
gl_Position = uProjectionMatrix * uViewMatrix * worldPosition;
}
`;
// 片段着色器
const normalMapFragmentShader = `
precision mediump float;
uniform sampler2D uDiffuseMap;
uniform sampler2D uNormalMap;
uniform vec3 uLightPosition;
uniform vec3 uViewPosition;
varying vec2 vTexCoord;
varying vec3 vPosition;
varying mat3 vTBN;
void main() {
// 从法线贴图获取法向量
vec3 normalMap = texture2D(uNormalMap, vTexCoord).rgb;
vec3 tangentNormal = normalMap * 2.0 - 1.0;
// 转换到世界空间
vec3 normal = normalize(vTBN * tangentNormal);
// 漫反射贴图
vec3 diffuseColor = texture2D(uDiffuseMap, vTexCoord).rgb;
// Blinn-Phong 光照
vec3 lightDir = normalize(uLightPosition - vPosition);
vec3 viewDir = normalize(uViewPosition - vPosition);
vec3 halfDir = normalize(lightDir + viewDir);
float diff = max(dot(normal, lightDir), 0.0);
float spec = pow(max(dot(normal, halfDir), 0.0), 32.0);
vec3 result = diffuseColor * (0.1 + diff * 0.7) + vec3(1.0) * spec * 0.3;
gl_FragColor = vec4(result, 1.0);
}
`;
/**
* 计算切线
*/
function computeTangents(positions, normals, uvs, indices) {
const tangents = new Float32Array(positions.length);
for (let i = 0; i < indices.length; i += 3) {
const i0 = indices[i];
const i1 = indices[i + 1];
const i2 = indices[i + 2];
// 顶点位置
const v0 = [positions[i0*3], positions[i0*3+1], positions[i0*3+2]];
const v1 = [positions[i1*3], positions[i1*3+1], positions[i1*3+2]];
const v2 = [positions[i2*3], positions[i2*3+1], positions[i2*3+2]];
// 纹理坐标
const uv0 = [uvs[i0*2], uvs[i0*2+1]];
const uv1 = [uvs[i1*2], uvs[i1*2+1]];
const uv2 = [uvs[i2*2], uvs[i2*2+1]];
// 边向量
const e1 = [v1[0]-v0[0], v1[1]-v0[1], v1[2]-v0[2]];
const e2 = [v2[0]-v0[0], v2[1]-v0[1], v2[2]-v0[2]];
// UV 差
const duv1 = [uv1[0]-uv0[0], uv1[1]-uv0[1]];
const duv2 = [uv2[0]-uv0[0], uv2[1]-uv0[1]];
const f = 1.0 / (duv1[0] * duv2[1] - duv2[0] * duv1[1]);
// 切线
const tangent = [
f * (duv2[1] * e1[0] - duv1[1] * e2[0]),
f * (duv2[1] * e1[1] - duv1[1] * e2[1]),
f * (duv2[1] * e1[2] - duv1[1] * e2[2])
];
// 累加到顶点
for (const idx of [i0, i1, i2]) {
tangents[idx*3] += tangent[0];
tangents[idx*3+1] += tangent[1];
tangents[idx*3+2] += tangent[2];
}
}
// 归一化
for (let i = 0; i < tangents.length; i += 3) {
const len = Math.sqrt(
tangents[i]*tangents[i] +
tangents[i+1]*tangents[i+1] +
tangents[i+2]*tangents[i+2]
);
if (len > 0) {
tangents[i] /= len;
tangents[i+1] /= len;
tangents[i+2] /= len;
}
}
return tangents;
}10.6 环境贴图
10.6.1 环境贴图类型
环境贴图(Environment Mapping)
用于模拟反射和环境光照。
1. 立方体贴图(Cube Map)
6 个面组成一个立方体
┌─────┐
│ +Y │
┌───┼─────┼───┬───┐
│-X │ +Z │+X │-Z │
└───┼─────┼───┴───┘
│ -Y │
└─────┘
采样:使用 3D 方向向量
2. 球形贴图(Sphere Map)
整个环境映射到一个球面
┌─────────────┐
│ ╱─────────╲ │
│╱ ╲│
│ ● │
│╲ ╱│
│ ╲─────────╱ │
└─────────────┘
3. 等距柱状投影(Equirectangular)
全景图格式
┌───────────────────────┐
│ │
│ 360° × 180° 全景 │
│ │
└───────────────────────┘10.6.2 立方体贴图实现
javascript
/**
* 立方体贴图反射
*/
// 片段着色器
const cubeMapFragmentShader = `
precision mediump float;
uniform samplerCube uEnvironmentMap;
uniform vec3 uViewPosition;
varying vec3 vPosition;
varying vec3 vNormal;
void main() {
vec3 normal = normalize(vNormal);
vec3 viewDir = normalize(vPosition - uViewPosition);
// 计算反射方向
vec3 reflectDir = reflect(viewDir, normal);
// 采样立方体贴图
vec3 envColor = textureCube(uEnvironmentMap, reflectDir).rgb;
gl_FragColor = vec4(envColor, 1.0);
}
`;
/**
* 创建立方体贴图
*/
function createCubeMap(gl, faces) {
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_CUBE_MAP, texture);
const targets = [
gl.TEXTURE_CUBE_MAP_POSITIVE_X,
gl.TEXTURE_CUBE_MAP_NEGATIVE_X,
gl.TEXTURE_CUBE_MAP_POSITIVE_Y,
gl.TEXTURE_CUBE_MAP_NEGATIVE_Y,
gl.TEXTURE_CUBE_MAP_POSITIVE_Z,
gl.TEXTURE_CUBE_MAP_NEGATIVE_Z
];
for (let i = 0; i < 6; i++) {
gl.texImage2D(
targets[i],
0,
gl.RGBA,
gl.RGBA,
gl.UNSIGNED_BYTE,
faces[i]
);
}
gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
return texture;
}10.7 本章小结
纹理技术对比
| 技术 | 用途 | 性能影响 |
|---|---|---|
| 基础纹理 | 表面颜色 | 低 |
| Mipmap | 减少闪烁 | 内存 +33% |
| 法线贴图 | 表面细节 | 中 |
| 环境贴图 | 反射 | 中 |
| 视差贴图 | 深度效果 | 高 |
过滤模式
| 模式 | 放大 | 缩小 | 质量 |
|---|---|---|---|
| NEAREST | 块状 | 闪烁 | 低 |
| LINEAR | 模糊 | 闪烁 | 中 |
| LINEAR_MIPMAP_NEAREST | 模糊 | 跳变 | 中高 |
| LINEAR_MIPMAP_LINEAR | 模糊 | 平滑 | 高 |
关键要点
- 纹理坐标 UV 范围通常是 [0,1]
- Mipmap 预计算不同分辨率,减少缩小时的锯齿
- 三线性过滤在 mipmap 级别间插值
- 法线贴图用切线空间存储法向量
- 立方体贴图用于环境反射
下一章预告:在第11章中,我们将学习碰撞检测算法,包括包围体、SAT 和 GJK 算法。
文档版本:v1.0
字数统计:约 10,000 字
