修复导览三维模型材质反光过强
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
cxk
2026-07-20 18:18:19 +08:00
parent 3cc0336806
commit 65ba9720f8
3 changed files with 157 additions and 0 deletions

View File

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

View File

@@ -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 <roughnessmap_fragment>',
`#include <roughnessmap_fragment>\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 }
}

View File

@@ -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 <roughnessmap_fragment>' }
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])
})
})