说明
【跟月影学可视化】学习笔记。
如何理解相机和视图矩阵?
用一个三维坐标(Position)和一个三维向量方向(LookAt Target)来表示 WebGL 的三维世界的一个相机。要绘制以相机为观察者的图形,需要用一个变换,将世界坐标转换为相机坐标。这个变换的矩阵就是视图矩阵(ViewMatrix)。
怎么计算视图矩阵?
- 先计算相机的模型矩阵
- 然后对矩阵使用 lookAt 函数,得到的矩阵就是视图矩阵的逆矩阵。
- 最后再对这个逆矩阵求一次逆,就可以得到视图矩阵。
用代码的方式表示:
function updateCamera(eye, target = [0, 0, 0]) {
const [x, y, z] = eye;
// 设置相机初始位置矩阵 m
const m = new Mat4(
1, 0,0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
x, y, z, 1,
);
const up = [0, 1, 0];
m.lookAt(eye, target, up).inverse();
renderer.uniforms.viewMatrix = m;
}
<!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 { Mat4 } from './common/lib/math/Mat4.js';
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 mat4 viewMatrix;
uniform mat3 normalMatrix;
const vec3 lightPosition = vec3(1, 0, 0);
void main() {
gl_PointSize = 1.0;
vColor = color;
vec4 pos = viewMatrix * modelMatrix * vec4(a_vertexPosition, 1.0);
vec4 lp = viewMatrix * vec4(lightPosition, 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 updateCamera(eye, target = [0, 0, 0]) {
const [x, y, z] = eye;
// 设置相机初始位置矩阵 m
const m = new Mat4(
1, 0,0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
x, y, z, 1,
);
const up = [0, 1, 0];
m.lookAt(eye, target, up).inverse();
renderer.uniforms.viewMatrix = m;
}
// 设置相机位置
updateCamera([0.5, 0, 0.5]);
renderer.setMeshData([
{
positions: geometry.positions,
attributes: {
color: geometry.color,
normal: geometry.normal
},
cells: geometry.cells,
},
]);
renderer.uniforms.modelMatrix = new Mat4(
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
);
function update() {
const modelViewMatrix = multiply([], renderer.uniforms.viewMatrix, renderer.uniforms.modelMatrix);
renderer.uniforms.modelViewMatrix = modelViewMatrix;
renderer.uniforms.normalMatrix = normalFromMat4([], modelViewMatrix);
requestAnimationFrame(update);
}
update();
renderer.render();
</script>
</body>
</html>
剪裁空间和投影对 3D 图像的影响
WebGL 的默认坐标范围是从 -1 到 1 的。只有当图像的 x、y、z 的值在 -1 到 1 区间内才会被显示在画布上,而在其他位置上的图像都会被剪裁掉。
给下面图形分别给 x、y、z 轴增加 0.5 的平移
<!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);
vec4 lp = vec4(lightPosition, 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.5, 1.0, 30,
[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;
}
renderer.setMeshData([
{
positions: geometry.positions,
attributes: {
color: geometry.color,
normal: geometry.normal
},
cells: geometry.cells,
},
]);
const rotationX = 0.5;
const rotationY = 0.5;
const rotationZ = 0;
function update() {
const modelMatrix = fromRotation(rotationX, rotationY, rotationZ);
modelMatrix[12] = 0.5; // 给 x 轴增加 0.5 的平移
modelMatrix[13] = 0.5; // 给 y 轴增加 0.5 的平移
modelMatrix[14] = 0.5; // 给 z 轴增加 0.5 的平移
renderer.uniforms.modelMatrix = modelMatrix;
renderer.uniforms.normalMatrix = normalFromMat4([], modelMatrix);
requestAnimationFrame(update);
}
update();
renderer.render();
</script>
</body>
</html>
为了让图形在剪裁空间中正确显示,我们不能只反转 z 轴,还需要将图像从三维空间中投影到剪裁坐标内。
正投影
正投影是将物体投影到一个长方体的空间(又称为视景体),并且无论相机与物体距离多远,投影的大小都不变。正投影又叫做平行投影。
下面 ortho 是计算正投影的函数,它的参数是视景体 x、y、z 三个方向的坐标范围,它的返回值就是投影矩阵。
// 计算正投影矩阵
function ortho(out, left, right, bottom, top, near, far) {
let lr = 1 / (left - right);
let bt = 1 / (bottom - top);
let nf = 1 / (near - far);
out[0] = -2 * lr;
out[1] = 0;
out[2] = 0;
out[3] = 0;
out[4] = 0;
out[5] = -2 * bt;
out[6] = 0;
out[7] = 0;
out[8] = 0;
out[9] = 0;
out[10] = 2 * nf;
out[11] = 0;
out[12] = (left + right) * lr;
out[13] = (top + bottom) * bt;
out[14] = (far + near) * nf;
out[15] = 1;
return out;
}
<!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 { Mat4 } from './common/lib/math/Mat4.js';
import { multiply, ortho } 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 mat4 viewMatrix;
uniform mat3 normalMatrix;
const vec3 lightPosition = vec3(1, 0, 0);
void main() {
gl_PointSize = 1.0;
vColor = color;
vec4 pos = viewMatrix * modelMatrix * vec4(a_vertexPosition, 1.0);
vec4 lp = viewMatrix * vec4(lightPosition, 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)
);
function projection(left, right, bottom, top, near, far) {
return ortho([], left, right, bottom, top, near, far);
}
// 让视景体三个方向的范围都是 (-1, 1)
const projectionMatrix = projection(-1, 1, -1, 1, -1, 1);
renderer.uniforms.projectionMatrix = projectionMatrix;
renderer.uniforms.lightColor = [218/255, 165/255, 32/255, 0.6];// goldenrod rgb(218, 165, 32)
function updateCamera(eye, target = [0, 0, 0]) {
const [x, y, z] = eye;
// 设置相机初始位置矩阵 m
const m = new Mat4(
1, 0,0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
x, y, z, 1,
);
const up = [0, 1, 0];
m.lookAt(eye, target, up).inverse();
renderer.uniforms.viewMatrix = m;
}
// 设置相机位置
updateCamera([0.5, 0, 0.5]);
renderer.setMeshData([
{
positions: geometry.positions,
attributes: {
color: geometry.color,
normal: geometry.normal
},
cells: geometry.cells,
},
]);
renderer.uniforms.modelMatrix = new Mat4(
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
);
function update() {
const modelViewMatrix = multiply([], renderer.uniforms.viewMatrix, renderer.uniforms.modelMatrix);
renderer.uniforms.modelViewMatrix = modelViewMatrix;
renderer.uniforms.normalMatrix = normalFromMat4([], modelViewMatrix);
requestAnimationFrame(update);
}
update();
renderer.render();
</script>
</body>
</html>
透视投影
透视投影离相机近的物体大,离相机远的物体小。与正投影不同,正投影的视景体是一个长方体,而透视投影的视景体是一个棱台。
下面 perspective
是计算透视投影的函数,它的参数有近景平面 near、远景平面 far、视角 fovy 和宽高比率 aspect,返回值也是投影矩阵。
// 计算透视投影矩阵
function perspective(out, fovy, aspect, near, far) {
let f = 1.0 / Math.tan(fovy / 2);
let nf = 1 / (near - far);
out[0] = f / aspect;
out[1] = 0;
out[2] = 0;
out[3] = 0;
out[4] = 0;
out[5] = f;
out[6] = 0;
out[7] = 0;
out[8] = 0;
out[9] = 0;
out[10] = (far + near) * nf;
out[11] = -1;
out[12] = 0;
out[13] = 0;
out[14] = 2 * far * near * nf;
out[15] = 0;
return out;
}
<!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 { Mat4 } from './common/lib/math/Mat4.js';
import { multiply, perspective } 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 mat4 viewMatrix;
uniform mat3 normalMatrix;
const vec3 lightPosition = vec3(1, 0, 0);
void main() {
gl_PointSize = 1.0;
vColor = color;
vec4 pos = viewMatrix * modelMatrix * vec4(a_vertexPosition, 1.0);
vec4 lp = viewMatrix * vec4(lightPosition, 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)
);
function projection(near = 0.1, far = 100, fov = 45, aspect = 1) {
return perspective([], fov * Math.PI / 180, aspect, near, far);
}
const projectionMatrix = projection();
renderer.uniforms.projectionMatrix = projectionMatrix;
renderer.uniforms.lightColor = [218/255, 165/255, 32/255, 0.6];// goldenrod rgb(218, 165, 32)
function updateCamera(eye, target = [0, 0, 0]) {
const [x, y, z] = eye;
// 设置相机初始位置矩阵 m
const m = new Mat4(
1, 0,0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
x, y, z, 1,
);
const up = [0, 1, 0];
m.lookAt(eye, target, up).inverse();
renderer.uniforms.viewMatrix = m;
}
// 设置相机位置
updateCamera([1.5, 0, 1.5]);
renderer.setMeshData([
{
positions: geometry.positions,
attributes: {
color: geometry.color,
normal: geometry.normal
},
cells: geometry.cells,
},
]);
renderer.uniforms.modelMatrix = new Mat4(
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
);
function update() {
const modelViewMatrix = multiply([], renderer.uniforms.viewMatrix, renderer.uniforms.modelMatrix);
renderer.uniforms.modelViewMatrix = modelViewMatrix;
renderer.uniforms.normalMatrix = normalFromMat4([], modelViewMatrix);
requestAnimationFrame(update);
}
update();
renderer.render();
</script>
</body>
</html>
在透视投影下,距离观察者(相机)近的部分大,距离它远的部分小,这更符合真实世界中我们看到的效果。
3D 绘图标准模型
3D 绘图的标准模型也就是3D 绘制几何体的基本数学模型,标准模型一共有四个矩阵,它们分别是:投影矩阵、视图矩阵(ViewMatrix)、模型矩阵(ModelMatrix)、法向量矩阵(NormalMatrix)
。
- 前三个矩阵用来计算最终显示的几何体的顶点位置
- 第四个法向量矩阵用来实现光照等效果
比较成熟的图形库,如 ThreeJS、BabylonJS,OGL:轻量级的图形库基本上都是采用这个标准模型来进行 3D 绘图的。
如何使用 OGL 绘制基本的几何体
OGL:https://github.com/oframe/ogl
OGL 是一个小型、高效的 WebGL 库,目标是那些喜欢最小抽象层并有兴趣创建自己的着色器的开发人员。这个 API 是用 es6 模块编写的,没有任何依赖,与 ThreeJS 有很多相似之处,但是它与 WebGL 紧密耦合,而且功能少得多。在其设计中,该库做了必要的最低抽象,因此开发人员仍然可以轻松地将其与原生 WebGL 命令一起使用。保持较低的抽象层次有助于使库更易于理解和扩展,也使它作为 WebGL 学习资源更实用。
OGL 库绘制几何体分成 7 个步骤:
下面我们参考这个 demo 来实操一下:https://oframe.github.io/ogl/examples/?src=base-primitives.html
demo 的源码
<!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>使用 OGL 绘制基本的几何体</title>
<style>
canvas {
border: 1px dashed rgb(250, 128, 114);
}
</style>
</head>
<body>
<canvas width="512" height="512"></canvas>
<script type="module">
import { Renderer, Camera, Transform, Plane, Sphere, Box, Cylinder, Torus, Program, Mesh } from './common/lib/ogl/index.mjs';
// 1、创建 Renderer 对象
const canvas = document.querySelector('canvas');
const renderer = new Renderer({
canvas,
width: 512,
height: 512,
dpr: 2
});
const gl = renderer.gl;
gl.clearColor(1, 1, 1, 1);
// 2、通过 new Camera 来创建相机(默认创建出的是透视投影相机)
const camera = new Camera(gl, {
fov: 35 // 视角设置为 35 度
});
// 位置设置为 (0, 1, 7)
camera.position.set(0, 1, 7);
// 朝向为 (0, 0, 0)
camera.lookAt([0, 0, 0]);
// 3、创建场景
const scene = new Transform(); // OGL 使用树形渲染的方式,需要使用 Transform 元素,它可以添加子元素和设置几何变换
// 4、创建几何体对象
const planeGeometry = new Plane(gl); // 平面
const sphereGeometry = new Sphere(gl); // 球体
const cubeGeometry = new Box(gl); // 立方体
const cylinderGeometry = new Cylinder(gl); // 圆柱
const torusGeometry = new Torus(gl); // 环面
// 5、创建 WebGL 程序
const vertex = `
attribute vec3 position;
attribute vec3 normal;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
uniform mat3 normalMatrix;
varying vec3 vNormal;
void main() {
vNormal = normalize(normalMatrix * normal);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const fragment = `
precision highp float;
varying vec3 vNormal;
void main() {
vec3 normal = normalize(vNormal);
float lighting = dot(normal, normalize(vec3(-0.3, 0.8, 0.6)));
gl_FragColor.rgb = vec3(0.98, 0.50, 0.44) + lighting * 0.1;
gl_FragColor.a = 1.0;
}
`;
const program = new Program(gl, {
vertex,
fragment,
cullFace: null // 加上能使平面是双面的,不然旋转的时候会有一段空白
});
// 6、构建网格(Mesh)元素:设置不同的位置,然后将它们添加到场景 scene 中去
// 平面
const plane = new Mesh(gl, {geometry: planeGeometry, program});
plane.position.set(0, 1.3, 0);
plane.setParent(scene);
// 球体
const sphere = new Mesh(gl, {geometry: sphereGeometry, program});
sphere.position.set(0, 0, 0);
sphere.setParent(scene);
// 立方体
const cube = new Mesh(gl, {geometry: cubeGeometry, program});
cube.position.set(0, -1.3, 0);
cube.setParent(scene);
// 圆柱
const cylinder = new Mesh(gl, {geometry: cylinderGeometry, program});
cylinder.position.set(-1.3, 0, 0);
cylinder.setParent(scene);
// 环面
const torus = new Mesh(gl, {geometry: torusGeometry, program});
torus.position.set(1.3, 0, 0);
torus.setParent(scene);
// 7、完成渲染
requestAnimationFrame(update);
function update() {
requestAnimationFrame(update);
plane.rotation.x -= 0.02;
sphere.rotation.y -= 0.03;
cube.rotation.y -= 0.04;
cylinder.rotation.z -= 0.02;
torus.rotation.y -= 0.02;
renderer.render({scene, camera});
}
</script>
</body>
</html>
下面我们优化一下,让圆看起来更圆,然后这个几个图形的颜色渲染的不一样。
圆可以通过加大参数 widthSegments
处理
颜色问题我们可以通过 Program 传不同颜色到 fragment 里去。
<!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>使用 OGL 绘制基本的几何体2</title>
<style>
canvas {
border: 1px dashed rgb(250, 128, 114);
}
</style>
</head>
<body>
<canvas width="512" height="512"></canvas>
<script type="module">
import { Renderer, Camera, Transform, Plane, Sphere, Box, Cylinder, Torus, Program, Mesh } from './common/lib/ogl/index.mjs';
import { Vec3 } from "./common/lib/math/vec3.js";
// 1、创建 Renderer 对象
const canvas = document.querySelector('canvas');
const renderer = new Renderer({
canvas,
width: 512,
height: 512,
dpr: 2
});
const gl = renderer.gl;
gl.clearColor(1, 1, 1, 1);
// 2、通过 new Camera 来创建相机(默认创建出的是透视投影相机)
const camera = new Camera(gl, {
fov: 35 // 视角设置为 35 度
});
// 位置设置为 (0, 1, 7)
camera.position.set(0, 1, 7);
// 朝向为 (0, 0, 0)
camera.lookAt([0, 0, 0]);
// 3、创建场景
const scene = new Transform(); // OGL 使用树形渲染的方式,需要使用 Transform 元素,它可以添加子元素和设置几何变换
// 4、创建几何体对象
const planeGeometry = new Plane(gl); // 平面
const sphereGeometry = new Sphere(gl, {
widthSegments: 400
}); // 球体
const cubeGeometry = new Box(gl); // 立方体
const cylinderGeometry = new Cylinder(gl); // 圆柱
const torusGeometry = new Torus(gl); // 环面
// 5、创建 WebGL 程序
const vertex = `
attribute vec3 position;
attribute vec3 normal;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
uniform mat3 normalMatrix;
varying vec3 vNormal;
void main() {
vNormal = normalize(normalMatrix * normal);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const fragment = `
precision highp float;
varying vec3 vNormal;
uniform vec3 uColor;
void main() {
vec3 normal = normalize(vNormal);
float lighting = dot(normal, normalize(vec3(-0.3, 0.8, 0.6)));
gl_FragColor.rgb = uColor + lighting * 0.1;
gl_FragColor.a = 1.0;
}
`;
function createdProgram(r, g, b) {
return new Program(gl, {
vertex,
fragment,
uniforms:{
uColor:{
value: new Vec3(r, g, b)
}
},
cullFace: null // 加上能使平面是双面的,不然旋转的时候会有一段空白
})
}
// 6、构建网格(Mesh)元素:设置不同的位置,然后将它们添加到场景 scene 中去
// 平面
const plane = new Mesh(gl, {
geometry: planeGeometry,
program: createdProgram(250/255, 128/255, 114/255) // salmon rgb(250, 128, 114)
});
plane.position.set(0, 1.3, 0);
plane.setParent(scene);
// 球体
const sphere = new Mesh(gl, {
geometry: sphereGeometry,
program: createdProgram(218/255, 165/255, 32/255) // goldenrod rgb(218, 165, 32)
});
sphere.position.set(0, 0, 0);
sphere.setParent(scene);
// 立方体
const cube = new Mesh(gl, {
geometry: cubeGeometry,
program: createdProgram(46/255, 139/255, 87/255) // seagreen rgb(46, 139, 87)
});
cube.position.set(0, -1.3, 0);
cube.setParent(scene);
// 圆柱
const cylinder = new Mesh(gl, {
geometry: cylinderGeometry,
program: createdProgram(135/255, 206/255, 235/255) // skyblue rgb(135, 206, 235)
});
cylinder.position.set(-1.3, 0, 0);
cylinder.setParent(scene);
// 环面
const torus = new Mesh(gl, {
geometry: torusGeometry,
program: createdProgram(106/255, 90/255, 205/255) // slateblue rgb(106, 90, 205)
});
torus.position.set(1.3, 0, 0);
torus.setParent(scene);
// 7、完成渲染
requestAnimationFrame(update);
function update() {
requestAnimationFrame(update);
plane.rotation.x -= 0.02;
sphere.rotation.y -= 0.03;
cube.rotation.y -= 0.04;
cylinder.rotation.z -= 0.02;
torus.rotation.y -= 0.02;
renderer.render({scene, camera});
}
</script>
</body>
</html>