【视觉高级篇】21 # 如何添加相机,用透视原理对物体进行投影?

news2025/6/26 16:11:37

说明

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

如何理解相机和视图矩阵?

用一个三维坐标(Position)和一个三维向量方向(LookAt Target)来表示 WebGL 的三维世界的一个相机。要绘制以相机为观察者的图形,需要用一个变换,将世界坐标转换为相机坐标。这个变换的矩阵就是视图矩阵(ViewMatrix)

怎么计算视图矩阵?

  1. 先计算相机的模型矩阵
  2. 然后对矩阵使用 lookAt 函数,得到的矩阵就是视图矩阵的逆矩阵。
  3. 最后再对这个逆矩阵求一次逆,就可以得到视图矩阵。

用代码的方式表示:

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>

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/10552.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ubuntu安装和启动redis命令步骤及其配置文件redis.conf

1、步骤一 依次执行如下命令 1.下载&#xff1a;wget http://download.redis.io/releases/redis-6.0.1.tar.gz 2.解压&#xff1a;tar xzf redis-6.0.1.tar.gz 2.将解压文件夹移动到usr/local/redis目录下:sudo mv ./redis-6.0.1 /usr/local/redis/ 4.进入到redis目录&#x…

这或许是全网最全时间序列特征工程构造的文章了

数据和特征决定了机器学习的上限&#xff0c;而模型和算法只是逼近这个上限而已。由此可见&#xff0c;特征工程在机器学习中占有相当重要的地位。在实际应用当中&#xff0c;可以说特征工程是机器学习成功的关键。 那特征工程是什么&#xff1f; 特征工程是利用数据领域的相关…

饼图、柱形图、堆积柱、折线图、散点图,到底应该怎么选?

“随着数字经济的发展&#xff0c;各行业的数据都出现了爆炸式的增长&#xff0c;如何快速从海量数据中提取出有效信息&#xff0c;最大化地挖掘数据价值&#xff0c;是所有转型的企业都在面临的问题。” 想要快速直观地以易于理解、内容简单的方式了解相关数据&#xff0c;就需…

[附源码]java毕业设计家政管理系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

软件测试面试真题 | TCP为什么要进行三次握手和四次挥手呢?

TCP为什么要进行三次握手和四次挥手呢&#xff1f; 在这个三次握手的过程中对应的消息内容是怎样进行传递的呢&#xff1f; 在四次挥手的过程中&#xff0c;是怎样告知对方断开连接的呢&#xff1f; 三次握手 在说对应概念之前&#xff0c;我们先来了解一个场景&#xff1a…

计算机网络 4 - 网络层

第4章 网络层&#xff1a;数据层面(Network Layer: Data Plane)4.1 网络层概述4.2 IP: Internet Protocol分类 IP 地址子网划分无分类域间路由 CIDRIP 地址的特点4.3 地址解析协议 ARP4.4 IP 数据包格式路由转发示例路由器转发算法使用二叉树查找转发表4.5 IP分配技术DHCPNAT 网…

celery

一 介绍 官网&#xff1a;https://docs.celeryq.dev/en/latest/index.html celery是一个简单、灵活、可靠的分布式系统&#xff0c;用于 处理大量消息&#xff0c;同时为操作提供 维护此类系统所需的工具。 Celery架构 Celery的架构由三部分组成&#xff0c;消息中间件&…

纸牌游戏新版小猫钓鱼设计制作

新版纸牌游戏《小猫钓鱼》设计制作 此游戏设计是我新创制的简单的卡牌游戏。属于儿童益智类游戏&#xff0c;适用于儿童的认知教育。 游戏规则很简单&#xff1a;找配对的牌消去。 游戏设置2个玩家对玩&#xff0c;鱼池置牌21张&#xff0c;玩家每人5张牌&#xff0c;二人轮转…

從turtle海龜動畫 學習 Python - 高中彈性課程系列 6.1 內嵌正多邊形 類似禪繞圖

Goal: 藉由有趣的「海龜動畫繪圖」學會基礎的 Python 程式設計 本篇介紹基礎的 Python 海龜動畫繪圖, 確實可以只以簡單的指令畫出極為複雜有趣或美麗的圖案: 內嵌正多邊形之圖案, 禪繞圖等 “Talk is cheap. Show me the code.” ― Linus Torvalds 老子第41章 上德若谷 大白…

Redis--1.CentOS8安装redis服务器

一、登录root账号 设置root密码&#xff1a; sudo passwd root切换到root账号&#xff1a; su root二、下载解压安装包 切换到根目录: cd / 1、创建存放路径: mkdir -p /usr/local/redis cd /usr/local/redis2、下载redis安装包&#xff1a;去官网找到redis连接地址如&…

基于jsp+mysql+ssm进销存管理系统-计算机毕业设计

本java进销存系统主要完成对超市的管理&#xff0c;包括会员管理、厂家管理、商品管理、退货管理&#xff0c;销售管理、进货管理、员工管理、系统管理等几个方面。系统可以完成对各类信息的浏览、查询、添加、删除、修改等功能。 系统采用了jsp的mvc框架,SSM(springMvcspringM…

告别手机自带浏览器,分享2022年好用的手机浏览器

对于喜欢使用手机上网冲浪的人来说&#xff0c;最喜欢用的一般都是小巧、强大、干净简洁的APP。作为上网常用的软件&#xff0c;好用的浏览器能够提高工作效率。而手机自带的浏览器往往占用资源大&#xff0c;而且广告很多&#xff0c;并夹带新闻、小说等无用功能&#xff0c;不…

自制操作系统日记(7):字符串显示

代码仓库地址&#xff1a;https://github.com/freedom-xiao007/operating-system 简介 上篇中我们在屏幕上画出了界面的大致轮廓&#xff0c;系统有了点模样&#xff0c;本篇继续跟着书籍&#xff0c;让程序中的字符串显示在屏幕上 效果展示 先放最终的效果&#xff0c;可以…

Linux磁盘分区,挂载介绍

分区的方式: mbr分区: 1.最多支持四个主分区 ⒉系统只能安装在主分区 3.扩展分区要占一个主分区 4.MBR最大只支持2TB&#xff0c;但拥有最好的兼容性 gtp分区: 1.支持无限多个主分区&#xff08;但操作系统可能限制&#xff0c;比如windows下最多128个分区) ⒉.最大支持18E…

仿大众点评——秒杀系统部分02

秒杀系统优化 接口限流和安全措施 令牌桶限流单用户访问频率限流抢购接口隐藏 接口限流&#xff1a; 在面临高并发的请购请求时&#xff0c;我们如果不对接口进行限流&#xff0c;可能会对后台系统造成极大的压力。尤其是对于下单的接口&#xff0c;过多的请求打到数据库会对…

MVCC 底层实现原理

文章目录概述事务并发出现的问题脏读不可重复读幻读事务隔离级别MVCC 底层实现原理隐式字段undo 日志Read View总结概述 MVCC(Multi-Version Concurrency Control) &#xff0c;叫做基于多版本的并发控制协议。 MVCC 是乐观锁的一种实现方式&#xff0c;它在很多情况下&#…

多线程增量下载K线数据

准备一份股票列表的CSV文件&#xff0c;文件格式如下 codenameclosecmvdate_ipo300434金石亚药12.89427982959020150424300380安硕信息19.31241993416320140128688123聚辰股份132.821114087266620191223300586美联新材20.34790882138120170104300534陇神戎发12.96389465063120…

Arduino与Proteus仿真实例-密码输入、验证与更新仿真

密码输入、验证与更新仿真 本次实例将通过4X4矩阵键盘、LCD1602、EEPROM实现一个密码输入匹配、储存、更新。 1、仿真电路原理图 在仿真电路原理图中,4X4矩阵键盘通过PCF8574 IO扩展器驱动,请参考前面文章: Arduino与Proteus仿真实例-PCF8574驱动4x4矩阵键盘仿真Arduino与…

大数据:Flume安装部署和配置

文章目录Flume 简介一&#xff0c;Flume下载和安装1&#xff09;登录[Flume官网](https://flume.apache.org/)&#xff0c;下载 apache-flume-1.9.0-bin.tar.gz2&#xff09;解压文件到 /opt 目录下3&#xff09;改名为 flume二&#xff0c;Flume配置1&#xff09;修改 /conf/ …

.Net开发——EFCore

1 EFCore是一个ORM框架 EFCore(EntityFramworkCore)是对底层ADO.NET重新封装的一个数据操作框架&#xff0c;因此ADO.NET支持的多种数据库都可以被EFCore支持。 EF Core 可用作对象关系映射程序 (O/RM)&#xff0c;这可以实现以下两点&#xff1a; 使 .NET 开发人员能够使用…