说明

【跟月影学可视化】学习笔记。

如何用 WebGL 绘制三维立方体

我们知道立方体有8个顶点,6个面,在 WebGL 中,需要用 12 个三角形来绘制它。把每个面的顶点分开,需要 24 个顶点。

【视觉高级篇】20 # 如何用WebGL绘制3D物体?_投影矩阵


绘制 3D 图形与绘制 2D 图形有一点不一样,必须要开启深度检测和启用深度缓冲区。

在 WebGL 中,可以通过 ​​gl.enable(gl.DEPTH_TEST)​​,来开启深度检测。

在清空画布的时候,也要用 ​​gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)​​,来同时清空颜色缓冲区和深度缓冲区。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>如何用 WebGL 绘制三维立方体</title>
<style>
canvas {
border: 1px dashed salmon;
}
</style>
</head>
<body>
<canvas width="512" height="512"></canvas>
<script src="./common/lib/gl-renderer.js"></script>
<script type="module">
const vertex = `
attribute vec3 a_vertexPosition;
attribute vec4 color;

varying vec4 vColor;

void main() {
gl_PointSize = 1.0;
vColor = color;
gl_Position = vec4(a_vertexPosition, 1);
}
`;

const fragment = `
#ifdef GL_ES
precision highp float;
#endif

varying vec4 vColor;

void main() {
gl_FragColor = vColor;
}
`;

const canvas = document.querySelector("canvas");
// 开启深度检测
const renderer = new GlRenderer(canvas, {
depth: true
});
const program = renderer.compileSync(fragment, vertex);
renderer.useProgram(program);

// 用来生成立方体 6 个面的 24 个顶点,以及 12 个三角形的索引
function cube(size = 1.0, colors = [[1, 0, 0, 1]]) {
const h = 0.5 * size;
// 立方体的顶点
const vertices = [
[-h, -h, -h],
[-h, h, -h],
[h, h, -h],
[h, -h, -h],
[-h, -h, h],
[-h, h, h],
[h, h, h],
[h, -h, h],
];

const positions = [];
const color = [];
const cells = [];

let colorIdx = 0;
let cellsIdx = 0;
const colorLen = colors.length;

function quad(a, b, c, d) {
[a, b, c, d].forEach((i) => {
positions.push(vertices[i]);
color.push(colors[colorIdx % colorLen]);
});
cells.push(
[0, 1, 2].map(i => i + cellsIdx),
[0, 2, 3].map(i => i + cellsIdx),
);
colorIdx++;
cellsIdx += 4;
}
// 立方体的六个面
quad(1, 0, 3, 2);
quad(4, 5, 6, 7);
quad(2, 3, 7, 6);
quad(5, 4, 0, 1);
quad(3, 0, 4, 7);
quad(6, 5, 1, 2);

return { positions, color, cells };
}

const geometry = cube(1.0, [
[250/255, 128/255, 114/255, 1], // salmon rgb(250 128 114)
[218/255, 165/255, 32/255, 1],// goldenrod rgb(218, 165, 32)
[46/255, 139/255, 87/255, 1], // seagreen rgb(46 139 87)
[255/255, 192/255, 203/255, 1], // pink rgb(255, 192, 203)
[135/255, 206/255, 235/255, 1],// skyblue rgb(135, 206, 235)
[106/255, 90/255, 205/255, 1], // slateblue rgb(106, 90, 205)
]);

renderer.setMeshData([
{
positions: geometry.positions,
attributes: {
color: geometry.color,
},
cells: geometry.cells,
},
]);
renderer.render();
</script>
</body>
</html>

【视觉高级篇】20 # 如何用WebGL绘制3D物体?_3d_02

投影矩阵:变换 WebGL 坐标系

上面朝向我们的面应该是 ​​goldenrod​​ 颜色, WebGL 默认的剪裁坐标的 z 轴方向,的确是朝内的。WebGL 坐标系就是一个左手系而不是右手系。下面我们需要将 WebGL 的坐标系从左手系转换为右手系。

实际上就是将 z 轴坐标方向反转,对应的齐次矩阵如下:

[
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, -1, 0,
0, 0, 0, 1
]
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>投影矩阵:变换 WebGL 坐标系</title>
<style>
canvas {
border: 1px dashed rgb(250, 128, 114);
}
</style>
</head>
<body>
<canvas width="512" height="512"></canvas>
<script src="./common/lib/gl-renderer.js"></script>
<script type="module">
const vertex = `
attribute vec3 a_vertexPosition;
attribute vec4 color;

varying vec4 vColor;
uniform mat4 projectionMatrix;

void main() {
gl_PointSize = 1.0;
vColor = color;
gl_Position = projectionMatrix * vec4(a_vertexPosition, 1);
}
`;

const fragment = `
#ifdef GL_ES
precision highp float;
#endif

varying vec4 vColor;

void main() {
gl_FragColor = vColor;
}
`;

const canvas = document.querySelector("canvas");
// 开启深度检测
const renderer = new GlRenderer(canvas, {
depth: true
});
const program = renderer.compileSync(fragment, vertex);
renderer.useProgram(program);

// 用来生成立方体 6 个面的 24 个顶点,以及 12 个三角形的索引
function cube(size = 1.0, colors = [[1, 0, 0, 1]]) {
const h = 0.5 * size;
// 立方体的顶点
const vertices = [
[-h, -h, -h],
[-h, h, -h],
[h, h, -h],
[h, -h, -h],
[-h, -h, h],
[-h, h, h],
[h, h, h],
[h, -h, h],
];

const positions = [];
const color = [];
const cells = [];

let colorIdx = 0;
let cellsIdx = 0;
const colorLen = colors.length;

function quad(a, b, c, d) {
[a, b, c, d].forEach((i) => {
positions.push(vertices[i]);
color.push(colors[colorIdx % colorLen]);
});
cells.push(
[0, 1, 2].map(i => i + cellsIdx),
[0, 2, 3].map(i => i + cellsIdx),
);
colorIdx++;
cellsIdx += 4;
}
// 立方体的六个面
quad(1, 0, 3, 2);
quad(4, 5, 6, 7);
quad(2, 3, 7, 6);
quad(5, 4, 0, 1);
quad(3, 0, 4, 7);
quad(6, 5, 1, 2);

return { positions, color, cells };
}

const geometry = cube(1.0, [
[250/255, 128/255, 114/255, 1], // salmon rgb(250 128 114)
[218/255, 165/255, 32/255, 1],// goldenrod rgb(218, 165, 32)
[46/255, 139/255, 87/255, 1], // seagreen rgb(46 139 87)
[255/255, 192/255, 203/255, 1], // pink rgb(255, 192, 203)
[135/255, 206/255, 235/255, 1],// skyblue rgb(135, 206, 235)
[106/255, 90/255, 205/255, 1], // slateblue rgb(106, 90, 205)
]);

// 将 z 轴坐标方向反转,对应的齐次矩阵如下,转换坐标的齐次矩阵,又被称为投影矩阵(ProjectionMatrix)
renderer.uniforms.projectionMatrix = [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, -1, 0,
0, 0, 0, 1,
];

renderer.setMeshData([
{
positions: geometry.positions,
attributes: {
color: geometry.color,
},
cells: geometry.cells,
},
]);
renderer.render();
</script>
</body>
</html>

【视觉高级篇】20 # 如何用WebGL绘制3D物体?_3d_03

模型矩阵:让立方体旋转起来

用立方体沿 x、y、z 轴的旋转来生成模型矩阵。以 x、y、z 三个方向的旋转得到三个齐次矩阵,然后将它们相乘,就能得到最终的模型矩阵。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>模型矩阵:让立方体旋转起来</title>
<style>
canvas {
border: 1px dashed rgb(250, 128, 114);
}
</style>
</head>
<body>
<canvas width="512" height="512"></canvas>
<script src="./common/lib/gl-renderer.js"></script>
<script type="module">
import { multiply } from './common/lib/math/functions/Mat4Func.js';

const vertex = `
attribute vec3 a_vertexPosition;
attribute vec4 color;

varying vec4 vColor;
uniform mat4 projectionMatrix;
uniform mat4 modelMatrix;

void main() {
gl_PointSize = 1.0;
vColor = color;
gl_Position = projectionMatrix * modelMatrix * vec4(a_vertexPosition, 1);
}
`;

const fragment = `
#ifdef GL_ES
precision highp float;
#endif

varying vec4 vColor;

void main() {
gl_FragColor = vColor;
}
`;

const canvas = document.querySelector("canvas");
// 开启深度检测
const renderer = new GlRenderer(canvas, {
depth: true
});
const program = renderer.compileSync(fragment, vertex);
renderer.useProgram(program);

// 用来生成立方体 6 个面的 24 个顶点,以及 12 个三角形的索引
function cube(size = 1.0, colors = [[1, 0, 0, 1]]) {
const h = 0.5 * size;
// 立方体的顶点
const vertices = [
[-h, -h, -h],
[-h, h, -h],
[h, h, -h],
[h, -h, -h],
[-h, -h, h],
[-h, h, h],
[h, h, h],
[h, -h, h],
];

const positions = [];
const color = [];
const cells = [];

let colorIdx = 0;
let cellsIdx = 0;
const colorLen = colors.length;

function quad(a, b, c, d) {
[a, b, c, d].forEach((i) => {
positions.push(vertices[i]);
color.push(colors[colorIdx % colorLen]);
});
cells.push(
[0, 1, 2].map(i => i + cellsIdx),
[0, 2, 3].map(i => i + cellsIdx),
);
colorIdx++;
cellsIdx += 4;
}
// 立方体的六个面
quad(1, 0, 3, 2);
quad(4, 5, 6, 7);
quad(2, 3, 7, 6);
quad(5, 4, 0, 1);
quad(3, 0, 4, 7);
quad(6, 5, 1, 2);

return { positions, color, cells };
}

const geometry = cube(1.0, [
[250/255, 128/255, 114/255, 1], // salmon rgb(250 128 114)
[218/255, 165/255, 32/255, 1],// goldenrod rgb(218, 165, 32)
[46/255, 139/255, 87/255, 1], // seagreen rgb(46 139 87)
[255/255, 192/255, 203/255, 1], // pink rgb(255, 192, 203)
[135/255, 206/255, 235/255, 1],// skyblue rgb(135, 206, 235)
[106/255, 90/255, 205/255, 1], // slateblue rgb(106, 90, 205)
]);

// 将 z 轴坐标方向反转,对应的齐次矩阵如下,转换坐标的齐次矩阵,又被称为投影矩阵(ProjectionMatrix)
renderer.uniforms.projectionMatrix = [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, -1, 0,
0, 0, 0, 1,
];

function fromRotation(rotationX, rotationY, rotationZ) {
let c = Math.cos(rotationX);
let s = Math.sin(rotationX);
const rx = [
1, 0, 0, 0,
0, c, s, 0,
0, -s, c, 0,
0, 0, 0, 1,
];

c = Math.cos(rotationY);
s = Math.sin(rotationY);
const ry = [
c, 0, s, 0,
0, 1, 0, 0,
-s, 0, c, 0,
0, 0, 0, 1,
];

c = Math.cos(rotationZ);
s = Math.sin(rotationZ);
const rz = [
c, s, 0, 0,
-s, c, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
];

const ret = [];
multiply(ret, rx, ry);
multiply(ret, ret, rz);

return ret;
}

renderer.setMeshData([
{
positions: geometry.positions,
attributes: {
color: geometry.color,
},
cells: geometry.cells,
},
]);

let rotationX = 0;
let rotationY = 0;
let rotationZ = 0;
function update() {
rotationX += 0.003;
rotationY += 0.005;
rotationZ += 0.007;
renderer.uniforms.modelMatrix = fromRotation(rotationX, rotationY, rotationZ);
requestAnimationFrame(update);
}
update();

renderer.render();
</script>
</body>
</html>

【视觉高级篇】20 # 如何用WebGL绘制3D物体?_#ifdef_04

如何用 WebGL 绘制圆柱体

圆柱体的两个底面都是圆,可以用割圆的方式对圆进行简单的三角剖分,然后把圆柱的侧面用上下两个圆上的顶点进行三角剖分。

【视觉高级篇】20 # 如何用WebGL绘制3D物体?_投影矩阵_05

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>如何用 WebGL 绘制圆柱体</title>
<style>
canvas {
border: 1px dashed rgb(250, 128, 114);
}
</style>
</head>
<body>
<canvas width="512" height="512"></canvas>
<script src="./common/lib/gl-renderer.js"></script>
<script type="module">
import { multiply } from './common/lib/math/functions/Mat4Func.js';

const vertex = `
attribute vec3 a_vertexPosition;
attribute vec4 color;

varying vec4 vColor;
uniform mat4 projectionMatrix;
uniform mat4 modelMatrix;

void main() {
gl_PointSize = 1.0;
vColor = color;
gl_Position = projectionMatrix * modelMatrix * vec4(a_vertexPosition, 1.0);
}
`;

const fragment = `
#ifdef GL_ES
precision highp float;
#endif

varying vec4 vColor;

void main() {
gl_FragColor = vColor;
}
`;

const canvas = document.querySelector("canvas");
// 开启深度检测
const renderer = new GlRenderer(canvas, {
depth: true
});
const program = renderer.compileSync(fragment, vertex);
renderer.useProgram(program);

function cylinder(radius = 1.0, height = 1.0, segments = 30, colorCap = [0, 0, 1, 1], colorSide = [1, 0, 0, 1]) {
const positions = [];
const cells = [];
const color = [];
const cap = [[0, 0]];
const h = 0.5 * height;

// 顶和底的圆
for(let i = 0; i <= segments; i++) {
const theta = Math.PI * 2 * i / segments;
const p = [radius * Math.cos(theta), radius * Math.sin(theta)];
cap.push(p);
}

positions.push(...cap.map(([x, y]) => [x, y, -h]));
for(let i = 1; i < cap.length - 1; i++) {
cells.push([0, i, i + 1]);
}
cells.push([0, cap.length - 1, 1]);

let offset = positions.length;
positions.push(...cap.map(([x, y]) => [x, y, h]));
for(let i = 1; i < cap.length - 1; i++) {
cells.push([offset, offset + i, offset + i + 1]);
}
cells.push([offset, offset + cap.length - 1, offset + 1]);

color.push(...positions.map(() => colorCap));

// 侧面
offset = positions.length;
for(let i = 1; i < cap.length; i++) {
const a = [...cap[i], h];
const b = [...cap[i], -h];
const nextIdx = i < cap.length - 1 ? i + 1 : 1;
const c = [...cap[nextIdx], -h];
const d = [...cap[nextIdx], h];

positions.push(a, b, c, d);
color.push(colorSide, colorSide, colorSide, colorSide);
cells.push([offset, offset + 1, offset + 2], [offset, offset + 2, offset + 3]);
offset += 4;
}

return { positions, cells, color };
}

const geometry = cylinder(0.2, 1.0, 400,
[250/255, 128/255, 114/255, 1], // salmon rgb(250 128 114)
[46/255, 139/255, 87/255, 1], // seagreen rgb(46 139 87)
);

// 将 z 轴坐标方向反转,对应的齐次矩阵如下,转换坐标的齐次矩阵,又被称为投影矩阵(ProjectionMatrix)
renderer.uniforms.projectionMatrix = [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, -1, 0,
0, 0, 0, 1,
];

function fromRotation(rotationX, rotationY, rotationZ) {
let c = Math.cos(rotationX);
let s = Math.sin(rotationX);
const rx = [
1, 0, 0, 0,
0, c, s, 0,
0, -s, c, 0,
0, 0, 0, 1,
];

c = Math.cos(rotationY);
s = Math.sin(rotationY);
const ry = [
c, 0, s, 0,
0, 1, 0, 0,
-s, 0, c, 0,
0, 0, 0, 1,
];

c = Math.cos(rotationZ);
s = Math.sin(rotationZ);
const rz = [
c, s, 0, 0,
-s, c, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
];

const ret = [];
multiply(ret, rx, ry);
multiply(ret, ret, rz);

return ret;
}

renderer.setMeshData([
{
positions: geometry.positions,
attributes: {
color: geometry.color,
},
cells: geometry.cells,
},
]);

let rotationX = 0;
let rotationY = 0;
let rotationZ = 0;
function update() {
rotationX += 0.003;
rotationY += 0.005;
rotationZ += 0.007;
renderer.uniforms.modelMatrix = fromRotation(rotationX, rotationY, rotationZ);
requestAnimationFrame(update);
}
update();

renderer.render();
</script>
</body>
</html>

【视觉高级篇】20 # 如何用WebGL绘制3D物体?_3d_06

上面是个六棱柱,我们可以修改 cylinder 函数里的 segments 参数,比如:400,就可以得到近似的圆柱体了

【视觉高级篇】20 # 如何用WebGL绘制3D物体?_投影矩阵_07

使用法向量和法向量矩阵来实现点光源光照效果

对于圆柱体来说,底面和顶面法线分别是 ​​(0, 0, -1)​​​ 和 ​​(0, 0, 1)​​,侧面的法向量可以通过三角网格来计算。

因为几何体是由三角网格构成的,而法线是垂直于三角网格的线,如果要计算法线,我们可以借助三角形的顶点,使用向量的叉积定理来求。假设在一个平面内,有向量 a 和 b,n 是它们的法向量,那我们可以得到公式:​​n = a X b​​。

【视觉高级篇】20 # 如何用WebGL绘制3D物体?_投影矩阵_08

在片元着色器中,拿到的是变换后的顶点坐标,需要对法向量也进行变换,可以通过一个矩阵来实现,这个矩阵叫做法向量矩阵(NormalMatrix)

在顶点着色器中,计算位于​​(1,0,0)​​坐标处的点光源与几何体法线的夹角余弦。根据物体漫反射模型,光照强度等于光线与法向量夹角的余弦,就能在片元着色器叠加光照。

【视觉高级篇】20 # 如何用WebGL绘制3D物体?_html_09

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>使用法向量和法向量矩阵来实现点光源光照效果</title>
<style>
canvas {
border: 1px dashed rgb(250, 128, 114);
}
</style>
</head>
<body>
<canvas width="512" height="512"></canvas>
<script src="./common/lib/gl-renderer.js"></script>
<script type="module">
import { multiply } from './common/lib/math/functions/Mat4Func.js';
import { cross, subtract, normalize } from './common/lib/math/functions/Vec3Func.js';
import { normalFromMat4 } from './common/lib/math/functions/Mat3Func.js';

const vertex = `
attribute vec3 a_vertexPosition;
attribute vec4 color;
attribute vec3 normal;

varying vec4 vColor;
varying float vCos;
uniform mat4 projectionMatrix;
uniform mat4 modelMatrix;
uniform mat3 normalMatrix;

const vec3 lightPosition = vec3(1, 0, 0);

void main() {
gl_PointSize = 1.0;
vColor = color;
vec4 pos = modelMatrix * vec4(a_vertexPosition, 1.0);
vec3 invLight = lightPosition - pos.xyz;
vec3 norm = normalize(normalMatrix * normal);
vCos = max(dot(normalize(invLight), norm), 0.0);
gl_Position = projectionMatrix * pos;
}
`;

const fragment = `
#ifdef GL_ES
precision highp float;
#endif

uniform vec4 lightColor;
varying vec4 vColor;
varying float vCos;

void main() {
gl_FragColor.rgb = vColor.rgb + vCos * lightColor.a * lightColor.rgb;
gl_FragColor.a = vColor.a;
}
`;

const canvas = document.querySelector("canvas");
// 开启深度检测
const renderer = new GlRenderer(canvas, {
depth: true
});
const program = renderer.compileSync(fragment, vertex);
renderer.useProgram(program);

function cylinder(radius = 1.0, height = 1.0, segments = 30, colorCap = [0, 0, 1, 1], colorSide = [1, 0, 0, 1]) {
const positions = [];
const cells = [];
const color = [];
const cap = [[0, 0]];
const h = 0.5 * height;
const normal = [];

// 顶和底的圆
for(let i = 0; i <= segments; i++) {
const theta = Math.PI * 2 * i / segments;
const p = [radius * Math.cos(theta), radius * Math.sin(theta)];
cap.push(p);
}

positions.push(...cap.map(([x, y]) => [x, y, -h]));
normal.push(...cap.map(() => [0, 0, -1]));

for(let i = 1; i < cap.length - 1; i++) {
cells.push([0, i, i + 1]);
}
cells.push([0, cap.length - 1, 1]);

let offset = positions.length;
positions.push(...cap.map(([x, y]) => [x, y, h]));
normal.push(...cap.map(() => [0, 0, 1]));

for(let i = 1; i < cap.length - 1; i++) {
cells.push([offset, offset + i, offset + i + 1]);
}
cells.push([offset, offset + cap.length - 1, offset + 1]);

color.push(...positions.map(() => colorCap));

const tmp1 = [];
const tmp2 = [];
// 侧面,这里需要求出侧面的法向量
offset = positions.length;
for(let i = 1; i < cap.length; i++) {
const a = [...cap[i], h];
const b = [...cap[i], -h];
const nextIdx = i < cap.length - 1 ? i + 1 : 1;
const c = [...cap[nextIdx], -h];
const d = [...cap[nextIdx], h];

positions.push(a, b, c, d);

const norm = [];
cross(norm, subtract(tmp1, b, a), subtract(tmp2, c, a));
normalize(norm, norm);
normal.push(norm, norm, norm, norm); // abcd四个点共面,它们的法向量相同

color.push(colorSide, colorSide, colorSide, colorSide);
cells.push([offset, offset + 1, offset + 2], [offset, offset + 2, offset + 3]);
offset += 4;
}

return { positions, cells, color, normal };
}

const geometry = cylinder(0.2, 1.0, 400,
[250/255, 128/255, 114/255, 1], // salmon rgb(250 128 114)
[46/255, 139/255, 87/255, 1], // seagreen rgb(46 139 87)
);

// 将 z 轴坐标方向反转,对应的齐次矩阵如下,转换坐标的齐次矩阵,又被称为投影矩阵(ProjectionMatrix)
renderer.uniforms.projectionMatrix = [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, -1, 0,
0, 0, 0, 1,
];

renderer.uniforms.lightColor = [218/255, 165/255, 32/255, 0.6];// goldenrod rgb(218, 165, 32)

function fromRotation(rotationX, rotationY, rotationZ) {
let c = Math.cos(rotationX);
let s = Math.sin(rotationX);
const rx = [
1, 0, 0, 0,
0, c, s, 0,
0, -s, c, 0,
0, 0, 0, 1,
];

c = Math.cos(rotationY);
s = Math.sin(rotationY);
const ry = [
c, 0, s, 0,
0, 1, 0, 0,
-s, 0, c, 0,
0, 0, 0, 1,
];

c = Math.cos(rotationZ);
s = Math.sin(rotationZ);
const rz = [
c, s, 0, 0,
-s, c, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
];

const ret = [];
multiply(ret, rx, ry);
multiply(ret, ret, rz);

return ret;
}

console.log(geometry);

renderer.setMeshData([
{
positions: geometry.positions,
attributes: {
color: geometry.color,
normal: geometry.normal
},
cells: geometry.cells,
},
]);

let rotationX = 0;
let rotationY = 0;
let rotationZ = 0;
function update() {
rotationX += 0.003;
rotationY += 0.005;
rotationZ += 0.007;
const modelMatrix = fromRotation(rotationX, rotationY, rotationZ);
renderer.uniforms.modelMatrix = modelMatrix;
renderer.uniforms.normalMatrix = normalFromMat4([], modelMatrix);
requestAnimationFrame(update);
}
update();

renderer.render();
</script>
</body>
</html>

【视觉高级篇】20 # 如何用WebGL绘制3D物体?_#ifdef_10