Visual LabINTERACTIVE LEARNING
OpenGL 与 3D精校教程

Three.js 实战技巧

汇集法线、UV、Raycaster、BatchedMesh、相机矩阵与曲线路径等高频三维问题。

Three.jsUV相机
专题预计 46 分钟查看源文 ↗

对webgl的api进行封装,倾向于面向对象的方式使用

webgl是使用canvas开启3D

/**
   * 设置物体位置到某一点并使它的面朝向一个向量
   * @param mesh 网格
   * @param position 位置
   * @param Vec3 朝向的向量
   */
  static setMeshFaceVec3(mesh: THREE.Object3D, position: THREE.Vector3, Vec3: THREE.Vector3) {
    mesh.position.copy(position)
    //方法一
    // 然后,计算mesh的朝向。这里我们需要一个辅助向量来表示mesh的正面(通常是Y轴正方向)
    const front = new THREE.Vector3(0, 1, 0) // 默认正面朝向Y轴正方向
    // 接下来,我们需要计算一个辅助向量,它与法向量垂直,并且与mesh的正面(front)也垂直
    const side = new THREE.Vector3().crossVectors(Vec3, front).normalize()
    // 使用辅助向量来构建一个正交基,这样我们可以确保mesh的正面朝向法向量
    const up = new THREE.Vector3().crossVectors(side, Vec3).normalize()
    // 使用lookAt方法,传入目标点和up向量,来设置mesh的朝向
    mesh.lookAt(position.clone().add(up))

    // 方法二、
    const quaternion = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), Vec3)
    // 使用这个四元数来旋转立方体,使其正面朝向目标点
    mesh.quaternion.multiplyQuaternions(quaternion, new THREE.Quaternion())
  }

获取面的法线

/**
   * 获取面的法线
   * @param mesh  目标网格
   * @param face  相交的面-如能获取,raycaster
   * @param faceIndex  面的索引 raycaster
   */
  static getNormal(mesh, face, faceIndex) {
    const geometry = mesh.geometry

    // 获取面索引(indices)和顶点位置(position)的属性
    const indices = geometry.index
    const positions = geometry.attributes.position
    console.log('indices :>> ', indices)

    // 假设你想要计算索引为0的面的法向量
    const a = new THREE.Vector3()
    const b = new THREE.Vector3()
    const c = new THREE.Vector3()
    console.log('faceIndex :>> ', faceIndex)

    // 第一个顶点
    // face.a
    // console.log('indices.getX(3 * faceIndex) :>> ', indices.array[faceIndex * 3])
    // console.log('indices.getX(3 * faceIndex) :>> ', indices.getX(3 * faceIndex))
    // 第二个顶点
    // face.b
    // console.log('indices.getX(3 * faceIndex) :>> ', indices.array[faceIndex * 3 +1 ])
    // console.log('indices.getX(3 * faceIndex) :>> ', indices.getX(3 * faceIndex + 1 ))  //连续顶点可以
    // console.log('indices.getX(3 * faceIndex) :>> ', indices.getY(3 * faceIndex))

    // 第三个顶点
    //1. face.c
    //2. console.log('indices.getX(3 * faceIndex) :>> ', indices.array[faceIndex * 3 +2])
    //3. console.log('indices.getX(3 * faceIndex) :>> ', indices.getX(3 * faceIndex + 2 ))  //连续顶点可以
    //4. console.log('indices.getX(3 * faceIndex) :>> ', indices.getZ(3 * faceIndex))

    // 获取面的三个顶点的位置
    a.fromBufferAttribute(positions, face.a)
    b.fromBufferAttribute(positions, face.b)
    c.fromBufferAttribute(positions, face.c)
    console.log('a :>> ', a)
    console.log('b :>> ', b)
    console.log('c :>> ', c)

    // 计算面的法向量
    const faceNormal = new THREE.Vector3()
    faceNormal.crossVectors(b.sub(a), c.sub(a))

    // 归一化法向量
    faceNormal.normalize()

    return faceNormal
  }

使用面向对象的思想封装

  1. 创建一个创建scene
  2. 把需要添加的物体、灯光、等添加进去
  3. 创建摄像机 camera,添加到场景中
  • 视锥体 角度 长宽比 近端面 远端面
  1. 创建一个渲染器,渲染这个场景,每次改动都需要重新渲染
import type { Curve, Group, Scene } from 'three'
import {
  BufferGeometry,
  Vector3,
  CurvePath,
  BufferAttribute,
  CatmullRomCurve3,
  Line,
  LineBasicMaterial,
  QuadraticBezierCurve3,
  MeshStandardMaterial,
  Mesh,
  BoxGeometry,
} from 'three'
import { MAX_POINTS, THREE_MODEL_TYPE_CURVE, ARC_SEGMENTS } from '@/constant/index'
import { genUUID } from '@/utils'

interface CurveObejct {
  mesh: Line
  curve: Curve<Vector3> | CurvePath<Vector3>
  guid: string
  drawVector: Vector3[] //线段端点矢量
  drawPoints: Mesh[] //线段端点对象
  controlPoints: Mesh[] //贝塞尔曲线的控制点
  scenerysObjects: Group[] //配景的对象
  scenerysOrigin: Group[] //配景组
  type: string //应该允许2种类型,样条曲线和贝塞尔曲线
  lineColor: number //线段颜色
  endPointColor: number //端点颜色
  controlPointColor: number //控制点颜色
  linewidth: number //线段宽度
  curveLens: number
  scenerySpace: number
}

/*
曲线对象的设计
1. 包含所有的必要数据,可以通过私有的数据操作曲线
2. 分离操作 - 类方法
3. 公共数据  包含scene 公共的属性
4. 私有数据 线段的信息

操作曲线的过程:
一、生成曲线
1. 生成一曲线
2. 从曲线中获取一些点
3. 根据这些点,去生成一条几何线段,以及线段端点、控制点
4. 从线段中获取均分点坐标,把配景放置在上面
5. 监听页面上的点击事件,如果点击的是控制点,则可进行对应操作
6. 结束绘制曲线,保存曲线对应的所有数据到userData中

二、更新线段
1. 拖拽控制点或者端点,触发曲线更新-->需要补充一些操作时的高亮
2. 根据新的曲线获取点来更新线段端点和控制点
3. 更新配景位置,以及数量(多了隐藏,少了补充)

三、变换曲线类型
1. 保持点不变
2. 根据点坐标,生成指定类型的曲线
3. 从曲线上获取相应的点,生成端点和控制点
4. 放置配景

四、配景的操作
成组操作配景的变换(移动-缩放-旋转)

脱离组,单独自由变换配景 -限制移动路径
脱离组,单独自由变换配景 -无限制

*/

export class DrawCurve {
  static pointSize: number = 400
  static geometry: BoxGeometry = new BoxGeometry(this.pointSize, this.pointSize, this.pointSize)
  static scene: Scene = null //传进来的scene
  static curveObject: CurveObejct = null

  static setScene(scene: Scene) {
    if (!this.scene) {
      this.scene = scene
    }
  }
  static setFromExists(object: CurveObejct) {
    this.curveObject = object
  }

  static getCurveObject(): CurveObejct {
    return this.curveObject
  }

  // 开始绘制一条新的线段
  static new({ type = 'spline', lineColor = 0xff0000, endPointColor = 0xff00ff, controlPointColor = 0xffff00, linewidth = 8, scenerySpace = 4000 }) {
    const curveObject: CurveObejct = {
      curve: null,
      mesh: null,
      controlPointColor,
      controlPoints: [],
      guid: genUUID(),
      drawVector: [],
      drawPoints: [],
      scenerysObjects: [],
      scenerysOrigin: [],
      type,
      lineColor,
      endPointColor,
      linewidth,
      curveLens: 0,
      scenerySpace,
    }
    this.curveObject = curveObject
  }

  //初始化一条新线段
  static initCurve() {
    const geometry = new BufferGeometry()
    geometry.setAttribute('position', new BufferAttribute(new Float32Array(MAX_POINTS * 3), 3)) // 预先设置了线段的点
    if (this.curveObject.type === 'spline') {
      this.curveObject.curve = new CatmullRomCurve3(this.curveObject.drawVector, false, 'catmullrom', 0.5)
    } else {
      this.curveObject.curve = new CurvePath<Vector3>()
    }
    this.curveObject.mesh = new Line(
      geometry.clone(),
      new LineBasicMaterial({
        color: this.curveObject.lineColor,
        linewidth: 4,
      })
    )
    this.curveObject.mesh.castShadow = true
    this.curveObject.mesh.visible = true
    this.scene.add(this.curveObject.mesh) //添加到组内
  }

  // 样条曲线
  static initSpline() {}

  // 贝塞尔曲线
  static addBezierCurve() {
    const lens = this.curveObject.drawVector.length
    if (lens < 2) return
    const startPoint = this.curveObject.drawVector[lens - 2] //起点
    const endPoint = this.curveObject.drawVector[lens - 1] //终点
    const cPoint = new Vector3((startPoint.x + endPoint.x) / 2, (startPoint.y + endPoint.y) / 2, (startPoint.z + endPoint.z) / 2) //控制点
    const object = this.addPointToScene(cPoint, this.curveObject.controlPointColor) //把控制点添加到场景中
    this.curveObject.controlPoints.push(object) //保存控制点对象
    const curve1 = new QuadraticBezierCurve3(startPoint, object.position, endPoint) //创建曲线
    if (this.curveObject.curve instanceof CurvePath) {
      this.curveObject.curve.add(curve1) //添加曲线
    }
  }

  //添加点到场景中
  static addPointToScene(point: Vector3, color = this.curveObject.endPointColor) {
    const material = new MeshStandardMaterial({ color }) //创建一个立方体点
    const object = new Mesh(this.geometry.clone(), material)

    object.position.copy(point) //放置到传入的位置
    object.castShadow = true
    object.receiveShadow = true
    object.userData = {
      type: THREE_MODEL_TYPE_CURVE,
      guid: this.curveObject.guid,
    }
    this.scene.add(object) //把点添加到场景中
    return object
  }

  // 添加点
  static addPoint(pos: Vector3) {
    const object = this.addPointToScene(pos) //创建一个点并添加到场景中
    this.curveObject.drawVector.push(object.position) //保存这个点的位置到数组中
    this.curveObject.drawPoints.push(object) //保存这个点对象到线段的数组中
    // 如果是贝塞尔曲线,还要添加一条线段
  }

  // 删除点
  static delPoint(object: Mesh) {
    this.scene.remove(object)
    const vector3Index = this.curveObject.drawVector.findIndex(obj => obj.equals(object.position))
    const pointIndex = this.curveObject.drawPoints.findIndex(obj => object.uuid == obj.uuid)
    // 删除哪个点
    vector3Index && this.curveObject.drawVector.splice(vector3Index, 1)
    pointIndex && this.curveObject.drawPoints.splice(pointIndex, 1)
  }

  // 更新曲线
  static updateCurve() {
    const spline = this.curveObject.curve
    const position = this.curveObject.mesh.geometry.attributes.position
    let point = new Vector3()
    for (let i = 0; i < ARC_SEGMENTS; i++) {
      const t = i / (ARC_SEGMENTS - 1)
      spline.getPoint(t, point)
      position.setXYZ(i, point.x, point.y + this.curveObject.linewidth * 2, point.z)
    }
    spline.updateArcLengths()
    this.curveObject.curveLens = spline.getLength()
    position.needsUpdate = true
    point = null
  }

  //画线流程
  static draw(pos: Vector3) {
    this.addPoint(pos)
    const lens = this.curveObject.drawVector.length

    if (lens === 2) {
      // 有2个点初始化线条
      this.initCurve()
    }

    if (lens > 1 && this.curveObject.curve) {
      // 新增的点
      this.updateCurve() //更新曲线的mesh
      this.updateScenerys() //更新配景

      //贝塞尔曲线
      if (this.curveObject.type == 'bezier') {
        this.addBezierCurve()
      }
    }
  }

  //结束画线
  static drawEnd() {
    const userData = {
      [this.curveObject.guid]: this.curveObject,
    }
    this.curveObject = null
    return userData
  }

  // 更新配景
  static updateScenerys() {
    if (this.curveObject.scenerysOrigin.length === 0) return
    const divisions = Math.floor(this.curveObject.curveLens / this.curveObject.scenerySpace)
    const positions = this.curveObject.curve.getSpacedPoints(divisions)
    positions.forEach((pos, index) => {
      // 原来的列表中就有配景
      if (this.curveObject.scenerysObjects[index]) {
        const object = this.curveObject.scenerysObjects[index]
        object.position.set(pos.x, pos.y, pos.z)
        object.visible = true
      } else {
        // 原来的配景不够,需要新增配景,按顺序取配景组中的配景对象
        const oIndex = index % this.curveObject.scenerysOrigin.length
        const object = this.curveObject.scenerysOrigin[oIndex].clone()
        object.position.set(pos.x, pos.y, pos.z)
        this.scene.add(object)
        this.curveObject.scenerysObjects.push(object)
      }
    })
    // 原有的配景超过了现需要的配景,隐藏配景,在编辑结束的时候去删除它
    const extLen = this.curveObject.scenerysObjects.length - positions.length
    if (extLen > 0) {
      for (let i = 0; i < extLen; i++) {
        this.curveObject.scenerysObjects[positions.length + i].visible = false
      }
    }
  }

  // 添加一个配景
  static addScenery(object: Group) {
    this.curveObject.scenerysOrigin.push(object)
  }
  // 切换曲线的类型
  static switchCurve() {}
}

变换控制器无法拖动的原因

  • 模型尺寸过大,导致拖动的距离不明显,肉眼看不出变化,常见隐秘的bug

Raycaster

raycaster射线与mesh交点的法线是通过插值得来,是mesh本地空间法线,如果要判断方向,需要转换到世界空间,或者把世界空间的方向转换到mesh的本地空间

batchedMesh

batchedMesh是如何实现使用同一个material,但是每个物体单独应用一个矩阵的

  1. 使用一个dataTexture存储矩阵数据

  1. 在WebGLRenderer中把纹理设置到uniform中

  1. 在顶点着色器中去获取纹理,并定义一个解析矩阵的函数

  1. 在顶点着色器中去使用 (threejs r165)

r166 已修改为内置的变量 gl_DrawID

两个对比的ppt

https://on-demand.gputechconf.com/gtc/2013/presentations/S3032-Advanced-Scenegraph-Rendering-Pipeline.pdf
https://on-demand.gputechconf.com/siggraph/2014/presentation/SG4117-OpenGL-Scene-Rendering-Techniques.pdf

标准化UV

  • 原理简述;假设是一个规则的图形,那么去找到uv的四个顶点,然后以uv范围,把全部uv坐标初始化到0~1范围内,然后找到uv坐标最值对应的顶点,用顶点的坐标来计算图形的边长
function standardizeUVs(geometry: THREE.BufferGeometry) {
  if (!geometry.attributes.uv || !geometry.attributes.position) {
    console.error('Geometry does not have UV or position attributes');
    return null;
  }

  const uvs = geometry.attributes.uv;
  const positions = geometry.attributes.position;
  const uvArray = uvs.array;

  // Find min and max UV coordinates
  let minU = Infinity, minV = Infinity;
  let maxU = -Infinity, maxV = -Infinity;
  let minUIndex = -1, minVIndex = -1, maxUIndex = -1, maxVIndex = -1;

  for (let i = 0; i < uvArray.length; i += 2) {
    const u = uvArray[i];
    const v = uvArray[i + 1];

    if (u < minU) {
      minU = u;
      minUIndex = i / 2;
    }
    if (v < minV) {
      minV = v;
      minVIndex = i / 2;
    }
    if (u > maxU) {
      maxU = u;
      maxUIndex = i / 2;
    }
    if (v > maxV) {
      maxV = v;
      maxVIndex = i / 2;
    }
  }
  // Calculate range
  const rangeU = maxU - minU;
  const rangeV = maxV - minV;

  // Normalize UV coordinates
  for (let i = 0; i < uvArray.length; i += 2) {
    uvArray[i] = (uvArray[i] - minU) / rangeU;
    uvArray[i + 1] = (uvArray[i + 1] - minV) / rangeV;
  }

  uvs.needsUpdate = true;

  // Get vertex positions for the extreme points
  const minUPos = new THREE.Vector3().fromBufferAttribute(positions, minUIndex);
  const maxUPos = new THREE.Vector3().fromBufferAttribute(positions, maxUIndex);
  const minVPos = new THREE.Vector3().fromBufferAttribute(positions, minVIndex);
  const maxVPos = new THREE.Vector3().fromBufferAttribute(positions, maxVIndex);

  // Calculate max edge lengths in XY plane
  const xEdgeLength = Math.sqrt(
    Math.pow(maxUPos.x - minUPos.x, 2) +
    Math.pow(maxUPos.y - minUPos.y, 2)
  );
  const yEdgeLength = Math.sqrt(
    Math.pow(maxVPos.x - minVPos.x, 2) +
    Math.pow(maxVPos.y - minVPos.y, 2)
  );

  return {
    maxEdges: {
      x: xEdgeLength,
      y: yEdgeLength
    }
  };
}

贝塞尔曲线路径绘制

使用贝塞尔曲线绘制有宽度的路面

 import * as THREE from 'three';
        import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
        import { TransformControls } from 'three/addons/controls/TransformControls.js';

    let scene, camera, renderer, orbitControls;
    let curvePath = new THREE.CurvePath();
    let roadMesh;
    let pointsGroup = new THREE.Group();
    let transformControl;
    
    const pointArr = []
    const cubeArr =[]
    const controlArr= []
    const ROAD_WIDTH = 5;
    const SMOOTHNESS = 20;
    const POINT_SIZE = 0.5;
    const CONTROL_POINT_OFFSET = 0.4; 

    function init() {
        scene = new THREE.Scene();
        camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
        renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);

        camera.position.set(0, 50, 50);
        camera.lookAt(0, 0, 0);

        orbitControls = new OrbitControls(camera, renderer.domElement);

        transformControl = new TransformControls(camera, renderer.domElement);
        transformControl.addEventListener('dragging-changed', function (event) {
            orbitControls.enabled = !event.value;
        });
        transformControl.addEventListener('objectChange', updateRoadFromPoints);
        scene.add(transformControl);

        const gridHelper = new THREE.GridHelper(100, 100);
        scene.add(gridHelper);

        scene.add(pointsGroup);

        renderer.domElement.addEventListener('click', onMouseClick, false);

        animate();
    }

    function onMouseClick(event) {
        if (transformControl.object) return; // Don't add new points if we're moving one

        const mouse = new THREE.Vector2();
        mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
        mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

        const raycaster = new THREE.Raycaster();
        raycaster.setFromCamera(mouse, camera);

        const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
        const intersectionPoint = new THREE.Vector3();
        raycaster.ray.intersectPlane(plane, intersectionPoint);

        addPointToRoad(intersectionPoint);
    }

    function addPointToRoad(point) {
      pointArr.push(point) // 添加点
      const cube = createPointCube(point);
      pointsGroup.add(cube);
      if(pointArr.length > 1){
        const lastPoint = pointArr[pointArr.length-2]
        const midPoint = new THREE.Vector3().addVectors(lastPoint, point).multiplyScalar(0.5);
        curvePath.add(new THREE.QuadraticBezierCurve3(lastPoint, midPoint, point));
        const newControlCube = createPointCube(midPoint, 0x00ff00);
        controlArr.push(newControlCube)
        pointsGroup.add(newControlCube);
      
        if (curvePath.curves.length > 1) {
          // 计算上一条曲线的控制点的偏移
          const lastCurve = curvePath.curves[curvePath.curves.length - 2];
          const tangent = lastCurve.getTangent(0.5)
          const normal = new THREE.Vector3().crossVectors( new THREE.Vector3(0,1,0),tangent ).normalize()

          // Calculate the direction and normal
          const direction = new THREE.Vector3().subVectors(point, lastPoint).normalize()
          const dot = direction.dot(normal)
          if(dot > 0){
            normal.negate()
          }
  
          // const normal = new THREE.Vector3(direction.z, 0, direction.x);
  
          // Calculate control points
          
          const distance = lastPoint.distanceTo(point);
          const offset = normal.multiplyScalar(distance * CONTROL_POINT_OFFSET);
          const lastControlPoint = new THREE.Vector3().addVectors(lastCurve.v1, offset);
          // Adjust the last curve's control point
          if (lastCurve instanceof THREE.QuadraticBezierCurve3) {
              lastCurve.v1.copy(lastControlPoint);
              updateControlPointVisual(curvePath.curves.length - 2);
          } 
        }
      }

    updateRoadGeometry();
}
    
    function createPointCube(position, color = 0xff0000) {
        const geometry = new THREE.BoxGeometry(POINT_SIZE, POINT_SIZE, POINT_SIZE);
        const material = new THREE.MeshBasicMaterial({ color: color });
        const cube = new THREE.Mesh(geometry, material);
        cube.position.copy(position);
        
        cube.userData.isControlPoint = color === 0x00ff00;

        cube.addEventListener('click', function(event) {
            event.stopPropagation();
            transformControl.attach(this);
            scene.add(transformControl);
        });

        return cube;
    }

    function updateControlPointVisual(curveIndex) {
    const curve = curvePath.curves[curveIndex];
    if (curve instanceof THREE.QuadraticBezierCurve3) {
        const controlPoint = controlArr[curveIndex]
        if (controlPoint && controlPoint.userData.isControlPoint) {
            controlPoint.position.copy(curve.v1);
        }
    }
}

    function updateRoadFromPoints() {
        curvePath = new THREE.CurvePath();
        const points = pointsGroup.children;

        for (let i = 0; i < points.length - 1; i += 2) {
            const startPoint = points[i].position;
            const endPoint = points[i + 2] ? points[i + 2].position : startPoint;
            const controlPoint = points[i + 1].position;

            curvePath.add(new THREE.QuadraticBezierCurve3(startPoint, controlPoint, endPoint));
        }

        updateRoadGeometry();
    }

    function updateRoadGeometry() {
        if (roadMesh) {
            scene.remove(roadMesh);
        }

        const roadGeometry = new THREE.BufferGeometry();
        const positions = [];
        const normals = [];
        const uvs = [];

        const points = curvePath.getPoints(SMOOTHNESS * curvePath.curves.length);

        for (let i = 0; i < points.length; i++) {
            const current = points[i];
            const next = points[Math.min(i + 1, points.length - 1)];

            const tangent = new THREE.Vector3().subVectors(next, current).normalize();
            tangent.y = 0;

            const normal = new THREE.Vector3(-tangent.z, 0, tangent.x).normalize();

            const leftEdge = new THREE.Vector3().addVectors(current, normal.clone().multiplyScalar(ROAD_WIDTH / 2));
            const rightEdge = new THREE.Vector3().addVectors(current, normal.clone().multiplyScalar(-ROAD_WIDTH / 2));

            positions.push(leftEdge.x, leftEdge.y, leftEdge.z);
            positions.push(rightEdge.x, rightEdge.y, rightEdge.z);

            normals.push(0, 1, 0, 0, 1, 0);

            uvs.push(0, i / (points.length - 1));
            uvs.push(1, i / (points.length - 1));
        }

        const indices = [];
        for (let i = 0; i < points.length - 1; i++) {
            const baseIndex = i * 2;
            indices.push(baseIndex, baseIndex + 2, baseIndex + 1);
            indices.push(baseIndex + 2, baseIndex + 3, baseIndex + 1);
        }

        roadGeometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
        roadGeometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3));
        roadGeometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
        roadGeometry.setIndex(indices);

        const roadMaterial = new THREE.MeshBasicMaterial({ color: 0x808080 });
        roadMesh = new THREE.Mesh(roadGeometry, roadMaterial);
        scene.add(roadMesh);
    }

    function animate() {
        requestAnimationFrame(animate);
        renderer.render(scene, camera);
    }

    init();

为什么相机的matrixWorldInverse 就是视图矩阵 view matrix?

因为相机(或者任何物体的)的matrixWorld都是把本地空间的坐标转换到世界空间,其逆矩阵就是把世界空间的坐标转换到本地空间中去,所以具体到相机, matrixWorldInverse就是把世界空间的坐标转换到相机空间中去,那就是视图矩阵