From 65ba9720f831efd6c53e975737b85b96dea07e2d Mon Sep 17 00:00:00 2001 From: cxk <119064883@qq.com> Date: Mon, 20 Jul 2026 18:18:19 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=AF=BC=E8=A7=88=E4=B8=89?= =?UTF-8?q?=E7=BB=B4=E6=A8=A1=E5=9E=8B=E6=9D=90=E8=B4=A8=E5=8F=8D=E5=85=89?= =?UTF-8?q?=E8=BF=87=E5=BC=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/map/ThreeMap.vue | 3 + src/components/map/modelMaterialPolicy.ts | 85 +++++++++++++++++++++++ tests/unit/modelMaterialPolicy.spec.ts | 69 ++++++++++++++++++ 3 files changed, 157 insertions(+) create mode 100644 src/components/map/modelMaterialPolicy.ts create mode 100644 tests/unit/modelMaterialPolicy.spec.ts diff --git a/src/components/map/ThreeMap.vue b/src/components/map/ThreeMap.vue index fdc0dc8..7f502d3 100644 --- a/src/components/map/ThreeMap.vue +++ b/src/components/map/ThreeMap.vue @@ -92,6 +92,7 @@ import { type PoiDomLabelAnchor, type PoiDomLabelKind } from './poiDomLabels' +import { applyGuideModelMaterialPolicy } from './modelMaterialPolicy' type ViewMode = 'overview' | 'floor' | 'multi' type CameraPreset = 'top' | 'oblique' @@ -2923,6 +2924,8 @@ const loadModelWithFallback = async ( } const prepareModel = (model: THREE.Object3D) => { + applyGuideModelMaterialPolicy(model) + model.traverse((child) => { if (child instanceof THREE.Mesh) { child.castShadow = false diff --git a/src/components/map/modelMaterialPolicy.ts b/src/components/map/modelMaterialPolicy.ts new file mode 100644 index 0000000..c48e7a1 --- /dev/null +++ b/src/components/map/modelMaterialPolicy.ts @@ -0,0 +1,85 @@ +import * as THREE from 'three' + +export const GUIDE_MODEL_MATERIAL_POLICY = { + envMapIntensity: 0.42, + nonMetalRoughnessFloor: 0.48, + nonMetalSpecularIntensityCap: 0.6, + metalnessThreshold: 0.5, + transparentOpacityThreshold: 0.99, + transmissionThreshold: 0.01 +} as const + +const isTransparentOrGlass = (material: THREE.MeshStandardMaterial) => { + const physicalMaterial = material as THREE.MeshPhysicalMaterial + return material.transparent + || material.opacity < GUIDE_MODEL_MATERIAL_POLICY.transparentOpacityThreshold + || physicalMaterial.transmission > GUIDE_MODEL_MATERIAL_POLICY.transmissionThreshold +} + +const MATERIAL_POLICY_KEY = '__guideModelMaterialPolicyApplied' + +const applyNonMetalRoughnessFloor = (material: THREE.MeshStandardMaterial) => { + if (material.roughness < GUIDE_MODEL_MATERIAL_POLICY.nonMetalRoughnessFloor) { + material.roughness = GUIDE_MODEL_MATERIAL_POLICY.nonMetalRoughnessFloor + } + + // GLB roughness textures can still produce mirror-like texels when the + // exported scalar factor is already 1. Clamp after Three.js samples that map. + if (!material.roughnessMap || material.userData[MATERIAL_POLICY_KEY]) return + + const originalOnBeforeCompile = material.onBeforeCompile + const originalCacheKey = material.customProgramCacheKey + material.onBeforeCompile = (shader, renderer) => { + originalOnBeforeCompile(shader, renderer) + shader.fragmentShader = shader.fragmentShader.replace( + '#include ', + `#include \nroughnessFactor = max(roughnessFactor, ${GUIDE_MODEL_MATERIAL_POLICY.nonMetalRoughnessFloor.toFixed(2)});` + ) + } + material.customProgramCacheKey = () => `${originalCacheKey.call(material)}|guide-non-metal-roughness-floor` + material.userData[MATERIAL_POLICY_KEY] = true + material.needsUpdate = true +} + +const applyNonMetalSpecularCap = (material: THREE.MeshStandardMaterial) => { + if (!(material instanceof THREE.MeshPhysicalMaterial)) return + + material.specularIntensity = Math.min( + material.specularIntensity, + GUIDE_MODEL_MATERIAL_POLICY.nonMetalSpecularIntensityCap + ) +} + +/** + * Normalizes imported GLB PBR factors at one rendering boundary. Textures, + * alpha and deliberate metal/glass material classes remain authored by the model. + */ +export const applyGuideModelMaterialPolicy = (object: THREE.Object3D) => { + let pbrMaterialCount = 0 + let nonMetalMaterialCount = 0 + let protectedMaterialCount = 0 + + object.traverse((child) => { + if (!(child instanceof THREE.Mesh)) return + + const materials = Array.isArray(child.material) ? child.material : [child.material] + materials.forEach((material) => { + if (!(material instanceof THREE.MeshStandardMaterial)) return + + pbrMaterialCount += 1 + material.envMapIntensity = GUIDE_MODEL_MATERIAL_POLICY.envMapIntensity + + const isMetal = material.metalness >= GUIDE_MODEL_MATERIAL_POLICY.metalnessThreshold + if (isMetal || isTransparentOrGlass(material)) { + protectedMaterialCount += 1 + return + } + + nonMetalMaterialCount += 1 + applyNonMetalRoughnessFloor(material) + applyNonMetalSpecularCap(material) + }) + }) + + return { pbrMaterialCount, nonMetalMaterialCount, protectedMaterialCount } +} diff --git a/tests/unit/modelMaterialPolicy.spec.ts b/tests/unit/modelMaterialPolicy.spec.ts new file mode 100644 index 0000000..2849adf --- /dev/null +++ b/tests/unit/modelMaterialPolicy.spec.ts @@ -0,0 +1,69 @@ +import { describe, expect, it } from 'vitest' +import * as THREE from 'three' +import { + applyGuideModelMaterialPolicy, + GUIDE_MODEL_MATERIAL_POLICY +} from '@/components/map/modelMaterialPolicy' + +const meshWith = (material: THREE.Material) => new THREE.Mesh( + new THREE.BoxGeometry(1, 1, 1), + material +) + +describe('guide GLB material policy', () => { + it('raises the roughness floor and limits environment reflection for opaque non-metal PBR materials', () => { + const root = new THREE.Group() + const wall = new THREE.MeshStandardMaterial({ roughness: 0.12, metalness: 0.05, envMapIntensity: 1 }) + root.add(meshWith(wall)) + + expect(applyGuideModelMaterialPolicy(root)).toEqual({ + pbrMaterialCount: 1, + nonMetalMaterialCount: 1, + protectedMaterialCount: 0 + }) + expect(wall.roughness).toBe(GUIDE_MODEL_MATERIAL_POLICY.nonMetalRoughnessFloor) + expect(wall.envMapIntensity).toBe(GUIDE_MODEL_MATERIAL_POLICY.envMapIntensity) + }) + + it('also applies the roughness floor after a non-metal roughness texture is sampled', () => { + const root = new THREE.Group() + const floor = new THREE.MeshStandardMaterial({ roughness: 1, metalness: 0 }) + floor.roughnessMap = new THREE.Texture() + root.add(meshWith(floor)) + + applyGuideModelMaterialPolicy(root) + const shader = { fragmentShader: '#include ' } + floor.onBeforeCompile(shader as any, {} as any) + + expect(shader.fragmentShader).toContain('roughnessFactor = max(roughnessFactor, 0.48);') + expect(floor.customProgramCacheKey()).toContain('guide-non-metal-roughness-floor') + }) + + it('caps imported specular extensions on opaque non-metal physical materials', () => { + const root = new THREE.Group() + const wall = new THREE.MeshPhysicalMaterial({ roughness: 0.55, metalness: 0, specularIntensity: 1 }) + root.add(meshWith(wall)) + + applyGuideModelMaterialPolicy(root) + + expect(wall.specularIntensity).toBe(GUIDE_MODEL_MATERIAL_POLICY.nonMetalSpecularIntensityCap) + }) + + it('keeps authored roughness on metal, transparent and glass materials while limiting environment reflection', () => { + const root = new THREE.Group() + const metal = new THREE.MeshStandardMaterial({ roughness: 0.08, metalness: 0.9, envMapIntensity: 1 }) + const transparent = new THREE.MeshStandardMaterial({ roughness: 0.08, transparent: true, opacity: 0.4, envMapIntensity: 1 }) + const glass = new THREE.MeshPhysicalMaterial({ roughness: 0.08, transmission: 0.9, envMapIntensity: 1, specularIntensity: 1 }) + root.add(meshWith(metal), meshWith(transparent), meshWith(glass)) + + expect(applyGuideModelMaterialPolicy(root)).toEqual({ + pbrMaterialCount: 3, + nonMetalMaterialCount: 0, + protectedMaterialCount: 3 + }) + expect([metal.roughness, transparent.roughness, glass.roughness]).toEqual([0.08, 0.08, 0.08]) + expect(glass.specularIntensity).toBe(1) + expect([metal.envMapIntensity, transparent.envMapIntensity, glass.envMapIntensity]) + .toEqual([0.42, 0.42, 0.42]) + }) +})