@marble.fun/track-catalog 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +15 -0
- package/assets.json +346 -0
- package/package.json +32 -0
- package/release.json +135 -0
- package/src/audio/game-audio-preference.ts +9 -0
- package/src/authoritative.ts +6 -0
- package/src/catalog.ts +1 -0
- package/src/index.ts +14 -0
- package/src/legacy-stock-catalog.ts +130 -0
- package/src/pages/lobby/lobby-types.ts +140 -0
- package/src/pages/lobby/marble-factory.ts +931 -0
- package/src/pages/play/race/egypt-elevator-blender.ts +159 -0
- package/src/pages/play/race/egypt-portal-frame.ts +382 -0
- package/src/pages/play/race/finish-detection.ts +1 -0
- package/src/pages/play/race/japanese-sakura-castle-support-blender.ts +510 -0
- package/src/pages/play/race/japanese-sakura-kawara-roof.ts +201 -0
- package/src/pages/play/race/japanese-sakura-portal-frame.ts +325 -0
- package/src/pages/play/race/jungle-portal-frame.ts +308 -0
- package/src/pages/play/race/jungle-temple-lift.ts +366 -0
- package/src/pages/play/race/marble-entity.ts +257 -0
- package/src/pages/play/race/marble-material.ts +20 -0
- package/src/pages/play/race/marble-race-engine.ts +2652 -0
- package/src/pages/play/race/path-pattern-layout.ts +142 -0
- package/src/pages/play/race/peg-obstacle-visual.ts +171 -0
- package/src/pages/play/race/physics-debug.ts +1 -0
- package/src/pages/play/race/physics-dry-run.ts +90 -0
- package/src/pages/play/race/portal-derived-elevator-skins.ts +729 -0
- package/src/pages/play/race/portal-pair-color.ts +20 -0
- package/src/pages/play/race/portal-proportions.ts +25 -0
- package/src/pages/play/race/portal-visual.ts +1108 -0
- package/src/pages/play/race/race-camera-director.ts +586 -0
- package/src/pages/play/race/race-camera-obstruction.ts +37 -0
- package/src/pages/play/race/race-camera-tour-planner.ts +274 -0
- package/src/pages/play/race/race-camera.ts +206 -0
- package/src/pages/play/race/race-completion.ts +5 -0
- package/src/pages/play/race/race-configuration.ts +57 -0
- package/src/pages/play/race/race-marble-focus-renderer.ts +134 -0
- package/src/pages/play/race/race-physics-profile.ts +7 -0
- package/src/pages/play/race/race-physics.ts +16 -0
- package/src/pages/play/race/race-playback.ts +41 -0
- package/src/pages/play/race/race-rolling-audio.ts +616 -0
- package/src/pages/play/race/race-simulation-renderer.ts +221 -0
- package/src/pages/play/race/race-terminal-checker-strip.ts +226 -0
- package/src/pages/play/race/race-transform-interpolation.ts +50 -0
- package/src/pages/play/race/race-types.ts +12 -0
- package/src/pages/play/race/rebound-visual.ts +70 -0
- package/src/pages/play/race/seeded-random.ts +23 -0
- package/src/pages/play/race/space-station-portal-frame.ts +275 -0
- package/src/pages/play/race/track-appearance-canopy.ts +946 -0
- package/src/pages/play/race/track-appearance-floor-runner.ts +206 -0
- package/src/pages/play/race/track-appearance-jungle-flower-hedge.ts +483 -0
- package/src/pages/play/race/track-appearance-rope-wall.ts +443 -0
- package/src/pages/play/race/track-appearance-sakura-bridge-fence.ts +305 -0
- package/src/pages/play/race/track-appearance-sakura-castle-wall.ts +1031 -0
- package/src/pages/play/race/track-appearance-space-station-wall.ts +497 -0
- package/src/pages/play/race/track-appearance-stone-block-shell.ts +517 -0
- package/src/pages/play/race/track-appearance-support.ts +983 -0
- package/src/pages/play/race/track-appearance-vampire-carved-wall.ts +545 -0
- package/src/pages/play/race/track-appearance-wall-terminal.ts +167 -0
- package/src/pages/play/race/track-course.ts +271 -0
- package/src/pages/play/race/track-definition.ts +443 -0
- package/src/pages/play/race/track-fence-architecture.ts +33 -0
- package/src/pages/play/race/track-floor-material-rules.ts +51 -0
- package/src/pages/play/race/track-mechanisms.ts +1350 -0
- package/src/pages/play/race/track-physics-surface.ts +6 -0
- package/src/pages/play/race/track-route-network.ts +9 -0
- package/src/pages/play/race/track-shadow-batching.ts +760 -0
- package/src/pages/play/race/track-surface-basis.ts +1 -0
- package/src/pages/play/race/track-surface-topology.ts +326 -0
- package/src/pages/play/race/track-world.ts +10181 -0
- package/src/pages/play/race/under-track-support-architecture.ts +122 -0
- package/src/pages/play/race/vortex-profile.ts +5 -0
- package/src/pages/play/race/vortex-water-effect.ts +863 -0
- package/src/pages/play/race/wild-west-mine-lift.ts +281 -0
- package/src/pages/play/race/wild-west-vortex-housing.ts +572 -0
- package/src/pages/track-lab/atlas-vortex-layout.ts +1 -0
- package/src/pages/track-lab/cone-field-layout.ts +1 -0
- package/src/pages/track-lab/conveyor-layout.ts +1 -0
- package/src/pages/track-lab/engine/canonical-track-assembly.ts +64 -0
- package/src/pages/track-lab/engine/editor-object-geometry.ts +170 -0
- package/src/pages/track-lab/engine/track-lab-connection-state.ts +123 -0
- package/src/pages/track-lab/engine/track-piece-visual.ts +1420 -0
- package/src/pages/track-lab/flipper-bank-layout.ts +1 -0
- package/src/pages/track-lab/flipper-component.ts +127 -0
- package/src/pages/track-lab/rising-wall-layout.ts +1 -0
- package/src/pages/track-lab/simple-obstacle-layout.ts +1 -0
- package/src/pages/track-lab/track-piece-layout.ts +28 -0
- package/src/pages/track-lab/track-runtime-adapter.ts +1152 -0
- package/src/three/geometry-attribute-aliases.ts +24 -0
- package/src/three/geometry-detail.ts +36 -0
- package/src/three/homepage-marble-geometry.ts +15 -0
- package/src/three/japanese-sakura-appearance.ts +252 -0
- package/src/three/jungle-flower-hedge-appearance.ts +7 -0
- package/src/three/jungle-temple-appearance.ts +5 -0
- package/src/three/low-quality-scene-renderer.ts +132 -0
- package/src/three/marble-sphere-geometry.ts +13 -0
- package/src/three/mobile-rendering-budget.ts +11 -0
- package/src/three/painted-valley-appearance.ts +270 -0
- package/src/three/railroad-appearance.ts +71 -0
- package/src/three/renderer-resize.ts +32 -0
- package/src/three/rope-wall-appearance.ts +91 -0
- package/src/three/rugged-track-appearance.ts +2 -0
- package/src/three/scene-lighting.ts +183 -0
- package/src/three/scene-shader-preparation.ts +64 -0
- package/src/three/scene-theme-controller.ts +461 -0
- package/src/three/shadow-shader-preparation.ts +162 -0
- package/src/three/shared-obstacle-metal.ts +44 -0
- package/src/three/soft-additive-particles.ts +57 -0
- package/src/three/space-station-appearance.ts +213 -0
- package/src/three/static-track-batched-mesh.ts +132 -0
- package/src/three/stone-track-shell-appearance.ts +12 -0
- package/src/three/theme-ambience-controller.ts +226 -0
- package/src/three/theme-asset-cache.ts +120 -0
- package/src/three/theme-audio-preference.ts +43 -0
- package/src/three/theme-catalog.ts +1457 -0
- package/src/three/theme-material-assets.ts +258 -0
- package/src/three/theme-material-ktx2-manifest.ts +190 -0
- package/src/three/theme-material-registry.ts +1952 -0
- package/src/three/theme-world-factory.ts +590 -0
- package/src/three/track-connector-batch.ts +125 -0
- package/src/three/track-material-continuity.ts +442 -0
- package/src/three/track-theme.ts +535 -0
- package/src/three/vampire-castle-appearance.ts +258 -0
- package/src/three/visual-frame-pacing.ts +19 -0
- package/src/three/visual-geometry.ts +120 -0
- package/src/three/visual-quality.ts +83 -0
- package/src/three/water-appearance.ts +29 -0
- package/src/three/wild-west-sand-appearance.ts +2 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import * as THREE from 'three'
|
|
2
|
+
|
|
3
|
+
interface SceneShaderPreparationOptions {
|
|
4
|
+
visibleRoot?: THREE.Object3D
|
|
5
|
+
yieldWork?: () => Promise<void>
|
|
6
|
+
shouldContinue?: () => boolean
|
|
7
|
+
includeVisibleScene?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Compile actual render objects under their final lighting, without drawing. */
|
|
11
|
+
export async function prepareSceneShaders(
|
|
12
|
+
renderer: THREE.WebGLRenderer,
|
|
13
|
+
root: THREE.Object3D,
|
|
14
|
+
camera: THREE.Camera,
|
|
15
|
+
scene: THREE.Scene,
|
|
16
|
+
options: SceneShaderPreparationOptions = {},
|
|
17
|
+
): Promise<void> {
|
|
18
|
+
const {
|
|
19
|
+
visibleRoot = root,
|
|
20
|
+
yieldWork = () => new Promise<void>(resolve => setTimeout(resolve, 0)),
|
|
21
|
+
shouldContinue = () => true,
|
|
22
|
+
includeVisibleScene = false,
|
|
23
|
+
} = options
|
|
24
|
+
const proxies: THREE.Object3D[] = []
|
|
25
|
+
const seen = new Set<THREE.Object3D>()
|
|
26
|
+
const collect = (object: THREE.Object3D): void => {
|
|
27
|
+
if (seen.has(object)) return
|
|
28
|
+
seen.add(object)
|
|
29
|
+
if (!(
|
|
30
|
+
object instanceof THREE.Mesh ||
|
|
31
|
+
object instanceof THREE.Line ||
|
|
32
|
+
object instanceof THREE.Points ||
|
|
33
|
+
object instanceof THREE.Sprite
|
|
34
|
+
))
|
|
35
|
+
return
|
|
36
|
+
// Preserve geometry/instancing/skinning identity without copying buffers.
|
|
37
|
+
// Exclude children so every render object is visited exactly once.
|
|
38
|
+
const proxy = Object.create(object) as THREE.Object3D
|
|
39
|
+
proxy.children = []
|
|
40
|
+
proxies.push(proxy)
|
|
41
|
+
}
|
|
42
|
+
root.traverse(collect)
|
|
43
|
+
// The active theme can place foliage/world objects outside the track root.
|
|
44
|
+
// Include them at race handoff, while excluding unrelated hidden views.
|
|
45
|
+
if (includeVisibleScene) scene.traverseVisible(collect)
|
|
46
|
+
for (let offset = 0; offset < proxies.length; offset += 128) {
|
|
47
|
+
if (!shouldContinue()) return
|
|
48
|
+
const batch = new THREE.Group()
|
|
49
|
+
batch.children = proxies.slice(offset, offset + 128)
|
|
50
|
+
const visible = visibleRoot.visible
|
|
51
|
+
let pending: Promise<THREE.Object3D>
|
|
52
|
+
try {
|
|
53
|
+
// Three gathers lights synchronously inside compileAsync. Restore the
|
|
54
|
+
// cover's visibility before awaiting, so a shared animation loop never
|
|
55
|
+
// sees the hidden track. Hidden portal lights still enter the shader key.
|
|
56
|
+
visibleRoot.visible = true
|
|
57
|
+
pending = renderer.compileAsync(batch, camera, scene)
|
|
58
|
+
} finally {
|
|
59
|
+
visibleRoot.visible = visible
|
|
60
|
+
}
|
|
61
|
+
await pending
|
|
62
|
+
if (offset + 128 < proxies.length) await yieldWork()
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
import * as THREE from 'three'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
computeShadowFocus,
|
|
5
|
+
configureKeyShadow,
|
|
6
|
+
configureSceneRenderer,
|
|
7
|
+
disposeSceneLightShadows,
|
|
8
|
+
resolveDynamicShadowRefreshRate,
|
|
9
|
+
resolveSceneLightingRig,
|
|
10
|
+
usesMobileSceneLighting,
|
|
11
|
+
type SceneLightingRig,
|
|
12
|
+
} from './scene-lighting'
|
|
13
|
+
import { themeAssetCache, type ThemePanoramaLease } from './theme-asset-cache'
|
|
14
|
+
import { ThemeAmbienceController } from './theme-ambience-controller'
|
|
15
|
+
import { ThemeMaterialRegistry } from './theme-material-registry'
|
|
16
|
+
import { configureThemeMaterialTextureLoader } from './theme-material-assets'
|
|
17
|
+
import { NEUTRAL_STUDIO_THEME, type TrackThemeDefinition } from './track-theme'
|
|
18
|
+
import { createThemeWorld, type ThemeWorldState } from './theme-world-factory'
|
|
19
|
+
import { visualQualityPolicy, type VisualQualityPolicy } from './visual-quality'
|
|
20
|
+
|
|
21
|
+
export interface SceneThemeDiagnostics {
|
|
22
|
+
themeId: string
|
|
23
|
+
materialCount: number
|
|
24
|
+
worldMeshes: number
|
|
25
|
+
instancedMeshes: number
|
|
26
|
+
particleCount: number
|
|
27
|
+
particleSample?: readonly [number, number, number]
|
|
28
|
+
practicalLightCount: number
|
|
29
|
+
practicalLightPositions: readonly (readonly [number, number, number])[]
|
|
30
|
+
shadowMapSize: number
|
|
31
|
+
shadowExtent: number
|
|
32
|
+
shadowIntensity: number
|
|
33
|
+
exposure: number
|
|
34
|
+
environmentIntensity: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class SceneThemeController {
|
|
38
|
+
readonly materials = new ThemeMaterialRegistry(NEUTRAL_STUDIO_THEME)
|
|
39
|
+
|
|
40
|
+
private readonly lightRoot = new THREE.Group()
|
|
41
|
+
private readonly practicalLights: THREE.PointLight[] = []
|
|
42
|
+
private readonly ambience = new ThemeAmbienceController()
|
|
43
|
+
private readonly keyOffset = new THREE.Vector3()
|
|
44
|
+
private readonly shadowFocus = new THREE.Vector3()
|
|
45
|
+
private readonly nextShadowFocus = new THREE.Vector3()
|
|
46
|
+
private keyLight?: THREE.DirectionalLight
|
|
47
|
+
private lightingRig = resolveSceneLightingRig(NEUTRAL_STUDIO_THEME, false)
|
|
48
|
+
private world?: ThemeWorldState
|
|
49
|
+
private activeTheme = NEUTRAL_STUDIO_THEME
|
|
50
|
+
private panoramaLease?: ThemePanoramaLease
|
|
51
|
+
private requestVersion = 0
|
|
52
|
+
private elapsedSeconds = 0
|
|
53
|
+
private practicalLightRefreshElapsed = 0
|
|
54
|
+
private shadowRefreshElapsed = Number.POSITIVE_INFINITY
|
|
55
|
+
private shadowFocusInitialized = false
|
|
56
|
+
private dynamicShadowBudgetApplied = false
|
|
57
|
+
private adaptiveDynamicShadowRefreshRate: 30 | 60 = 60
|
|
58
|
+
private dynamicShadowRefreshRate = 60
|
|
59
|
+
private visualQualityPolicy = visualQualityPolicy('quality')
|
|
60
|
+
private disposed = false
|
|
61
|
+
|
|
62
|
+
constructor(
|
|
63
|
+
private readonly scene: THREE.Scene,
|
|
64
|
+
private readonly renderer: THREE.WebGLRenderer,
|
|
65
|
+
private readonly container: HTMLElement,
|
|
66
|
+
) {
|
|
67
|
+
configureThemeMaterialTextureLoader(this.renderer)
|
|
68
|
+
this.scene.add(this.lightRoot)
|
|
69
|
+
this.applyPreparedTheme(NEUTRAL_STUDIO_THEME, undefined)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get theme(): TrackThemeDefinition {
|
|
73
|
+
return this.activeTheme
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async applyTheme(theme: TrackThemeDefinition): Promise<boolean> {
|
|
77
|
+
this.assertActive()
|
|
78
|
+
const requestVersion = ++this.requestVersion
|
|
79
|
+
this.container.dataset.themeLoading = theme.id
|
|
80
|
+
let pendingPanoramaLease: ThemePanoramaLease | undefined
|
|
81
|
+
try {
|
|
82
|
+
pendingPanoramaLease = await themeAssetCache.acquirePanorama(theme)
|
|
83
|
+
if (requestVersion !== this.requestVersion || this.disposed) {
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
this.container.classList.add('is-theme-transitioning')
|
|
88
|
+
this.applyPreparedTheme(theme, pendingPanoramaLease.texture)
|
|
89
|
+
this.panoramaLease?.release()
|
|
90
|
+
this.panoramaLease = pendingPanoramaLease
|
|
91
|
+
pendingPanoramaLease = undefined
|
|
92
|
+
window.setTimeout(() => this.container.classList.remove('is-theme-transitioning'), 320)
|
|
93
|
+
return true
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (requestVersion === this.requestVersion && !this.disposed) {
|
|
96
|
+
this.container.dataset.themeError = theme.id
|
|
97
|
+
}
|
|
98
|
+
console.error(`Unable to load the "${theme.id}" theme panorama.`, error)
|
|
99
|
+
return false
|
|
100
|
+
} finally {
|
|
101
|
+
pendingPanoramaLease?.release()
|
|
102
|
+
if (requestVersion === this.requestVersion) delete this.container.dataset.themeLoading
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
update(deltaSeconds: number, camera?: THREE.Camera, dynamicShadows = false, shadowFocusTarget?: THREE.Vector3): void {
|
|
107
|
+
if (this.disposed) return
|
|
108
|
+
this.elapsedSeconds += deltaSeconds
|
|
109
|
+
this.world?.update(deltaSeconds, this.elapsedSeconds)
|
|
110
|
+
if (camera && this.updateKeyShadowFocus(camera, shadowFocusTarget)) this.requestShadowUpdate()
|
|
111
|
+
if (dynamicShadows) {
|
|
112
|
+
this.applyDynamicShadowBudget()
|
|
113
|
+
this.shadowRefreshElapsed += deltaSeconds
|
|
114
|
+
this.adaptiveDynamicShadowRefreshRate = resolveDynamicShadowRefreshRate(
|
|
115
|
+
this.container.clientWidth,
|
|
116
|
+
deltaSeconds,
|
|
117
|
+
this.adaptiveDynamicShadowRefreshRate,
|
|
118
|
+
)
|
|
119
|
+
this.dynamicShadowRefreshRate = Math.min(
|
|
120
|
+
this.adaptiveDynamicShadowRefreshRate,
|
|
121
|
+
this.visualQualityPolicy.shadowRefreshRate,
|
|
122
|
+
)
|
|
123
|
+
if (this.shadowRefreshElapsed >= 1 / this.dynamicShadowRefreshRate) this.requestShadowUpdate()
|
|
124
|
+
} else {
|
|
125
|
+
this.restoreStaticShadowBudget()
|
|
126
|
+
}
|
|
127
|
+
if (this.practicalLights.length > 0) {
|
|
128
|
+
this.practicalLightRefreshElapsed += deltaSeconds
|
|
129
|
+
if (this.practicalLightRefreshElapsed >= 1.5) {
|
|
130
|
+
this.practicalLightRefreshElapsed = 0
|
|
131
|
+
this.refreshPracticalLightPositions()
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
requestShadowUpdate(): void {
|
|
137
|
+
this.shadowRefreshElapsed = 0
|
|
138
|
+
this.renderer.shadowMap.needsUpdate = true
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
setVisualQualityPolicy(policy: VisualQualityPolicy): void {
|
|
142
|
+
if (this.disposed || this.visualQualityPolicy === policy) return
|
|
143
|
+
this.visualQualityPolicy = policy
|
|
144
|
+
this.renderer.shadowMap.enabled = policy.tier !== 'low'
|
|
145
|
+
this.dynamicShadowRefreshRate = Math.min(this.dynamicShadowRefreshRate, policy.shadowRefreshRate)
|
|
146
|
+
this.applyParticleBudget()
|
|
147
|
+
if (!this.keyLight) return
|
|
148
|
+
const shadow = this.keyLight.shadow
|
|
149
|
+
const mapSize = this.desiredShadowMapSize(this.dynamicShadowBudgetApplied)
|
|
150
|
+
if (shadow.mapSize.width === mapSize && shadow.mapSize.height === mapSize) return
|
|
151
|
+
shadow.map?.dispose()
|
|
152
|
+
shadow.map = null
|
|
153
|
+
shadow.mapSize.set(mapSize, mapSize)
|
|
154
|
+
this.requestShadowUpdate()
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
diagnostics(): SceneThemeDiagnostics {
|
|
158
|
+
let worldMeshes = 0
|
|
159
|
+
let instancedMeshes = 0
|
|
160
|
+
let particleCount = 0
|
|
161
|
+
let particleSample: readonly [number, number, number] | undefined
|
|
162
|
+
this.world?.root.traverse(object => {
|
|
163
|
+
if (object instanceof THREE.InstancedMesh) {
|
|
164
|
+
instancedMeshes += 1
|
|
165
|
+
worldMeshes += 1
|
|
166
|
+
} else if (object instanceof THREE.Mesh) {
|
|
167
|
+
worldMeshes += 1
|
|
168
|
+
} else if (object instanceof THREE.Points) {
|
|
169
|
+
const positions = object.geometry.getAttribute('position')
|
|
170
|
+
particleCount += positions?.count ?? 0
|
|
171
|
+
if (!particleSample && positions?.count) {
|
|
172
|
+
particleSample = [positions.getX(0), positions.getY(0), positions.getZ(0)]
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
return {
|
|
177
|
+
themeId: this.activeTheme.id,
|
|
178
|
+
materialCount: Object.keys(this.activeTheme.materials).length,
|
|
179
|
+
worldMeshes,
|
|
180
|
+
instancedMeshes,
|
|
181
|
+
particleCount,
|
|
182
|
+
particleSample,
|
|
183
|
+
practicalLightCount: this.practicalLights.length,
|
|
184
|
+
practicalLightPositions: this.practicalLights.map(
|
|
185
|
+
light => [light.position.x, light.position.y, light.position.z] as const,
|
|
186
|
+
),
|
|
187
|
+
shadowMapSize: this.keyLight?.shadow.mapSize.width ?? this.lightingRig.shadow.mapSize,
|
|
188
|
+
shadowExtent: this.lightingRig.shadow.extent,
|
|
189
|
+
shadowIntensity: this.lightingRig.shadow.intensity,
|
|
190
|
+
exposure: this.lightingRig.exposure,
|
|
191
|
+
environmentIntensity: this.lightingRig.environmentIntensity,
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
dispose(): void {
|
|
196
|
+
if (this.disposed) return
|
|
197
|
+
this.disposed = true
|
|
198
|
+
this.requestVersion += 1
|
|
199
|
+
this.world?.dispose()
|
|
200
|
+
this.world = undefined
|
|
201
|
+
disposeSceneLightShadows(this.lightRoot)
|
|
202
|
+
this.lightRoot.removeFromParent()
|
|
203
|
+
this.materials.dispose()
|
|
204
|
+
this.panoramaLease?.release()
|
|
205
|
+
this.panoramaLease = undefined
|
|
206
|
+
this.ambience.dispose()
|
|
207
|
+
this.scene.environment = null
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private applyPreparedTheme(theme: TrackThemeDefinition, panorama: THREE.Texture | undefined): void {
|
|
211
|
+
this.materials.applyTheme(theme)
|
|
212
|
+
const mobile = usesMobileSceneLighting(this.container.clientWidth)
|
|
213
|
+
const preparedWorld = createThemeWorld(theme, this.materials, mobile)
|
|
214
|
+
this.world?.dispose()
|
|
215
|
+
this.world = preparedWorld
|
|
216
|
+
this.applyParticleBudget()
|
|
217
|
+
this.scene.add(preparedWorld.root)
|
|
218
|
+
disposeSceneLightShadows(this.lightRoot)
|
|
219
|
+
this.lightRoot.clear()
|
|
220
|
+
this.keyLight = undefined
|
|
221
|
+
this.practicalLights.length = 0
|
|
222
|
+
this.practicalLightRefreshElapsed = 0
|
|
223
|
+
this.shadowFocusInitialized = false
|
|
224
|
+
this.shadowRefreshElapsed = Number.POSITIVE_INFINITY
|
|
225
|
+
this.dynamicShadowBudgetApplied = false
|
|
226
|
+
this.adaptiveDynamicShadowRefreshRate = mobile ? 30 : 60
|
|
227
|
+
this.dynamicShadowRefreshRate = Math.min(
|
|
228
|
+
this.adaptiveDynamicShadowRefreshRate,
|
|
229
|
+
this.visualQualityPolicy.shadowRefreshRate,
|
|
230
|
+
)
|
|
231
|
+
this.lightingRig = resolveSceneLightingRig(theme, mobile)
|
|
232
|
+
this.configureLights(theme, this.lightingRig, mobile)
|
|
233
|
+
this.setKeyShadowMapSize(this.desiredShadowMapSize(false))
|
|
234
|
+
|
|
235
|
+
this.scene.background = panorama ?? new THREE.Color(theme.environment.background)
|
|
236
|
+
this.scene.environment = panorama ?? null
|
|
237
|
+
this.scene.environmentIntensity = this.lightingRig.environmentIntensity
|
|
238
|
+
this.scene.fog = new THREE.Fog(
|
|
239
|
+
theme.environment.fog,
|
|
240
|
+
theme.environment.fogNear,
|
|
241
|
+
Math.max(theme.environment.fogFar, 480),
|
|
242
|
+
)
|
|
243
|
+
configureSceneRenderer(this.renderer, this.lightingRig.exposure)
|
|
244
|
+
this.renderer.shadowMap.enabled = this.visualQualityPolicy.tier !== 'low'
|
|
245
|
+
this.activeTheme = theme
|
|
246
|
+
this.ambience.applyTheme(theme)
|
|
247
|
+
this.container.dataset.themeId = theme.id
|
|
248
|
+
delete this.container.dataset.themeError
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private configureLights(theme: TrackThemeDefinition, rig: SceneLightingRig, mobile: boolean): void {
|
|
252
|
+
const hemisphere = new THREE.HemisphereLight(
|
|
253
|
+
rig.hemisphere.skyColor,
|
|
254
|
+
rig.hemisphere.groundColor,
|
|
255
|
+
rig.hemisphere.intensity,
|
|
256
|
+
)
|
|
257
|
+
hemisphere.name = 'theme-hemisphere'
|
|
258
|
+
|
|
259
|
+
const key = new THREE.DirectionalLight(rig.key.color, rig.key.intensity)
|
|
260
|
+
key.name = 'theme-key'
|
|
261
|
+
key.position.set(...rig.key.position)
|
|
262
|
+
this.keyOffset.copy(key.position)
|
|
263
|
+
configureKeyShadow(key, rig)
|
|
264
|
+
key.target.name = 'theme-key-target'
|
|
265
|
+
this.keyLight = key
|
|
266
|
+
|
|
267
|
+
const rim = new THREE.DirectionalLight(rig.rim.color, rig.rim.intensity)
|
|
268
|
+
rim.name = 'theme-rim'
|
|
269
|
+
rim.position.set(...rig.rim.position)
|
|
270
|
+
this.lightRoot.add(hemisphere, key, key.target, rim)
|
|
271
|
+
|
|
272
|
+
const practical = theme.lighting.practical
|
|
273
|
+
if (!practical) return
|
|
274
|
+
const lightCount = Math.max(1, Math.min(mobile ? 2 : 4, Math.round(practical.count)))
|
|
275
|
+
for (let index = 0; index < lightCount; index += 1) {
|
|
276
|
+
const light = new THREE.PointLight(practical.color, practical.intensity, practical.distance, practical.decay)
|
|
277
|
+
light.name = `theme-practical-${index + 1}`
|
|
278
|
+
light.castShadow = false
|
|
279
|
+
this.practicalLights.push(light)
|
|
280
|
+
this.lightRoot.add(light)
|
|
281
|
+
}
|
|
282
|
+
this.refreshPracticalLightPositions(practical)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private updateKeyShadowFocus(camera: THREE.Camera, focusTarget?: THREE.Vector3): boolean {
|
|
286
|
+
if (!this.keyLight) return false
|
|
287
|
+
computeShadowFocus(camera, this.lightingRig, this.nextShadowFocus, focusTarget, this.keyLight.shadow.mapSize.width)
|
|
288
|
+
const recenterDistance = this.lightingRig.shadow.extent * 0.18
|
|
289
|
+
if (
|
|
290
|
+
this.shadowFocusInitialized &&
|
|
291
|
+
this.nextShadowFocus.distanceToSquared(this.shadowFocus) < recenterDistance * recenterDistance
|
|
292
|
+
) {
|
|
293
|
+
return false
|
|
294
|
+
}
|
|
295
|
+
this.shadowFocus.copy(this.nextShadowFocus)
|
|
296
|
+
this.shadowFocusInitialized = true
|
|
297
|
+
this.keyLight.target.position.copy(this.shadowFocus)
|
|
298
|
+
this.keyLight.position.copy(this.shadowFocus).add(this.keyOffset)
|
|
299
|
+
this.keyLight.target.updateMatrixWorld()
|
|
300
|
+
return true
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private applyDynamicShadowBudget(): void {
|
|
304
|
+
if (this.dynamicShadowBudgetApplied || !this.keyLight) return
|
|
305
|
+
this.dynamicShadowBudgetApplied = true
|
|
306
|
+
const shadow = this.keyLight.shadow
|
|
307
|
+
shadow.map?.dispose()
|
|
308
|
+
shadow.map = null
|
|
309
|
+
shadow.mapSize.set(this.desiredShadowMapSize(true), this.desiredShadowMapSize(true))
|
|
310
|
+
this.requestShadowUpdate()
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private restoreStaticShadowBudget(): void {
|
|
314
|
+
if (!this.dynamicShadowBudgetApplied || !this.keyLight) return
|
|
315
|
+
this.dynamicShadowBudgetApplied = false
|
|
316
|
+
const shadow = this.keyLight.shadow
|
|
317
|
+
shadow.map?.dispose()
|
|
318
|
+
shadow.map = null
|
|
319
|
+
const mapSize = this.desiredShadowMapSize(false)
|
|
320
|
+
shadow.mapSize.set(mapSize, mapSize)
|
|
321
|
+
this.requestShadowUpdate()
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private applyParticleBudget(): void {
|
|
325
|
+
this.world?.root.traverse(object => {
|
|
326
|
+
if (!(object instanceof THREE.Points)) return
|
|
327
|
+
const count = object.geometry.getAttribute('position')?.count ?? 0
|
|
328
|
+
object.geometry.setDrawRange(0, Math.max(0, Math.round(count * this.visualQualityPolicy.particleScale)))
|
|
329
|
+
})
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
private desiredShadowMapSize(dynamic: boolean): number {
|
|
333
|
+
if (dynamic) return this.visualQualityPolicy.shadowMapSize
|
|
334
|
+
return this.visualQualityPolicy.tier === 'performance'
|
|
335
|
+
? Math.min(this.lightingRig.shadow.mapSize, this.visualQualityPolicy.shadowMapSize)
|
|
336
|
+
: this.lightingRig.shadow.mapSize
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private setKeyShadowMapSize(mapSize: number): void {
|
|
340
|
+
if (!this.keyLight) return
|
|
341
|
+
const shadow = this.keyLight.shadow
|
|
342
|
+
if (shadow.mapSize.width === mapSize && shadow.mapSize.height === mapSize) return
|
|
343
|
+
shadow.map?.dispose()
|
|
344
|
+
shadow.map = null
|
|
345
|
+
shadow.mapSize.set(mapSize, mapSize)
|
|
346
|
+
this.requestShadowUpdate()
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private refreshPracticalLightPositions(
|
|
350
|
+
practical: TrackThemeDefinition['lighting']['practical'] = this.activeTheme.lighting.practical,
|
|
351
|
+
): void {
|
|
352
|
+
if (!practical || this.practicalLights.length === 0) return
|
|
353
|
+
const positions = distributePracticalLightPositions(
|
|
354
|
+
this.collectTrackLightCandidates(),
|
|
355
|
+
this.practicalLights.length,
|
|
356
|
+
practical.heightOffset,
|
|
357
|
+
)
|
|
358
|
+
for (const [index, light] of this.practicalLights.entries()) {
|
|
359
|
+
light.position.copy(positions[index] ?? new THREE.Vector3(0, practical.heightOffset, 0))
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
private collectTrackLightCandidates(): THREE.Vector3[] {
|
|
364
|
+
const pieceBounds = new Map<string, THREE.Box3>()
|
|
365
|
+
const ungroupedBounds: THREE.Box3[] = []
|
|
366
|
+
|
|
367
|
+
for (const root of this.scene.children) {
|
|
368
|
+
if (root === this.lightRoot || root === this.world?.root) continue
|
|
369
|
+
root.updateWorldMatrix(true, true)
|
|
370
|
+
root.traverseVisible(object => {
|
|
371
|
+
if (!(object instanceof THREE.Mesh) || !hasPracticalFloorMaterial(object.material)) return
|
|
372
|
+
const geometryBounds = object.geometry.boundingBox
|
|
373
|
+
if (!geometryBounds) object.geometry.computeBoundingBox()
|
|
374
|
+
if (!object.geometry.boundingBox) return
|
|
375
|
+
const worldBounds = object.geometry.boundingBox.clone().applyMatrix4(object.matrixWorld)
|
|
376
|
+
const pieceId = object.userData.pieceInstanceId
|
|
377
|
+
if (typeof pieceId === 'string') {
|
|
378
|
+
const bounds = pieceBounds.get(pieceId)
|
|
379
|
+
if (bounds) bounds.union(worldBounds)
|
|
380
|
+
else pieceBounds.set(pieceId, worldBounds)
|
|
381
|
+
} else {
|
|
382
|
+
ungroupedBounds.push(worldBounds)
|
|
383
|
+
}
|
|
384
|
+
})
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return [...pieceBounds.values(), ...ungroupedBounds].map(bounds => bounds.getCenter(new THREE.Vector3()))
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private assertActive(): void {
|
|
391
|
+
if (this.disposed) throw new Error('SceneThemeController has been disposed.')
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function hasPracticalFloorMaterial(material: THREE.Material | THREE.Material[]): boolean {
|
|
396
|
+
const materials = Array.isArray(material) ? material : [material]
|
|
397
|
+
return materials.some(
|
|
398
|
+
candidate =>
|
|
399
|
+
candidate instanceof THREE.MeshStandardMaterial &&
|
|
400
|
+
candidate.emissiveMap instanceof THREE.Texture &&
|
|
401
|
+
candidate.emissiveIntensity > 0,
|
|
402
|
+
)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export function distributePracticalLightPositions(
|
|
406
|
+
candidates: readonly THREE.Vector3[],
|
|
407
|
+
count: number,
|
|
408
|
+
heightOffset: number,
|
|
409
|
+
): THREE.Vector3[] {
|
|
410
|
+
const safeCount = Math.max(0, Math.round(count))
|
|
411
|
+
if (safeCount === 0) return []
|
|
412
|
+
if (candidates.length === 0) {
|
|
413
|
+
return Array.from({ length: safeCount }, (_, index) => {
|
|
414
|
+
const angle = (index / safeCount) * Math.PI * 2 + Math.PI / 4
|
|
415
|
+
return new THREE.Vector3(Math.cos(angle) * 5, heightOffset, Math.sin(angle) * 5)
|
|
416
|
+
})
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const bounds = new THREE.Box3().setFromPoints([...candidates])
|
|
420
|
+
const size = bounds.getSize(new THREE.Vector3())
|
|
421
|
+
const layouts: readonly (readonly [number, number])[][] = [
|
|
422
|
+
[[0.5, 0.5]],
|
|
423
|
+
[
|
|
424
|
+
[0.25, 0.3],
|
|
425
|
+
[0.75, 0.7],
|
|
426
|
+
],
|
|
427
|
+
[
|
|
428
|
+
[0.2, 0.25],
|
|
429
|
+
[0.8, 0.3],
|
|
430
|
+
[0.5, 0.78],
|
|
431
|
+
],
|
|
432
|
+
[
|
|
433
|
+
[0.2, 0.22],
|
|
434
|
+
[0.78, 0.28],
|
|
435
|
+
[0.28, 0.76],
|
|
436
|
+
[0.82, 0.8],
|
|
437
|
+
],
|
|
438
|
+
]
|
|
439
|
+
const layout = layouts[Math.min(safeCount, layouts.length) - 1]!
|
|
440
|
+
const available = candidates.map(candidate => candidate.clone())
|
|
441
|
+
const positions: THREE.Vector3[] = []
|
|
442
|
+
|
|
443
|
+
for (let index = 0; index < safeCount; index += 1) {
|
|
444
|
+
const normalized = layout[index % layout.length]!
|
|
445
|
+
const targetX = bounds.min.x + size.x * normalized[0]
|
|
446
|
+
const targetZ = bounds.min.z + size.z * normalized[1]
|
|
447
|
+
let nearestIndex = 0
|
|
448
|
+
let nearestDistance = Number.POSITIVE_INFINITY
|
|
449
|
+
for (const [candidateIndex, candidate] of available.entries()) {
|
|
450
|
+
const distance = Math.hypot(candidate.x - targetX, candidate.z - targetZ)
|
|
451
|
+
if (distance < nearestDistance) {
|
|
452
|
+
nearestIndex = candidateIndex
|
|
453
|
+
nearestDistance = distance
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
const candidate = available.splice(nearestIndex, 1)[0] ?? candidates[index % candidates.length]!.clone()
|
|
457
|
+
positions.push(candidate.add(new THREE.Vector3(0, heightOffset, 0)))
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return positions
|
|
461
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import * as THREE from 'three'
|
|
2
|
+
|
|
3
|
+
type ShadowMaterial = THREE.MeshDepthMaterial | THREE.MeshDistanceMaterial
|
|
4
|
+
type Caster = THREE.Mesh<THREE.BufferGeometry, THREE.Material | THREE.Material[]>
|
|
5
|
+
|
|
6
|
+
/** Shader-only preparation; never draws a scene or copies/uploads track geometry. */
|
|
7
|
+
export class ShadowShaderPreparation {
|
|
8
|
+
private retained = new Set<ShadowMaterial>()
|
|
9
|
+
private generation = 0
|
|
10
|
+
private disposed = false
|
|
11
|
+
|
|
12
|
+
async prepare(
|
|
13
|
+
renderer: THREE.WebGLRenderer,
|
|
14
|
+
root: THREE.Object3D,
|
|
15
|
+
camera: THREE.Camera,
|
|
16
|
+
scene: THREE.Scene,
|
|
17
|
+
yieldWork: () => Promise<void> = () => new Promise(resolve => setTimeout(resolve, 0)),
|
|
18
|
+
): Promise<void> {
|
|
19
|
+
if (this.disposed) return
|
|
20
|
+
const generation = ++this.generation
|
|
21
|
+
if (!renderer.shadowMap.enabled) {
|
|
22
|
+
this.releaseRetained()
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
const lighting = new THREE.Scene()
|
|
26
|
+
let directional = false
|
|
27
|
+
let point = false
|
|
28
|
+
// Shadow draws use the current light configuration, but no scene fog or
|
|
29
|
+
// environment. Copies keep the actual light hierarchy entirely untouched.
|
|
30
|
+
const lights = new Set<THREE.Light>()
|
|
31
|
+
const collectLight = (object: THREE.Object3D): void => {
|
|
32
|
+
if (!(object instanceof THREE.Light) || !object.layers.test(camera.layers)) return
|
|
33
|
+
if (lights.has(object)) return
|
|
34
|
+
lights.add(object)
|
|
35
|
+
lighting.add(object.clone(false))
|
|
36
|
+
if (object.castShadow) {
|
|
37
|
+
if (object instanceof THREE.PointLight) point = true
|
|
38
|
+
else directional = true
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
scene.traverseVisible(collectLight)
|
|
42
|
+
// A loading cover can hide the prepared track root. Its visible children
|
|
43
|
+
// will contribute lights when the track is revealed; compile that state.
|
|
44
|
+
collectLight(root)
|
|
45
|
+
root.children.forEach(child => child.traverseVisible(collectLight))
|
|
46
|
+
if (!directional && !point) {
|
|
47
|
+
this.releaseRetained()
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const retained = new Set<ShadowMaterial>()
|
|
52
|
+
const depthCache = new Map<THREE.Material, ShadowMaterial>()
|
|
53
|
+
const distanceCache = new Map<THREE.Material, ShadowMaterial>()
|
|
54
|
+
const proxies: THREE.Object3D[] = []
|
|
55
|
+
scene.traverse(object => {
|
|
56
|
+
if (!(object instanceof THREE.Mesh) || !object.castShadow) return
|
|
57
|
+
const caster = object as Caster
|
|
58
|
+
for (const distance of point ? (directional ? [false, true] : [true]) : [false]) {
|
|
59
|
+
const cache = distance ? distanceCache : depthCache
|
|
60
|
+
const custom = distance ? caster.customDistanceMaterial : caster.customDepthMaterial
|
|
61
|
+
const materials = (Array.isArray(caster.material) ? caster.material : [caster.material]).map(source => {
|
|
62
|
+
// Custom shadow shaders are object-specific; ordinary casters can
|
|
63
|
+
// share one preparation material for each source material.
|
|
64
|
+
let material = custom ? undefined : cache.get(source)
|
|
65
|
+
if (!material) {
|
|
66
|
+
material = custom
|
|
67
|
+
? (custom.clone() as ShadowMaterial)
|
|
68
|
+
: distance
|
|
69
|
+
? new THREE.MeshDistanceMaterial()
|
|
70
|
+
: new THREE.MeshDepthMaterial()
|
|
71
|
+
if (custom) {
|
|
72
|
+
material.onBeforeCompile = custom.onBeforeCompile
|
|
73
|
+
material.customProgramCacheKey = custom.customProgramCacheKey
|
|
74
|
+
}
|
|
75
|
+
configureShadowMaterial(material, source, renderer.shadowMap.type)
|
|
76
|
+
retained.add(material)
|
|
77
|
+
if (!custom) cache.set(source, material)
|
|
78
|
+
}
|
|
79
|
+
return material
|
|
80
|
+
})
|
|
81
|
+
// A read-only view preserves Three's instancing, batching, morph and
|
|
82
|
+
// skinning flags without cloning their buffers or reparenting objects.
|
|
83
|
+
const proxy = Object.create(caster) as Caster
|
|
84
|
+
proxy.material = Array.isArray(caster.material) ? materials : materials[0]!
|
|
85
|
+
proxy.children = []
|
|
86
|
+
proxies.push(proxy)
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// compileAsync derives the output colour space from the active target.
|
|
91
|
+
// A single temporary pixel matches a shadow pass without allocating a
|
|
92
|
+
// shadow-sized buffer, uploading geometry, or rendering hidden views.
|
|
93
|
+
const target = new THREE.WebGLRenderTarget(1, 1, { depthBuffer: false })
|
|
94
|
+
let committed = false
|
|
95
|
+
try {
|
|
96
|
+
for (let offset = 0; offset < proxies.length; offset += 128) {
|
|
97
|
+
if (this.disposed || generation !== this.generation) return
|
|
98
|
+
const batch = new THREE.Group()
|
|
99
|
+
batch.children = proxies.slice(offset, offset + 128)
|
|
100
|
+
const previous = renderer.getRenderTarget()
|
|
101
|
+
const face = renderer.getActiveCubeFace()
|
|
102
|
+
const mip = renderer.getActiveMipmapLevel()
|
|
103
|
+
let pending: Promise<THREE.Object3D>
|
|
104
|
+
try {
|
|
105
|
+
renderer.setRenderTarget(target)
|
|
106
|
+
pending = renderer.compileAsync(batch, camera, lighting)
|
|
107
|
+
} finally {
|
|
108
|
+
// Restore synchronously: the shared lobby may render while shader
|
|
109
|
+
// completion is awaited. Never leave it pointed at the scratch pixel.
|
|
110
|
+
renderer.setRenderTarget(previous, face, mip)
|
|
111
|
+
}
|
|
112
|
+
await pending
|
|
113
|
+
if (offset + 128 < proxies.length) await yieldWork()
|
|
114
|
+
}
|
|
115
|
+
if (this.disposed || generation !== this.generation) return
|
|
116
|
+
// Hold program references until replacement/disposal; disposing them at
|
|
117
|
+
// the end of preparation would evict shaders not yet used by a shadow.
|
|
118
|
+
this.retained.forEach(material => material.dispose())
|
|
119
|
+
this.retained = retained
|
|
120
|
+
committed = true
|
|
121
|
+
} finally {
|
|
122
|
+
target.dispose()
|
|
123
|
+
if (!committed) retained.forEach(material => material.dispose())
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
dispose(): void {
|
|
128
|
+
this.disposed = true
|
|
129
|
+
this.generation++
|
|
130
|
+
this.releaseRetained()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private releaseRetained(): void {
|
|
134
|
+
this.retained.forEach(material => material.dispose())
|
|
135
|
+
this.retained.clear()
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function configureShadowMaterial(target: ShadowMaterial, source: THREE.Material, type: THREE.ShadowMapType): void {
|
|
140
|
+
// Mirrors WebGLShadowMap.getDepthMaterial in pinned Three 0.185.1. Keep
|
|
141
|
+
// alpha/displacement/clipping and side variants identical to real shadows.
|
|
142
|
+
const material = source as THREE.MeshStandardMaterial
|
|
143
|
+
target.side =
|
|
144
|
+
source.shadowSide ??
|
|
145
|
+
(type === THREE.VSMShadowMap
|
|
146
|
+
? source.side
|
|
147
|
+
: source.side === THREE.FrontSide
|
|
148
|
+
? THREE.BackSide
|
|
149
|
+
: source.side === THREE.BackSide
|
|
150
|
+
? THREE.FrontSide
|
|
151
|
+
: THREE.DoubleSide)
|
|
152
|
+
target.map = material.map ?? null
|
|
153
|
+
target.alphaMap = material.alphaMap ?? null
|
|
154
|
+
target.alphaTest = source.alphaToCoverage ? 0.5 : source.alphaTest
|
|
155
|
+
target.displacementMap = material.displacementMap ?? null
|
|
156
|
+
target.displacementScale = material.displacementScale ?? 1
|
|
157
|
+
target.displacementBias = material.displacementBias ?? 0
|
|
158
|
+
target.clippingPlanes = source.clippingPlanes
|
|
159
|
+
target.clipShadows = source.clipShadows
|
|
160
|
+
target.clipIntersection = source.clipIntersection
|
|
161
|
+
if (target instanceof THREE.MeshDepthMaterial) target.wireframe = material.wireframe ?? false
|
|
162
|
+
}
|