@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,275 @@
|
|
|
1
|
+
import { geometryDetailModelUrl } from '../../../three/geometry-detail'
|
|
2
|
+
import * as THREE from 'three'
|
|
3
|
+
|
|
4
|
+
import { VisualTubeGeometry } from '../../../three/visual-geometry'
|
|
5
|
+
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
|
6
|
+
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
|
7
|
+
|
|
8
|
+
import { PORTAL_FRAME_MAX_HALF_WIDTH, PORTAL_RIFT_HALF_HEIGHT, PORTAL_RIFT_HALF_WIDTH } from './portal-proportions'
|
|
9
|
+
|
|
10
|
+
export const SPACE_STATION_PORTAL_FRAME_THEME_ID = 'intergalactic-space-station'
|
|
11
|
+
export const SPACE_STATION_PORTAL_FRAME_MODEL_URL =
|
|
12
|
+
'/models/portals/space-station-projector-portal-v1.glb?revision=projector-arms-v17-narrow-articulated-tentacles'
|
|
13
|
+
|
|
14
|
+
export interface SpaceStationPortalFrameMaterials {
|
|
15
|
+
chassis: THREE.Material
|
|
16
|
+
panel: THREE.Material
|
|
17
|
+
rib: THREE.Material
|
|
18
|
+
trim: THREE.Material
|
|
19
|
+
light: THREE.Material
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const FRAME_STYLE = 'space-station-curved-projector-arms'
|
|
23
|
+
const RIFT_CENTER_Y = -0.14
|
|
24
|
+
const RIFT_FIT_SCALE = 0.9
|
|
25
|
+
const PROJECTOR_POINTS = [
|
|
26
|
+
[1.4976, -0.9],
|
|
27
|
+
[1.6512, -0.28],
|
|
28
|
+
[1.6224, 0.48],
|
|
29
|
+
[1.3728, 1.16],
|
|
30
|
+
] as const
|
|
31
|
+
const PROJECTOR_VERTICAL_AIM = [0.18, 0.14, -0.16, -0.24] as const
|
|
32
|
+
const MODEL_ROLES = ['CHASSIS', 'PANEL', 'RIB', 'TRIM', 'LIGHT'] as const
|
|
33
|
+
type ModelRole = (typeof MODEL_ROLES)[number]
|
|
34
|
+
|
|
35
|
+
let modelTemplatePromise: Promise<THREE.Object3D> | undefined
|
|
36
|
+
const MODEL_PLACEHOLDER_MATERIAL = new THREE.MeshBasicMaterial({ visible: false })
|
|
37
|
+
|
|
38
|
+
function markVisualOnly(object: THREE.Object3D, role: string): void {
|
|
39
|
+
object.userData.themeOnly = SPACE_STATION_PORTAL_FRAME_THEME_ID
|
|
40
|
+
object.userData.themeRole = role
|
|
41
|
+
object.userData.decorative = true
|
|
42
|
+
object.userData.visualOnly = true
|
|
43
|
+
object.userData.portalFrameStyle = FRAME_STYLE
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function roleFromName(name: string): ModelRole | undefined {
|
|
47
|
+
return MODEL_ROLES.find(role => name.toUpperCase().includes(`SS_${role}`))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function loadModelTemplate(): Promise<THREE.Object3D> {
|
|
51
|
+
modelTemplatePromise ??= new GLTFLoader().loadAsync(geometryDetailModelUrl(SPACE_STATION_PORTAL_FRAME_MODEL_URL)).then(gltf => {
|
|
52
|
+
gltf.scene.name = 'space-station-projector-portal-template'
|
|
53
|
+
gltf.scene.traverse(object => {
|
|
54
|
+
if (!(object instanceof THREE.Mesh)) return
|
|
55
|
+
const materials = Array.isArray(object.material) ? object.material : [object.material]
|
|
56
|
+
materials.forEach(material => material.dispose())
|
|
57
|
+
object.material = MODEL_PLACEHOLDER_MATERIAL
|
|
58
|
+
})
|
|
59
|
+
return gltf.scene
|
|
60
|
+
})
|
|
61
|
+
return modelTemplatePromise
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function createPairLightMaterial(source: THREE.Material, color: string): THREE.Material {
|
|
65
|
+
const material = source.clone()
|
|
66
|
+
if (material instanceof THREE.MeshStandardMaterial) {
|
|
67
|
+
const pairColor = new THREE.Color(color)
|
|
68
|
+
material.color.copy(pairColor)
|
|
69
|
+
material.emissive.copy(pairColor)
|
|
70
|
+
material.emissiveIntensity = Math.max(3, material.emissiveIntensity)
|
|
71
|
+
}
|
|
72
|
+
material.name = `space-station-projector-light-${color}`
|
|
73
|
+
return material
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const PROJECTED_ENERGY_VERTEX_SHADER = /* glsl */ `
|
|
77
|
+
varying vec2 vBeamUv;
|
|
78
|
+
void main() {
|
|
79
|
+
vBeamUv = uv;
|
|
80
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
81
|
+
}
|
|
82
|
+
`
|
|
83
|
+
|
|
84
|
+
const PROJECTED_ENERGY_FRAGMENT_SHADER = /* glsl */ `
|
|
85
|
+
uniform float uTime;
|
|
86
|
+
uniform vec3 uColor;
|
|
87
|
+
uniform float uCore;
|
|
88
|
+
varying vec2 vBeamUv;
|
|
89
|
+
void main() {
|
|
90
|
+
float travel = 0.78 + 0.22 * sin(vBeamUv.x * 22.0 - uTime * 7.0);
|
|
91
|
+
float longitudinalFade = smoothstep(0.0, 0.08, vBeamUv.x) * (1.0 - smoothstep(0.82, 1.0, vBeamUv.x));
|
|
92
|
+
float intensity = travel * longitudinalFade;
|
|
93
|
+
vec3 hotColor = mix(uColor, vec3(1.0), uCore * 0.78);
|
|
94
|
+
float layerStrength = mix(1.18, 1.62, uCore);
|
|
95
|
+
float layerAlpha = mix(0.50, 0.98, uCore);
|
|
96
|
+
gl_FragColor = vec4(hotColor * intensity * layerStrength, intensity * layerAlpha);
|
|
97
|
+
}
|
|
98
|
+
`
|
|
99
|
+
|
|
100
|
+
function createProjectedEnergyMaterial(name: string, color: string, core: boolean): THREE.ShaderMaterial {
|
|
101
|
+
const material = new THREE.ShaderMaterial({
|
|
102
|
+
uniforms: {
|
|
103
|
+
uTime: { value: 0 },
|
|
104
|
+
uColor: { value: new THREE.Color(color) },
|
|
105
|
+
uCore: { value: core ? 1 : 0 },
|
|
106
|
+
},
|
|
107
|
+
vertexShader: PROJECTED_ENERGY_VERTEX_SHADER,
|
|
108
|
+
fragmentShader: PROJECTED_ENERGY_FRAGMENT_SHADER,
|
|
109
|
+
transparent: true,
|
|
110
|
+
depthWrite: false,
|
|
111
|
+
blending: THREE.AdditiveBlending,
|
|
112
|
+
toneMapped: false,
|
|
113
|
+
})
|
|
114
|
+
material.name = name
|
|
115
|
+
return material
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function riftBoundaryX(y: number): number {
|
|
119
|
+
const normalizedY = THREE.MathUtils.clamp(
|
|
120
|
+
(y - RIFT_CENTER_Y) / (PORTAL_RIFT_HALF_HEIGHT * RIFT_FIT_SCALE),
|
|
121
|
+
-0.995,
|
|
122
|
+
0.995,
|
|
123
|
+
)
|
|
124
|
+
const angle = Math.asin(normalizedY)
|
|
125
|
+
const variation =
|
|
126
|
+
Math.sin(angle * 3 + 0.7) * 0.038 +
|
|
127
|
+
Math.sin(angle * 7 - 0.4) * 0.022 +
|
|
128
|
+
Math.sin(angle * 13 + 1.2) * 0.015 +
|
|
129
|
+
Math.sin(angle * 19 - 0.3) * 0.009 +
|
|
130
|
+
Math.sin(angle * 31) * 0.006
|
|
131
|
+
return PORTAL_RIFT_HALF_WIDTH * Math.sqrt(1 - normalizedY * normalizedY) * (1 + variation) * RIFT_FIT_SCALE
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function createProjectedRiftBeams(name: string, color: string): THREE.Group {
|
|
135
|
+
const group = new THREE.Group()
|
|
136
|
+
group.name = `${name}-projected-rift-beams`
|
|
137
|
+
const auraParts: THREE.BufferGeometry[] = []
|
|
138
|
+
const coreParts: THREE.BufferGeometry[] = []
|
|
139
|
+
for (const side of [-1, 1] as const) {
|
|
140
|
+
for (const [index, [emitterX, emitterY]] of PROJECTOR_POINTS.entries()) {
|
|
141
|
+
const targetY = emitterY + PROJECTOR_VERTICAL_AIM[index]!
|
|
142
|
+
const start = new THREE.Vector3(side * (emitterX - 0.245), emitterY, 0.02)
|
|
143
|
+
const boundary = Math.max(0.18, riftBoundaryX(targetY) - 0.055)
|
|
144
|
+
const end = new THREE.Vector3(side * boundary, targetY, 0.04)
|
|
145
|
+
const curve = new THREE.LineCurve3(start, end)
|
|
146
|
+
auraParts.push(new VisualTubeGeometry(curve, 5, 0.045, 12, false))
|
|
147
|
+
coreParts.push(new VisualTubeGeometry(curve, 5, 0.009, 10, false))
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const auraGeometry = mergeGeometries(auraParts, false)
|
|
151
|
+
const coreGeometry = mergeGeometries(coreParts, false)
|
|
152
|
+
auraParts.forEach(geometry => geometry.dispose())
|
|
153
|
+
coreParts.forEach(geometry => geometry.dispose())
|
|
154
|
+
if (!auraGeometry || !coreGeometry) return group
|
|
155
|
+
const aura = new THREE.Mesh(auraGeometry, createProjectedEnergyMaterial(`${name}-beam-aura-material`, color, false))
|
|
156
|
+
const core = new THREE.Mesh(coreGeometry, createProjectedEnergyMaterial(`${name}-beam-core-material`, color, true))
|
|
157
|
+
aura.name = `${name}-projected-beam-aura`
|
|
158
|
+
core.name = `${name}-projected-beam-core`
|
|
159
|
+
for (const layer of [aura, core]) {
|
|
160
|
+
layer.renderOrder = 4
|
|
161
|
+
layer.frustumCulled = false
|
|
162
|
+
layer.userData.ownsMaterial = true
|
|
163
|
+
layer.userData.portalPairColor = color
|
|
164
|
+
layer.userData.portalLightRole = 'rift-matched-projection-beam'
|
|
165
|
+
markVisualOnly(layer, 'space-station-projected-rift-beam')
|
|
166
|
+
group.add(layer)
|
|
167
|
+
}
|
|
168
|
+
markVisualOnly(group, 'space-station-projected-rift-beams')
|
|
169
|
+
return group
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function attachModel(
|
|
173
|
+
frame: THREE.Group,
|
|
174
|
+
name: string,
|
|
175
|
+
color: string,
|
|
176
|
+
materials: SpaceStationPortalFrameMaterials,
|
|
177
|
+
template: THREE.Object3D,
|
|
178
|
+
): void {
|
|
179
|
+
const model = template.clone(true)
|
|
180
|
+
model.name = `${name}-space-station-projector-model`
|
|
181
|
+
const pairLight = createPairLightMaterial(materials.light, color)
|
|
182
|
+
let lightMaterialUsed = false
|
|
183
|
+
let loadedMeshCount = 0
|
|
184
|
+
|
|
185
|
+
model.traverse(object => {
|
|
186
|
+
markVisualOnly(object, 'space-station-projector-model')
|
|
187
|
+
if (!(object instanceof THREE.Mesh)) return
|
|
188
|
+
const role = roleFromName(object.name)
|
|
189
|
+
if (!role) {
|
|
190
|
+
object.visible = false
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
loadedMeshCount += 1
|
|
194
|
+
object.material =
|
|
195
|
+
role === 'CHASSIS'
|
|
196
|
+
? materials.chassis
|
|
197
|
+
: role === 'PANEL'
|
|
198
|
+
? materials.panel
|
|
199
|
+
: role === 'RIB'
|
|
200
|
+
? materials.rib
|
|
201
|
+
: role === 'TRIM'
|
|
202
|
+
? materials.trim
|
|
203
|
+
: pairLight
|
|
204
|
+
object.castShadow = role !== 'LIGHT'
|
|
205
|
+
object.receiveShadow = role !== 'LIGHT'
|
|
206
|
+
object.frustumCulled = true
|
|
207
|
+
object.userData.sharedGeometry = true
|
|
208
|
+
if (role === 'LIGHT') {
|
|
209
|
+
lightMaterialUsed = true
|
|
210
|
+
object.userData.portalPairColor = color
|
|
211
|
+
object.userData.portalLightRole = 'projector-lenses'
|
|
212
|
+
object.userData.ownsMaterial = true
|
|
213
|
+
}
|
|
214
|
+
markVisualOnly(object, `space-station-${role.toLowerCase()}`)
|
|
215
|
+
})
|
|
216
|
+
frame.userData.loadedMeshCount = loadedMeshCount
|
|
217
|
+
if (!lightMaterialUsed) pairLight.dispose()
|
|
218
|
+
frame.add(model)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Adds only the station-local projector arms. Shared aperture, rift, glow and dust stay global. */
|
|
222
|
+
export function addSpaceStationPortalFrame(
|
|
223
|
+
root: THREE.Group,
|
|
224
|
+
name: string,
|
|
225
|
+
color: string,
|
|
226
|
+
materials: SpaceStationPortalFrameMaterials,
|
|
227
|
+
modelTemplate?: THREE.Object3D,
|
|
228
|
+
): THREE.Group {
|
|
229
|
+
const frame = new THREE.Group()
|
|
230
|
+
frame.name = `${name}-space-station-frame`
|
|
231
|
+
frame.userData.portalApertureClearHalfWidth = 1.0
|
|
232
|
+
frame.userData.portalSupportMaxHalfWidth = PORTAL_FRAME_MAX_HALF_WIDTH
|
|
233
|
+
frame.userData.portalSupportOuterHalfWidth = 1.9
|
|
234
|
+
frame.userData.drawCallBudget = 7
|
|
235
|
+
frame.userData.modelUrl = SPACE_STATION_PORTAL_FRAME_MODEL_URL
|
|
236
|
+
frame.userData.portalMechanism = 'two-curved-projector-arms'
|
|
237
|
+
markVisualOnly(frame, 'space-station-curved-projector-arms')
|
|
238
|
+
|
|
239
|
+
const pointLight = new THREE.PointLight(color, 0.68, 4.0, 2)
|
|
240
|
+
pointLight.name = `${name}-space-station-projector-light`
|
|
241
|
+
pointLight.position.set(0, 0.15, 0.55)
|
|
242
|
+
pointLight.userData.portalPairColor = color
|
|
243
|
+
pointLight.userData.portalLightRole = 'projected-rift-source'
|
|
244
|
+
markVisualOnly(pointLight, 'projected-rift-source')
|
|
245
|
+
frame.add(pointLight)
|
|
246
|
+
frame.add(createProjectedRiftBeams(name, color))
|
|
247
|
+
root.add(frame)
|
|
248
|
+
|
|
249
|
+
if (modelTemplate) {
|
|
250
|
+
attachModel(frame, name, color, materials, modelTemplate)
|
|
251
|
+
frame.userData.modelReady = Promise.resolve(frame)
|
|
252
|
+
} else {
|
|
253
|
+
frame.userData.modelReady = loadModelTemplate()
|
|
254
|
+
.then(template => {
|
|
255
|
+
attachModel(frame, name, color, materials, template)
|
|
256
|
+
return frame
|
|
257
|
+
})
|
|
258
|
+
.catch(error => {
|
|
259
|
+
// A missing optional facade must not create an unhandled rejection in
|
|
260
|
+
// headless catalog audits or take down the shared portal/rift visual.
|
|
261
|
+
frame.userData.modelLoadError = error instanceof Error ? error.message : String(error)
|
|
262
|
+
modelTemplatePromise = undefined
|
|
263
|
+
return frame
|
|
264
|
+
})
|
|
265
|
+
}
|
|
266
|
+
return frame
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Projector arms are static; the globally shared rift remains the animated element. */
|
|
270
|
+
export function updateSpaceStationPortalFrame(frame: THREE.Group | undefined, timeSeconds: number): void {
|
|
271
|
+
frame?.traverse(object => {
|
|
272
|
+
if (!(object instanceof THREE.Mesh) || object.userData.portalLightRole !== 'rift-matched-projection-beam') return
|
|
273
|
+
if (object.material instanceof THREE.ShaderMaterial) object.material.uniforms.uTime!.value = timeSeconds
|
|
274
|
+
})
|
|
275
|
+
}
|