2.7 变换 (Transformations)


文档摘要

2.7 变换 (Transformations) WebGL 核心概念:2.7 变换 (Transformations) 详解 在 WebGL 的世界中,变换 (Transformations) 是至关重要的概念。它们允许我们操纵场景中的物体,控制物体的位置、方向、大小,以及最终如何在屏幕上呈现。理解变换是构建复杂 3D 场景、实现动画效果和用户交互的基础。本章将深入探讨 WebGL 中的变换,包括其数学基础、类型、组合以及在代码中的实践应用。 2.7.1 变换的重要性 在现实世界中,物体可以自由移动、旋转和缩放。在 3D 图形中,我们也需要模拟这些操作。变换就是一套数学工具,让我们能够在虚拟世界中实现这些效果。通过变换,我们可以: 定位物体: 将物体放置在场景中的特定位置。

2.7 变换 (Transformations)

WebGL 核心概念:2.7 变换 (Transformations) 详解

在 WebGL 的世界中,变换 (Transformations) 是至关重要的概念。它们允许我们操纵场景中的物体,控制物体的位置、方向、大小,以及最终如何在屏幕上呈现。理解变换是构建复杂 3D 场景、实现动画效果和用户交互的基础。本章将深入探讨 WebGL 中的变换,包括其数学基础、类型、组合以及在代码中的实践应用。

2.7.1 变换的重要性

在现实世界中,物体可以自由移动、旋转和缩放。在 3D 图形中,我们也需要模拟这些操作。变换就是一套数学工具,让我们能够在虚拟世界中实现这些效果。通过变换,我们可以:

  • 定位物体: 将物体放置在场景中的特定位置。

  • 定向物体: 改变物体的朝向,例如让一个模型面向不同的方向。

  • 调整大小: 放大或缩小物体,改变其尺寸。

  • 构建复杂的场景: 通过组合多个变换,创建由多个物体组成的复杂场景。

  • 实现动画效果: 随着时间推移不断改变变换参数,实现物体的动画效果。

  • 控制视角 (相机): 通过变换相机,观察场景的不同部分。

简而言之,变换是 WebGL 图形渲染管线中不可或缺的一部分,它赋予我们控制和塑造虚拟世界的能力。

2.7.2 变换的数学基础:矩阵

在 WebGL 中,变换通常使用 矩阵 (Matrices) 来表示和实现。矩阵是一种数学工具,特别适用于线性变换。在 3D 图形学中,我们主要使用 4x4 矩阵。使用矩阵进行变换的主要原因包括:

  • 统一表示: 矩阵可以统一表示平移、旋转、缩放等多种变换。

  • 高效计算: 图形硬件 (GPU) 针对矩阵运算进行了高度优化,能够快速执行矩阵乘法等操作。

  • 变换的组合: 多个变换可以通过矩阵乘法组合成一个单一的变换矩阵,提高效率。

为了理解 4x4 矩阵在 3D 变换中的应用,我们需要了解 齐次坐标 (Homogeneous Coordinates) 的概念。在 3D 图形中,我们通常使用三维坐标 (x, y, z) 来表示点和向量。为了使用 4x4 矩阵统一处理平移变换,我们引入了齐次坐标,将三维坐标扩展为四维坐标 (x, y, z, w)。

  • 对于 ,通常设置 w = 1。例如,点 (x, y, z) 表示为 (x, y, z, 1)。

  • 对于 向量,通常设置 w = 0。例如,向量 (x, y, z) 表示为 (x, y, z, 0)。

使用齐次坐标后,平移、旋转和缩放等变换都可以表示为 4x4 矩阵与齐次坐标向量的乘法。

2.7.3 基本变换类型

WebGL 中常用的基本变换类型包括:

  1. 平移 (Translation)

  2. 旋转 (Rotation)

  3. 缩放 (Scaling)

接下来我们将分别详细介绍这些变换类型,并给出相应的矩阵表示和代码示例。

2.7.3.1 平移 (Translation)

平移变换是指将物体沿着某个方向移动一定的距离。在 3D 空间中,我们可以沿着 X、Y、Z 轴方向进行平移。

平移矩阵 (Translation Matrix):

一个沿 X 轴平移 tx,沿 Y 轴平移 ty,沿 Z 轴平移 tz 的平移矩阵 T 可以表示为:

[ 1 0 0 tx ] [ 0 1 0 ty ] [ 0 0 1 tz ] [ 0 0 0 1 ]

代码实践 (JavaScript - 使用 glMatrix 库):

glMatrix 是一个常用的 JavaScript 矩阵和向量运算库,非常适合 WebGL 开发。

首先,确保你已经引入了 glMatrix 库。

<script src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/3.4.3/gl-matrix-min.js"></script>

然后,在 JavaScript 代码中,我们可以创建平移矩阵并应用于顶点坐标:

// 创建平移矩阵 let translationMatrix = glMatrix.mat4.create(); let tx = 1.0; // X 轴平移量 let ty = 2.0; // Y 轴平移量 let tz = 3.0; // Z 轴平移量 glMatrix.mat4.translate(translationMatrix, translationMatrix, [tx, ty, tz]); // 假设 vertexPosition 是顶点坐标 (vec4) // 在顶点着色器中,将平移矩阵乘以顶点坐标 // vertexPosition = translationMatrix * originalVertexPosition; // 在 JavaScript 中模拟矩阵乘法 (仅为演示,实际在着色器中进行) let originalVertexPosition = glMatrix.vec4.fromValues(1.0, 1.0, 1.0, 1.0); // 示例顶点坐标 let translatedVertexPosition = glMatrix.vec4.create(); glMatrix.vec4.transformMat4(translatedVertexPosition, originalVertexPosition, translationMatrix); console.log("原始顶点坐标:", originalVertexPosition); console.log("平移后的顶点坐标:", translatedVertexPosition); // 输出: // 原始顶点坐标: Float32Array(4) [1, 1, 1, 1] // 平移后的顶点坐标: Float32Array(4) [2, 3, 4, 1]

代码详解:

  1. glMatrix.mat4.create(): 创建一个新的 4x4 单位矩阵。

  2. glMatrix.mat4.translate(out, a, v): 创建一个平移矩阵,并将其结果存储在 out 中。a 是初始矩阵 (通常是单位矩阵),v 是平移向量 [tx, ty, tz]

  3. glMatrix.vec4.fromValues(x, y, z, w): 创建一个四维向量。

  4. glMatrix.vec4.transformMat4(out, v, m): 将向量 v 乘以矩阵 m,并将结果存储在 out 中。

顶点着色器代码示例:

在顶点着色器中,我们需要接收平移矩阵 uniform,并将其与顶点坐标相乘:

attribute vec4 aVertexPosition; uniform mat4 uTranslationMatrix; void main() { gl_Position = uTranslationMatrix * aVertexPosition; }

在 JavaScript 代码中,我们需要获取 uTranslationMatrix uniform 变量的位置,并将计算好的平移矩阵传递给着色器:

// 获取 uniform 变量位置 const translationMatrixLocation = gl.getUniformLocation(shaderProgram, 'uTranslationMatrix'); // ... (创建 translationMatrix 代码,如上所示) ... // 将平移矩阵传递给 uniform 变量 gl.uniformMatrix4fv(translationMatrixLocation, false, translationMatrix); // ... (绘制代码) ...

2.7.3.2 旋转 (Rotation)

旋转变换是指将物体绕着某个轴旋转一定的角度。在 3D 空间中,我们通常绕着 X、Y、Z 轴进行旋转。

旋转矩阵 (Rotation Matrix):

  • 绕 X 轴旋转 (Rotation around X-axis): 角度为 θ (弧度)
[ 1 0 0 0 ] [ 0 cos(θ) -sin(θ) 0 ] [ 0 sin(θ) cos(θ) 0 ] [ 0 0 0 1 ]
  • 绕 Y 轴旋转 (Rotation around Y-axis): 角度为 θ (弧度)
[ cos(θ) 0 sin(θ) 0 ] [ 0 1 0 0 ] [ -sin(θ) 0 cos(θ) 0 ] [ 0 0 0 1 ]
  • 绕 Z 轴旋转 (Rotation around Z-axis): 角度为 θ (弧度)
[ cos(θ) -sin(θ) 0 0 ] [ sin(θ) cos(θ) 0 0 ] [ 0 0 1 0 ] [ 0 0 0 1 ]

代码实践 (JavaScript - 使用 glMatrix 库):

// 创建旋转矩阵 (绕 X 轴) let rotationMatrixX = glMatrix.mat4.create(); let angleX = glMatrix.glMatrix.toRadian(30); // 旋转角度,转换为弧度 glMatrix.mat4.rotateX(rotationMatrixX, rotationMatrixX, angleX); // 创建旋转矩阵 (绕 Y 轴) let rotationMatrixY = glMatrix.mat4.create(); let angleY = glMatrix.glMatrix.toRadian(45); glMatrix.mat4.rotateY(rotationMatrixY, rotationMatrixY, angleY); // 创建旋转矩阵 (绕 Z 轴) let rotationMatrixZ = glMatrix.mat4.create(); let angleZ = glMatrix.glMatrix.toRadian(60); glMatrix.mat4.rotateZ(rotationMatrixZ, rotationMatrixZ, angleZ); // ... (将旋转矩阵应用于顶点坐标,与平移类似,传递 uniform 变量到顶点着色器) ...

代码详解:

  • glMatrix.mat4.rotateX(out, a, rad): 创建绕 X 轴旋转 rad 弧度的旋转矩阵,并将结果存储在 out 中。a 是初始矩阵。

  • glMatrix.mat4.rotateY(out, a, rad): 创建绕 Y 轴旋转 rad 弧度的旋转矩阵。

  • glMatrix.mat4.rotateZ(out, a, rad): 创建绕 Z 轴旋转 rad 弧度的旋转矩阵。

  • glMatrix.glMatrix.toRadian(degrees): 将角度从度数转换为弧度。WebGL 中角度通常使用弧度表示。

顶点着色器代码示例 (与平移类似,只需要修改 uniform 变量名):

attribute vec4 aVertexPosition; uniform mat4 uRotationMatrix; // 或者 uRotationMatrixX, uRotationMatrixY, uRotationMatrixZ void main() { gl_Position = uRotationMatrix * aVertexPosition; }

2.7.3.3 缩放 (Scaling)

缩放变换是指改变物体的大小。在 3D 空间中,我们可以沿着 X、Y、Z 轴方向进行缩放。

缩放矩阵 (Scaling Matrix):

一个沿 X 轴缩放 sx,沿 Y 轴缩放 sy,沿 Z 轴缩放 sz 的缩放矩阵 S 可以表示为:

[ sx 0 0 0 ] [ 0 sy 0 0 ] [ 0 0 sz 0 ] [ 0 0 0 1 ]

代码实践 (JavaScript - 使用 glMatrix 库):

// 创建缩放矩阵 let scalingMatrix = glMatrix.mat4.create(); let sx = 2.0; // X 轴缩放比例 let sy = 0.5; // Y 轴缩放比例 let sz = 1.0; // Z 轴缩放比例 glMatrix.mat4.scale(scalingMatrix, scalingMatrix, [sx, sy, sz]); // ... (将缩放矩阵应用于顶点坐标,与平移和旋转类似,传递 uniform 变量到顶点着色器) ...

代码详解:

  • glMatrix.mat4.scale(out, a, v): 创建一个缩放矩阵,并将结果存储在 out 中。a 是初始矩阵,v 是缩放向量 [sx, sy, sz]

顶点着色器代码示例 (与平移和旋转类似,只需要修改 uniform 变量名):

attribute vec4 aVertexPosition; uniform mat4 uScalingMatrix; void main() { gl_Position = uScalingMatrix * aVertexPosition; }

2.7.4 变换的组合:矩阵乘法

在实际应用中,我们通常需要对物体进行多种变换,例如先旋转再平移,或者先缩放再旋转。变换的组合可以通过 矩阵乘法 来实现。

变换顺序的重要性:

矩阵乘法不满足交换律,这意味着 变换的顺序会影响最终结果。例如,先旋转再平移与先平移再旋转通常会得到不同的结果。

在 WebGL 中,我们通常按照以下顺序进行变换: 缩放 -> 旋转 -> 平移 (Scale -> Rotate -> Translate,SRT)。 这意味着,如果我们要先缩放,再旋转,最后平移一个物体,我们需要将缩放矩阵 S、旋转矩阵 R 和平移矩阵 T 按照 从右到左 的顺序相乘:

M = T * R * S

其中 M 是最终的变换矩阵。当我们将顶点坐标 V 与 M 相乘时,变换的顺序将按照 S -> R -> T 的顺序应用到顶点上。

Mermaid 图表示意变换组合:

代码实践 (JavaScript - 组合变换):

// 创建单位矩阵作为初始矩阵 let modelMatrix = glMatrix.mat4.create(); // 1. 缩放 let scalingMatrix = glMatrix.mat4.create(); glMatrix.mat4.scale(scalingMatrix, scalingMatrix, [0.5, 0.5, 0.5]); glMatrix.mat4.multiply(modelMatrix, modelMatrix, scalingMatrix); // modelMatrix = modelMatrix * scalingMatrix // 2. 旋转 (绕 Y 轴) let rotationMatrixY = glMatrix.mat4.create(); let angleY = glMatrix.glMatrix.toRadian(45); glMatrix.mat4.rotateY(rotationMatrixY, rotationMatrixY, angleY); glMatrix.mat4.multiply(modelMatrix, modelMatrix, rotationMatrixY); // modelMatrix = modelMatrix * rotationMatrixY // 3. 平移 let translationMatrix = glMatrix.mat4.create(); glMatrix.mat4.translate(translationMatrix, translationMatrix, [2.0, 0.0, 0.0]); glMatrix.mat4.multiply(modelMatrix, modelMatrix, translationMatrix); // modelMatrix = modelMatrix * translationMatrix // 现在 modelMatrix 就是最终的 模型矩阵 (Model Matrix),包含了缩放、旋转和平移的组合变换 // ... (将 modelMatrix 传递给顶点着色器的 uniform 变量 uModelMatrix) ...

代码详解:

  • glMatrix.mat4.multiply(out, a, b): 将矩阵 a 和矩阵 b 相乘,并将结果存储在 out 中。注意矩阵乘法的顺序: out = a * b

  • 我们从单位矩阵 modelMatrix 开始,逐步应用缩放、旋转和平移变换。

  • 每次使用 glMatrix.mat4.multiply 时,都是将新的变换矩阵 右乘modelMatrix 上,从而实现变换的组合。

顶点着色器代码示例 (使用模型矩阵):

attribute vec4 aVertexPosition; uniform mat4 uModelMatrix; // 模型矩阵 void main() { gl_Position = uModelMatrix * aVertexPosition; }

2.7.5 模型矩阵、视图矩阵和投影矩阵

在 WebGL 的渲染管线中,变换通常涉及到三个关键的矩阵:

  1. 模型矩阵 (Model Matrix): 也称为世界矩阵 (World Matrix)。模型矩阵将物体的局部坐标系 (模型空间) 转换到世界坐标系 (World Space)。我们之前讨论的平移、旋转和缩放等变换通常用于构建模型矩阵。每个物体都有自己的模型矩阵,用于定义其在世界空间中的位置、方向和大小。

  2. 视图矩阵 (View Matrix): 也称为相机矩阵 (Camera Matrix)。视图矩阵将世界坐标系转换到相机坐标系 (View Space 或 Eye Space)。视图矩阵定义了相机的位置、朝向和视点方向。通过调整视图矩阵,我们可以模拟相机在场景中的移动和旋转,从而观察场景的不同部分。

  3. 投影矩阵 (Projection Matrix): 投影矩阵将相机坐标系转换到裁剪坐标系 (Clip Space)。投影矩阵负责将 3D 场景投影到 2D 屏幕上。常见的投影方式有两种:

    • 透视投影 (Perspective Projection): 模拟人眼观察世界的透视效果,近大远小。

    • 正交投影 (Orthographic Projection): 平行投影,物体大小不随距离变化。

变换管线示意图:

在顶点着色器中,顶点坐标通常会依次乘以模型矩阵、视图矩阵和投影矩阵,最终得到裁剪空间坐标,然后进行裁剪和投影到屏幕空间。

顶点着色器代码 (完整变换管线):

attribute vec4 aVertexPosition; uniform mat4 uModelMatrix; uniform mat4 uViewMatrix; uniform mat4 uProjectionMatrix; void main() { // 模型变换 -> 视图变换 -> 投影变换 gl_Position = uProjectionMatrix * uViewMatrix * uModelMatrix * aVertexPosition; }

在 JavaScript 代码中,我们需要分别计算模型矩阵、视图矩阵和投影矩阵,并将它们作为 uniform 变量传递给顶点着色器。

2.7.6 代码实践:综合示例

以下是一个简单的综合示例,演示如何使用平移、旋转和缩放变换,以及模型矩阵、视图矩阵和投影矩阵,来渲染一个简单的几何体 (例如三角形)。

HTML (canvas 元素和引入 glMatrix):

<!DOCTYPE html> <html> <head> <title>WebGL Transformations</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/3.4.3/gl-matrix-min.js"></script> <style> body { margin: 0; } canvas { display: block; } </style> </head> <body> <canvas id="glCanvas"></canvas> <script src="app.js"></script> </body> </html>

JavaScript (app.js):

const canvas = document.querySelector('#glCanvas'); const gl = canvas.getContext('webgl'); if (!gl) { alert('Unable to initialize WebGL. Your browser or hardware may not support it.'); throw new Error('Unable to initialize WebGL'); } // 顶点着色器代码 const vsSource = ` attribute vec4 aVertexPosition; uniform mat4 uModelMatrix; uniform mat4 uViewMatrix; uniform mat4 uProjectionMatrix; void main() { gl_Position = uProjectionMatrix * uViewMatrix * uModelMatrix * aVertexPosition; } `; // 片段着色器代码 const fsSource = ` void main() { gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); // 白色 } `; // 初始化着色器程序 const shaderProgram = initShaderProgram(gl, vsSource, fsSource); // 获取 attribute 和 uniform 变量位置 const programInfo = { program: shaderProgram, attribLocations: { vertexPosition: gl.getAttribLocation(shaderProgram, 'aVertexPosition'), }, uniformLocations: { modelMatrix: gl.getUniformLocation(shaderProgram, 'uModelMatrix'), viewMatrix: gl.getUniformLocation(shaderProgram, 'uViewMatrix'), projectionMatrix: gl.getUniformLocation(shaderProgram, 'uProjectionMatrix'), }, }; // 顶点数据 (三角形) const positions = [ 0.0, 0.5, 0.0, -0.5, -0.5, 0.0, 0.5, -0.5, 0.0, ]; // 创建顶点缓冲区 const positionBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW); function drawScene() { gl.clearColor(0.0, 0.0, 0.0, 1.0); // 黑色背景 gl.clearDepth(1.0); gl.enable(gl.DEPTH_TEST); gl.depthFunc(gl.LEQUAL); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // 创建投影矩阵 (透视投影) const fieldOfView = 45 * Math.PI / 180; // in radians const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight; const zNear = 0.1; const zFar = 100.0; const projectionMatrix = glMatrix.mat4.create(); glMatrix.mat4.perspective(projectionMatrix, fieldOfView, aspect, zNear, zFar); // 创建视图矩阵 (相机位置) const viewMatrix = glMatrix.mat4.create(); glMatrix.mat4.translate(viewMatrix, viewMatrix, [0.0, 0.0, -5.0]); // 相机向后移动 5 个单位 // 创建模型矩阵 (初始为单位矩阵) const modelMatrix = glMatrix.mat4.create(); // 应用变换 (例如: 旋转和缩放) glMatrix.mat4.rotateY(modelMatrix, modelMatrix, performance.now() / 1000); // 绕 Y 轴旋转 glMatrix.mat4.scale(modelMatrix, modelMatrix, [0.7, 0.7, 0.7]); // 缩放 // 设置顶点属性指针 { const numComponents = 3; // 每个顶点坐标有 3 个分量 (x, y, z) const type = gl.FLOAT; const normalize = false; const stride = 0; const offset = 0; gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); gl.vertexAttribPointer( programInfo.attribLocations.vertexPosition, numComponents, type, normalize, stride, offset); gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition); } // 设置 uniform 变量 gl.uniformMatrix4fv(programInfo.uniformLocations.projectionMatrix, false, projectionMatrix); gl.uniformMatrix4fv(programInfo.uniformLocations.viewMatrix, false, viewMatrix); gl.uniformMatrix4fv(programInfo.uniformLocations.modelMatrix, false, modelMatrix); // 绘制三角形 { const offset = 0; const vertexCount = 3; gl.drawArrays(gl.TRIANGLES, offset, vertexCount); } requestAnimationFrame(drawScene); // 动画循环 } // 初始化着色器程序 (辅助函数,省略具体实现,参考 WebGL 基础教程) function initShaderProgram(gl, vsSource, fsSource) { const vertexShader = createShader(gl, gl.VERTEX_SHADER, vsSource); const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fsSource); const shaderProgram = gl.createProgram(); gl.attachShader(shaderProgram, vertexShader); gl.attachShader(shaderProgram, fragmentShader); gl.linkProgram(shaderProgram); if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { alert('Unable to initialize the shader program: ' + gl.getProgramInfoLog(shaderProgram)); return null; } return shaderProgram; } // 创建着色器 (辅助函数,省略具体实现,参考 WebGL 基础教程) function createShader(gl, type, source) { const shader = gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { alert('An error occurred compiling the shaders: ' + gl.getShaderInfoLog(shader)); gl.deleteShader(shader); return null; } return shader; } drawScene(); // 开始绘制循环

代码运行说明:

  1. 将 HTML 文件和 JavaScript 文件放在同一个目录下。

  2. 打开 HTML 文件,你将看到一个在黑色背景上旋转的白色三角形。

代码详解 (关键部分):

  • 着色器代码: 顶点着色器接收模型矩阵 uModelMatrix、视图矩阵 uViewMatrix 和投影矩阵 uProjectionMatrix,并将它们与顶点坐标相乘。片段着色器简单地输出白色。

  • 矩阵创建:drawScene 函数中,我们使用 glMatrix.mat4.create() 创建单位矩阵,然后使用 glMatrix.mat4.perspective() 创建透视投影矩阵,glMatrix.mat4.translate() 创建视图矩阵 (相机后移),并使用 glMatrix.mat4.rotateY()glMatrix.mat4.scale() 创建模型矩阵,实现旋转和缩放效果。

  • uniform 变量传递: 使用 gl.uniformMatrix4fv() 将矩阵数据传递给顶点着色器的 uniform 变量。

  • 动画循环: 使用 requestAnimationFrame(drawScene) 创建动画循环,不断更新旋转角度并重绘场景,实现旋转动画效果。

2.7.7 总结

变换是 WebGL 图形编程的核心概念之一。通过理解平移、旋转、缩放等基本变换类型,以及如何使用矩阵来表示和组合变换,我们可以有效地控制和操纵场景中的物体。掌握模型矩阵、视图矩阵和投影矩阵的概念,能够帮助我们构建完整的 3D 渲染管线,创建复杂的 3D 场景和动画效果。希望本章的详细讲解和代码实践能够帮助你深入理解 WebGL 中的变换。


发布者: 作者: 转发
评论区 (0)
U