Add wireframe render mode for model instances and viewport display

This commit is contained in:
2026-04-04 19:06:06 +02:00
parent efad6ab92a
commit 4c4fde5aae
2 changed files with 108 additions and 5 deletions

View File

@@ -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<Texture>) {
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);

View File

@@ -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,