From d2387947a4c5f29dc97c240e85e5b922bb349fa7 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Mon, 13 Apr 2026 17:23:34 +0200 Subject: [PATCH] Refactor NPC rendering to include collider and facing markers --- src/viewport-three/viewport-host.ts | 135 ++++++++++++++++++++++++++-- 1 file changed, 129 insertions(+), 6 deletions(-) diff --git a/src/viewport-three/viewport-host.ts b/src/viewport-three/viewport-host.ts index ad4898fe..d3fa1e70 100644 --- a/src/viewport-three/viewport-host.ts +++ b/src/viewport-three/viewport-host.ts @@ -3999,29 +3999,152 @@ export class ViewportHost { }; } - private createNpcRenderObjects( + private createNpcColliderRenderObjects( entityId: string, position: Vec3, yawDegrees: number, + collider: NpcEntity["collider"], selected: boolean, markerColor = selected ? NPC_SELECTED_COLOR : NPC_COLOR ): EntityRenderObjects { const group = new Group(); group.position.set(position.x, position.y, position.z); - group.rotation.y = (yawDegrees * Math.PI) / 180; + const colliderMaterial = new MeshStandardMaterial({ + color: markerColor, + emissive: markerColor, + emissiveIntensity: selected ? 0.14 : 0.05, + roughness: 0.5, + metalness: 0.02, + transparent: true, + opacity: selected ? 0.46 : 0.28 + }); + const facingMaterial = new MeshStandardMaterial({ + color: markerColor, + emissive: markerColor, + emissiveIntensity: selected ? 0.22 : 0.08, + roughness: 0.4, + metalness: 0.03 + }); + const meshes: Mesh[] = []; - const markerMeshes = createNpcMarkerMeshes(markerColor, selected); + switch (collider.mode) { + case "capsule": { + const collisionMesh = new Mesh( + new CapsuleGeometry( + collider.capsuleRadius, + Math.max(0, collider.capsuleHeight - collider.capsuleRadius * 2), + 6, + 12 + ), + colliderMaterial + ); + collisionMesh.position.y = collider.capsuleHeight * 0.5; + this.tagEntityMesh(collisionMesh, entityId, "npc", group); + meshes.push(collisionMesh); + break; + } + case "box": { + const collisionMesh = new Mesh( + new BoxGeometry( + collider.boxSize.x, + collider.boxSize.y, + collider.boxSize.z + ), + colliderMaterial + ); + collisionMesh.position.y = collider.boxSize.y * 0.5; + this.tagEntityMesh(collisionMesh, entityId, "npc", group); + meshes.push(collisionMesh); + break; + } + case "none": + break; + } - for (const mesh of markerMeshes) { - this.tagEntityMesh(mesh, entityId, "npc", group); + const facingGroup = new Group(); + facingGroup.rotation.y = (yawDegrees * Math.PI) / 180; + group.add(facingGroup); + const colliderTop = getNpcColliderHeight(collider) ?? 0.18; + + const body = new Mesh(new BoxGeometry(0.08, 0.08, 0.34), facingMaterial); + body.position.set(0, colliderTop + 0.12, 0.06); + + const arrowHead = new Mesh( + new ConeGeometry(0.1, 0.22, 14), + facingMaterial + ); + arrowHead.rotation.x = Math.PI * 0.5; + arrowHead.position.set(0, colliderTop + 0.12, 0.28); + + for (const mesh of [body, arrowHead]) { + this.tagEntityMesh(mesh, entityId, "npc", facingGroup); + meshes.push(mesh); } return { group, - meshes: markerMeshes + meshes }; } + private createNpcRenderObjects( + entity: NpcEntity, + selected: boolean, + previewShellColor?: number + ): EntityRenderObjects { + if (entity.modelAssetId !== null) { + const asset = this.projectAssets[entity.modelAssetId]; + const loadedAsset = this.loadedModelAssets[entity.modelAssetId]; + const renderGroup = createModelInstanceRenderGroup( + { + id: entity.id, + kind: "modelInstance", + assetId: entity.modelAssetId, + name: entity.name, + visible: entity.visible, + enabled: entity.enabled, + position: entity.position, + rotationDegrees: { + x: 0, + y: entity.yawDegrees, + z: 0 + }, + scale: { + x: 1, + y: 1, + z: 1 + }, + collision: { + mode: "none", + visible: false + } + }, + asset, + loadedAsset, + selected, + previewShellColor, + this.displayMode === "wireframe" ? "wireframe" : "normal" + ); + + return { + group: renderGroup, + meshes: this.tagEntityGroup(renderGroup, entity.id, "npc"), + dispose: () => { + disposeModelInstance(renderGroup); + } + }; + } + + return this.createNpcColliderRenderObjects( + entity.id, + entity.position, + entity.yawDegrees, + entity.collider, + selected, + previewShellColor ?? (selected ? NPC_SELECTED_COLOR : NPC_COLOR) + ); + } + private createTriggerVolumeRenderObjects( entityId: string, position: Vec3,