Visual LabINTERACTIVE LEARNING
OpenGL 与 3D第 2 / 5 章

几何法线与对象封装

从面的法线计算进入 Three.js 对象封装实践。

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

获取面的法线

/**
   * 获取面的法线
   * @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() {}
}