@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,983 @@
|
|
|
1
|
+
import type { MaterialSlot, TrackPieceCategory } from '@marble.fun/physics-engine'
|
|
2
|
+
import * as THREE from 'three'
|
|
3
|
+
|
|
4
|
+
import { applyTransparentSupportFadeShader } from '../../../three/theme-material-registry'
|
|
5
|
+
import {
|
|
6
|
+
JAPANESE_SAKURA_DRAGON_STONE_SUPPORT_ID,
|
|
7
|
+
JAPANESE_SAKURA_VERMILION_UNDERBRIDGE_SUPPORT_ID,
|
|
8
|
+
} from '../../../three/japanese-sakura-appearance'
|
|
9
|
+
import { addJapaneseSakuraCastleSupportModel } from './japanese-sakura-castle-support-blender'
|
|
10
|
+
import {
|
|
11
|
+
createUnderTrackSupportLayout,
|
|
12
|
+
UNDER_TRACK_SUPPORT_BAY_LENGTH,
|
|
13
|
+
UNDER_TRACK_SUPPORT_DEPTH,
|
|
14
|
+
UNDER_TRACK_SUPPORT_FADE_DEPTH,
|
|
15
|
+
UNDER_TRACK_SUPPORT_FADE_START_DEPTH,
|
|
16
|
+
UNDER_TRACK_SUPPORT_FOOTPRINT_SCALE,
|
|
17
|
+
UNDER_TRACK_SUPPORT_MEMBER_SCALE,
|
|
18
|
+
underTrackSupportFadeAtDepth,
|
|
19
|
+
underTrackSupportHalfSpan,
|
|
20
|
+
underTrackSupportStationClearance,
|
|
21
|
+
} from './under-track-support-architecture'
|
|
22
|
+
|
|
23
|
+
export const WILD_WEST_TIMBER_TRESTLE_SUPPORT_ID = 'wild-west.timber-trestle-support'
|
|
24
|
+
export const WILD_WEST_CANYON_BUTTRESS_SUPPORT_ID = 'wild-west.canyon-buttress-support'
|
|
25
|
+
export const WILD_WEST_TRESTLE_METAL_MATERIAL_ID = 'wild-west.trestle-iron'
|
|
26
|
+
|
|
27
|
+
const WILD_WEST_THEME_ID = 'rocky-mountain-wild-west'
|
|
28
|
+
const JAPANESE_SAKURA_THEME_ID = 'japanese-sakura-lake'
|
|
29
|
+
const WORLD_UP = new THREE.Vector3(0, 1, 0)
|
|
30
|
+
const UNSUPPORTED_RUNTIME_KINDS = new Set(['conveyor', 'elevator', 'vortex'])
|
|
31
|
+
const SUPPORT_DECK_UNDERSIDE_OFFSET = 0.32
|
|
32
|
+
const CANYON_SLOPE_TRANSITION_DEPTH_FRACTION = 0.36
|
|
33
|
+
const CANYON_COURSE_SCHEDULE = [
|
|
34
|
+
[0, 0.055, 1.05, 1.02, false],
|
|
35
|
+
[0.058, 0.11, 0.88, 0.96, true],
|
|
36
|
+
[0.113, 0.17, 0.82, 0.93, false],
|
|
37
|
+
[0.173, 0.23, 0.78, 0.91, true],
|
|
38
|
+
[0.233, 0.295, 0.7, 0.88, false],
|
|
39
|
+
[0.298, 0.36, 0.64, 0.84, true],
|
|
40
|
+
[0.363, 0.43, 0.78, 0.88, false],
|
|
41
|
+
[0.433, 0.5, 0.74, 0.85, false],
|
|
42
|
+
[0.503, 0.57, 0.68, 0.82, true],
|
|
43
|
+
[0.573, 0.64, 0.7, 0.82, false],
|
|
44
|
+
[0.643, 0.71, 0.76, 0.84, true],
|
|
45
|
+
[0.713, 0.78, 0.82, 0.85, false],
|
|
46
|
+
[0.783, 0.85, 0.86, 0.86, true],
|
|
47
|
+
[0.853, 0.925, 0.9, 0.88, false],
|
|
48
|
+
[0.928, 1, 0.94, 0.9, true],
|
|
49
|
+
] as const
|
|
50
|
+
|
|
51
|
+
export interface TrackAppearanceSupportFrame {
|
|
52
|
+
point: THREE.Vector3
|
|
53
|
+
tangent: THREE.Vector3
|
|
54
|
+
right: THREE.Vector3
|
|
55
|
+
up: THREE.Vector3
|
|
56
|
+
width: number
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface TrackAppearanceSupportMaterials {
|
|
60
|
+
support: THREE.Material
|
|
61
|
+
timber: THREE.Material
|
|
62
|
+
metal: THREE.Material
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface TrackAppearanceSupportOptions {
|
|
66
|
+
capStart?: boolean
|
|
67
|
+
capEnd?: boolean
|
|
68
|
+
/** Cumulative path length at the input connector, shared with material continuity. */
|
|
69
|
+
connectionPhase?: number
|
|
70
|
+
/** Existing topology-owned stations which this lower-priority route must not overlap. */
|
|
71
|
+
excludedStationPoints?: readonly THREE.Vector3[]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface SampledSupportLayout {
|
|
75
|
+
stations: TrackAppearanceSupportFrame[]
|
|
76
|
+
ownedBays: readonly {
|
|
77
|
+
start: TrackAppearanceSupportFrame
|
|
78
|
+
end: TrackAppearanceSupportFrame
|
|
79
|
+
startDistance: number
|
|
80
|
+
endDistance: number
|
|
81
|
+
}[]
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface Beam {
|
|
85
|
+
start: THREE.Vector3
|
|
86
|
+
end: THREE.Vector3
|
|
87
|
+
width: number
|
|
88
|
+
depth: number
|
|
89
|
+
fadeAnchorY: number
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function supportsTrackAppearanceSupport(
|
|
93
|
+
_category: TrackPieceCategory,
|
|
94
|
+
runtimeKind: string,
|
|
95
|
+
materialSlots: readonly MaterialSlot[],
|
|
96
|
+
): boolean {
|
|
97
|
+
return materialSlots.includes('lane.surface') && !UNSUPPORTED_RUNTIME_KINDS.has(runtimeKind)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function addTrackAppearanceSupport(
|
|
101
|
+
root: THREE.Group,
|
|
102
|
+
pieceId: string,
|
|
103
|
+
frames: readonly TrackAppearanceSupportFrame[],
|
|
104
|
+
supportAppearanceId: string | undefined,
|
|
105
|
+
materials: TrackAppearanceSupportMaterials,
|
|
106
|
+
options: TrackAppearanceSupportOptions = {},
|
|
107
|
+
): THREE.Group | undefined {
|
|
108
|
+
if (frames.length < 2) return undefined
|
|
109
|
+
if (
|
|
110
|
+
supportAppearanceId !== WILD_WEST_TIMBER_TRESTLE_SUPPORT_ID &&
|
|
111
|
+
supportAppearanceId !== WILD_WEST_CANYON_BUTTRESS_SUPPORT_ID &&
|
|
112
|
+
supportAppearanceId !== JAPANESE_SAKURA_VERMILION_UNDERBRIDGE_SUPPORT_ID &&
|
|
113
|
+
supportAppearanceId !== JAPANESE_SAKURA_DRAGON_STONE_SUPPORT_ID
|
|
114
|
+
) {
|
|
115
|
+
return undefined
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
applyTransparentSupportFadeShader(materials.support)
|
|
119
|
+
applyTransparentSupportFadeShader(materials.metal)
|
|
120
|
+
const group = new THREE.Group()
|
|
121
|
+
group.name = `${pieceId}-under-track-support`
|
|
122
|
+
const themeOnly = supportAppearanceId.startsWith('japanese-sakura.') ? JAPANESE_SAKURA_THEME_ID : WILD_WEST_THEME_ID
|
|
123
|
+
group.userData.themeOnly = themeOnly
|
|
124
|
+
group.userData.decorative = true
|
|
125
|
+
group.userData.visualOnly = true
|
|
126
|
+
group.userData.supportAppearanceId = supportAppearanceId
|
|
127
|
+
group.userData.depthFadeTechnique = 'transparent-height-fade-with-exterior-depth-prepass'
|
|
128
|
+
group.userData.supportDepth = UNDER_TRACK_SUPPORT_DEPTH
|
|
129
|
+
group.userData.supportFadeStartDepth = UNDER_TRACK_SUPPORT_FADE_START_DEPTH
|
|
130
|
+
group.userData.supportFadeEndDepth = UNDER_TRACK_SUPPORT_DEPTH
|
|
131
|
+
group.userData.supportBayLength = UNDER_TRACK_SUPPORT_BAY_LENGTH
|
|
132
|
+
group.userData.supportArchitecture = 'fixed-depth-continuous-metric-bays-v2'
|
|
133
|
+
group.userData.supportFootprintScale = UNDER_TRACK_SUPPORT_FOOTPRINT_SCALE
|
|
134
|
+
group.userData.supportMemberScale = UNDER_TRACK_SUPPORT_MEMBER_SCALE
|
|
135
|
+
root.add(group)
|
|
136
|
+
|
|
137
|
+
let content: THREE.Group | undefined
|
|
138
|
+
const renderAtConnectionPhase = (connectionPhase: number) => {
|
|
139
|
+
if (!Number.isFinite(connectionPhase)) return
|
|
140
|
+
if (content && Math.abs((group.userData.supportConnectionPhase as number) - connectionPhase) < 0.000_001) return
|
|
141
|
+
if (content) {
|
|
142
|
+
group.remove(content)
|
|
143
|
+
disposeSupportContent(content)
|
|
144
|
+
}
|
|
145
|
+
content = new THREE.Group()
|
|
146
|
+
content.name = `${pieceId}-under-track-support-content`
|
|
147
|
+
group.add(content)
|
|
148
|
+
const excludedStationPoints: THREE.Vector3[] = [...(options.excludedStationPoints ?? [])]
|
|
149
|
+
root.traverse(object => {
|
|
150
|
+
if (!Array.isArray(object.userData.supportStationPoints)) return
|
|
151
|
+
for (const point of object.userData.supportStationPoints as [number, number, number][]) {
|
|
152
|
+
excludedStationPoints.push(new THREE.Vector3(...point))
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
const resolvedOptions = { ...options, connectionPhase, excludedStationPoints }
|
|
156
|
+
if (supportAppearanceId === WILD_WEST_TIMBER_TRESTLE_SUPPORT_ID) {
|
|
157
|
+
addTimberTrestle(content, pieceId, frames, materials, resolvedOptions)
|
|
158
|
+
} else if (supportAppearanceId === WILD_WEST_CANYON_BUTTRESS_SUPPORT_ID) {
|
|
159
|
+
addStackedCanyonButtresses(content, pieceId, frames, materials.support, resolvedOptions)
|
|
160
|
+
} else if (supportAppearanceId === JAPANESE_SAKURA_VERMILION_UNDERBRIDGE_SUPPORT_ID) {
|
|
161
|
+
addSakuraVermilionUnderbridge(content, pieceId, frames, materials, resolvedOptions)
|
|
162
|
+
} else {
|
|
163
|
+
addSakuraDragonStoneSupport(content, pieceId, frames, materials.support, resolvedOptions)
|
|
164
|
+
}
|
|
165
|
+
Object.assign(group.userData, content.userData)
|
|
166
|
+
group.userData.supportConnectionPhase = connectionPhase
|
|
167
|
+
content.traverse(object => {
|
|
168
|
+
object.userData.themeOnly = themeOnly
|
|
169
|
+
object.userData.decorative = true
|
|
170
|
+
object.userData.visualOnly = true
|
|
171
|
+
object.castShadow = true
|
|
172
|
+
object.receiveShadow = true
|
|
173
|
+
})
|
|
174
|
+
addSupportDepthPrepasses(content)
|
|
175
|
+
}
|
|
176
|
+
group.userData.setSupportConnectionPhase = renderAtConnectionPhase
|
|
177
|
+
renderAtConnectionPhase(options.connectionPhase ?? 0)
|
|
178
|
+
return group
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function disposeSupportContent(content: THREE.Object3D): void {
|
|
182
|
+
content.userData.supportDisposed = true
|
|
183
|
+
content.traverse(object => {
|
|
184
|
+
if (!(object instanceof THREE.Mesh)) return
|
|
185
|
+
object.geometry.dispose()
|
|
186
|
+
const materials = Array.isArray(object.material) ? object.material : [object.material]
|
|
187
|
+
for (const material of materials) {
|
|
188
|
+
if (material.userData.supportDepthPrepassMaterial === true) material.dispose()
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function addSupportDepthPrepasses(group: THREE.Group): void {
|
|
194
|
+
const meshes: THREE.Mesh[] = []
|
|
195
|
+
group.traverse(object => {
|
|
196
|
+
if (
|
|
197
|
+
object instanceof THREE.Mesh &&
|
|
198
|
+
object.userData.supportDepthPrepass !== true &&
|
|
199
|
+
object.userData.supportSkipDepthPrepass !== true
|
|
200
|
+
) {
|
|
201
|
+
meshes.push(object)
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
for (const mesh of meshes) {
|
|
206
|
+
const visibleMaterials = Array.isArray(mesh.material) ? mesh.material : [mesh.material]
|
|
207
|
+
visibleMaterials.forEach(material => {
|
|
208
|
+
material.depthFunc = THREE.EqualDepth
|
|
209
|
+
})
|
|
210
|
+
const depthMaterials = visibleMaterials.map(material => {
|
|
211
|
+
const depthMaterial = material.clone()
|
|
212
|
+
depthMaterial.onBeforeCompile = THREE.Material.prototype.onBeforeCompile
|
|
213
|
+
depthMaterial.customProgramCacheKey = THREE.Material.prototype.customProgramCacheKey
|
|
214
|
+
delete depthMaterial.userData.transparentSupportFadeShader
|
|
215
|
+
applyTransparentSupportFadeShader(depthMaterial, true, true)
|
|
216
|
+
depthMaterial.name = `${material.name || 'support'}:exterior-depth-prepass`
|
|
217
|
+
depthMaterial.transparent = false
|
|
218
|
+
depthMaterial.depthWrite = true
|
|
219
|
+
depthMaterial.colorWrite = false
|
|
220
|
+
depthMaterial.blending = THREE.NoBlending
|
|
221
|
+
depthMaterial.depthFunc = THREE.LessEqualDepth
|
|
222
|
+
depthMaterial.userData.supportDepthPrepassMaterial = true
|
|
223
|
+
depthMaterial.userData.supportDepthCoverage = 'complete-fade-exterior'
|
|
224
|
+
depthMaterial.needsUpdate = true
|
|
225
|
+
return depthMaterial
|
|
226
|
+
})
|
|
227
|
+
const depthMesh = mesh.clone(false)
|
|
228
|
+
depthMesh.name = `${mesh.name}-exterior-depth-prepass`
|
|
229
|
+
depthMesh.material = Array.isArray(mesh.material) ? depthMaterials : depthMaterials[0]!
|
|
230
|
+
depthMesh.userData = {
|
|
231
|
+
...mesh.userData,
|
|
232
|
+
supportDepthPrepass: true,
|
|
233
|
+
decorative: true,
|
|
234
|
+
visualOnly: true,
|
|
235
|
+
}
|
|
236
|
+
depthMesh.castShadow = false
|
|
237
|
+
depthMesh.receiveShadow = false
|
|
238
|
+
const parent = mesh.parent ?? group
|
|
239
|
+
parent.add(depthMesh)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function trestleBentAtFrame(frame: TrackAppearanceSupportFrame) {
|
|
244
|
+
const halfSpan = underTrackSupportHalfSpan(frame.width)
|
|
245
|
+
const topCenter = frame.point.clone().addScaledVector(frame.up, -0.39)
|
|
246
|
+
const left = topCenter.clone().addScaledVector(frame.right, -halfSpan)
|
|
247
|
+
const right = topCenter.clone().addScaledVector(frame.right, halfSpan)
|
|
248
|
+
return {
|
|
249
|
+
left,
|
|
250
|
+
right,
|
|
251
|
+
bottomLeft: left.clone().addScaledVector(WORLD_UP, -UNDER_TRACK_SUPPORT_DEPTH),
|
|
252
|
+
bottomRight: right.clone().addScaledVector(WORLD_UP, -UNDER_TRACK_SUPPORT_DEPTH),
|
|
253
|
+
fadeAnchorY: Math.max(left.y, right.y),
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function sakuraBentAtFrame(frame: TrackAppearanceSupportFrame) {
|
|
258
|
+
const halfSpan = underTrackSupportHalfSpan(frame.width)
|
|
259
|
+
const topCenter = frame.point.clone().addScaledVector(frame.up, -0.43)
|
|
260
|
+
const left = topCenter.clone().addScaledVector(frame.right, -halfSpan)
|
|
261
|
+
const right = topCenter.clone().addScaledVector(frame.right, halfSpan)
|
|
262
|
+
return { left, right, fadeAnchorY: Math.max(left.y, right.y) }
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function addTimberTrestle(
|
|
266
|
+
root: THREE.Group,
|
|
267
|
+
pieceId: string,
|
|
268
|
+
pathFrames: readonly TrackAppearanceSupportFrame[],
|
|
269
|
+
materials: TrackAppearanceSupportMaterials,
|
|
270
|
+
options: TrackAppearanceSupportOptions,
|
|
271
|
+
): void {
|
|
272
|
+
const layout = sampleContinuousSupportLayout(pathFrames, options)
|
|
273
|
+
const sampled = layout.stations
|
|
274
|
+
const posts: Beam[] = []
|
|
275
|
+
const ties: Beam[] = []
|
|
276
|
+
const braces: Beam[] = []
|
|
277
|
+
const straps: Beam[] = []
|
|
278
|
+
|
|
279
|
+
for (const [index, frame] of sampled.entries()) {
|
|
280
|
+
const halfSpan = underTrackSupportHalfSpan(frame.width)
|
|
281
|
+
const topCenter = frame.point.clone().addScaledVector(frame.up, -0.39)
|
|
282
|
+
const topLeft = topCenter.clone().addScaledVector(frame.right, -halfSpan)
|
|
283
|
+
const topRight = topCenter.clone().addScaledVector(frame.right, halfSpan)
|
|
284
|
+
const fadeAnchorY = Math.max(topLeft.y, topRight.y)
|
|
285
|
+
const bottomLeft = topLeft.clone().addScaledVector(WORLD_UP, -UNDER_TRACK_SUPPORT_DEPTH)
|
|
286
|
+
const bottomRight = topRight.clone().addScaledVector(WORLD_UP, -UNDER_TRACK_SUPPORT_DEPTH)
|
|
287
|
+
const postWidth = (0.3 + deterministic(`${pieceId}:post:${index}`) * 0.055) * UNDER_TRACK_SUPPORT_MEMBER_SCALE
|
|
288
|
+
posts.push(
|
|
289
|
+
beam(bottomLeft, topLeft, postWidth, postWidth * 0.92, fadeAnchorY),
|
|
290
|
+
beam(bottomRight, topRight, postWidth * 0.96, postWidth, fadeAnchorY),
|
|
291
|
+
)
|
|
292
|
+
ties.push(
|
|
293
|
+
beam(
|
|
294
|
+
topLeft,
|
|
295
|
+
topRight,
|
|
296
|
+
0.27 * UNDER_TRACK_SUPPORT_MEMBER_SCALE,
|
|
297
|
+
0.21 * UNDER_TRACK_SUPPORT_MEMBER_SCALE,
|
|
298
|
+
fadeAnchorY,
|
|
299
|
+
),
|
|
300
|
+
beam(
|
|
301
|
+
bottomLeft.clone().lerp(topLeft, 0.38),
|
|
302
|
+
bottomRight.clone().lerp(topRight, 0.38),
|
|
303
|
+
0.18 * UNDER_TRACK_SUPPORT_MEMBER_SCALE,
|
|
304
|
+
0.15 * UNDER_TRACK_SUPPORT_MEMBER_SCALE,
|
|
305
|
+
fadeAnchorY,
|
|
306
|
+
),
|
|
307
|
+
)
|
|
308
|
+
braces.push(
|
|
309
|
+
beam(
|
|
310
|
+
bottomLeft,
|
|
311
|
+
topRight,
|
|
312
|
+
0.14 * UNDER_TRACK_SUPPORT_MEMBER_SCALE,
|
|
313
|
+
0.11 * UNDER_TRACK_SUPPORT_MEMBER_SCALE,
|
|
314
|
+
fadeAnchorY,
|
|
315
|
+
),
|
|
316
|
+
beam(
|
|
317
|
+
bottomRight,
|
|
318
|
+
topLeft,
|
|
319
|
+
0.14 * UNDER_TRACK_SUPPORT_MEMBER_SCALE,
|
|
320
|
+
0.11 * UNDER_TRACK_SUPPORT_MEMBER_SCALE,
|
|
321
|
+
fadeAnchorY,
|
|
322
|
+
),
|
|
323
|
+
)
|
|
324
|
+
for (const top of [topLeft, topRight]) {
|
|
325
|
+
straps.push(
|
|
326
|
+
beam(
|
|
327
|
+
top.clone().addScaledVector(frame.right, -0.055),
|
|
328
|
+
top.clone().addScaledVector(frame.right, 0.055),
|
|
329
|
+
postWidth + 0.1 * UNDER_TRACK_SUPPORT_MEMBER_SCALE,
|
|
330
|
+
0.075 * UNDER_TRACK_SUPPORT_MEMBER_SCALE,
|
|
331
|
+
fadeAnchorY,
|
|
332
|
+
),
|
|
333
|
+
)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Load-bearing stringers and the evenly distributed bent schedule both
|
|
338
|
+
// meet the exact connector planes, matching a space-between layout.
|
|
339
|
+
for (let index = 0; index < pathFrames.length - 1; index += 1) {
|
|
340
|
+
const current = pathFrames[index]!
|
|
341
|
+
const next = pathFrames[index + 1]!
|
|
342
|
+
for (const side of [-1, 1] as const) {
|
|
343
|
+
ties.push(
|
|
344
|
+
beam(
|
|
345
|
+
supportContact(current, side),
|
|
346
|
+
supportContact(next, side),
|
|
347
|
+
0.21 * UNDER_TRACK_SUPPORT_MEMBER_SCALE,
|
|
348
|
+
0.18 * UNDER_TRACK_SUPPORT_MEMBER_SCALE,
|
|
349
|
+
),
|
|
350
|
+
)
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
for (const ownedBay of layout.ownedBays) {
|
|
354
|
+
const current = trestleBentAtFrame(ownedBay.start)
|
|
355
|
+
const next = trestleBentAtFrame(ownedBay.end)
|
|
356
|
+
const fadeAnchorY = Math.max(current.fadeAnchorY, next.fadeAnchorY)
|
|
357
|
+
braces.push(
|
|
358
|
+
beam(
|
|
359
|
+
current.bottomLeft,
|
|
360
|
+
next.left,
|
|
361
|
+
0.14 * UNDER_TRACK_SUPPORT_MEMBER_SCALE,
|
|
362
|
+
0.11 * UNDER_TRACK_SUPPORT_MEMBER_SCALE,
|
|
363
|
+
fadeAnchorY,
|
|
364
|
+
),
|
|
365
|
+
beam(
|
|
366
|
+
current.left,
|
|
367
|
+
next.bottomLeft,
|
|
368
|
+
0.14 * UNDER_TRACK_SUPPORT_MEMBER_SCALE,
|
|
369
|
+
0.11 * UNDER_TRACK_SUPPORT_MEMBER_SCALE,
|
|
370
|
+
fadeAnchorY,
|
|
371
|
+
),
|
|
372
|
+
beam(
|
|
373
|
+
current.bottomRight,
|
|
374
|
+
next.right,
|
|
375
|
+
0.14 * UNDER_TRACK_SUPPORT_MEMBER_SCALE,
|
|
376
|
+
0.11 * UNDER_TRACK_SUPPORT_MEMBER_SCALE,
|
|
377
|
+
fadeAnchorY,
|
|
378
|
+
),
|
|
379
|
+
beam(
|
|
380
|
+
current.right,
|
|
381
|
+
next.bottomRight,
|
|
382
|
+
0.14 * UNDER_TRACK_SUPPORT_MEMBER_SCALE,
|
|
383
|
+
0.11 * UNDER_TRACK_SUPPORT_MEMBER_SCALE,
|
|
384
|
+
fadeAnchorY,
|
|
385
|
+
),
|
|
386
|
+
)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
addBeamBatch(root, `${pieceId}-trestle-posts`, posts, materials.support, 'trestle-post')
|
|
390
|
+
addBeamBatch(root, `${pieceId}-trestle-ties`, ties, materials.support, 'trestle-tie')
|
|
391
|
+
addBeamBatch(root, `${pieceId}-trestle-braces`, braces, materials.support, 'trestle-brace')
|
|
392
|
+
addBeamBatch(root, `${pieceId}-trestle-iron-straps`, straps, materials.metal, 'trestle-strap')
|
|
393
|
+
root.userData.supportBentCount = sampled.length
|
|
394
|
+
root.userData.supportBentLayout = 'continuous-metric-grid'
|
|
395
|
+
root.userData.supportBentPoints = sampled.map(frame => frame.point.toArray())
|
|
396
|
+
root.userData.supportStationPoints = sampled.map(frame => frame.point.toArray())
|
|
397
|
+
root.userData.supportOwnedBayCount = layout.ownedBays.length
|
|
398
|
+
const pathLength = cumulativeDistances(pathFrames).at(-1) ?? 0
|
|
399
|
+
root.userData.openStartInset = sampled[0]?.point.distanceTo(pathFrames[0]!.point) ?? pathLength
|
|
400
|
+
root.userData.openEndInset = sampled.at(-1)?.point.distanceTo(pathFrames.at(-1)!.point) ?? pathLength
|
|
401
|
+
root.userData.stringersMeetConnectorPlanes = true
|
|
402
|
+
root.userData.everyAdjacentBentConnected = true
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function addSakuraVermilionUnderbridge(
|
|
406
|
+
root: THREE.Group,
|
|
407
|
+
pieceId: string,
|
|
408
|
+
pathFrames: readonly TrackAppearanceSupportFrame[],
|
|
409
|
+
materials: TrackAppearanceSupportMaterials,
|
|
410
|
+
options: TrackAppearanceSupportOptions,
|
|
411
|
+
): void {
|
|
412
|
+
const layout = sampleContinuousSupportLayout(pathFrames, options)
|
|
413
|
+
const sampled = layout.stations
|
|
414
|
+
const timber: Beam[] = []
|
|
415
|
+
const hardware: Beam[] = []
|
|
416
|
+
|
|
417
|
+
for (const frame of sampled) {
|
|
418
|
+
const halfSpan = underTrackSupportHalfSpan(frame.width)
|
|
419
|
+
const topCenter = frame.point.clone().addScaledVector(frame.up, -0.43)
|
|
420
|
+
const left = topCenter.clone().addScaledVector(frame.right, -halfSpan)
|
|
421
|
+
const right = topCenter.clone().addScaledVector(frame.right, halfSpan)
|
|
422
|
+
const fadeAnchorY = Math.max(left.y, right.y)
|
|
423
|
+
const leftBottom = left.clone().addScaledVector(WORLD_UP, -UNDER_TRACK_SUPPORT_DEPTH)
|
|
424
|
+
const rightBottom = right.clone().addScaledVector(WORLD_UP, -UNDER_TRACK_SUPPORT_DEPTH)
|
|
425
|
+
timber.push(
|
|
426
|
+
beam(
|
|
427
|
+
leftBottom,
|
|
428
|
+
left,
|
|
429
|
+
0.72 * UNDER_TRACK_SUPPORT_MEMBER_SCALE,
|
|
430
|
+
0.66 * UNDER_TRACK_SUPPORT_MEMBER_SCALE,
|
|
431
|
+
fadeAnchorY,
|
|
432
|
+
),
|
|
433
|
+
beam(
|
|
434
|
+
rightBottom,
|
|
435
|
+
right,
|
|
436
|
+
0.72 * UNDER_TRACK_SUPPORT_MEMBER_SCALE,
|
|
437
|
+
0.66 * UNDER_TRACK_SUPPORT_MEMBER_SCALE,
|
|
438
|
+
fadeAnchorY,
|
|
439
|
+
),
|
|
440
|
+
beam(
|
|
441
|
+
left.clone().addScaledVector(frame.right, -0.28 * UNDER_TRACK_SUPPORT_FOOTPRINT_SCALE),
|
|
442
|
+
right.clone().addScaledVector(frame.right, 0.28 * UNDER_TRACK_SUPPORT_FOOTPRINT_SCALE),
|
|
443
|
+
0.82 * UNDER_TRACK_SUPPORT_MEMBER_SCALE,
|
|
444
|
+
0.52 * UNDER_TRACK_SUPPORT_MEMBER_SCALE,
|
|
445
|
+
fadeAnchorY,
|
|
446
|
+
),
|
|
447
|
+
)
|
|
448
|
+
for (const depth of [0.65, 1.15].map(value => value * UNDER_TRACK_SUPPORT_FOOTPRINT_SCALE)) {
|
|
449
|
+
for (const post of [left, right]) {
|
|
450
|
+
hardware.push(
|
|
451
|
+
beam(
|
|
452
|
+
post
|
|
453
|
+
.clone()
|
|
454
|
+
.addScaledVector(frame.right, -0.41 * UNDER_TRACK_SUPPORT_FOOTPRINT_SCALE)
|
|
455
|
+
.addScaledVector(WORLD_UP, -depth),
|
|
456
|
+
post
|
|
457
|
+
.clone()
|
|
458
|
+
.addScaledVector(frame.right, 0.41 * UNDER_TRACK_SUPPORT_FOOTPRINT_SCALE)
|
|
459
|
+
.addScaledVector(WORLD_UP, -depth),
|
|
460
|
+
0.15 * UNDER_TRACK_SUPPORT_MEMBER_SCALE,
|
|
461
|
+
0.78 * UNDER_TRACK_SUPPORT_MEMBER_SCALE,
|
|
462
|
+
fadeAnchorY,
|
|
463
|
+
),
|
|
464
|
+
)
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
for (const side of [-1, 1] as const) {
|
|
468
|
+
const post = side < 0 ? left : right
|
|
469
|
+
for (const [step, width] of [
|
|
470
|
+
[0.55, 1.28],
|
|
471
|
+
[0.91, 1.82],
|
|
472
|
+
[1.27, 2.32],
|
|
473
|
+
] as const) {
|
|
474
|
+
timber.push(
|
|
475
|
+
beam(
|
|
476
|
+
post
|
|
477
|
+
.clone()
|
|
478
|
+
.addScaledVector(frame.right, -side * width * 0.5 * UNDER_TRACK_SUPPORT_FOOTPRINT_SCALE)
|
|
479
|
+
.addScaledVector(WORLD_UP, -step * UNDER_TRACK_SUPPORT_FOOTPRINT_SCALE),
|
|
480
|
+
post
|
|
481
|
+
.clone()
|
|
482
|
+
.addScaledVector(frame.right, side * width * 0.5 * UNDER_TRACK_SUPPORT_FOOTPRINT_SCALE)
|
|
483
|
+
.addScaledVector(WORLD_UP, -step * UNDER_TRACK_SUPPORT_FOOTPRINT_SCALE),
|
|
484
|
+
0.31 * UNDER_TRACK_SUPPORT_MEMBER_SCALE,
|
|
485
|
+
0.38 * UNDER_TRACK_SUPPORT_MEMBER_SCALE,
|
|
486
|
+
fadeAnchorY,
|
|
487
|
+
),
|
|
488
|
+
)
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
for (let index = 0; index < pathFrames.length - 1; index += 1) {
|
|
494
|
+
for (const side of [-1, 1] as const) {
|
|
495
|
+
const current = supportContact(pathFrames[index]!, side)
|
|
496
|
+
const next = supportContact(pathFrames[index + 1]!, side)
|
|
497
|
+
timber.push(
|
|
498
|
+
beam(current, next, 0.5 * UNDER_TRACK_SUPPORT_MEMBER_SCALE, 0.42 * UNDER_TRACK_SUPPORT_MEMBER_SCALE),
|
|
499
|
+
beam(
|
|
500
|
+
current.clone().addScaledVector(WORLD_UP, -0.52 * UNDER_TRACK_SUPPORT_FOOTPRINT_SCALE),
|
|
501
|
+
next.clone().addScaledVector(WORLD_UP, -0.52 * UNDER_TRACK_SUPPORT_FOOTPRINT_SCALE),
|
|
502
|
+
0.42 * UNDER_TRACK_SUPPORT_MEMBER_SCALE,
|
|
503
|
+
0.38 * UNDER_TRACK_SUPPORT_MEMBER_SCALE,
|
|
504
|
+
),
|
|
505
|
+
)
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
for (const ownedBay of layout.ownedBays) {
|
|
509
|
+
const current = sakuraBentAtFrame(ownedBay.start)
|
|
510
|
+
const next = sakuraBentAtFrame(ownedBay.end)
|
|
511
|
+
const fadeAnchorY = Math.max(current.fadeAnchorY, next.fadeAnchorY)
|
|
512
|
+
for (const side of ['left', 'right'] as const) {
|
|
513
|
+
timber.push(
|
|
514
|
+
beam(
|
|
515
|
+
current[side].clone().addScaledVector(WORLD_UP, -UNDER_TRACK_SUPPORT_FADE_START_DEPTH),
|
|
516
|
+
next[side].clone().addScaledVector(WORLD_UP, -1.65 * UNDER_TRACK_SUPPORT_FOOTPRINT_SCALE),
|
|
517
|
+
0.28 * UNDER_TRACK_SUPPORT_MEMBER_SCALE,
|
|
518
|
+
0.34 * UNDER_TRACK_SUPPORT_MEMBER_SCALE,
|
|
519
|
+
fadeAnchorY,
|
|
520
|
+
),
|
|
521
|
+
beam(
|
|
522
|
+
current[side].clone().addScaledVector(WORLD_UP, -1.65 * UNDER_TRACK_SUPPORT_FOOTPRINT_SCALE),
|
|
523
|
+
next[side].clone().addScaledVector(WORLD_UP, -UNDER_TRACK_SUPPORT_FADE_START_DEPTH),
|
|
524
|
+
0.28 * UNDER_TRACK_SUPPORT_MEMBER_SCALE,
|
|
525
|
+
0.34 * UNDER_TRACK_SUPPORT_MEMBER_SCALE,
|
|
526
|
+
fadeAnchorY,
|
|
527
|
+
),
|
|
528
|
+
)
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
addBeamBatch(root, `${pieceId}-sakura-underbridge-timber`, timber, materials.support, 'sakura-underbridge-timber')
|
|
533
|
+
addBeamBatch(root, `${pieceId}-sakura-underbridge-hardware`, hardware, materials.metal, 'sakura-underbridge-hardware')
|
|
534
|
+
root.userData.supportBentCount = sampled.length
|
|
535
|
+
root.userData.supportBentLayout = 'continuous-metric-grid'
|
|
536
|
+
root.userData.stringersMeetConnectorPlanes = true
|
|
537
|
+
root.userData.supportStationPoints = sampled.map(frame => frame.point.toArray())
|
|
538
|
+
root.userData.supportOwnedBayCount = layout.ownedBays.length
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function addSakuraDragonStoneSupport(
|
|
542
|
+
root: THREE.Group,
|
|
543
|
+
pieceId: string,
|
|
544
|
+
pathFrames: readonly TrackAppearanceSupportFrame[],
|
|
545
|
+
material: THREE.Material,
|
|
546
|
+
options: TrackAppearanceSupportOptions,
|
|
547
|
+
): void {
|
|
548
|
+
const layout = sampleContinuousSupportLayout(pathFrames, options)
|
|
549
|
+
const sampled = layout.stations
|
|
550
|
+
void addJapaneseSakuraCastleSupportModel(root, pieceId, pathFrames, sampled, material, {
|
|
551
|
+
...options,
|
|
552
|
+
ownedBayDistances: layout.ownedBays.map(bay => [bay.startDistance, bay.endDistance]),
|
|
553
|
+
}).then(() => {
|
|
554
|
+
if (root.userData.supportDisposed !== true) addSupportDepthPrepasses(root)
|
|
555
|
+
})
|
|
556
|
+
root.userData.supportBentCount = sampled.length
|
|
557
|
+
root.userData.supportBentLayout = 'continuous-metric-grid-approved-blender-modules'
|
|
558
|
+
root.userData.hasDragonHero = false
|
|
559
|
+
root.userData.dragonSideCoverage = 'none'
|
|
560
|
+
root.userData.supportStationPoints = sampled.map(frame => frame.point.toArray())
|
|
561
|
+
root.userData.supportOwnedBayCount = layout.ownedBays.length
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function supportContact(frame: TrackAppearanceSupportFrame, side: -1 | 1): THREE.Vector3 {
|
|
565
|
+
return frame.point
|
|
566
|
+
.clone()
|
|
567
|
+
.addScaledVector(frame.up, -0.39)
|
|
568
|
+
.addScaledVector(frame.right, side * underTrackSupportHalfSpan(frame.width))
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function sampleContinuousSupportLayout(
|
|
572
|
+
frames: readonly TrackAppearanceSupportFrame[],
|
|
573
|
+
options: TrackAppearanceSupportOptions,
|
|
574
|
+
): SampledSupportLayout {
|
|
575
|
+
const distances = cumulativeDistances(frames)
|
|
576
|
+
const length = distances.at(-1) ?? 0
|
|
577
|
+
const layout = createUnderTrackSupportLayout(
|
|
578
|
+
length,
|
|
579
|
+
options.connectionPhase,
|
|
580
|
+
options.capStart === false,
|
|
581
|
+
options.capEnd === false,
|
|
582
|
+
)
|
|
583
|
+
const excluded = options.excludedStationPoints ?? []
|
|
584
|
+
const allowed = (frame: TrackAppearanceSupportFrame) =>
|
|
585
|
+
excluded.every(point => point.distanceTo(frame.point) >= underTrackSupportStationClearance(frame.width))
|
|
586
|
+
const stations = layout.centers.map(center => frameAtDistance(frames, distances, center)).filter(allowed)
|
|
587
|
+
const ownedBays = layout.ownedBays
|
|
588
|
+
.map(bay => ({
|
|
589
|
+
start: frameAtDistance(frames, distances, bay.start),
|
|
590
|
+
end: frameAtDistance(frames, distances, bay.end),
|
|
591
|
+
startDistance: bay.start,
|
|
592
|
+
endDistance: bay.end,
|
|
593
|
+
}))
|
|
594
|
+
.filter(bay => allowed(bay.start) && allowed(bay.end))
|
|
595
|
+
return { stations, ownedBays }
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function addStackedCanyonButtresses(
|
|
599
|
+
root: THREE.Group,
|
|
600
|
+
pieceId: string,
|
|
601
|
+
pathFrames: readonly TrackAppearanceSupportFrame[],
|
|
602
|
+
material: THREE.Material,
|
|
603
|
+
options: TrackAppearanceSupportOptions,
|
|
604
|
+
): void {
|
|
605
|
+
const layout = sampleContinuousSupportLayout(pathFrames, options)
|
|
606
|
+
const frames = layout.stations
|
|
607
|
+
const geometry = createStackedCanyonGeometry(pieceId, frames)
|
|
608
|
+
const buttresses = new THREE.Mesh(geometry, material)
|
|
609
|
+
buttresses.name = `${pieceId}-stacked-canyon-buttresses`
|
|
610
|
+
buttresses.userData.supportRole = 'stacked-canyon-buttresses'
|
|
611
|
+
buttresses.userData.supportCourseCount = CANYON_COURSE_SCHEDULE.length
|
|
612
|
+
buttresses.userData.supportFadeAttribute = 'supportFade'
|
|
613
|
+
root.userData.supportBentCount = frames.length
|
|
614
|
+
root.userData.supportStationPoints = frames.map(frame => frame.point.toArray())
|
|
615
|
+
root.userData.supportOwnedBayCount = layout.ownedBays.length
|
|
616
|
+
const pathLength = cumulativeDistances(pathFrames).at(-1) ?? 0
|
|
617
|
+
root.userData.openStartInset =
|
|
618
|
+
options.capStart === false ? (frames[0]?.point.distanceTo(pathFrames[0]!.point) ?? pathLength) : 0
|
|
619
|
+
root.userData.openEndInset =
|
|
620
|
+
options.capEnd === false ? (frames.at(-1)?.point.distanceTo(pathFrames.at(-1)!.point) ?? pathLength) : 0
|
|
621
|
+
root.add(buttresses)
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function createStackedCanyonGeometry(
|
|
625
|
+
pieceId: string,
|
|
626
|
+
frames: readonly TrackAppearanceSupportFrame[],
|
|
627
|
+
): THREE.BufferGeometry {
|
|
628
|
+
const vertices: number[] = []
|
|
629
|
+
const uvs: number[] = []
|
|
630
|
+
const fades: number[] = []
|
|
631
|
+
const indices: number[] = []
|
|
632
|
+
const topContactVertexIndices: number[] = []
|
|
633
|
+
|
|
634
|
+
for (const [buttressIndex, frame] of frames.entries()) {
|
|
635
|
+
const contactPlanePoint = frame.point.clone().addScaledVector(frame.up, -SUPPORT_DECK_UNDERSIDE_OFFSET)
|
|
636
|
+
const topY = contactPlanePoint.y
|
|
637
|
+
const right = frame.right.clone().setY(0)
|
|
638
|
+
if (right.lengthSq() < 0.000_001) right.set(frame.tangent.z, 0, -frame.tangent.x)
|
|
639
|
+
right.normalize()
|
|
640
|
+
const tangent = new THREE.Vector3().crossVectors(right, WORLD_UP).normalize()
|
|
641
|
+
if (tangent.dot(frame.tangent) < 0) tangent.negate()
|
|
642
|
+
const baseWidth = Math.max(2.9, frame.width * 0.9) * UNDER_TRACK_SUPPORT_FOOTPRINT_SCALE
|
|
643
|
+
const baseDepth = 2.9 * UNDER_TRACK_SUPPORT_FOOTPRINT_SCALE
|
|
644
|
+
const moduleWidth = 0.96 + deterministic(`${pieceId}:${buttressIndex}:stack-width`) * 0.08
|
|
645
|
+
const moduleDepth = 0.96 + deterministic(`${pieceId}:${buttressIndex}:stack-depth`) * 0.08
|
|
646
|
+
|
|
647
|
+
for (const [
|
|
648
|
+
courseIndex,
|
|
649
|
+
[topFraction, bottomFraction, widthScale, depthScale, split],
|
|
650
|
+
] of CANYON_COURSE_SCHEDULE.entries()) {
|
|
651
|
+
const courseWidth = baseWidth * widthScale * moduleWidth
|
|
652
|
+
const courseDepth = baseDepth * depthScale * moduleDepth
|
|
653
|
+
const seed = `${pieceId}:${buttressIndex}:${courseIndex}`
|
|
654
|
+
const centerRight = (deterministic(`${seed}:stack-x`) - 0.5) * baseWidth * 0.09
|
|
655
|
+
const centerTangent = (deterministic(`${seed}:stack-z`) - 0.5) * baseDepth * 0.055
|
|
656
|
+
const inset = UNDER_TRACK_SUPPORT_DEPTH * 0.000_35
|
|
657
|
+
const courseTopY = topY - topFraction * UNDER_TRACK_SUPPORT_DEPTH - inset
|
|
658
|
+
const courseBottomY = topY - bottomFraction * UNDER_TRACK_SUPPORT_DEPTH + inset
|
|
659
|
+
const topContactBlend = Math.max(0, 1 - topFraction / CANYON_SLOPE_TRANSITION_DEPTH_FRACTION)
|
|
660
|
+
const bottomContactBlend = Math.max(0, 1 - bottomFraction / CANYON_SLOPE_TRANSITION_DEPTH_FRACTION)
|
|
661
|
+
const splitThisCourse = split && deterministic(`${seed}:split`) > 0.52
|
|
662
|
+
const blocks: { width: number; centerRight: number }[] = []
|
|
663
|
+
if (splitThisCourse) {
|
|
664
|
+
const joint = baseWidth * 0.025
|
|
665
|
+
const leftShare = 0.46 + deterministic(`${seed}:split-share`) * 0.1
|
|
666
|
+
const leftWidth = courseWidth * leftShare - joint / 2
|
|
667
|
+
const rightWidth = courseWidth * (1 - leftShare) - joint / 2
|
|
668
|
+
blocks.push(
|
|
669
|
+
{ width: leftWidth, centerRight: centerRight - courseWidth / 2 + leftWidth / 2 },
|
|
670
|
+
{ width: rightWidth, centerRight: centerRight + courseWidth / 2 - rightWidth / 2 },
|
|
671
|
+
)
|
|
672
|
+
} else {
|
|
673
|
+
blocks.push({ width: courseWidth, centerRight })
|
|
674
|
+
}
|
|
675
|
+
for (const [blockIndex, block] of blocks.entries()) {
|
|
676
|
+
appendCanyonCourse(
|
|
677
|
+
{ vertices, uvs, fades, indices, topContactVertexIndices },
|
|
678
|
+
{
|
|
679
|
+
topY: courseTopY,
|
|
680
|
+
bottomY: courseBottomY,
|
|
681
|
+
width: block.width,
|
|
682
|
+
depth: courseDepth,
|
|
683
|
+
center: frame.point
|
|
684
|
+
.clone()
|
|
685
|
+
.addScaledVector(right, block.centerRight)
|
|
686
|
+
.addScaledVector(tangent, centerTangent),
|
|
687
|
+
right,
|
|
688
|
+
tangent,
|
|
689
|
+
fadeAnchorY: topY,
|
|
690
|
+
seed: `${seed}:${blockIndex}`,
|
|
691
|
+
topContactPlane: topContactBlend > 0 ? { point: contactPlanePoint, normal: frame.up } : undefined,
|
|
692
|
+
topContactBlendRange: [bottomContactBlend, topContactBlend],
|
|
693
|
+
},
|
|
694
|
+
)
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const geometry = new THREE.BufferGeometry()
|
|
700
|
+
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3))
|
|
701
|
+
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2))
|
|
702
|
+
geometry.setAttribute('uv1', new THREE.Float32BufferAttribute(uvs, 2))
|
|
703
|
+
const themeNetUvs = new Float32Array((uvs.length / 2) * 4)
|
|
704
|
+
for (let index = 0; index < uvs.length; index += 2) {
|
|
705
|
+
const target = index * 2
|
|
706
|
+
themeNetUvs[target] = uvs[index] ?? 0
|
|
707
|
+
themeNetUvs[target + 1] = uvs[index + 1] ?? 0
|
|
708
|
+
themeNetUvs[target + 2] = 1
|
|
709
|
+
themeNetUvs[target + 3] = 1
|
|
710
|
+
}
|
|
711
|
+
geometry.setAttribute('themeNetUv', new THREE.BufferAttribute(themeNetUvs, 4))
|
|
712
|
+
geometry.setAttribute('supportFade', new THREE.Float32BufferAttribute(fades, 1))
|
|
713
|
+
geometry.setIndex(indices)
|
|
714
|
+
geometry.computeVertexNormals()
|
|
715
|
+
geometry.computeBoundingBox()
|
|
716
|
+
geometry.computeBoundingSphere()
|
|
717
|
+
geometry.userData.supportGeometry = 'spaced-stacked-canyon-buttresses'
|
|
718
|
+
geometry.userData.supportCourseCount = CANYON_COURSE_SCHEDULE.length
|
|
719
|
+
geometry.userData.supportButtressCount = frames.length
|
|
720
|
+
geometry.userData.supportDepth = UNDER_TRACK_SUPPORT_DEPTH
|
|
721
|
+
geometry.userData.supportFadeDepth = UNDER_TRACK_SUPPORT_FADE_DEPTH
|
|
722
|
+
geometry.userData.supportCapSlopeMatched = true
|
|
723
|
+
geometry.userData.supportTopContactVertexIndices = topContactVertexIndices
|
|
724
|
+
return geometry
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
interface CanyonGeometryBuffers {
|
|
728
|
+
vertices: number[]
|
|
729
|
+
uvs: number[]
|
|
730
|
+
fades: number[]
|
|
731
|
+
indices: number[]
|
|
732
|
+
topContactVertexIndices: number[]
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
interface CanyonCourseOptions {
|
|
736
|
+
topY: number
|
|
737
|
+
bottomY: number
|
|
738
|
+
width: number
|
|
739
|
+
depth: number
|
|
740
|
+
center: THREE.Vector3
|
|
741
|
+
right: THREE.Vector3
|
|
742
|
+
tangent: THREE.Vector3
|
|
743
|
+
fadeAnchorY: number
|
|
744
|
+
seed: string
|
|
745
|
+
topContactPlane?: {
|
|
746
|
+
point: THREE.Vector3
|
|
747
|
+
normal: THREE.Vector3
|
|
748
|
+
}
|
|
749
|
+
topContactBlendRange: readonly [bottom: number, top: number]
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function appendCanyonCourse(buffers: CanyonGeometryBuffers, options: CanyonCourseOptions): void {
|
|
753
|
+
const {
|
|
754
|
+
topY,
|
|
755
|
+
bottomY,
|
|
756
|
+
width,
|
|
757
|
+
depth,
|
|
758
|
+
center,
|
|
759
|
+
right,
|
|
760
|
+
tangent,
|
|
761
|
+
fadeAnchorY,
|
|
762
|
+
seed,
|
|
763
|
+
topContactPlane,
|
|
764
|
+
topContactBlendRange,
|
|
765
|
+
} = options
|
|
766
|
+
const courseHeight = topY - bottomY
|
|
767
|
+
const bevel = Math.min(
|
|
768
|
+
courseHeight * (0.12 + deterministic(`${seed}:bevel-height`) * 0.13),
|
|
769
|
+
Math.min(width, depth) * 0.07,
|
|
770
|
+
)
|
|
771
|
+
const ringSchedule = [
|
|
772
|
+
[bottomY, 0.93 + deterministic(`${seed}:bottom-inset`) * 0.045],
|
|
773
|
+
[bottomY + bevel, 0.96 + deterministic(`${seed}:lower-shoulder`) * 0.08],
|
|
774
|
+
[topY - bevel, 0.97 + deterministic(`${seed}:upper-shoulder`) * 0.09],
|
|
775
|
+
[topY, 0.93 + deterministic(`${seed}:top-inset`) * 0.045],
|
|
776
|
+
] as const
|
|
777
|
+
const rings = ringSchedule.map(([y, scale], ringIndex) => ({
|
|
778
|
+
y,
|
|
779
|
+
topContactBlend: topContactPlane
|
|
780
|
+
? THREE.MathUtils.lerp(
|
|
781
|
+
topContactBlendRange[0],
|
|
782
|
+
topContactBlendRange[1],
|
|
783
|
+
THREE.MathUtils.clamp((y - bottomY) / courseHeight, 0, 1),
|
|
784
|
+
)
|
|
785
|
+
: 0,
|
|
786
|
+
points: createCanyonCourseContour((width * scale) / 2, (depth * scale) / 2, seed, ringIndex),
|
|
787
|
+
}))
|
|
788
|
+
|
|
789
|
+
const worldPoint = (point: THREE.Vector2, y: number, topContactBlend = 0) => {
|
|
790
|
+
const world = center.clone().addScaledVector(right, point.x).addScaledVector(tangent, point.y).setY(y)
|
|
791
|
+
if (!topContactPlane || topContactBlend <= 0 || Math.abs(topContactPlane.normal.y) <= 0.000_001) return world
|
|
792
|
+
const contactY =
|
|
793
|
+
topContactPlane.point.y -
|
|
794
|
+
(topContactPlane.normal.x * (world.x - topContactPlane.point.x) +
|
|
795
|
+
topContactPlane.normal.z * (world.z - topContactPlane.point.z)) /
|
|
796
|
+
topContactPlane.normal.y
|
|
797
|
+
world.y += (contactY - topY) * topContactBlend
|
|
798
|
+
return world
|
|
799
|
+
}
|
|
800
|
+
const fadeAt = (y: number) => underTrackSupportFadeAtDepth(fadeAnchorY - y)
|
|
801
|
+
|
|
802
|
+
for (let ringIndex = 0; ringIndex < rings.length - 1; ringIndex += 1) {
|
|
803
|
+
const lower = rings[ringIndex]!
|
|
804
|
+
const upper = rings[ringIndex + 1]!
|
|
805
|
+
let lowerU = 0
|
|
806
|
+
let upperU = 0
|
|
807
|
+
for (let side = 0; side < lower.points.length; side += 1) {
|
|
808
|
+
const next = (side + 1) % lower.points.length
|
|
809
|
+
const a = lower.points[side]!
|
|
810
|
+
const b = lower.points[next]!
|
|
811
|
+
const c = upper.points[side]!
|
|
812
|
+
const d = upper.points[next]!
|
|
813
|
+
const base = buffers.vertices.length / 3
|
|
814
|
+
const world = [
|
|
815
|
+
worldPoint(a, lower.y, lower.topContactBlend),
|
|
816
|
+
worldPoint(b, lower.y, lower.topContactBlend),
|
|
817
|
+
worldPoint(c, upper.y, upper.topContactBlend),
|
|
818
|
+
worldPoint(d, upper.y, upper.topContactBlend),
|
|
819
|
+
]
|
|
820
|
+
world.forEach(point => buffers.vertices.push(point.x, point.y, point.z))
|
|
821
|
+
if (topContactPlane && upper.topContactBlend === 1) {
|
|
822
|
+
buffers.topContactVertexIndices.push(base + 2, base + 3)
|
|
823
|
+
}
|
|
824
|
+
const lowerNextU = lowerU + a.distanceTo(b)
|
|
825
|
+
const upperNextU = upperU + c.distanceTo(d)
|
|
826
|
+
buffers.uvs.push(lowerU, -lower.y, lowerNextU, -lower.y, upperU, -upper.y, upperNextU, -upper.y)
|
|
827
|
+
buffers.fades.push(fadeAt(lower.y), fadeAt(lower.y), fadeAt(upper.y), fadeAt(upper.y))
|
|
828
|
+
buffers.indices.push(base, base + 2, base + 1, base + 1, base + 2, base + 3)
|
|
829
|
+
lowerU = lowerNextU
|
|
830
|
+
upperU = upperNextU
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
for (const [ring, isTop] of [
|
|
835
|
+
[rings[0]!, false],
|
|
836
|
+
[rings.at(-1)!, true],
|
|
837
|
+
] as const) {
|
|
838
|
+
const centerIndex = buffers.vertices.length / 3
|
|
839
|
+
const capCenter = worldPoint(new THREE.Vector2(), ring.y, ring.topContactBlend)
|
|
840
|
+
buffers.vertices.push(capCenter.x, capCenter.y, capCenter.z)
|
|
841
|
+
if (topContactPlane && isTop && ring.topContactBlend === 1) {
|
|
842
|
+
buffers.topContactVertexIndices.push(centerIndex)
|
|
843
|
+
}
|
|
844
|
+
buffers.uvs.push(center.x, center.z)
|
|
845
|
+
buffers.fades.push(fadeAt(ring.y))
|
|
846
|
+
for (let side = 0; side < ring.points.length; side += 1) {
|
|
847
|
+
const next = (side + 1) % ring.points.length
|
|
848
|
+
const base = buffers.vertices.length / 3
|
|
849
|
+
const a = worldPoint(ring.points[side]!, ring.y, ring.topContactBlend)
|
|
850
|
+
const b = worldPoint(ring.points[next]!, ring.y, ring.topContactBlend)
|
|
851
|
+
buffers.vertices.push(a.x, a.y, a.z, b.x, b.y, b.z)
|
|
852
|
+
if (topContactPlane && isTop && ring.topContactBlend === 1) {
|
|
853
|
+
buffers.topContactVertexIndices.push(base, base + 1)
|
|
854
|
+
}
|
|
855
|
+
buffers.uvs.push(a.x, a.z, b.x, b.z)
|
|
856
|
+
buffers.fades.push(fadeAt(ring.y), fadeAt(ring.y))
|
|
857
|
+
buffers.indices.push(...(isTop ? [centerIndex, base + 1, base] : [centerIndex, base, base + 1]))
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function createCanyonCourseContour(
|
|
863
|
+
halfWidth: number,
|
|
864
|
+
halfDepth: number,
|
|
865
|
+
seed: string,
|
|
866
|
+
ringIndex: number,
|
|
867
|
+
): THREE.Vector2[] {
|
|
868
|
+
const pointCount = 20
|
|
869
|
+
const exponent = 4.6 + deterministic(`${seed}:course-boxiness`) * 1.8
|
|
870
|
+
const phase = deterministic(`${seed}:course-phase`) * Math.PI * 2
|
|
871
|
+
return Array.from({ length: pointCount }, (_, index) => {
|
|
872
|
+
const angle = -Math.PI * 0.75 + (index / pointCount) * Math.PI * 2
|
|
873
|
+
const cosine = Math.cos(angle)
|
|
874
|
+
const sine = Math.sin(angle)
|
|
875
|
+
const x = Math.sign(cosine) * Math.pow(Math.abs(cosine), 2 / exponent) * halfWidth
|
|
876
|
+
const z = Math.sign(sine) * Math.pow(Math.abs(sine), 2 / exponent) * halfDepth
|
|
877
|
+
const broadChip = Math.sin(angle * 3 + phase) * 0.052 + Math.sin(angle * 5 - phase) * 0.028
|
|
878
|
+
const cornerLoss = (deterministic(`${seed}:course-chip:${index}`) - 0.5) * 0.105
|
|
879
|
+
const ringVariation = (deterministic(`${seed}:course-ring:${ringIndex}:${index}`) - 0.5) * 0.05
|
|
880
|
+
const displacement = Math.min(halfWidth, halfDepth) * (broadChip + cornerLoss + ringVariation)
|
|
881
|
+
const length = Math.hypot(x, z) || 1
|
|
882
|
+
return new THREE.Vector2(x + (x / length) * displacement, z + (z / length) * displacement)
|
|
883
|
+
})
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function beam(
|
|
887
|
+
start: THREE.Vector3,
|
|
888
|
+
end: THREE.Vector3,
|
|
889
|
+
width: number,
|
|
890
|
+
depth: number,
|
|
891
|
+
fadeAnchorY = Math.max(start.y, end.y),
|
|
892
|
+
): Beam {
|
|
893
|
+
return { start, end, width, depth, fadeAnchorY }
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function addBeamBatch(
|
|
897
|
+
root: THREE.Group,
|
|
898
|
+
name: string,
|
|
899
|
+
beams: readonly Beam[],
|
|
900
|
+
material: THREE.Material,
|
|
901
|
+
role: string,
|
|
902
|
+
): void {
|
|
903
|
+
if (beams.length === 0) return
|
|
904
|
+
const geometry = new THREE.BoxGeometry(1, 1, 1)
|
|
905
|
+
const bottomAttribute = new THREE.InstancedBufferAttribute(
|
|
906
|
+
new Float32Array(beams.map(item => item.fadeAnchorY - UNDER_TRACK_SUPPORT_DEPTH)),
|
|
907
|
+
1,
|
|
908
|
+
)
|
|
909
|
+
const topAttribute = new THREE.InstancedBufferAttribute(
|
|
910
|
+
new Float32Array(beams.map(item => item.fadeAnchorY - UNDER_TRACK_SUPPORT_FADE_START_DEPTH)),
|
|
911
|
+
1,
|
|
912
|
+
)
|
|
913
|
+
geometry.setAttribute('supportFadeBottom', bottomAttribute)
|
|
914
|
+
geometry.setAttribute('supportFadeTop', topAttribute)
|
|
915
|
+
const mesh = new THREE.InstancedMesh(geometry, material, beams.length)
|
|
916
|
+
mesh.name = name
|
|
917
|
+
for (const [index, item] of beams.entries()) {
|
|
918
|
+
const direction = item.end.clone().sub(item.start)
|
|
919
|
+
const length = direction.length()
|
|
920
|
+
const quaternion = new THREE.Quaternion().setFromUnitVectors(WORLD_UP, direction.normalize())
|
|
921
|
+
mesh.setMatrixAt(
|
|
922
|
+
index,
|
|
923
|
+
new THREE.Matrix4().compose(
|
|
924
|
+
item.start.clone().lerp(item.end, 0.5),
|
|
925
|
+
quaternion,
|
|
926
|
+
new THREE.Vector3(item.width, length, item.depth),
|
|
927
|
+
),
|
|
928
|
+
)
|
|
929
|
+
}
|
|
930
|
+
mesh.instanceMatrix.needsUpdate = true
|
|
931
|
+
mesh.computeBoundingBox()
|
|
932
|
+
mesh.computeBoundingSphere()
|
|
933
|
+
mesh.userData.supportRole = role
|
|
934
|
+
mesh.userData.supportFadeAttributes = ['supportFadeBottom', 'supportFadeTop']
|
|
935
|
+
root.add(mesh)
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function cumulativeDistances(frames: readonly TrackAppearanceSupportFrame[]): number[] {
|
|
939
|
+
const distances = [0]
|
|
940
|
+
for (let index = 0; index < frames.length - 1; index += 1) {
|
|
941
|
+
distances.push(distances.at(-1)! + frames[index]!.point.distanceTo(frames[index + 1]!.point))
|
|
942
|
+
}
|
|
943
|
+
return distances
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function frameAtDistance(
|
|
947
|
+
frames: readonly TrackAppearanceSupportFrame[],
|
|
948
|
+
distances: readonly number[],
|
|
949
|
+
target: number,
|
|
950
|
+
): TrackAppearanceSupportFrame {
|
|
951
|
+
let segment = 0
|
|
952
|
+
while (segment < distances.length - 2 && distances[segment + 1]! < target) segment += 1
|
|
953
|
+
const length = distances[segment + 1]! - distances[segment]!
|
|
954
|
+
const progress = length <= 0.000_001 ? 0 : (target - distances[segment]!) / length
|
|
955
|
+
const start = frames[segment]!
|
|
956
|
+
const end = frames[segment + 1]!
|
|
957
|
+
const tangent = start.tangent.clone().lerp(end.tangent, progress).normalize()
|
|
958
|
+
const right = start.right.clone().lerp(end.right, progress)
|
|
959
|
+
right.addScaledVector(tangent, -right.dot(tangent))
|
|
960
|
+
if (right.lengthSq() < 0.000_001) right.crossVectors(start.up, tangent)
|
|
961
|
+
right.normalize()
|
|
962
|
+
const up = new THREE.Vector3().crossVectors(tangent, right).normalize()
|
|
963
|
+
if (up.dot(start.up.clone().lerp(end.up, progress)) < 0) {
|
|
964
|
+
right.negate()
|
|
965
|
+
up.negate()
|
|
966
|
+
}
|
|
967
|
+
return {
|
|
968
|
+
point: start.point.clone().lerp(end.point, progress),
|
|
969
|
+
tangent,
|
|
970
|
+
right,
|
|
971
|
+
up,
|
|
972
|
+
width: THREE.MathUtils.lerp(start.width, end.width, progress),
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function deterministic(seed: string): number {
|
|
977
|
+
let hash = 2_166_136_261
|
|
978
|
+
for (const character of seed) {
|
|
979
|
+
hash ^= character.charCodeAt(0)
|
|
980
|
+
hash = Math.imul(hash, 16_777_619)
|
|
981
|
+
}
|
|
982
|
+
return (hash >>> 0) / 4_294_967_296
|
|
983
|
+
}
|