OpenGL 与 3D第 2 / 5 章
几何法线与对象封装
从面的法线计算进入 Three.js 对象封装实践。
获取面的法线
/**
* 获取面的法线
* @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
}
使用面向对象的思想封装
- 创建一个创建scene
- 把需要添加的物体、灯光、等添加进去
- 创建摄像机 camera,添加到场景中
- 视锥体 角度 长宽比 近端面 远端面
- 创建一个渲染器,渲染这个场景,每次改动都需要重新渲染
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() {}
}