diff --git a/src/assets/model-instance-rendering.ts b/src/assets/model-instance-rendering.ts index 64a90123..b8efe110 100644 --- a/src/assets/model-instance-rendering.ts +++ b/src/assets/model-instance-rendering.ts @@ -10,6 +10,8 @@ const MODEL_PLACEHOLDER_COLOR = 0x89b6ff; const MODEL_SELECTION_COLOR = 0xf7d2aa; const MODEL_PREVIEW_SHELL_OPACITY = 0.5; +export type ModelInstanceRenderMode = "normal" | "wireframe"; + interface ModelInstanceBounds { center: Vec3; size: Vec3; @@ -60,6 +62,40 @@ function createWireframeBox(size: Vec3, color: number, opacity: number): Mesh { ); } +function createWireframeMaterial(material: Material): MeshBasicMaterial { + const source = material as Material & { + color?: { getHex(): number }; + opacity?: number; + transparent?: boolean; + }; + const opacity = typeof source.opacity === "number" ? source.opacity : 1; + + return new MeshBasicMaterial({ + color: source.color?.getHex() ?? MODEL_PLACEHOLDER_COLOR, + wireframe: true, + transparent: source.transparent === true || opacity < 1, + opacity, + depthWrite: false + }); +} + +function applyWireframeMaterialPresentation(group: Group) { + group.traverse((object) => { + const maybeMesh = object as Mesh & { isMesh?: boolean }; + + if (maybeMesh.isMesh !== true) { + return; + } + + if (Array.isArray(maybeMesh.material)) { + maybeMesh.material = maybeMesh.material.map((material) => createWireframeMaterial(material)); + return; + } + + maybeMesh.material = createWireframeMaterial(maybeMesh.material); + }); +} + function disposeTexture(texture: Texture, seenTextures: Set) { if (seenTextures.has(texture)) { return; @@ -118,7 +154,8 @@ export function createModelInstanceRenderGroup( asset: ProjectAssetRecord | undefined, loadedAsset: LoadedModelAsset | undefined, selected = false, - previewShellColor?: number + previewShellColor?: number, + renderMode: ModelInstanceRenderMode = "normal" ): Group { const bounds = getLocalModelBounds(asset); const group = new Group(); @@ -134,7 +171,13 @@ export function createModelInstanceRenderGroup( group.userData.assetId = modelInstance.assetId; if (loadedAsset !== undefined) { - group.add(instantiateModelTemplate(loadedAsset.template)); + const instantiatedModel = instantiateModelTemplate(loadedAsset.template); + + if (renderMode === "wireframe") { + applyWireframeMaterialPresentation(instantiatedModel); + } + + group.add(instantiatedModel); } else { const placeholder = createWireframeBox(bounds.size, previewShellColor ?? MODEL_PLACEHOLDER_COLOR, previewShellColor === undefined ? 0.28 : MODEL_PREVIEW_SHELL_OPACITY); placeholder.position.set(bounds.center.x, bounds.center.y, bounds.center.z); diff --git a/src/viewport-three/viewport-host.ts b/src/viewport-three/viewport-host.ts index 12bd4efa..a1148f8c 100644 --- a/src/viewport-three/viewport-host.ts +++ b/src/viewport-three/viewport-host.ts @@ -700,6 +700,33 @@ export class ViewportHost { this.applyOrthographicCameraPose(); } + private createWireframeDisplayMaterial(material: MeshStandardMaterial | MeshBasicMaterial): MeshBasicMaterial { + return new MeshBasicMaterial({ + color: material.color.getHex(), + wireframe: true, + transparent: material.transparent === true || material.opacity < 1, + opacity: material.opacity, + depthWrite: false + }); + } + + private applyWireframePresentation(object: Object3D) { + object.traverse((child) => { + const maybeMesh = child as Mesh & { isMesh?: boolean }; + + if (maybeMesh.isMesh !== true) { + return; + } + + if (Array.isArray(maybeMesh.material)) { + maybeMesh.material = maybeMesh.material.map((material) => this.createWireframeDisplayMaterial(material)); + return; + } + + maybeMesh.material = this.createWireframeDisplayMaterial(maybeMesh.material); + }); + } + private getBoxCreatePlane() { switch (this.viewMode) { case "perspective": @@ -721,7 +748,7 @@ export class ViewportHost { const world = this.currentWorld; const rendererSettings = - this.displayMode === "authoring" + this.displayMode !== "normal" ? { ...cloneAdvancedRenderingSettings(world.advancedRendering), enabled: false @@ -732,8 +759,11 @@ export class ViewportHost { this.sunLight.color.set(world.sunLight.colorHex); this.sunLight.intensity = world.sunLight.intensity; this.sunLight.position.set(world.sunLight.direction.x, world.sunLight.direction.y, world.sunLight.direction.z).normalize().multiplyScalar(18); + this.ambientLight.visible = this.displayMode !== "wireframe"; + this.sunLight.visible = this.displayMode !== "wireframe"; + this.localLightGroup.visible = this.displayMode !== "wireframe"; - if (this.displayMode === "authoring") { + if (this.displayMode !== "normal") { this.scene.background = null; this.scene.environment = null; this.scene.environmentIntensity = 1; @@ -1675,6 +1705,10 @@ export class ViewportHost { const selected = selection.kind === "entities" && selection.ids.includes(entity.id); const renderObjects = this.createEntityRenderObjects(entity, selected); + if (this.displayMode === "wireframe") { + this.applyWireframePresentation(renderObjects.group); + } + this.entityGroup.add(renderObjects.group); this.entityRenderObjects.set(entity.id, renderObjects); } @@ -1687,7 +1721,14 @@ export class ViewportHost { const selected = isModelInstanceSelected(selection, modelInstance.id); const asset = this.projectAssets[modelInstance.assetId]; const loadedAsset = this.loadedModelAssets[modelInstance.assetId]; - const renderGroup = createModelInstanceRenderGroup(modelInstance, asset, loadedAsset, selected); + const renderGroup = createModelInstanceRenderGroup( + modelInstance, + asset, + loadedAsset, + selected, + undefined, + this.displayMode === "wireframe" ? "wireframe" : "normal" + ); if (asset?.kind === "model" && modelInstance.collision.visible) { try { @@ -2175,6 +2216,25 @@ export class ViewportHost { }); } + if (this.displayMode === "wireframe") { + const colorHex = + material === undefined || face.materialId === null + ? selectedFace + ? SELECTED_FACE_FALLBACK_COLOR + : FALLBACK_FACE_COLOR + : selectedFace + ? material.accentColorHex + : material.baseColorHex; + + return new MeshBasicMaterial({ + color: colorHex, + wireframe: true, + transparent: true, + opacity: selectedFace ? 0.95 : 0.76, + depthWrite: false + }); + } + if (material === undefined || face.materialId === null) { return new MeshStandardMaterial({ color: selectedFace ? SELECTED_FACE_FALLBACK_COLOR : FALLBACK_FACE_COLOR,