6 changed files with 526 additions and 58 deletions
@ -1,30 +1,177 @@ |
|||||
import * as THREE from 'three' |
import * as THREE from 'three' |
||||
import type IControls from '@/core/controls/IControls.ts' |
import type IControls from '@/core/controls/IControls.ts' |
||||
import type Viewport from '@/core/engine/Viewport.ts' |
import type Viewport from '@/core/engine/Viewport.ts' |
||||
|
import { Vector3 } from 'three/src/math/Vector3' |
||||
|
import { Euler } from 'three/src/math/Euler' |
||||
|
|
||||
export default class InstancePointManager implements IControls { |
export default class InstancePointManager { |
||||
viewport: Viewport |
private readonly viewport: Viewport |
||||
mesh: THREE.InstancedMesh |
private readonly instancedMesh: THREE.InstancedMesh |
||||
dummy: THREE.Object3D = new THREE.Object3D() |
private readonly freeIndices: number[] = [] |
||||
count: number = 0 |
private readonly maxInstanceCount: number |
||||
maxInstances: number = 100000 |
private readonly geometry: THREE.BufferGeometry |
||||
|
private readonly material: THREE.Material |
||||
|
private readonly dummy: THREE.Object3D = new THREE.Object3D() |
||||
|
// itemId -> instanceId
|
||||
|
private instanceData: Map<string, number> = new Map() |
||||
|
|
||||
init(viewport: Viewport): void { |
/** |
||||
|
* 创建点实例 |
||||
|
* @param item 点数据 |
||||
|
* @returns 分配的实例ID (如果失败返回-1) |
||||
|
*/ |
||||
|
createPoint(item: ItemJson): number { |
||||
|
if (this.freeIndices.length === 0) { |
||||
|
system.showErrorDialog('InstancePointManager is full') |
||||
|
return -1 |
||||
|
} |
||||
|
|
||||
|
const instanceId = this.freeIndices.pop()! |
||||
|
this.instanceData.set(item.id, instanceId) |
||||
|
|
||||
|
this.updatePoint(item) |
||||
|
return instanceId |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 更新点实例 |
||||
|
* @param item 点数据 |
||||
|
* @param option 更新选项 |
||||
|
*/ |
||||
|
updatePoint(item: ItemJson, option: { |
||||
|
position?: Vector3 |
||||
|
rotation?: Vector3 |
||||
|
scale?: Vector3 |
||||
|
} = {}): void { |
||||
|
const instanceId = this.instanceData.get(item.id) |
||||
|
if (instanceId === undefined) return |
||||
|
|
||||
|
let [position, rotation, scale] = item.tf |
||||
|
if (option.position) { |
||||
|
position = option.position.toArray() |
||||
|
} |
||||
|
if (option.rotation) { |
||||
|
rotation = option.rotation.toArray() |
||||
|
} |
||||
|
if (option.scale) { |
||||
|
scale = option.scale.toArray() |
||||
|
} |
||||
|
|
||||
|
this.dummy.position.set(position[0], position[1], position[2]) |
||||
|
this.dummy.rotation.set( |
||||
|
THREE.MathUtils.degToRad(rotation[0]), |
||||
|
THREE.MathUtils.degToRad(rotation[1]), |
||||
|
THREE.MathUtils.degToRad(rotation[2]) |
||||
|
) |
||||
|
this.dummy.scale.set(scale[0], scale[1], scale[2]) |
||||
|
|
||||
|
if (item.v === false) { |
||||
|
this.dummy.scale.set(0, 0, 0) |
||||
|
} |
||||
|
this.dummy.updateMatrix() |
||||
|
|
||||
|
this.instancedMesh.setMatrixAt(instanceId, this.dummy.matrix) |
||||
|
this.instancedMesh.instanceMatrix.needsUpdate = true |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 删除点实例 |
||||
|
* @param id 点ID |
||||
|
*/ |
||||
|
deletePoint(id: string): void { |
||||
|
const instanceId = this.instanceData.get(id) |
||||
|
if (instanceId === undefined) return |
||||
|
|
||||
|
// 隐藏实例
|
||||
|
this.dummy.scale.set(0, 0, 0) |
||||
|
this.dummy.updateMatrix() |
||||
|
this.instancedMesh.setMatrixAt(instanceId, this.dummy.matrix) |
||||
|
this.instancedMesh.instanceMatrix.needsUpdate = true |
||||
|
|
||||
|
// 回收索引
|
||||
|
this.freeIndices.push(instanceId) |
||||
|
this.instanceData.delete(id) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取点实例的世界位置 |
||||
|
* @param id 点ID |
||||
|
* @param target 目标向量 |
||||
|
*/ |
||||
|
getWorldPosition(id: string, target: THREE.Vector3): void { |
||||
|
const instanceId = this.instanceData.get(id) |
||||
|
if (instanceId === undefined) return |
||||
|
|
||||
|
const matrix = new THREE.Matrix4() |
||||
|
this.instancedMesh.getMatrixAt(instanceId, matrix) |
||||
|
target.setFromMatrixPosition(matrix) |
||||
|
} |
||||
|
|
||||
|
// init(viewport: Viewport): void {
|
||||
|
// this.viewport = viewport
|
||||
|
//
|
||||
|
// const geometry = new THREE.PlaneGeometry(0.25, 0.25)
|
||||
|
// const material = new THREE.MeshBasicMaterial({
|
||||
|
// color: 0xFFFF99,
|
||||
|
// transparent: true,
|
||||
|
// depthWrite: false,
|
||||
|
// side: THREE.DoubleSide
|
||||
|
// })
|
||||
|
//
|
||||
|
// this.mesh = new THREE.InstancedMesh(geometry, material, this.maxInstances)
|
||||
|
// this.mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage)
|
||||
|
// viewport.scene.add(this.mesh)
|
||||
|
// }
|
||||
|
|
||||
|
|
||||
|
public static create( |
||||
|
viewport: Viewport, |
||||
|
geometry: THREE.BufferGeometry, |
||||
|
material: THREE.Material, |
||||
|
maxInstances: number = 1000 |
||||
|
): InstancePointManager { |
||||
|
return new InstancePointManager(viewport, geometry, material, maxInstances) |
||||
|
} |
||||
|
|
||||
|
constructor( |
||||
|
viewport: Viewport, |
||||
|
geometry: THREE.BufferGeometry, |
||||
|
material: THREE.Material, |
||||
|
maxInstances: number = 1000 |
||||
|
) { |
||||
this.viewport = viewport |
this.viewport = viewport |
||||
|
this.geometry = geometry |
||||
|
this.material = material |
||||
|
this.maxInstanceCount = maxInstances |
||||
|
|
||||
const geometry = new THREE.PlaneGeometry(0.25, 0.25) |
this.instancedMesh = new THREE.InstancedMesh(geometry, material, maxInstances) |
||||
const material = new THREE.MeshBasicMaterial({ |
this.instancedMesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage) |
||||
color: 0xFFFF99, |
viewport.scene.add(this.instancedMesh) |
||||
transparent: true, |
|
||||
depthWrite: false, |
this.dummy.scale.set(0, 0, 0) |
||||
side: THREE.DoubleSide |
for (let i = 0; i < maxInstances; i++) { |
||||
}) |
this.dummy.updateMatrix() |
||||
|
this.instancedMesh.setMatrixAt(i, this.dummy.matrix) |
||||
|
this.freeIndices.push(i) |
||||
|
} |
||||
|
|
||||
this.mesh = new THREE.InstancedMesh(geometry, material, this.maxInstances) |
this.instancedMesh.instanceMatrix.needsUpdate = true |
||||
this.mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage) |
|
||||
viewport.scene.add(this.mesh) |
|
||||
} |
} |
||||
|
|
||||
dispose() { |
dispose() { |
||||
|
this.viewport.scene.remove(this.instancedMesh) |
||||
|
this.instancedMesh.geometry.dispose() |
||||
|
|
||||
|
if (this.instancedMesh.material) { |
||||
|
if (Array.isArray(this.instancedMesh.material)) { |
||||
|
this.instancedMesh.material.forEach(mat => mat.dispose()) |
||||
|
} else { |
||||
|
this.instancedMesh.material.dispose() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
this.instancedMesh.dispose() |
||||
|
this.instanceData.clear() |
||||
|
this.freeIndices.length = 0 |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -1,16 +1,269 @@ |
|||||
import * as THREE from 'three' |
import * as THREE from 'three' |
||||
import type IControls from '@/core/controls/IControls.ts' |
|
||||
import type Viewport from '@/core/engine/Viewport.ts' |
import type Viewport from '@/core/engine/Viewport.ts' |
||||
|
import { LineSegmentsGeometry } from 'three/examples/jsm/lines/LineSegmentsGeometry' |
||||
|
import { LineSegments2 } from 'three/examples/jsm/lines/LineSegments2' |
||||
|
import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial' |
||||
|
|
||||
export default class LineSegmentManager implements IControls { |
/** |
||||
viewport: Viewport |
* 线段管理器 |
||||
|
*/ |
||||
|
export default class LineSegmentManager { |
||||
|
private readonly viewport: Viewport |
||||
|
private readonly lineGeometry: LineSegmentsGeometry |
||||
|
private readonly lineMaterial: LineMaterial |
||||
|
private readonly lineSegments: LineSegments2 |
||||
|
private readonly segments: Map<string, LineSegmentData> = new Map() |
||||
|
public needsUpdate: boolean = false |
||||
|
private colorArray: Float32Array | null = null |
||||
|
private positionArray: Float32Array | null = null |
||||
|
|
||||
init(viewport: Viewport): void { |
/** |
||||
|
* 创建或更新线段 |
||||
|
* @param key 线段唯一标识 |
||||
|
* @param start 起点 |
||||
|
* @param end 终点 |
||||
|
* @param color 线段颜色 (可选) |
||||
|
* @param userData 自定义数据 (可选) |
||||
|
*/ |
||||
|
createLine(key: string, start: THREE.Vector3Like, end: THREE.Vector3Like, color?: THREE.Color | number | string, userData?: any): void { |
||||
|
const startVec = start instanceof THREE.Vector3 |
||||
|
? start.clone() |
||||
|
: new THREE.Vector3(start[0], start[1], start[2]) |
||||
|
|
||||
|
const endVec = end instanceof THREE.Vector3 |
||||
|
? end.clone() |
||||
|
: new THREE.Vector3(end[0], end[1], end[2]) |
||||
|
|
||||
|
let colorObj: THREE.Color | undefined |
||||
|
if (color instanceof THREE.Color) { |
||||
|
colorObj = color |
||||
|
} else if (typeof color === 'number') { |
||||
|
colorObj = new THREE.Color(color) |
||||
|
} else if (typeof color === 'string') { |
||||
|
colorObj = new THREE.Color(color) |
||||
|
} |
||||
|
|
||||
|
if (this.segments.has(key)) { |
||||
|
// 更新现有线段
|
||||
|
const segment = this.segments.get(key)! |
||||
|
segment.start.copy(startVec) |
||||
|
segment.end.copy(endVec) |
||||
|
if (colorObj) segment.color = colorObj |
||||
|
if (userData) segment.userData = userData |
||||
|
} else { |
||||
|
// 创建新线段
|
||||
|
this.segments.set(key, { |
||||
|
key, |
||||
|
start: startVec, |
||||
|
end: endVec, |
||||
|
color: colorObj, |
||||
|
visible: true, |
||||
|
userData |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
this.needsUpdate = true |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 更新线段位置 |
||||
|
* @param key 线段唯一标识 |
||||
|
* @param start 新起点 |
||||
|
* @param end 新终点 |
||||
|
*/ |
||||
|
updateLinePosition(key: string, start: THREE.Vector3Like, end: THREE.Vector3Like): void { |
||||
|
const segment = this.segments.get(key) |
||||
|
if (!segment) return |
||||
|
|
||||
|
if (start instanceof THREE.Vector3) { |
||||
|
segment.start.copy(start) |
||||
|
} else { |
||||
|
segment.start.set(start[0], start[1], start[2]) |
||||
|
} |
||||
|
|
||||
|
if (end instanceof THREE.Vector3) { |
||||
|
segment.end.copy(end) |
||||
|
} else { |
||||
|
segment.end.set(end[0], end[1], end[2]) |
||||
|
} |
||||
|
|
||||
|
this.needsUpdate = true |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 更新线段颜色 |
||||
|
* @param key 线段唯一标识 |
||||
|
* @param color 新颜色 |
||||
|
*/ |
||||
|
updateLineColor(key: string, color: THREE.Color | number | string): void { |
||||
|
const segment = this.segments.get(key) |
||||
|
if (!segment) return |
||||
|
|
||||
|
if (color instanceof THREE.Color) { |
||||
|
segment.color = color |
||||
|
} else if (typeof color === 'number') { |
||||
|
segment.color = new THREE.Color(color) |
||||
|
} else if (typeof color === 'string') { |
||||
|
segment.color = new THREE.Color(color) |
||||
|
} else { |
||||
|
segment.color = undefined |
||||
|
} |
||||
|
|
||||
|
this.needsUpdate = true |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 设置线段可见性 |
||||
|
* @param key 线段唯一标识 |
||||
|
* @param visible 是否可见 |
||||
|
*/ |
||||
|
setLineVisible(key: string, visible: boolean): void { |
||||
|
const segment = this.segments.get(key) |
||||
|
if (segment && segment.visible !== visible) { |
||||
|
segment.visible = visible |
||||
|
this.needsUpdate = true |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 删除线段 |
||||
|
* @param key 线段唯一标识 |
||||
|
*/ |
||||
|
deleteLine(key: string): void { |
||||
|
if (this.segments.delete(key)) { |
||||
|
this.needsUpdate = true |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取线段数据 |
||||
|
* @param key 线段唯一标识 |
||||
|
*/ |
||||
|
getLineData(key: string): LineSegmentData | undefined { |
||||
|
return this.segments.get(key) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 更新几何体数据 (应在渲染循环中调用) |
||||
|
*/ |
||||
|
updateGeometry(): void { |
||||
|
if (!this.needsUpdate) return |
||||
|
|
||||
|
const segmentCount = this.segments.size |
||||
|
const positionCount = segmentCount * 6 // 每条线段2个点,每个点3个分量
|
||||
|
const colorCount = segmentCount * 6 // 每个顶点3个颜色分量
|
||||
|
|
||||
|
// 初始化或调整数组大小
|
||||
|
if (!this.positionArray || this.positionArray.length !== positionCount) { |
||||
|
this.positionArray = new Float32Array(positionCount) |
||||
|
} |
||||
|
|
||||
|
if (!this.colorArray || this.colorArray.length !== colorCount) { |
||||
|
this.colorArray = new Float32Array(colorCount) |
||||
|
} |
||||
|
|
||||
|
// 填充数据
|
||||
|
let positionIndex = 0 |
||||
|
let colorIndex = 0 |
||||
|
|
||||
|
const defaultColor = new THREE.Color(this.lineMaterial.color) |
||||
|
|
||||
|
this.segments.forEach(segment => { |
||||
|
if (!segment.visible) { |
||||
|
// 对于不可见的线段,跳过位置设置但保留颜色为黑色
|
||||
|
positionIndex += 6 |
||||
|
|
||||
|
// 设置颜色为黑色(透明)
|
||||
|
this.colorArray![colorIndex++] = 0 |
||||
|
this.colorArray![colorIndex++] = 0 |
||||
|
this.colorArray![colorIndex++] = 0 |
||||
|
|
||||
|
this.colorArray![colorIndex++] = 0 |
||||
|
this.colorArray![colorIndex++] = 0 |
||||
|
this.colorArray![colorIndex++] = 0 |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// 设置起点位置
|
||||
|
this.positionArray![positionIndex++] = segment.start.x |
||||
|
this.positionArray![positionIndex++] = segment.start.y |
||||
|
this.positionArray![positionIndex++] = segment.start.z |
||||
|
|
||||
|
// 设置终点位置
|
||||
|
this.positionArray![positionIndex++] = segment.end.x |
||||
|
this.positionArray![positionIndex++] = segment.end.y |
||||
|
this.positionArray![positionIndex++] = segment.end.z |
||||
|
|
||||
|
// 设置起点颜色
|
||||
|
const startColor = segment.color || defaultColor |
||||
|
this.colorArray![colorIndex++] = startColor.r |
||||
|
this.colorArray![colorIndex++] = startColor.g |
||||
|
this.colorArray![colorIndex++] = startColor.b |
||||
|
|
||||
|
// 设置终点颜色
|
||||
|
const endColor = segment.color || defaultColor |
||||
|
this.colorArray![colorIndex++] = endColor.r |
||||
|
this.colorArray![colorIndex++] = endColor.g |
||||
|
this.colorArray![colorIndex++] = endColor.b |
||||
|
}) |
||||
|
|
||||
|
// 更新几何体
|
||||
|
this.lineGeometry.setPositions(this.positionArray) |
||||
|
this.lineGeometry.setColors(this.colorArray) |
||||
|
|
||||
|
// 设置实例计数为可见线段数量
|
||||
|
const visibleCount = Array.from(this.segments.values()).filter(s => s.visible).length |
||||
|
this.lineGeometry.instanceCount = visibleCount |
||||
|
|
||||
|
this.needsUpdate = false |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 创建线段管理器实例 |
||||
|
*/ |
||||
|
public static create(viewport: Viewport, lineMaterial: LineMaterial): LineSegmentManager { |
||||
|
return new LineSegmentManager(viewport, lineMaterial) |
||||
|
} |
||||
|
|
||||
|
private constructor(viewport: Viewport, lineMaterial: LineMaterial) { |
||||
this.viewport = viewport |
this.viewport = viewport |
||||
|
this.lineMaterial = lineMaterial |
||||
|
|
||||
|
this.lineGeometry = new LineSegmentsGeometry() |
||||
|
|
||||
|
// 创建线段的渲染对象
|
||||
|
this.lineSegments = new LineSegments2(this.lineGeometry, this.lineMaterial) |
||||
|
this.lineSegments.name = 'batch_line_segments' |
||||
|
this.lineSegments.frustumCulled = false |
||||
|
this.viewport.scene.add(this.lineSegments) |
||||
} |
} |
||||
|
|
||||
dispose(): void { |
dispose() { |
||||
// 清理资源
|
this.viewport.scene.remove(this.lineSegments) |
||||
this.viewport = undefined |
this.lineGeometry.dispose() |
||||
|
|
||||
|
if (this.lineMaterial) { |
||||
|
if (Array.isArray(this.lineMaterial)) { |
||||
|
this.lineMaterial.forEach(mat => mat.dispose()) |
||||
|
} else { |
||||
|
this.lineMaterial.dispose() |
||||
} |
} |
||||
} |
} |
||||
|
|
||||
|
this.segments.clear() |
||||
|
this.positionArray = null |
||||
|
this.colorArray = null |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 线段数据接口 |
||||
|
*/ |
||||
|
interface LineSegmentData { |
||||
|
key: string; |
||||
|
start: THREE.Vector3; |
||||
|
end: THREE.Vector3; |
||||
|
color?: THREE.Color; |
||||
|
visible?: boolean; |
||||
|
userData?: any; |
||||
|
} |
||||
|
|||||
Loading…
Reference in new issue