利用多个视角中的技术,我们可以拥有2个视角,对于每个视角,我们会使用不同的相机。然后让第2个视角看向第1个视角,从第2个视角中可以看到正在绘制的物体,而这个物体表示的是第1个视角中的相机。具体实现如下:

创建一个立方体,然后在立方体的末端添加一个圆锥,用于表示相机。我们会使用线段来绘制这个物体。我们会使用 索引 来连接顶点。 相机 看向的是 -Z 方向,所以,让我们把立方体和圆锥放到 +Z 这边,而圆锥的开口方向是 -Z 方向。

# 1.1 创建立方体

绘制的是立方体线框。

// 为一个相机创建几何
function createCameraBufferInfo(gl) {
  // 首先,让我们添加一个立方体。它的范围是 1 到 3,
  // 因为相机看向的是 -Z 方向,所以我们想要相机在 Z = 0 处开始
  const positions = [
    -1, -1,  1,  // 立方体的顶点
     1, -1,  1,
    -1,  1,  1,
     1,  1,  1,
    -1, -1,  3,
     1, -1,  3,
    -1,  1,  3,
     1,  1,  3,
  ];
  const indices = [
    0, 1, 1, 3, 3, 2, 2, 0, // 立方体的索引
    4, 5, 5, 7, 7, 6, 6, 4,
    0, 4, 1, 5, 3, 7, 2, 6,
  ];
  return webglUtils.createBufferInfoFromArrays(gl, {
    position: positions,
    indices,
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 1.2 创建圆锥

// 为一个相机创建几何
function createCameraBufferInfo(gl) {
  // 首先,让我们添加一个立方体。它的范围是 1 到 3,
  // 因为相机看向的是 -Z 方向,所以我们想要相机在 Z = 0 处开始。
  // 我们会把一个圆锥放到该立方体的前面,
  // 且该圆锥的开口方向朝 -Z 方向。
  const positions = [
    -1, -1,  1,  // 立方体的顶点
     1, -1,  1,
    -1,  1,  1,
     1,  1,  1,
    -1, -1,  3,
     1, -1,  3,
    -1,  1,  3,
     1,  1,  3,
     0,  0,  1,  // 圆锥的尖头
  ];
  const indices = [
    0, 1, 1, 3, 3, 2, 2, 0, // 立方体的索引
    4, 5, 5, 7, 7, 6, 6, 4,
    0, 4, 1, 5, 3, 7, 2, 6,
  ];
  // 添加圆锥的片段
  const numSegments = 6;
  const coneBaseIndex = positions.length / 3; 
  const coneTipIndex =  coneBaseIndex - 1;
  for (let i = 0; i < numSegments; ++i) {
    const u = i / numSegments;
    const angle = u * Math.PI * 2;
    const x = Math.cos(angle);
    const y = Math.sin(angle);
    positions.push(x, y, 0);
    // 从圆锥尖头到圆锥边缘的线段
    indices.push(coneTipIndex, coneBaseIndex + i);
    // 从圆锥边缘一点到圆锥边缘下一点的线段
    indices.push(coneBaseIndex + i, coneBaseIndex + (i + 1) % numSegments);
  }
  return webglUtils.createBufferInfoFromArrays(gl, {
    position: positions,
    indices,
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# 1.3 缩放立方体以适配物体

function createCameraBufferInfo(gl, scale = 1) {
  // 首先,让我们添加一个立方体。它的范围是 1 到 3,
  // 因为相机看向的是 -Z 方向,所以我们想要相机在 Z = 0 处开始。
  // 我们会把一个圆锥放到该立方体的前面,
  // 且该圆锥的开口方向朝 -Z 方向。
  const positions = [
    -1, -1,  1,  // 立方体的顶点
     1, -1,  1,
    -1,  1,  1,
     1,  1,  1,
    -1, -1,  3,
     1, -1,  3,
    -1,  1,  3,
     1,  1,  3,
     0,  0,  1,  // 圆锥的尖头
  ];
  const indices = [
    0, 1, 1, 3, 3, 2, 2, 0, // 立方体的索引
    4, 5, 5, 7, 7, 6, 6, 4,
    0, 4, 1, 5, 3, 7, 2, 6,
  ];
  // 添加圆锥的片段
  const numSegments = 6;
  const coneBaseIndex = positions.length / 3; 
  const coneTipIndex =  coneBaseIndex - 1;
  for (let i = 0; i < numSegments; ++i) {
    const u = i / numSegments;
    const angle = u * Math.PI * 2;
    const x = Math.cos(angle);
    const y = Math.sin(angle);
    positions.push(x, y, 0);
    // 从圆锥尖头到圆锥边缘的线段
    indices.push(coneTipIndex, coneBaseIndex + i);
    // 从圆锥边缘一点到圆锥边缘下一点的线段
    indices.push(coneBaseIndex + i, coneBaseIndex + (i + 1) % numSegments);
  }
  positions.forEach((v, ndx) => {
    positions[ndx] *= scale;
  });
  return webglUtils.createBufferInfoFromArrays(gl, {
    position: positions,
    indices,
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

# 1.4 着色器

  • 绘制顶点颜色的着色器
  • 纯色着色器
<script id="solid-color-vertex-shader" type="x-shader/x-vertex">
attribute vec4 a_position;
 
uniform mat4 u_matrix;
 
void main() {
  // 将 position 乘以矩阵
  gl_Position = u_matrix * a_position;
}
</script>
<!-- fragment shader -->
<script id="solid-color-fragment-shader" type="x-shader/x-fragment">
precision mediump float;
 
uniform vec4 u_color;
 
void main() {
  gl_FragColor = u_color;
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 1.5 绘制场景

  • 获取WebGL上下文
const canvas = document.querySelector('#canvas');
const gl = canvas.getContext('webgl');
1
2
  • 创建着色器程序
// 设置 GLSL 程序
// 编译着色器、链接程序、查找 locations
const vertexColorProgramInfo = webglUtils.createProgramInfo(gl, ['vertex-shader-3d', 'fragment-shader-3d']);
const solidColorProgramInfo = webglUtils.createProgramInfo(gl, ['solid-color-vertex-shader', 'solid-color-fragment-shader']);
1
2
3
4
  • 创建缓冲区并用数据填充
// 为一个 3D 的 'F' 创建 buffers 并用数据来填充
const fBufferInfo = primitives.create3DFBufferInfo(gl);
const cameraScale = 20;
// 为一个相机 创建 buffers 并用数据来填充
const cameraBufferInfo = createCameraBufferInfo(gl, cameraScale);
// 为一个裁剪空间 创建 buffers 并用数据来填充
const clipspaceCubeBufferInfo = createClipspaceCubeBufferInfo(gl);
1
2
3
4
5
6
7
  • 绘制左边的 F 模型
const settings = {
    rotation: 150,  // in degrees
    cam1FieldOfView: 60,  // in degrees
    cam1PosX: 0,
    cam1PosY: 0,
    cam1PosZ: -200,// 相机位置
    cam1Near: 30,
    cam1Far: 500,
};
function render() {
    webglUtils.resizeCanvasToDisplaySize(gl.canvas);

    gl.enable(gl.CULL_FACE);
    gl.enable(gl.DEPTH_TEST);
    gl.enable(gl.SCISSOR_TEST);

    // 模型变换矩阵
    let worldMatrix = m4.yRotation(degToRad(settings.rotation));
    worldMatrix = m4.xRotate(worldMatrix, degToRad(settings.rotation));
    // 使 F 围绕着它的原点
    worldMatrix = m4.translate(worldMatrix, -35, -75, -5);

    const { width, height } = gl.canvas;
    // 我们要把视角分成 2 个
    const effectiveWidth = width / 2;
    const aspect = effectiveWidth / height;
    const near = 1;
    const far = 2000;
    
    //--------绘制左边的F模型------
    // 创建透视投影矩阵
    const perspectiveProjectionMatrix =
        m4.perspective(degToRad(settings.cam1FieldOfView),
            aspect,
            settings.cam1Near,
            settings.cam1Far);

    // 使用 look at 计算相机的矩阵
    const cameraPosition = [settings.cam1PosX,settings.cam1PosY,settings.cam1PosZ,];
    const target = [0, 0, 0];
    const up = [0, 1, 0];
    const cameraMatrix = m4.lookAt(cameraPosition, target, up);

    const leftWidth = effectiveWidth;
    //  使用透视相机绘制在左边
    gl.viewport(0, 0, leftWidth, height);
    gl.scissor(0, 0, leftWidth, height);
    gl.clearColor(1, 0.8, 0.8, 1);

    drawScene(perspectiveProjectionMatrix, cameraMatrix, worldMatrix);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
  • 绘制右边的 F 模型
const settings = {
    rotation: 150,  // in degrees
    cam1FieldOfView: 60,  // in degrees
    cam1PosX: 0,
    cam1PosY: 0,
    cam1PosZ: -200,// 相机位置
    cam1Near: 30,
    cam1Far: 500,
};
function render() {
    webglUtils.resizeCanvasToDisplaySize(gl.canvas);

    gl.enable(gl.CULL_FACE);
    gl.enable(gl.DEPTH_TEST);
    gl.enable(gl.SCISSOR_TEST);

    // 模型变换矩阵
    let worldMatrix = m4.yRotation(degToRad(settings.rotation));
    worldMatrix = m4.xRotate(worldMatrix, degToRad(settings.rotation));
    // 使 F 围绕着它的原点
    worldMatrix = m4.translate(worldMatrix, -35, -75, -5);

    const { width, height } = gl.canvas;
    // 我们要把视角分成 2 个
    const effectiveWidth = width / 2;
    const aspect = effectiveWidth / height;
    const near = 1;
    const far = 2000;
    //--------绘制右边的F模型------
    const perspectiveProjectionMatrix2 =
    m4.perspective(degToRad(60), aspect, near, far);

    // 使用 look at 计算相机的矩阵
    const cameraPosition2 = [-600, 400, -400];
    const target2 = [0, 0, 0];
    const cameraMatrix2 = m4.lookAt(cameraPosition2, target2, up);

    // 使用透视相机绘制在右边
    const rightWidth = width - leftWidth;
    gl.viewport(leftWidth, 0, rightWidth, height);
    gl.scissor(leftWidth, 0, rightWidth, height);
    gl.clearColor(0.8, 0.8, 1, 1);

    drawScene(perspectiveProjectionMatrix2, cameraMatrix2, worldMatrix);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

# 1.6 绘制模拟相机的物体

注意

  • 这里使用的视图矩阵是基于右侧场景的相机矩阵,而不是基于左侧场景的相机矩阵。
  • 这里使用的模型矩阵是左侧场景的相机矩阵。那么物体的世界坐标则是相对于左侧场景的相机坐标系而言的。
// 从第 2 个相机矩阵(右侧场景中的相机)中创建一个视图矩阵
const viewMatrix = m4.inverse(cameraMatrix2);

let mat = m4.multiply(perspectiveProjectionMatrix2, viewMatrix);
// 使用第一个相机的矩阵作为表示相机的物体的世界矩阵
// 表示将立方体和锥体使用第一个相机的矩阵进行模型变换
mat = m4.multiply(mat, cameraMatrix);

gl.useProgram(solidColorProgramInfo.program);
// 绘制表示相机的立方体和锥体
drawObjects(gl, mat, solidColorProgramInfo, cameraBufferInfo);
....
// 
function drawObjects(gl, mat, solidColorProgramInfo, cameraBufferInfo) {
    // ------  绘制表示相机的物体 --------
    // 设置所有需要的 attributes
    webglUtils.setBuffersAndAttributes(gl, solidColorProgramInfo, cameraBufferInfo);

    // 设置 uniforms
    webglUtils.setUniforms(solidColorProgramInfo, {
        u_matrix: mat,
        u_color: [0, 0, 0, 1],
    });

    webglUtils.drawBufferInfo(gl, cameraBufferInfo, gl.LINES);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 1.6 绘制视锥体

因为视椎体表示的是将某一空间的坐标转换到裁剪空间的转换,这样我们就可以创建一个表示裁剪空间的立方体,然后使用投影矩阵的逆矩阵把该立方体放置到场景内。

注意: 因为本身我们定义的视锥体的坐标就是裁剪空间坐标了,所以需要使用投影矩阵的逆矩阵,投影矩阵的逆矩阵用于将裁剪空间中的点转换回相机空间。

mat = m4.multiply(mat, m4.inverse(perspectiveProjectionMatrix));
1
// 从第 2 个相机矩阵中创建一个视图矩阵
const viewMatrix = m4.inverse(cameraMatrix2);
let mat = m4.multiply(perspectiveProjectionMatrix2, viewMatrix);
// 使用第一个相机的矩阵作为表示相机的物体的世界矩阵
// 表示将立方体和锥体使用第一个相机的矩阵进行模型变换
mat = m4.multiply(mat, cameraMatrix);

gl.useProgram(solidColorProgramInfo.program);
// m4.inverse(perspectiveProjectionMatrix) 返回的是透视投影矩阵的逆矩阵,
// 投影矩阵的逆矩阵总是会把我们传入的 +1 到 -1 立方体进行适当的扭曲。
mat = m4.multiply(mat, m4.inverse(perspectiveProjectionMatrix));
// 绘制视锥体
drawObjects(gl, mat, solidColorProgramInfo, clipspaceCubeBufferInfo);
1
2
3
4
5
6
7
8
9
10
11
12
13

具体效果如下所示

上面代码为什么使用 perspectiveProjectionMatrix 而不使用 perspectiveProjectionMatrix2?

因为我们的目的是使用第二个相机来可视化第一个相机的视锥体,所以我们才需要将第一个相机的视锥体转换回相机空间。

而且在NDC(标准化设备坐标) 空间中,视锥体的顶点被定义为从[-1, -1, -1] 到 [1, 1, 1] 的立方体。

demo地址 可视化相机 (opens new window)

参考文档

WebGL 可视化相机 (opens new window)

评 论:

Last Updated: 7/4/2024, 3:46:39 PM