@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,497 @@
|
|
|
1
|
+
import * as THREE from 'three'
|
|
2
|
+
|
|
3
|
+
import { VisualCircleGeometry } from '../../../three/visual-geometry'
|
|
4
|
+
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
|
5
|
+
|
|
6
|
+
import { TRACK_RAIL_WALL_THICKNESS } from './track-definition'
|
|
7
|
+
import {
|
|
8
|
+
canonicalWallFrameAt,
|
|
9
|
+
type CanonicalWallRun,
|
|
10
|
+
type RawWallFrame,
|
|
11
|
+
} from './track-surface-topology'
|
|
12
|
+
|
|
13
|
+
export type SpaceStationWallPathFrame = RawWallFrame
|
|
14
|
+
export type SpaceStationWallPathRun = CanonicalWallRun
|
|
15
|
+
|
|
16
|
+
export interface SpaceStationWallMaterials {
|
|
17
|
+
chassis: THREE.Material
|
|
18
|
+
panel: THREE.Material
|
|
19
|
+
rib: THREE.Material
|
|
20
|
+
trim: THREE.Material
|
|
21
|
+
light: THREE.Material
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface BandBatch {
|
|
25
|
+
name: string
|
|
26
|
+
material: THREE.Material
|
|
27
|
+
geometries: THREE.BufferGeometry[]
|
|
28
|
+
castShadow: boolean
|
|
29
|
+
receiveShadow: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const BAY_TARGET = 3.2
|
|
33
|
+
const SAMPLE_SPACING = 0.9
|
|
34
|
+
const SAMPLE_MAXIMUM_TURN = THREE.MathUtils.degToRad(8)
|
|
35
|
+
const RIB_WIDTH = 0.2
|
|
36
|
+
const RIB_DEPTH = 0.03
|
|
37
|
+
const RIB_BACKING_WIDTH = 0.29
|
|
38
|
+
const RIB_BACKING_DEPTH = 0.024
|
|
39
|
+
const MAX_FACADE_LIFT = 0.029
|
|
40
|
+
const WORLD_UP = new THREE.Vector3(0, 1, 0)
|
|
41
|
+
|
|
42
|
+
/** Adds the visual-only, two-sided armored facade over the existing wall collider. */
|
|
43
|
+
export function addSpaceStationWallVisual(
|
|
44
|
+
root: THREE.Group,
|
|
45
|
+
id: string,
|
|
46
|
+
runs: readonly SpaceStationWallPathRun[],
|
|
47
|
+
wallHeight: number,
|
|
48
|
+
materials: SpaceStationWallMaterials,
|
|
49
|
+
): THREE.Group | undefined {
|
|
50
|
+
if (wallHeight <= 0.18) return undefined
|
|
51
|
+
const group = new THREE.Group()
|
|
52
|
+
group.name = `${id}-space-station-armored-wall`
|
|
53
|
+
group.userData.themeOnly = 'intergalactic-space-station'
|
|
54
|
+
group.userData.decorative = true
|
|
55
|
+
group.userData.visualOnly = true
|
|
56
|
+
group.userData.appearance = 'space-station-armored-wall'
|
|
57
|
+
|
|
58
|
+
const ribMatrices: THREE.Matrix4[] = []
|
|
59
|
+
const ribBackingMatrices: THREE.Matrix4[] = []
|
|
60
|
+
const ribSpineMatrices: THREE.Matrix4[] = []
|
|
61
|
+
const clampMatrices: THREE.Matrix4[] = []
|
|
62
|
+
const headerPlateMatrices: THREE.Matrix4[] = []
|
|
63
|
+
const boltMatrices: THREE.Matrix4[] = []
|
|
64
|
+
const panelBands: BandBatch = {
|
|
65
|
+
name: `${id}-space-wall-panel-batch`,
|
|
66
|
+
material: materials.panel,
|
|
67
|
+
geometries: [],
|
|
68
|
+
castShadow: true,
|
|
69
|
+
receiveShadow: true,
|
|
70
|
+
}
|
|
71
|
+
const chassisBands: BandBatch = {
|
|
72
|
+
name: `${id}-space-wall-chassis-batch`,
|
|
73
|
+
material: materials.chassis,
|
|
74
|
+
geometries: [],
|
|
75
|
+
castShadow: true,
|
|
76
|
+
receiveShadow: true,
|
|
77
|
+
}
|
|
78
|
+
const trimBands: BandBatch = {
|
|
79
|
+
name: `${id}-space-wall-trim-batch`,
|
|
80
|
+
material: materials.trim,
|
|
81
|
+
geometries: [],
|
|
82
|
+
castShadow: true,
|
|
83
|
+
receiveShadow: true,
|
|
84
|
+
}
|
|
85
|
+
const lightBands: BandBatch = {
|
|
86
|
+
name: `${id}-space-wall-light-batch`,
|
|
87
|
+
material: materials.light,
|
|
88
|
+
geometries: [],
|
|
89
|
+
castShadow: false,
|
|
90
|
+
receiveShadow: false,
|
|
91
|
+
}
|
|
92
|
+
let bayCount = 0
|
|
93
|
+
let runCount = 0
|
|
94
|
+
|
|
95
|
+
for (const run of runs) {
|
|
96
|
+
if (run.frames.length < 2) continue
|
|
97
|
+
const canonicalRun = run
|
|
98
|
+
const totalLength = canonicalRun.length
|
|
99
|
+
if (totalLength <= 0.08) continue
|
|
100
|
+
runCount += 1
|
|
101
|
+
const count = Math.max(1, Math.round(totalLength / BAY_TARGET))
|
|
102
|
+
const bayLength = totalLength / count
|
|
103
|
+
bayCount += count
|
|
104
|
+
const chassisFrames = sampleCanonicalRange(canonicalRun, 0, totalLength)
|
|
105
|
+
for (const face of [-1, 1] as const) {
|
|
106
|
+
queueBand(chassisBands, chassisFrames, face, 0.03, wallHeight - 0.01, 0.001)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
for (let bayIndex = 0; bayIndex < count; bayIndex += 1) {
|
|
110
|
+
const bayStart = bayIndex * bayLength
|
|
111
|
+
const bayEnd = (bayIndex + 1) * bayLength
|
|
112
|
+
const border = Math.min(0.11, bayLength * 0.12)
|
|
113
|
+
const panelFrames = sampleCanonicalRange(canonicalRun, bayStart + border, bayEnd - border)
|
|
114
|
+
const lightInset = Math.min(0.2, bayLength * 0.18)
|
|
115
|
+
const lightFrames = sampleCanonicalRange(
|
|
116
|
+
canonicalRun,
|
|
117
|
+
bayStart + border + lightInset,
|
|
118
|
+
bayEnd - border - lightInset,
|
|
119
|
+
)
|
|
120
|
+
for (const face of [-1, 1] as const) {
|
|
121
|
+
queueBand(
|
|
122
|
+
panelBands,
|
|
123
|
+
panelFrames,
|
|
124
|
+
face,
|
|
125
|
+
0.16,
|
|
126
|
+
wallHeight - 0.16,
|
|
127
|
+
0.006,
|
|
128
|
+
0.07,
|
|
129
|
+
)
|
|
130
|
+
for (const [, bottom, top, lift, material] of [
|
|
131
|
+
['footer', 0.07, 0.16, 0.008, materials.panel],
|
|
132
|
+
['lower-bezel', 0.15, 0.245, 0.012, materials.trim],
|
|
133
|
+
['upper-bezel', wallHeight - 0.245, wallHeight - 0.15, 0.012, materials.trim],
|
|
134
|
+
['header', wallHeight - 0.16, wallHeight - 0.07, 0.008, materials.panel],
|
|
135
|
+
] as const) {
|
|
136
|
+
queueBand(
|
|
137
|
+
material === materials.panel ? panelBands : trimBands,
|
|
138
|
+
panelFrames,
|
|
139
|
+
face,
|
|
140
|
+
bottom,
|
|
141
|
+
top,
|
|
142
|
+
lift,
|
|
143
|
+
0.035,
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
for (const center of [0.215, wallHeight - 0.215]) {
|
|
147
|
+
queueBand(
|
|
148
|
+
trimBands,
|
|
149
|
+
lightFrames,
|
|
150
|
+
face,
|
|
151
|
+
center - 0.035,
|
|
152
|
+
center + 0.035,
|
|
153
|
+
0.018,
|
|
154
|
+
0.012,
|
|
155
|
+
)
|
|
156
|
+
queueBand(
|
|
157
|
+
lightBands,
|
|
158
|
+
lightFrames,
|
|
159
|
+
face,
|
|
160
|
+
center - 0.014,
|
|
161
|
+
center + 0.014,
|
|
162
|
+
0.022,
|
|
163
|
+
0.007,
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
queueBand(
|
|
167
|
+
trimBands,
|
|
168
|
+
lightFrames,
|
|
169
|
+
face,
|
|
170
|
+
wallHeight - 0.132,
|
|
171
|
+
wallHeight - 0.118,
|
|
172
|
+
0.015,
|
|
173
|
+
0.006,
|
|
174
|
+
)
|
|
175
|
+
queueBand(
|
|
176
|
+
trimBands,
|
|
177
|
+
panelFrames,
|
|
178
|
+
face,
|
|
179
|
+
wallHeight - 0.072,
|
|
180
|
+
wallHeight - 0.035,
|
|
181
|
+
0.014,
|
|
182
|
+
0.014,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
const headerFrame = canonicalWallFrameAt(canonicalRun, (bayStart + bayEnd) / 2)
|
|
186
|
+
const headerOutward = horizontalWallOutward(
|
|
187
|
+
headerFrame,
|
|
188
|
+
headerFrame.outward.clone().multiplyScalar(face),
|
|
189
|
+
)
|
|
190
|
+
const headerRotation = verticalFrameQuaternion(headerFrame, headerOutward)
|
|
191
|
+
const headerTangent = horizontalFrameTangent(headerFrame, headerOutward)
|
|
192
|
+
const headerPosition = headerFrame.base
|
|
193
|
+
.clone()
|
|
194
|
+
.addScaledVector(WORLD_UP, wallHeight - 0.105)
|
|
195
|
+
.addScaledVector(headerOutward, TRACK_RAIL_WALL_THICKNESS / 2 + 0.023)
|
|
196
|
+
headerPlateMatrices.push(
|
|
197
|
+
new THREE.Matrix4().compose(headerPosition, headerRotation, new THREE.Vector3(0.16, 0.052, 0.008)),
|
|
198
|
+
)
|
|
199
|
+
for (const tangentOffset of [-0.052, 0.052]) {
|
|
200
|
+
const boltPosition = headerPosition
|
|
201
|
+
.clone()
|
|
202
|
+
.addScaledVector(headerTangent, tangentOffset)
|
|
203
|
+
.addScaledVector(headerOutward, 0.007)
|
|
204
|
+
const boltRotation = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), headerOutward)
|
|
205
|
+
boltMatrices.push(
|
|
206
|
+
new THREE.Matrix4().compose(boltPosition, boltRotation, new THREE.Vector3(0.72, 0.72, 0.72)),
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
for (let boundary = 0; boundary <= count; boundary += 1) {
|
|
213
|
+
const frame = canonicalWallFrameAt(canonicalRun, Math.min(totalLength, boundary * bayLength))
|
|
214
|
+
for (const face of [-1, 1] as const) {
|
|
215
|
+
const outward = horizontalWallOutward(frame, frame.outward.clone().multiplyScalar(face))
|
|
216
|
+
const rotation = verticalFrameQuaternion(frame, outward)
|
|
217
|
+
const ribHeight = wallHeight + 0.08
|
|
218
|
+
const position = frame.base
|
|
219
|
+
.clone()
|
|
220
|
+
.addScaledVector(WORLD_UP, ribHeight / 2 - 0.025)
|
|
221
|
+
.addScaledVector(outward, TRACK_RAIL_WALL_THICKNESS / 2 + RIB_DEPTH / 2 + 0.006)
|
|
222
|
+
ribMatrices.push(
|
|
223
|
+
new THREE.Matrix4().compose(position, rotation, new THREE.Vector3(1, ribHeight / wallHeight, 1)),
|
|
224
|
+
)
|
|
225
|
+
const backingPosition = frame.base
|
|
226
|
+
.clone()
|
|
227
|
+
.addScaledVector(WORLD_UP, ribHeight / 2 - 0.025)
|
|
228
|
+
.addScaledVector(outward, TRACK_RAIL_WALL_THICKNESS / 2 + RIB_BACKING_DEPTH / 2 + 0.002)
|
|
229
|
+
ribBackingMatrices.push(
|
|
230
|
+
new THREE.Matrix4().compose(backingPosition, rotation, new THREE.Vector3(1, ribHeight / wallHeight, 1)),
|
|
231
|
+
)
|
|
232
|
+
const spinePosition = frame.base
|
|
233
|
+
.clone()
|
|
234
|
+
.addScaledVector(WORLD_UP, ribHeight / 2 - 0.025)
|
|
235
|
+
.addScaledVector(outward, TRACK_RAIL_WALL_THICKNESS / 2 + MAX_FACADE_LIFT - 0.004)
|
|
236
|
+
ribSpineMatrices.push(
|
|
237
|
+
new THREE.Matrix4().compose(
|
|
238
|
+
spinePosition,
|
|
239
|
+
rotation,
|
|
240
|
+
new THREE.Vector3(0.075, Math.max(0.3, ribHeight - 0.46), 0.008),
|
|
241
|
+
),
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
for (const clampHeight of [0.2, wallHeight - 0.2]) {
|
|
245
|
+
const clampPosition = frame.base
|
|
246
|
+
.clone()
|
|
247
|
+
.addScaledVector(WORLD_UP, clampHeight)
|
|
248
|
+
.addScaledVector(outward, TRACK_RAIL_WALL_THICKNESS / 2 + MAX_FACADE_LIFT)
|
|
249
|
+
clampMatrices.push(
|
|
250
|
+
new THREE.Matrix4().compose(clampPosition, rotation, new THREE.Vector3(0.105, 0.12, 0.008)),
|
|
251
|
+
)
|
|
252
|
+
for (const boltOffset of [-0.045, 0.045]) {
|
|
253
|
+
const boltPosition = clampPosition
|
|
254
|
+
.clone()
|
|
255
|
+
.addScaledVector(WORLD_UP, boltOffset)
|
|
256
|
+
.addScaledVector(outward, 0.005)
|
|
257
|
+
const boltRotation = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), outward)
|
|
258
|
+
boltMatrices.push(new THREE.Matrix4().compose(boltPosition, boltRotation, new THREE.Vector3(1, 1, 1)))
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (runCount === 0) return undefined
|
|
266
|
+
addBandBatch(group, chassisBands)
|
|
267
|
+
addBandBatch(group, panelBands)
|
|
268
|
+
addBandBatch(group, trimBands)
|
|
269
|
+
addBandBatch(group, lightBands)
|
|
270
|
+
if (ribBackingMatrices.length > 0)
|
|
271
|
+
group.add(
|
|
272
|
+
instancedBatch(
|
|
273
|
+
`${id}-space-wall-rib-backings`,
|
|
274
|
+
createWrappedRibGeometry(wallHeight, RIB_BACKING_WIDTH, RIB_BACKING_DEPTH),
|
|
275
|
+
materials.trim,
|
|
276
|
+
ribBackingMatrices,
|
|
277
|
+
),
|
|
278
|
+
)
|
|
279
|
+
if (ribMatrices.length > 0)
|
|
280
|
+
group.add(
|
|
281
|
+
instancedBatch(
|
|
282
|
+
`${id}-space-wall-ribs`,
|
|
283
|
+
createWrappedRibGeometry(wallHeight, RIB_WIDTH, RIB_DEPTH),
|
|
284
|
+
materials.rib,
|
|
285
|
+
ribMatrices,
|
|
286
|
+
),
|
|
287
|
+
)
|
|
288
|
+
if (ribSpineMatrices.length > 0)
|
|
289
|
+
group.add(
|
|
290
|
+
instancedBatch(`${id}-space-wall-rib-spines`, new THREE.BoxGeometry(1, 1, 1), materials.panel, ribSpineMatrices),
|
|
291
|
+
)
|
|
292
|
+
if (clampMatrices.length > 0)
|
|
293
|
+
group.add(instancedBatch(`${id}-space-wall-clamps`, new THREE.BoxGeometry(1, 1, 1), materials.trim, clampMatrices))
|
|
294
|
+
if (headerPlateMatrices.length > 0)
|
|
295
|
+
group.add(
|
|
296
|
+
instancedBatch(
|
|
297
|
+
`${id}-space-wall-header-service-plates`,
|
|
298
|
+
new THREE.BoxGeometry(1, 1, 1),
|
|
299
|
+
materials.trim,
|
|
300
|
+
headerPlateMatrices,
|
|
301
|
+
),
|
|
302
|
+
)
|
|
303
|
+
if (boltMatrices.length > 0)
|
|
304
|
+
group.add(
|
|
305
|
+
instancedBatch(
|
|
306
|
+
`${id}-space-wall-bolts`,
|
|
307
|
+
// Six sides are already sub-pixel at the authored bolt radius.
|
|
308
|
+
new VisualCircleGeometry(0.009, 6).rotateX(-Math.PI / 2).translate(0, 0.004, 0),
|
|
309
|
+
materials.trim,
|
|
310
|
+
boltMatrices,
|
|
311
|
+
),
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
group.userData.runCount = runCount
|
|
315
|
+
group.userData.bayCount = bayCount
|
|
316
|
+
group.userData.ribCount = ribMatrices.length
|
|
317
|
+
root.add(group)
|
|
318
|
+
return group
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function queueBand(
|
|
322
|
+
batch: BandBatch,
|
|
323
|
+
frames: readonly SpaceStationWallPathFrame[],
|
|
324
|
+
face: -1 | 1,
|
|
325
|
+
bottom: number,
|
|
326
|
+
top: number,
|
|
327
|
+
lift: number,
|
|
328
|
+
chamfer = 0,
|
|
329
|
+
): void {
|
|
330
|
+
if (frames.length < 2 || top <= bottom) return
|
|
331
|
+
batch.geometries.push(createBandGeometry(frames, face, bottom, top, lift, chamfer))
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function addBandBatch(group: THREE.Group, batch: BandBatch): void {
|
|
335
|
+
if (batch.geometries.length === 0) return
|
|
336
|
+
const geometry = mergeGeometries(batch.geometries, false)
|
|
337
|
+
batch.geometries.forEach(part => part.dispose())
|
|
338
|
+
if (!geometry) throw new Error(`Could not merge ${batch.name}`)
|
|
339
|
+
geometry.name = 'space-station-armored-wall-batched-band-geometry'
|
|
340
|
+
geometry.computeBoundingBox()
|
|
341
|
+
geometry.computeBoundingSphere()
|
|
342
|
+
const mesh = new THREE.Mesh(geometry, batch.material)
|
|
343
|
+
mesh.name = batch.name
|
|
344
|
+
mesh.userData.decorative = true
|
|
345
|
+
mesh.userData.visualOnly = true
|
|
346
|
+
mesh.userData.bandCount = batch.geometries.length
|
|
347
|
+
mesh.castShadow = batch.castShadow
|
|
348
|
+
mesh.receiveShadow = batch.receiveShadow
|
|
349
|
+
group.add(mesh)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function createBandGeometry(
|
|
353
|
+
frames: readonly SpaceStationWallPathFrame[],
|
|
354
|
+
face: -1 | 1,
|
|
355
|
+
bottom: number,
|
|
356
|
+
top: number,
|
|
357
|
+
lift: number,
|
|
358
|
+
chamfer: number,
|
|
359
|
+
): THREE.BufferGeometry {
|
|
360
|
+
const distances = bandPathDistances(frames)
|
|
361
|
+
const totalLength = distances.at(-1) ?? 1
|
|
362
|
+
const positions: number[] = []
|
|
363
|
+
const uvs: number[] = []
|
|
364
|
+
const metricUvs: number[] = []
|
|
365
|
+
const indices: number[] = []
|
|
366
|
+
frames.forEach((frame, index) => {
|
|
367
|
+
const outward = horizontalWallOutward(frame, frame.outward.clone().multiplyScalar(face))
|
|
368
|
+
const endChamfer = index === 0 || index === frames.length - 1 ? chamfer : 0
|
|
369
|
+
for (const height of [bottom + endChamfer, top - endChamfer]) {
|
|
370
|
+
const point = frame.base
|
|
371
|
+
.clone()
|
|
372
|
+
.addScaledVector(outward, TRACK_RAIL_WALL_THICKNESS / 2 + lift)
|
|
373
|
+
.addScaledVector(WORLD_UP, height - 0.03)
|
|
374
|
+
positions.push(point.x, point.y, point.z)
|
|
375
|
+
uvs.push(distances[index]! / totalLength, (height - bottom) / Math.max(0.001, top - bottom))
|
|
376
|
+
metricUvs.push(distances[index]!, height, 1, totalLength)
|
|
377
|
+
}
|
|
378
|
+
})
|
|
379
|
+
for (let index = 0; index < frames.length - 1; index += 1) {
|
|
380
|
+
const a = index * 2
|
|
381
|
+
const b = a + 1
|
|
382
|
+
const c = a + 2
|
|
383
|
+
const d = a + 3
|
|
384
|
+
const desired = horizontalWallOutward(frames[index]!, frames[index]!.outward.clone().multiplyScalar(face))
|
|
385
|
+
appendOrientedTriangle(indices, positions, a, b, c, desired)
|
|
386
|
+
appendOrientedTriangle(indices, positions, b, d, c, desired)
|
|
387
|
+
}
|
|
388
|
+
const geometry = new THREE.BufferGeometry()
|
|
389
|
+
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3))
|
|
390
|
+
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2))
|
|
391
|
+
geometry.setAttribute('themeNetUv', new THREE.Float32BufferAttribute(metricUvs, 4))
|
|
392
|
+
geometry.setIndex(indices)
|
|
393
|
+
geometry.computeVertexNormals()
|
|
394
|
+
geometry.computeBoundingBox()
|
|
395
|
+
geometry.computeBoundingSphere()
|
|
396
|
+
geometry.name = 'space-station-armored-wall-band-geometry'
|
|
397
|
+
return geometry
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function appendOrientedTriangle(
|
|
401
|
+
indices: number[],
|
|
402
|
+
positions: readonly number[],
|
|
403
|
+
a: number,
|
|
404
|
+
b: number,
|
|
405
|
+
c: number,
|
|
406
|
+
desired: THREE.Vector3,
|
|
407
|
+
): void {
|
|
408
|
+
const point = (index: number) =>
|
|
409
|
+
new THREE.Vector3(positions[index * 3]!, positions[index * 3 + 1]!, positions[index * 3 + 2]!)
|
|
410
|
+
const normal = point(b)
|
|
411
|
+
.sub(point(a))
|
|
412
|
+
.cross(point(c).sub(point(a)))
|
|
413
|
+
if (normal.dot(desired) >= 0) indices.push(a, b, c)
|
|
414
|
+
else indices.push(a, c, b)
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function createWrappedRibGeometry(wallHeight: number, width: number, depth: number): THREE.BoxGeometry {
|
|
418
|
+
// The repeated ribs are millimetres deep: a closed rectangular strip keeps
|
|
419
|
+
// their silhouette without bevel loops on thousands of instances.
|
|
420
|
+
const geometry = new THREE.BoxGeometry(width, wallHeight, depth)
|
|
421
|
+
geometry.name = 'space-station-wrapped-rib-geometry'
|
|
422
|
+
return geometry
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function verticalFrameQuaternion(frame: SpaceStationWallPathFrame, outwardHint: THREE.Vector3): THREE.Quaternion {
|
|
426
|
+
const up = WORLD_UP
|
|
427
|
+
const outward = horizontalWallOutward(frame, outwardHint)
|
|
428
|
+
const tangent = new THREE.Vector3().crossVectors(up, outward).normalize()
|
|
429
|
+
outward.crossVectors(tangent, up).normalize()
|
|
430
|
+
return new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().makeBasis(tangent, up, outward))
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function horizontalWallOutward(
|
|
434
|
+
frame: SpaceStationWallPathFrame,
|
|
435
|
+
outwardHint: THREE.Vector3,
|
|
436
|
+
): THREE.Vector3 {
|
|
437
|
+
const outward = outwardHint.clone().addScaledVector(WORLD_UP, -outwardHint.dot(WORLD_UP))
|
|
438
|
+
if (outward.lengthSq() < 0.000_001) outward.crossVectors(frame.tangent, WORLD_UP)
|
|
439
|
+
return outward.normalize()
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function horizontalFrameTangent(
|
|
443
|
+
frame: SpaceStationWallPathFrame,
|
|
444
|
+
outwardHint: THREE.Vector3,
|
|
445
|
+
): THREE.Vector3 {
|
|
446
|
+
const outward = horizontalWallOutward(frame, outwardHint)
|
|
447
|
+
return new THREE.Vector3().crossVectors(WORLD_UP, outward).normalize()
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function bandPathDistances(frames: readonly SpaceStationWallPathFrame[]): number[] {
|
|
451
|
+
const distances = [0]
|
|
452
|
+
for (let index = 1; index < frames.length; index += 1) {
|
|
453
|
+
distances.push(distances[index - 1]! + frames[index]!.base.distanceTo(frames[index - 1]!.base))
|
|
454
|
+
}
|
|
455
|
+
return distances
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function sampleCanonicalRange(
|
|
459
|
+
run: CanonicalWallRun,
|
|
460
|
+
start: number,
|
|
461
|
+
end: number,
|
|
462
|
+
): SpaceStationWallPathFrame[] {
|
|
463
|
+
const rangeStart = Math.max(0, Math.min(start, end))
|
|
464
|
+
const rangeEnd = Math.min(run.length, Math.max(start, end))
|
|
465
|
+
const sampled = [canonicalWallFrameAt(run, rangeStart)]
|
|
466
|
+
let previous = sampled[0]!
|
|
467
|
+
|
|
468
|
+
for (const frame of run.frames) {
|
|
469
|
+
if (frame.distance <= rangeStart || frame.distance >= rangeEnd) continue
|
|
470
|
+
const turn = Math.acos(THREE.MathUtils.clamp(previous.tangent.dot(frame.tangent), -1, 1))
|
|
471
|
+
if (frame.distance - previous.distance < SAMPLE_SPACING && turn < SAMPLE_MAXIMUM_TURN) continue
|
|
472
|
+
sampled.push(frame)
|
|
473
|
+
previous = frame
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
sampled.push(canonicalWallFrameAt(run, rangeEnd))
|
|
477
|
+
return sampled
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function instancedBatch(
|
|
481
|
+
name: string,
|
|
482
|
+
geometry: THREE.BufferGeometry,
|
|
483
|
+
material: THREE.Material,
|
|
484
|
+
matrices: readonly THREE.Matrix4[],
|
|
485
|
+
): THREE.InstancedMesh {
|
|
486
|
+
const mesh = new THREE.InstancedMesh(geometry, material, matrices.length)
|
|
487
|
+
mesh.name = name
|
|
488
|
+
mesh.userData.decorative = true
|
|
489
|
+
mesh.userData.visualOnly = true
|
|
490
|
+
mesh.castShadow = true
|
|
491
|
+
mesh.receiveShadow = true
|
|
492
|
+
matrices.forEach((matrix, index) => mesh.setMatrixAt(index, matrix))
|
|
493
|
+
mesh.instanceMatrix.needsUpdate = true
|
|
494
|
+
mesh.computeBoundingBox()
|
|
495
|
+
mesh.computeBoundingSphere()
|
|
496
|
+
return mesh
|
|
497
|
+
}
|