@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,443 @@
|
|
|
1
|
+
import * as THREE from 'three'
|
|
2
|
+
|
|
3
|
+
import { VisualTorusGeometry, VisualTubeGeometry } from '../../../three/visual-geometry'
|
|
4
|
+
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
|
5
|
+
|
|
6
|
+
import { ROPE_WALL_SUPPORTED_THEME_IDS } from '../../../three/rope-wall-appearance'
|
|
7
|
+
import { TRACK_RAIL_WALL_THICKNESS } from './track-definition'
|
|
8
|
+
import {
|
|
9
|
+
canonicalWallFrameAt,
|
|
10
|
+
type CanonicalWallRun,
|
|
11
|
+
type RawWallFrame,
|
|
12
|
+
} from './track-surface-topology'
|
|
13
|
+
import { woodenFenceHeight, woodenFencePostCount, WOODEN_FENCE_HEIGHT_RATIO } from './track-fence-architecture'
|
|
14
|
+
|
|
15
|
+
export type RopeWallPathFrame = RawWallFrame
|
|
16
|
+
export type RopeWallPathRun = CanonicalWallRun
|
|
17
|
+
|
|
18
|
+
export interface RopeWallMaterials {
|
|
19
|
+
rope: THREE.Material
|
|
20
|
+
timber: THREE.Material
|
|
21
|
+
timberEndGrain: THREE.Material
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const HANGER_SPACING = 0.58
|
|
25
|
+
const CABLE_SEGMENTS = 8
|
|
26
|
+
const HANDRAIL_HEIGHT = 0.79
|
|
27
|
+
const LOWER_CABLE_HEIGHT = 0.2
|
|
28
|
+
const OUTWARD_OFFSET = 0.075
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Builds the side wall of a hand-lashed rope bridge: substantial timber
|
|
32
|
+
* uprights, wrapped rope collars, sagging handrail and lower cables, dense
|
|
33
|
+
* vertical suspension ties, knots, and irregular loose ends. The continuous
|
|
34
|
+
* wall created by the track builder remains the only collision geometry.
|
|
35
|
+
*/
|
|
36
|
+
export function addRopeWallVisual(
|
|
37
|
+
root: THREE.Group,
|
|
38
|
+
id: string,
|
|
39
|
+
runs: readonly RopeWallPathRun[],
|
|
40
|
+
wallHeight: number,
|
|
41
|
+
materials: RopeWallMaterials,
|
|
42
|
+
): THREE.Group | undefined {
|
|
43
|
+
const visualHeight = woodenFenceHeight(wallHeight)
|
|
44
|
+
const postMatrices: THREE.Matrix4[] = []
|
|
45
|
+
const handrailMatrices: THREE.Matrix4[] = []
|
|
46
|
+
const lowerCableMatrices: THREE.Matrix4[] = []
|
|
47
|
+
const hangerMatrices: THREE.Matrix4[] = []
|
|
48
|
+
const lashingMatrices: THREE.Matrix4[] = []
|
|
49
|
+
const crossLashingMatrices: THREE.Matrix4[] = []
|
|
50
|
+
const knotMatrices: THREE.Matrix4[] = []
|
|
51
|
+
const tailMatrices: THREE.Matrix4[] = []
|
|
52
|
+
|
|
53
|
+
for (const [runIndex, run] of runs.entries()) {
|
|
54
|
+
if (run.frames.length < 2) continue
|
|
55
|
+
// Rope dressing has a small authored stand-off from the canonical wall
|
|
56
|
+
// boundary. The topology itself remains shared with every other theme.
|
|
57
|
+
const canonicalRun: CanonicalWallRun = { ...run, frames: run.frames.map(frame => ({
|
|
58
|
+
...frame,
|
|
59
|
+
base: frame.base
|
|
60
|
+
.clone()
|
|
61
|
+
.addScaledVector(frame.outward, TRACK_RAIL_WALL_THICKNESS / 2 + 0.025)
|
|
62
|
+
.addScaledVector(frame.up, -0.03),
|
|
63
|
+
})) }
|
|
64
|
+
const totalLength = canonicalRun.length
|
|
65
|
+
if (totalLength <= 0.001) continue
|
|
66
|
+
const postCount = woodenFencePostCount(totalLength, visualHeight)
|
|
67
|
+
const posts = Array.from({ length: postCount }, (_, postIndex) =>
|
|
68
|
+
canonicalWallFrameAt(canonicalRun, (postIndex / (postCount - 1)) * totalLength),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
posts.forEach((frame, postIndex) => {
|
|
72
|
+
const seed = deterministic(`${id}:${runIndex}:${postIndex}`)
|
|
73
|
+
const postHeight = visualHeight
|
|
74
|
+
const lean = (seed - 0.5) * 0.11
|
|
75
|
+
const postRotation = frameQuaternion(frame.tangent, frame.up)
|
|
76
|
+
postRotation.premultiply(new THREE.Quaternion().setFromAxisAngle(frame.tangent, lean))
|
|
77
|
+
postMatrices.push(
|
|
78
|
+
new THREE.Matrix4().compose(
|
|
79
|
+
frame.base.clone().addScaledVector(frame.up, postHeight / 2 - 0.025),
|
|
80
|
+
postRotation,
|
|
81
|
+
new THREE.Vector3(0.96 + seed * 0.16, postHeight, 0.96 + seed * 0.16),
|
|
82
|
+
),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
for (const [anchorHeight, scaleY] of [
|
|
86
|
+
[visualHeight * HANDRAIL_HEIGHT, 1],
|
|
87
|
+
[visualHeight * LOWER_CABLE_HEIGHT, 0.78],
|
|
88
|
+
] as const) {
|
|
89
|
+
lashingMatrices.push(
|
|
90
|
+
new THREE.Matrix4().compose(
|
|
91
|
+
frame.base.clone().addScaledVector(frame.up, anchorHeight),
|
|
92
|
+
postRotation,
|
|
93
|
+
new THREE.Vector3(1.02 + seed * 0.08, scaleY, 1.02 + seed * 0.08),
|
|
94
|
+
),
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
appendCrossLashings(crossLashingMatrices, frame, visualHeight * LOWER_CABLE_HEIGHT)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
for (let postIndex = 0; postIndex < posts.length - 1; postIndex += 1) {
|
|
101
|
+
const start = posts[postIndex]!
|
|
102
|
+
const end = posts[postIndex + 1]!
|
|
103
|
+
const spanLength = start.base.distanceTo(end.base)
|
|
104
|
+
appendSaggingCable(handrailMatrices, start, end, visualHeight * HANDRAIL_HEIGHT, visualHeight * 0.12)
|
|
105
|
+
appendSaggingCable(lowerCableMatrices, start, end, visualHeight * LOWER_CABLE_HEIGHT, visualHeight * 0.035)
|
|
106
|
+
|
|
107
|
+
const hangerCount = Math.max(3, Math.ceil(spanLength / HANGER_SPACING))
|
|
108
|
+
for (let hangerIndex = 1; hangerIndex < hangerCount; hangerIndex += 1) {
|
|
109
|
+
const progress = hangerIndex / hangerCount
|
|
110
|
+
const upper = cablePoint(start, end, progress, visualHeight * HANDRAIL_HEIGHT, visualHeight * 0.12)
|
|
111
|
+
const lower = cablePoint(start, end, progress, visualHeight * LOWER_CABLE_HEIGHT, visualHeight * 0.035)
|
|
112
|
+
appendCylinderMatrix(hangerMatrices, upper, lower, 0.015)
|
|
113
|
+
|
|
114
|
+
const frame = interpolateFrame(start, end, progress)
|
|
115
|
+
const knotRotation = frameQuaternion(frame.tangent, frame.up).multiply(
|
|
116
|
+
new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), Math.PI / 2),
|
|
117
|
+
)
|
|
118
|
+
for (const point of [upper, lower]) {
|
|
119
|
+
knotMatrices.push(new THREE.Matrix4().compose(point, knotRotation, new THREE.Vector3(1, 1, 1)))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if ((hangerIndex + postIndex + runIndex) % 2 === 0) {
|
|
123
|
+
const tailLength = 0.13 + deterministic(`${id}:${runIndex}:${postIndex}:${hangerIndex}:tail`) * 0.19
|
|
124
|
+
const tailEnd = lower
|
|
125
|
+
.clone()
|
|
126
|
+
.addScaledVector(frame.up, -tailLength)
|
|
127
|
+
.addScaledVector(frame.tangent, (deterministic(`${id}:${hangerIndex}:sway`) - 0.5) * 0.09)
|
|
128
|
+
appendCylinderMatrix(tailMatrices, lower, tailEnd, 0.016)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (postMatrices.length === 0 || handrailMatrices.length === 0) return undefined
|
|
135
|
+
const group = new THREE.Group()
|
|
136
|
+
group.name = `${id}-rope-wall`
|
|
137
|
+
group.userData.themeOnly = [...ROPE_WALL_SUPPORTED_THEME_IDS]
|
|
138
|
+
group.userData.decorative = true
|
|
139
|
+
group.userData.visualOnly = true
|
|
140
|
+
group.userData.construction = 'hand-lashed-suspension-bridge-wall'
|
|
141
|
+
group.userData.wallHeightRatio = WOODEN_FENCE_HEIGHT_RATIO
|
|
142
|
+
|
|
143
|
+
group.add(
|
|
144
|
+
instancedBatch(`${id}-rope-wall-timber-posts`, createHandHewnPostGeometry(), materials.timber, postMatrices),
|
|
145
|
+
instancedBatch(
|
|
146
|
+
`${id}-rope-wall-timber-post-caps`,
|
|
147
|
+
createHandHewnPostCapGeometry(),
|
|
148
|
+
materials.timberEndGrain,
|
|
149
|
+
postMatrices,
|
|
150
|
+
),
|
|
151
|
+
instancedBatch(
|
|
152
|
+
`${id}-rope-wall-handrail-cable`,
|
|
153
|
+
createBraidedRopeSegmentGeometry(0.028, 0.024),
|
|
154
|
+
materials.rope,
|
|
155
|
+
handrailMatrices,
|
|
156
|
+
),
|
|
157
|
+
instancedBatch(
|
|
158
|
+
`${id}-rope-wall-lower-cable`,
|
|
159
|
+
createBraidedRopeSegmentGeometry(0.023, 0.019),
|
|
160
|
+
materials.rope,
|
|
161
|
+
lowerCableMatrices,
|
|
162
|
+
),
|
|
163
|
+
instancedBatch(
|
|
164
|
+
`${id}-rope-wall-vertical-hangers`,
|
|
165
|
+
createBraidedRopeSegmentGeometry(0.011, 0.009, 2),
|
|
166
|
+
materials.rope,
|
|
167
|
+
hangerMatrices,
|
|
168
|
+
),
|
|
169
|
+
instancedBatch(`${id}-rope-wall-post-lashings`, createRopeLashingGeometry(), materials.rope, lashingMatrices),
|
|
170
|
+
instancedBatch(
|
|
171
|
+
`${id}-rope-wall-cross-lashings`,
|
|
172
|
+
createBraidedRopeSegmentGeometry(0.013, 0.011, 2),
|
|
173
|
+
materials.rope,
|
|
174
|
+
crossLashingMatrices,
|
|
175
|
+
),
|
|
176
|
+
instancedBatch(
|
|
177
|
+
`${id}-rope-wall-hanger-knots`,
|
|
178
|
+
withAuthoredMaterialUv(new VisualTorusGeometry(0.057, 0.016, 5, 9)),
|
|
179
|
+
materials.rope,
|
|
180
|
+
knotMatrices,
|
|
181
|
+
),
|
|
182
|
+
instancedBatch(
|
|
183
|
+
`${id}-rope-wall-loose-ends`,
|
|
184
|
+
createBraidedRopeSegmentGeometry(0.0085, 0.0075, 2),
|
|
185
|
+
materials.rope,
|
|
186
|
+
tailMatrices,
|
|
187
|
+
),
|
|
188
|
+
)
|
|
189
|
+
root.add(group)
|
|
190
|
+
return group
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function createHandHewnPostGeometry(): THREE.BufferGeometry {
|
|
194
|
+
const sides = 12
|
|
195
|
+
const levels = [-0.5, -0.34, 0.3, 0.5] as const
|
|
196
|
+
const positions: number[] = []
|
|
197
|
+
const uvs: number[] = []
|
|
198
|
+
const metricUvs: number[] = []
|
|
199
|
+
const indices: number[] = []
|
|
200
|
+
|
|
201
|
+
for (const [levelIndex, y] of levels.entries()) {
|
|
202
|
+
for (let side = 0; side < sides; side += 1) {
|
|
203
|
+
const angle = (side / sides) * Math.PI * 2
|
|
204
|
+
const irregularity = 1 + Math.sin(side * 4.37 + levelIndex * 2.11) * 0.055
|
|
205
|
+
const taper = levelIndex === 0 ? 1.08 : levelIndex === levels.length - 1 ? 0.91 : 1
|
|
206
|
+
const topChip = 0
|
|
207
|
+
positions.push(
|
|
208
|
+
Math.cos(angle) * 0.225 * irregularity * taper,
|
|
209
|
+
y + topChip,
|
|
210
|
+
Math.sin(angle) * 0.225 * irregularity * taper,
|
|
211
|
+
)
|
|
212
|
+
uvs.push(side / sides, levelIndex / (levels.length - 1))
|
|
213
|
+
metricUvs.push(side / sides, levelIndex / (levels.length - 1), 1, 1)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
for (let level = 0; level < levels.length - 1; level += 1) {
|
|
218
|
+
for (let side = 0; side < sides; side += 1) {
|
|
219
|
+
const next = (side + 1) % sides
|
|
220
|
+
const lower = level * sides
|
|
221
|
+
const upper = (level + 1) * sides
|
|
222
|
+
indices.push(lower + side, upper + side, lower + next, lower + next, upper + side, upper + next)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const bottomCenter = positions.length / 3
|
|
227
|
+
positions.push(0, levels[0], 0)
|
|
228
|
+
uvs.push(0.5, 0.5)
|
|
229
|
+
metricUvs.push(0.5, 0.5, 1, 1)
|
|
230
|
+
for (let side = 0; side < sides; side += 1) {
|
|
231
|
+
indices.push(bottomCenter, side, (side + 1) % sides)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const geometry = new THREE.BufferGeometry()
|
|
235
|
+
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3))
|
|
236
|
+
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2))
|
|
237
|
+
geometry.setAttribute('themeNetUv', new THREE.Float32BufferAttribute(metricUvs, 4))
|
|
238
|
+
geometry.setIndex(indices)
|
|
239
|
+
geometry.computeVertexNormals()
|
|
240
|
+
geometry.computeBoundingBox()
|
|
241
|
+
geometry.computeBoundingSphere()
|
|
242
|
+
geometry.name = 'hand-hewn-rope-wall-post-geometry'
|
|
243
|
+
return geometry
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function createHandHewnPostCapGeometry(): THREE.BufferGeometry {
|
|
247
|
+
const sides = 12
|
|
248
|
+
const positions: number[] = []
|
|
249
|
+
const uvs: number[] = []
|
|
250
|
+
const metricUvs: number[] = []
|
|
251
|
+
const indices: number[] = []
|
|
252
|
+
|
|
253
|
+
for (let side = 0; side < sides; side += 1) {
|
|
254
|
+
const angle = (side / sides) * Math.PI * 2
|
|
255
|
+
const irregularity = 1 + Math.sin(side * 4.37 + 3 * 2.11) * 0.055
|
|
256
|
+
const topChip = 0
|
|
257
|
+
const x = Math.cos(angle) * 0.225 * irregularity * 0.91
|
|
258
|
+
const z = Math.sin(angle) * 0.225 * irregularity * 0.91
|
|
259
|
+
positions.push(x, 0.5008 + topChip, z)
|
|
260
|
+
const u = x / 0.51 + 0.5
|
|
261
|
+
const v = z / 0.51 + 0.5
|
|
262
|
+
uvs.push(u, v)
|
|
263
|
+
metricUvs.push(u, v, 1, 1)
|
|
264
|
+
}
|
|
265
|
+
const center = positions.length / 3
|
|
266
|
+
positions.push(0, 0.5008, 0)
|
|
267
|
+
uvs.push(0.5, 0.5)
|
|
268
|
+
metricUvs.push(0.5, 0.5, 1, 1)
|
|
269
|
+
for (let side = 0; side < sides; side += 1) {
|
|
270
|
+
indices.push(center, (side + 1) % sides, side)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const geometry = new THREE.BufferGeometry()
|
|
274
|
+
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3))
|
|
275
|
+
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2))
|
|
276
|
+
geometry.setAttribute('themeNetUv', new THREE.Float32BufferAttribute(metricUvs, 4))
|
|
277
|
+
geometry.setIndex(indices)
|
|
278
|
+
geometry.computeVertexNormals()
|
|
279
|
+
geometry.computeBoundingBox()
|
|
280
|
+
geometry.computeBoundingSphere()
|
|
281
|
+
geometry.name = 'hand-hewn-rope-wall-post-cap-geometry'
|
|
282
|
+
return geometry
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function createRopeLashingGeometry(): THREE.TubeGeometry {
|
|
286
|
+
const turns = 4.25
|
|
287
|
+
const points = Array.from({ length: 38 }, (_, index) => {
|
|
288
|
+
const progress = index / 37
|
|
289
|
+
const angle = progress * Math.PI * 2 * turns
|
|
290
|
+
const radius = 0.247 + Math.sin(progress * Math.PI * 6) * 0.006
|
|
291
|
+
return new THREE.Vector3(Math.cos(angle) * radius, (progress - 0.5) * 0.2, Math.sin(angle) * radius)
|
|
292
|
+
})
|
|
293
|
+
const geometry = withAuthoredMaterialUv(
|
|
294
|
+
new VisualTubeGeometry(new THREE.CatmullRomCurve3(points), 48, 0.032, 7, false),
|
|
295
|
+
)
|
|
296
|
+
geometry.name = 'hand-wrapped-rope-lashing-geometry'
|
|
297
|
+
return geometry
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function createBraidedRopeSegmentGeometry(helixRadius: number, strandRadius: number, turns = 3): THREE.BufferGeometry {
|
|
301
|
+
const segments = 30
|
|
302
|
+
const strands = Array.from({ length: 3 }, (_, strandIndex) => {
|
|
303
|
+
const phase = (strandIndex / 3) * Math.PI * 2
|
|
304
|
+
const points = Array.from({ length: segments + 1 }, (_, index) => {
|
|
305
|
+
const progress = index / segments
|
|
306
|
+
const angle = phase + progress * Math.PI * 2 * turns
|
|
307
|
+
return new THREE.Vector3(Math.cos(angle) * helixRadius, progress - 0.5, Math.sin(angle) * helixRadius)
|
|
308
|
+
})
|
|
309
|
+
return new VisualTubeGeometry(new THREE.CatmullRomCurve3(points), segments, strandRadius, 6, false)
|
|
310
|
+
})
|
|
311
|
+
const geometry = mergeGeometries(strands, false)
|
|
312
|
+
strands.forEach(strand => strand.dispose())
|
|
313
|
+
if (!geometry) throw new Error('Failed to build braided rope geometry.')
|
|
314
|
+
geometry.name = 'three-strand-braided-rope-segment-geometry'
|
|
315
|
+
return withAuthoredMaterialUv(geometry)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function appendCrossLashings(matrices: THREE.Matrix4[], frame: RopeWallPathFrame, anchorHeight: number): void {
|
|
319
|
+
for (const face of [-1, 1] as const) {
|
|
320
|
+
const center = frame.base
|
|
321
|
+
.clone()
|
|
322
|
+
.addScaledVector(frame.up, anchorHeight - 0.12)
|
|
323
|
+
.addScaledVector(frame.outward, face * 0.235)
|
|
324
|
+
const upperLeft = center.clone().addScaledVector(frame.tangent, -0.18).addScaledVector(frame.up, 0.15)
|
|
325
|
+
const upperRight = center.clone().addScaledVector(frame.tangent, 0.18).addScaledVector(frame.up, 0.15)
|
|
326
|
+
const lowerLeft = center.clone().addScaledVector(frame.tangent, -0.18).addScaledVector(frame.up, -0.15)
|
|
327
|
+
const lowerRight = center.clone().addScaledVector(frame.tangent, 0.18).addScaledVector(frame.up, -0.15)
|
|
328
|
+
appendCylinderMatrix(matrices, upperLeft, lowerRight, 0.035)
|
|
329
|
+
appendCylinderMatrix(matrices, upperRight, lowerLeft, 0.035)
|
|
330
|
+
}
|
|
331
|
+
for (const face of [-1, 1] as const) {
|
|
332
|
+
const center = frame.base
|
|
333
|
+
.clone()
|
|
334
|
+
.addScaledVector(frame.up, anchorHeight - 0.12)
|
|
335
|
+
.addScaledVector(frame.tangent, face * 0.235)
|
|
336
|
+
const upperLeft = center.clone().addScaledVector(frame.outward, -0.18).addScaledVector(frame.up, 0.15)
|
|
337
|
+
const upperRight = center.clone().addScaledVector(frame.outward, 0.18).addScaledVector(frame.up, 0.15)
|
|
338
|
+
const lowerLeft = center.clone().addScaledVector(frame.outward, -0.18).addScaledVector(frame.up, -0.15)
|
|
339
|
+
const lowerRight = center.clone().addScaledVector(frame.outward, 0.18).addScaledVector(frame.up, -0.15)
|
|
340
|
+
appendCylinderMatrix(matrices, upperLeft, lowerRight, 0.035)
|
|
341
|
+
appendCylinderMatrix(matrices, upperRight, lowerLeft, 0.035)
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function withAuthoredMaterialUv<T extends THREE.BufferGeometry>(geometry: T): T {
|
|
346
|
+
const uv = geometry.getAttribute('uv')
|
|
347
|
+
if (!uv) return geometry
|
|
348
|
+
const materialUvs: number[] = []
|
|
349
|
+
for (let index = 0; index < uv.count; index += 1) {
|
|
350
|
+
materialUvs.push(uv.getX(index), uv.getY(index), 1, 1)
|
|
351
|
+
}
|
|
352
|
+
geometry.setAttribute('themeNetUv', new THREE.Float32BufferAttribute(materialUvs, 4))
|
|
353
|
+
return geometry
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function appendSaggingCable(
|
|
357
|
+
matrices: THREE.Matrix4[],
|
|
358
|
+
start: RopeWallPathFrame,
|
|
359
|
+
end: RopeWallPathFrame,
|
|
360
|
+
height: number,
|
|
361
|
+
sag: number,
|
|
362
|
+
): void {
|
|
363
|
+
let previous = cablePoint(start, end, 0, height, sag)
|
|
364
|
+
for (let segment = 1; segment <= CABLE_SEGMENTS; segment += 1) {
|
|
365
|
+
const progress = segment / CABLE_SEGMENTS
|
|
366
|
+
const next = cablePoint(start, end, progress, height, sag)
|
|
367
|
+
appendCylinderMatrix(matrices, previous, next, 0.014)
|
|
368
|
+
previous = next
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function cablePoint(
|
|
373
|
+
start: RopeWallPathFrame,
|
|
374
|
+
end: RopeWallPathFrame,
|
|
375
|
+
progress: number,
|
|
376
|
+
height: number,
|
|
377
|
+
sag: number,
|
|
378
|
+
): THREE.Vector3 {
|
|
379
|
+
const frame = interpolateFrame(start, end, progress)
|
|
380
|
+
return frame.base
|
|
381
|
+
.clone()
|
|
382
|
+
.addScaledVector(frame.up, height - Math.sin(progress * Math.PI) * sag)
|
|
383
|
+
.addScaledVector(frame.outward, OUTWARD_OFFSET)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function appendCylinderMatrix(
|
|
387
|
+
matrices: THREE.Matrix4[],
|
|
388
|
+
start: THREE.Vector3,
|
|
389
|
+
end: THREE.Vector3,
|
|
390
|
+
overlap: number,
|
|
391
|
+
): void {
|
|
392
|
+
const direction = end.clone().sub(start)
|
|
393
|
+
const length = direction.length()
|
|
394
|
+
if (length <= 0.001) return
|
|
395
|
+
matrices.push(
|
|
396
|
+
new THREE.Matrix4().compose(
|
|
397
|
+
start.clone().lerp(end, 0.5),
|
|
398
|
+
new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), direction.normalize()),
|
|
399
|
+
new THREE.Vector3(1, length + overlap, 1),
|
|
400
|
+
),
|
|
401
|
+
)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function instancedBatch(
|
|
405
|
+
name: string,
|
|
406
|
+
geometry: THREE.BufferGeometry,
|
|
407
|
+
material: THREE.Material | THREE.Material[],
|
|
408
|
+
matrices: readonly THREE.Matrix4[],
|
|
409
|
+
): THREE.InstancedMesh {
|
|
410
|
+
const mesh = new THREE.InstancedMesh(geometry, material, matrices.length)
|
|
411
|
+
mesh.name = name
|
|
412
|
+
mesh.userData.decorative = true
|
|
413
|
+
mesh.userData.visualOnly = true
|
|
414
|
+
mesh.castShadow = true
|
|
415
|
+
mesh.receiveShadow = true
|
|
416
|
+
matrices.forEach((matrix, index) => mesh.setMatrixAt(index, matrix))
|
|
417
|
+
mesh.instanceMatrix.needsUpdate = true
|
|
418
|
+
return mesh
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function interpolateFrame(start: RopeWallPathFrame, end: RopeWallPathFrame, progress: number): RopeWallPathFrame {
|
|
422
|
+
const tangent = start.tangent.clone().lerp(end.tangent, progress).normalize()
|
|
423
|
+
const up = start.up.clone().lerp(end.up, progress).normalize()
|
|
424
|
+
const outward = start.outward.clone().lerp(end.outward, progress)
|
|
425
|
+
outward.addScaledVector(tangent, -outward.dot(tangent)).addScaledVector(up, -outward.dot(up)).normalize()
|
|
426
|
+
return { base: start.base.clone().lerp(end.base, progress), tangent, up, outward }
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function frameQuaternion(tangent: THREE.Vector3, up: THREE.Vector3): THREE.Quaternion {
|
|
430
|
+
const y = up.clone().normalize()
|
|
431
|
+
const x = tangent.clone().addScaledVector(y, -tangent.dot(y)).normalize()
|
|
432
|
+
const z = new THREE.Vector3().crossVectors(x, y).normalize()
|
|
433
|
+
return new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().makeBasis(x, y, z))
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function deterministic(seed: string): number {
|
|
437
|
+
let hash = 2_166_136_261
|
|
438
|
+
for (const character of seed) {
|
|
439
|
+
hash ^= character.charCodeAt(0)
|
|
440
|
+
hash = Math.imul(hash, 16_777_619)
|
|
441
|
+
}
|
|
442
|
+
return (hash >>> 0) / 4_294_967_296
|
|
443
|
+
}
|