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

Posted 凯小默

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了视觉高级篇21 # 如何添加相机,用透视原理对物体进行投影?相关的知识,希望对你有一定的参考价值。

说明

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

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

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

以上是关于视觉高级篇21 # 如何添加相机,用透视原理对物体进行投影?的主要内容,如果未能解决你的问题,请参考以下文章

相机矩阵(Camera Matrix)

新型无镜头相机助力计算机视觉

视觉高级篇23 # 如何模拟光照让3D场景更逼真?(上)

视觉高级篇23 # 如何模拟光照让3D场景更逼真?(上)

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

视觉高级篇24 # 如何模拟光照让3D场景更逼真?(下)