This commit is contained in:
@@ -92,6 +92,7 @@ import {
|
|||||||
type PoiDomLabelAnchor,
|
type PoiDomLabelAnchor,
|
||||||
type PoiDomLabelKind
|
type PoiDomLabelKind
|
||||||
} from './poiDomLabels'
|
} from './poiDomLabels'
|
||||||
|
import { applyGuideModelMaterialPolicy } from './modelMaterialPolicy'
|
||||||
|
|
||||||
type ViewMode = 'overview' | 'floor' | 'multi'
|
type ViewMode = 'overview' | 'floor' | 'multi'
|
||||||
type CameraPreset = 'top' | 'oblique'
|
type CameraPreset = 'top' | 'oblique'
|
||||||
@@ -2923,6 +2924,8 @@ const loadModelWithFallback = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const prepareModel = (model: THREE.Object3D) => {
|
const prepareModel = (model: THREE.Object3D) => {
|
||||||
|
applyGuideModelMaterialPolicy(model)
|
||||||
|
|
||||||
model.traverse((child) => {
|
model.traverse((child) => {
|
||||||
if (child instanceof THREE.Mesh) {
|
if (child instanceof THREE.Mesh) {
|
||||||
child.castShadow = false
|
child.castShadow = false
|
||||||
|
|||||||
85
src/components/map/modelMaterialPolicy.ts
Normal file
85
src/components/map/modelMaterialPolicy.ts
Normal 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 }
|
||||||
|
}
|
||||||
69
tests/unit/modelMaterialPolicy.spec.ts
Normal file
69
tests/unit/modelMaterialPolicy.spec.ts
Normal 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])
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user