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

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

说明

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



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


用一个三维坐标(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>

3001721b4e7c4c74bcd59823196d3933.png



剪裁空间和投影对 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>


c7f53fd9f354494a92a897c523160bb6.png

为了让图形在剪裁空间中正确显示,我们不能只反转 z 轴,还需要将图像从三维空间中投影到剪裁坐标内。



正投影

正投影是将物体投影到一个长方体的空间(又称为视景体),并且无论相机与物体距离多远,投影的大小都不变。正投影又叫做平行投影

57d5d2d1afa34bfc8bd7b76d1d85a1a7.png


下面 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>
 
         

48dd2bf4d7c34aef9c88feefe2897e04.png



透视投影

透视投影离相机近的物体大,离相机远的物体小。与正投影不同,正投影的视景体是一个长方体,而透视投影的视景体是一个棱台。


image.png


下面 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>

73f29a7bf2c54b9dba6bcb97fb71d2ff.png

在透视投影下,距离观察者(相机)近的部分大,距离它远的部分小,这更符合真实世界中我们看到的效果。



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 个步骤:

2eb21e340eb34121886cc872b6daec00.png



下面我们参考这个 demo 来实操一下:https://oframe.github.io/ogl/examples/?src=base-primitives.html


f33388bc001042bbb90fd074d3cab23b.png



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>



image.gif



下面我们优化一下,让圆看起来更圆,然后这个几个图形的颜色渲染的不一样。

圆可以通过加大参数 widthSegments 处理


f2f4343b13f14757bdbb7a91c5502c62.png


颜色问题我们可以通过 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>

dc5648d8375143cea00e6277b6cd204a.gif








目录
相关文章
|
6月前
三维手部关键点
三维手部关键点
|
3月前
|
API 算法框架/工具
【threejs教程】三维物体与三维向量
【8月更文挑战第7天】threejs教程:三维物体与三维向量
72 3
【threejs教程】三维物体与三维向量
|
6月前
THREE实战2_正交投影相机与透视相机
THREE实战2_正交投影相机与透视相机
83 1
|
机器学习/深度学习 人工智能 算法
LabVIEW深度相机与三维定位实战(三)
立体匹配与ACV算法理论知识
138 0
LabVIEW深度相机与三维定位实战(三)
|
6月前
|
文字识别 Python
Halcon 学习笔记五:几何定位+仿射变换+测量
Halcon 学习笔记五:几何定位+仿射变换+测量
610 0
|
机器学习/深度学习 人工智能 算法
LabVIEW深度相机与三维定位实战(四)
立体匹配与ACV算法在LabVIEW中的调用
111 0
LabVIEW深度相机与三维定位实战(四)
|
机器学习/深度学习 传感器 编解码
LabVIEW实现深度相机与三维定位实战(一)
LabVIEW实现深度相机与三维定位实战(一)
287 0
LabVIEW实现深度相机与三维定位实战(一)
|
机器学习/深度学习 传感器 编解码
LabVIEW实现深度相机与三维定位实战(二)
LabVIEW实现深度相机与三维定位实战(二)
227 0
LabVIEW实现深度相机与三维定位实战(二)
|
存储 数据可视化 数据管理
处理RGB-D图像数据以构建室内环境地图并估计相机的轨迹
视觉同步定位和映射 (vSLAM) 是指计算摄像机相对于周围环境的位置和方向,同时映射环境的过程。 您可以使用单眼摄像头执行 vSLAM。但是,深度无法准确计算,估计的轨迹未知,并且随着时间的推移而漂移。要生成无法从第一帧开始三角测量的初始地图,必须使用单眼相机的多个视图。更好、更可靠的解决方案是使用 RGB-D 相机,它由一个 RGB 彩色图像和一个深度图像组成。
189 0
|
数据可视化 C++
【影像配准】配准之棋盘网格图(镶嵌图像)(附有 C++ 代码)
【影像配准】配准之棋盘网格图(镶嵌图像)(附有 C++ 代码)