@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,1031 @@
|
|
|
1
|
+
import { geometryDetailProfile } from '../../../three/geometry-detail'
|
|
2
|
+
import * as THREE from 'three'
|
|
3
|
+
|
|
4
|
+
import { distributePathPattern } from './path-pattern-layout'
|
|
5
|
+
import { addCanonicalWallTerminalSkins } from './track-appearance-wall-terminal'
|
|
6
|
+
import { TRACK_RAIL_WALL_THICKNESS } from './track-definition'
|
|
7
|
+
import {
|
|
8
|
+
resampleCanonicalWallRun,
|
|
9
|
+
type CanonicalWallRun,
|
|
10
|
+
type RawWallFrame,
|
|
11
|
+
} from './track-surface-topology'
|
|
12
|
+
|
|
13
|
+
export type SakuraCastleWallPathFrame = RawWallFrame
|
|
14
|
+
export type SakuraCastleWallPathRun = CanonicalWallRun
|
|
15
|
+
|
|
16
|
+
export interface SakuraCastleWallMaterials {
|
|
17
|
+
timber: THREE.Material
|
|
18
|
+
masonry: THREE.Material
|
|
19
|
+
kawara: THREE.Material
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const FRAME_SPACING = 1.2 * geometryDetailProfile.ribbonTolerance
|
|
23
|
+
const POST_MAXIMUM_SPACING = 1.2
|
|
24
|
+
const KAWARA_TILE_SPACING = 0.22
|
|
25
|
+
const KAWARA_TILE_ROWS = 2
|
|
26
|
+
const ROOF_HALF_SPAN = 0.27
|
|
27
|
+
const ROOF_RISE = 0.17
|
|
28
|
+
const ROOF_EAVE_LIFT = 0.015
|
|
29
|
+
const CONNECTED_ROOF_SEAM_OVERLAP = 0.03
|
|
30
|
+
let sharedTimberPostGeometry: THREE.BoxGeometry | undefined
|
|
31
|
+
let sharedKawaraTileGeometry: THREE.BufferGeometry | undefined
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Adds the visual language selected by the Sakura castle-wall appearance:
|
|
35
|
+
* masonry sill, timber framing, and a shallow kawara roof. The ordinary wall
|
|
36
|
+
* mesh remains the plaster body and the only corresponding Rapier collider.
|
|
37
|
+
*/
|
|
38
|
+
export function addSakuraCastleWallVisual(
|
|
39
|
+
root: THREE.Group,
|
|
40
|
+
id: string,
|
|
41
|
+
runs: readonly SakuraCastleWallPathRun[],
|
|
42
|
+
wallHeight: number,
|
|
43
|
+
materials: SakuraCastleWallMaterials,
|
|
44
|
+
): THREE.Group | undefined {
|
|
45
|
+
if (wallHeight <= 0.08) return undefined
|
|
46
|
+
const group = new THREE.Group()
|
|
47
|
+
group.name = `${id}-sakura-castle-wall`
|
|
48
|
+
group.userData.themeOnly = 'japanese-sakura-lake'
|
|
49
|
+
group.userData.decorative = true
|
|
50
|
+
group.userData.visualOnly = true
|
|
51
|
+
group.userData.appearance = 'japanese-sakura-castle-wall'
|
|
52
|
+
|
|
53
|
+
const postMatrices: THREE.Matrix4[] = []
|
|
54
|
+
const railPositions: number[] = []
|
|
55
|
+
const railUvs: number[] = []
|
|
56
|
+
const railMetricUvs: number[] = []
|
|
57
|
+
const railIndices: number[] = []
|
|
58
|
+
const tileMatrices: THREE.Matrix4[] = []
|
|
59
|
+
let tilePlacementRunCount = 0
|
|
60
|
+
let connectedRoofOverlapRunCount = 0
|
|
61
|
+
let tJunctionRoofOverlapRunCount = 0
|
|
62
|
+
let tJunctionTileMinimumStartProjection = Number.POSITIVE_INFINITY
|
|
63
|
+
let runCount = 0
|
|
64
|
+
const masonryHeight = Math.min(0.27, wallHeight * 0.24)
|
|
65
|
+
const terminalKey = (frame: SakuraCastleWallPathFrame): string =>
|
|
66
|
+
frame.base.toArray().map(value => value.toFixed(4)).join(':')
|
|
67
|
+
const connectedEndOwner = new Map<string, number>()
|
|
68
|
+
runs.forEach((run, runIndex) => {
|
|
69
|
+
if (run.endOwnership !== 'connected' || run.frames.length < 2) return
|
|
70
|
+
const key = terminalKey(run.frames.at(-1)!)
|
|
71
|
+
if (!connectedEndOwner.has(key)) connectedEndOwner.set(key, runIndex)
|
|
72
|
+
})
|
|
73
|
+
const tJunctionConnectedStarts = new Set<number>()
|
|
74
|
+
runs.forEach((run, runIndex) => {
|
|
75
|
+
if (run.startOwnership !== 'connected' || run.frames.length < 2) return
|
|
76
|
+
const start = run.frames[0]!.base
|
|
77
|
+
const joinsReceiver = runs.some((receiver, receiverIndex) => {
|
|
78
|
+
if (receiverIndex === runIndex || receiver.frames.length < 2) return false
|
|
79
|
+
for (let index = 0; index < receiver.frames.length - 1; index += 1) {
|
|
80
|
+
const segmentStart = receiver.frames[index]!.base
|
|
81
|
+
const segmentEnd = receiver.frames[index + 1]!.base
|
|
82
|
+
const segment = segmentEnd.clone().sub(segmentStart).setY(0)
|
|
83
|
+
const lengthSquared = segment.lengthSq()
|
|
84
|
+
if (lengthSquared <= 0.000_001) continue
|
|
85
|
+
const progress = THREE.MathUtils.clamp(
|
|
86
|
+
start.clone().sub(segmentStart).setY(0).dot(segment) / lengthSquared,
|
|
87
|
+
0,
|
|
88
|
+
1,
|
|
89
|
+
)
|
|
90
|
+
// T-junction contacts lie inside the receiver segment. Ordinary
|
|
91
|
+
// end-to-start wall seams remain governed by the cadence phase logic.
|
|
92
|
+
if (progress <= 0.001 || progress >= 0.999) continue
|
|
93
|
+
const closest = segmentStart.clone().addScaledVector(segment, progress)
|
|
94
|
+
const receiverContactTolerance = TRACK_RAIL_WALL_THICKNESS / 2 + 0.001
|
|
95
|
+
if (
|
|
96
|
+
closest.setY(start.y).distanceToSquared(start) <=
|
|
97
|
+
receiverContactTolerance * receiverContactTolerance
|
|
98
|
+
) return true
|
|
99
|
+
}
|
|
100
|
+
return false
|
|
101
|
+
})
|
|
102
|
+
if (joinsReceiver) tJunctionConnectedStarts.add(runIndex)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
for (const [runIndex, run] of runs.entries()) {
|
|
106
|
+
if (run.frames.length < 2) continue
|
|
107
|
+
const canonicalRun = run
|
|
108
|
+
const frames = resampleCanonicalWallRun(canonicalRun, FRAME_SPACING).frames
|
|
109
|
+
const distances = pathDistances(frames)
|
|
110
|
+
const totalLength = distances.at(-1) ?? 0
|
|
111
|
+
if (totalLength <= 0.001) continue
|
|
112
|
+
runCount += 1
|
|
113
|
+
const capStart = run.capStart
|
|
114
|
+
const capEnd = run.capEnd
|
|
115
|
+
const ownsConnectedRoofEnd =
|
|
116
|
+
capEnd || connectedEndOwner.get(terminalKey(canonicalRun.frames.at(-1)!)) === runIndex
|
|
117
|
+
if (!capEnd && ownsConnectedRoofEnd) connectedRoofOverlapRunCount += 1
|
|
118
|
+
|
|
119
|
+
for (const face of [-1, 1] as const) {
|
|
120
|
+
const masonry = new THREE.Mesh(
|
|
121
|
+
// The sill is a facade over the canonical plaster backing, so it must
|
|
122
|
+
// use the exact same path rows. A separately resampled curve uses
|
|
123
|
+
// different chords and can pass behind the plaster between vertices.
|
|
124
|
+
createFaceBandGeometry(
|
|
125
|
+
canonicalRun.frames,
|
|
126
|
+
canonicalRun.frames.map(frame => frame.distance),
|
|
127
|
+
canonicalRun.length,
|
|
128
|
+
face,
|
|
129
|
+
0.008,
|
|
130
|
+
masonryHeight,
|
|
131
|
+
capStart,
|
|
132
|
+
capEnd,
|
|
133
|
+
),
|
|
134
|
+
materials.masonry,
|
|
135
|
+
)
|
|
136
|
+
masonry.name = `${id}-sakura-castle-masonry-${runIndex + 1}-${face < 0 ? 'inside' : 'outside'}`
|
|
137
|
+
masonry.castShadow = true
|
|
138
|
+
masonry.receiveShadow = true
|
|
139
|
+
group.add(masonry)
|
|
140
|
+
|
|
141
|
+
// The roof is one continuous sweep over the canonical wall run. The old
|
|
142
|
+
// sharp-corner split created independently capped roof chunks, leaving
|
|
143
|
+
// holes and overlaps precisely where curved pieces changed direction.
|
|
144
|
+
const roofFrames = resampleCanonicalWallRun(canonicalRun, FRAME_SPACING).frames
|
|
145
|
+
const roofTileFrames = roofFramesWithTJunctionStartOverlap(
|
|
146
|
+
roofFrames,
|
|
147
|
+
tJunctionConnectedStarts.has(runIndex),
|
|
148
|
+
)
|
|
149
|
+
if (face < 0 && tJunctionConnectedStarts.has(runIndex)) tJunctionRoofOverlapRunCount += 1
|
|
150
|
+
const roofBackingFrames = roofFramesWithOwnedConnectedEnd(
|
|
151
|
+
roofTileFrames,
|
|
152
|
+
capEnd,
|
|
153
|
+
ownsConnectedRoofEnd,
|
|
154
|
+
)
|
|
155
|
+
const roofDistances = pathDistances(roofBackingFrames)
|
|
156
|
+
const roofLength = roofDistances.at(-1) ?? 0
|
|
157
|
+
const roof = new THREE.Mesh(
|
|
158
|
+
createRoofSlopeGeometry(roofBackingFrames, roofDistances, roofLength, wallHeight, face),
|
|
159
|
+
materials.kawara,
|
|
160
|
+
)
|
|
161
|
+
roof.name = `${id}-sakura-castle-kawara-${runIndex + 1}-${face < 0 ? 'inside' : 'outside'}`
|
|
162
|
+
roof.castShadow = true
|
|
163
|
+
roof.receiveShadow = true
|
|
164
|
+
group.add(roof)
|
|
165
|
+
const fascia = new THREE.Mesh(
|
|
166
|
+
createRoofEaveFasciaGeometry(roofBackingFrames, roofDistances, roofLength, wallHeight, face),
|
|
167
|
+
materials.kawara,
|
|
168
|
+
)
|
|
169
|
+
fascia.name = `${id}-sakura-castle-kawara-eave-${runIndex + 1}-${face < 0 ? 'inside' : 'outside'}`
|
|
170
|
+
fascia.castShadow = true
|
|
171
|
+
fascia.receiveShadow = true
|
|
172
|
+
group.add(fascia)
|
|
173
|
+
// Tiles must use the same canonical, uniformly sampled frame field as
|
|
174
|
+
// the roof sweep. Raw authored frames can contain duplicate seam points
|
|
175
|
+
// or abrupt basis changes at piece boundaries; sampling those directly
|
|
176
|
+
// occasionally rotates a tile's slope axis upright.
|
|
177
|
+
const tileRuns = kawaraTilePlacementRuns(roofTileFrames)
|
|
178
|
+
const tileMatrixStart = tileMatrices.length
|
|
179
|
+
for (const tileFrames of tileRuns) {
|
|
180
|
+
tilePlacementRunCount += 1
|
|
181
|
+
appendKawaraTileMatrices(
|
|
182
|
+
tileMatrices,
|
|
183
|
+
tileFrames,
|
|
184
|
+
wallHeight,
|
|
185
|
+
face,
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
if (tJunctionConnectedStarts.has(runIndex) && tileMatrices.length > tileMatrixStart) {
|
|
189
|
+
const terminal = canonicalRun.frames[0]!.base
|
|
190
|
+
const continuation = canonicalRun.frames[1]!.base.clone().sub(terminal).normalize()
|
|
191
|
+
for (let matrixIndex = tileMatrixStart; matrixIndex < tileMatrices.length; matrixIndex += 1) {
|
|
192
|
+
const matrix = tileMatrices[matrixIndex]!
|
|
193
|
+
const center = new THREE.Vector3().setFromMatrixPosition(matrix)
|
|
194
|
+
tJunctionTileMinimumStartProjection = Math.min(
|
|
195
|
+
tJunctionTileMinimumStartProjection,
|
|
196
|
+
center.clone().sub(terminal).dot(continuation),
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const ridgeFrames = roofFramesWithOwnedConnectedEnd(
|
|
203
|
+
roofFramesWithTJunctionStartOverlap(frames, tJunctionConnectedStarts.has(runIndex)),
|
|
204
|
+
capEnd,
|
|
205
|
+
ownsConnectedRoofEnd,
|
|
206
|
+
)
|
|
207
|
+
const ridgeDistances = pathDistances(ridgeFrames)
|
|
208
|
+
const ridgeLength = ridgeDistances.at(-1) ?? 0
|
|
209
|
+
const ridge = new THREE.Mesh(
|
|
210
|
+
createContinuousKawaraRidgeGeometry(
|
|
211
|
+
ridgeFrames,
|
|
212
|
+
ridgeDistances,
|
|
213
|
+
ridgeLength,
|
|
214
|
+
wallHeight,
|
|
215
|
+
capStart,
|
|
216
|
+
capEnd,
|
|
217
|
+
),
|
|
218
|
+
materials.kawara,
|
|
219
|
+
)
|
|
220
|
+
ridge.name = `${id}-sakura-castle-kawara-ridge-${runIndex + 1}`
|
|
221
|
+
ridge.userData.decorative = true
|
|
222
|
+
ridge.userData.visualOnly = true
|
|
223
|
+
ridge.userData.pathConstruction = 'continuous-swept-tube'
|
|
224
|
+
ridge.castShadow = true
|
|
225
|
+
ridge.receiveShadow = true
|
|
226
|
+
group.add(ridge)
|
|
227
|
+
|
|
228
|
+
for (const face of [-1, 1] as const) {
|
|
229
|
+
for (const height of [masonryHeight + 0.045, wallHeight - 0.065]) {
|
|
230
|
+
appendSweptTimberRail(
|
|
231
|
+
railPositions,
|
|
232
|
+
railUvs,
|
|
233
|
+
railMetricUvs,
|
|
234
|
+
railIndices,
|
|
235
|
+
frames,
|
|
236
|
+
distances,
|
|
237
|
+
totalLength,
|
|
238
|
+
face,
|
|
239
|
+
height,
|
|
240
|
+
capStart,
|
|
241
|
+
capEnd,
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const postPlacements = distributePathPattern(
|
|
248
|
+
runs.map(run => ({
|
|
249
|
+
frames: run.frames.map(frame => ({
|
|
250
|
+
point: frame.base,
|
|
251
|
+
tangent: frame.tangent,
|
|
252
|
+
distance: frame.distance,
|
|
253
|
+
})),
|
|
254
|
+
length: run.length,
|
|
255
|
+
closed: run.closed,
|
|
256
|
+
// A connected wall terminal needs one architectural owner. The upstream
|
|
257
|
+
// run owns its end and the downstream run omits its start, producing one
|
|
258
|
+
// continuous vertical timber at the join instead of either a duplicate
|
|
259
|
+
// pair or the previous empty post bay.
|
|
260
|
+
includeStart: run.capStart,
|
|
261
|
+
includeEnd: true,
|
|
262
|
+
})),
|
|
263
|
+
{
|
|
264
|
+
maximumSpacing: POST_MAXIMUM_SPACING,
|
|
265
|
+
dedupeDistance: 0.025,
|
|
266
|
+
},
|
|
267
|
+
)
|
|
268
|
+
const worldUp = new THREE.Vector3(0, 1, 0)
|
|
269
|
+
// The masonry sill owns the lower wall band. Timber posts begin at its top
|
|
270
|
+
// seam (under the lower swept rail) instead of continuing through the
|
|
271
|
+
// brickwork to the deck.
|
|
272
|
+
const verticalPostBottom = masonryHeight - 0.03
|
|
273
|
+
const verticalPostHeight = Math.max(0.12, wallHeight - 0.06 - verticalPostBottom)
|
|
274
|
+
for (const placement of postPlacements) {
|
|
275
|
+
const tangent = placement.tangent.clone().setY(0)
|
|
276
|
+
if (tangent.lengthSq() <= 0.000_001) tangent.set(0, 0, 1)
|
|
277
|
+
tangent.normalize()
|
|
278
|
+
const frame: SakuraCastleWallPathFrame = {
|
|
279
|
+
base: placement.point,
|
|
280
|
+
tangent,
|
|
281
|
+
up: worldUp,
|
|
282
|
+
outward: new THREE.Vector3().crossVectors(tangent, worldUp).normalize(),
|
|
283
|
+
}
|
|
284
|
+
postMatrices.push(
|
|
285
|
+
new THREE.Matrix4().compose(
|
|
286
|
+
placement.point.clone().addScaledVector(worldUp, verticalPostBottom + verticalPostHeight / 2),
|
|
287
|
+
verticalFrameQuaternion(frame),
|
|
288
|
+
new THREE.Vector3(0.11, verticalPostHeight, TRACK_RAIL_WALL_THICKNESS + 0.055),
|
|
289
|
+
),
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (runCount === 0) return undefined
|
|
294
|
+
if (postMatrices.length > 0) {
|
|
295
|
+
group.add(
|
|
296
|
+
instancedBatch(
|
|
297
|
+
`${id}-sakura-castle-timber-posts`,
|
|
298
|
+
getSharedTimberPostGeometry(),
|
|
299
|
+
materials.timber,
|
|
300
|
+
postMatrices,
|
|
301
|
+
true,
|
|
302
|
+
),
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
if (railPositions.length > 0) {
|
|
306
|
+
const rails = new THREE.Mesh(
|
|
307
|
+
createGeometry(
|
|
308
|
+
railPositions,
|
|
309
|
+
railUvs,
|
|
310
|
+
railMetricUvs,
|
|
311
|
+
railIndices,
|
|
312
|
+
'sakura-castle-continuous-timber-rail-geometry',
|
|
313
|
+
),
|
|
314
|
+
materials.timber,
|
|
315
|
+
)
|
|
316
|
+
rails.name = `${id}-sakura-castle-timber-rails`
|
|
317
|
+
rails.userData.decorative = true
|
|
318
|
+
rails.userData.visualOnly = true
|
|
319
|
+
rails.userData.pathConstruction = 'continuous-swept-beam'
|
|
320
|
+
rails.castShadow = true
|
|
321
|
+
rails.receiveShadow = true
|
|
322
|
+
group.add(rails)
|
|
323
|
+
}
|
|
324
|
+
if (tileMatrices.length > 0) {
|
|
325
|
+
group.add(
|
|
326
|
+
instancedBatch(
|
|
327
|
+
`${id}-sakura-castle-kawara-tiles`,
|
|
328
|
+
getSharedKawaraTileGeometry(),
|
|
329
|
+
materials.kawara,
|
|
330
|
+
tileMatrices,
|
|
331
|
+
true,
|
|
332
|
+
),
|
|
333
|
+
)
|
|
334
|
+
}
|
|
335
|
+
addSakuraCastleTerminalGables(group, `${id}-sakura-castle`, runs, wallHeight, materials.kawara)
|
|
336
|
+
addCanonicalWallTerminalSkins(group, `${id}-sakura-castle`, runs, masonryHeight, {
|
|
337
|
+
material: materials.masonry,
|
|
338
|
+
})
|
|
339
|
+
const upperTerminalHeight = Math.max(0, wallHeight - masonryHeight)
|
|
340
|
+
if (upperTerminalHeight > 0.001) {
|
|
341
|
+
addCanonicalWallTerminalSkins(
|
|
342
|
+
group,
|
|
343
|
+
`${id}-sakura-castle-timber`,
|
|
344
|
+
runs,
|
|
345
|
+
upperTerminalHeight,
|
|
346
|
+
{
|
|
347
|
+
material: materials.timber,
|
|
348
|
+
verticalOffset: masonryHeight,
|
|
349
|
+
},
|
|
350
|
+
)
|
|
351
|
+
}
|
|
352
|
+
group.userData.runCount = runCount
|
|
353
|
+
group.userData.postCount = postMatrices.length
|
|
354
|
+
group.userData.roofTileCount = tileMatrices.length
|
|
355
|
+
group.userData.tilePlacementRunCount = tilePlacementRunCount
|
|
356
|
+
group.userData.connectedRoofOverlapRunCount = connectedRoofOverlapRunCount
|
|
357
|
+
group.userData.tJunctionRoofOverlapRunCount = tJunctionRoofOverlapRunCount
|
|
358
|
+
group.userData.tJunctionRoofOverlapDistance = TRACK_RAIL_WALL_THICKNESS / 2 + CONNECTED_ROOF_SEAM_OVERLAP
|
|
359
|
+
group.userData.tJunctionTileMinimumStartProjection = Number.isFinite(tJunctionTileMinimumStartProjection)
|
|
360
|
+
? tJunctionTileMinimumStartProjection
|
|
361
|
+
: undefined
|
|
362
|
+
group.userData.roofConstruction = 'continuous-canonical-sweep'
|
|
363
|
+
group.userData.runTerminalOwnership = runs.map(run => ({
|
|
364
|
+
start: run.startOwnership,
|
|
365
|
+
end: run.endOwnership,
|
|
366
|
+
startBase: run.frames[0]!.base.toArray(),
|
|
367
|
+
endBase: run.frames.at(-1)!.base.toArray(),
|
|
368
|
+
}))
|
|
369
|
+
root.add(group)
|
|
370
|
+
return group
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function addSakuraCastleTerminalGables(
|
|
374
|
+
root: THREE.Group,
|
|
375
|
+
id: string,
|
|
376
|
+
runs: readonly SakuraCastleWallPathRun[],
|
|
377
|
+
wallHeight: number,
|
|
378
|
+
material: THREE.Material,
|
|
379
|
+
): void {
|
|
380
|
+
const positions: number[] = []
|
|
381
|
+
const indices: number[] = []
|
|
382
|
+
for (const run of runs) {
|
|
383
|
+
if (run.closed || run.frames.length < 2) continue
|
|
384
|
+
for (const [terminal, ownership] of [
|
|
385
|
+
['start', run.startOwnership],
|
|
386
|
+
['end', run.endOwnership],
|
|
387
|
+
] as const) {
|
|
388
|
+
if (ownership !== 'exposed') continue
|
|
389
|
+
const frame = terminal === 'start' ? run.frames[0]! : run.frames.at(-1)!
|
|
390
|
+
const normal = frame.tangent.clone().multiplyScalar(terminal === 'start' ? -1 : 1).normalize()
|
|
391
|
+
const left = frame.base
|
|
392
|
+
.clone()
|
|
393
|
+
.addScaledVector(frame.outward, -ROOF_HALF_SPAN)
|
|
394
|
+
.addScaledVector(frame.up, wallHeight + ROOF_EAVE_LIFT - 0.03)
|
|
395
|
+
const ridge = frame.base.clone().addScaledVector(frame.up, wallHeight + ROOF_RISE - 0.03)
|
|
396
|
+
const right = frame.base
|
|
397
|
+
.clone()
|
|
398
|
+
.addScaledVector(frame.outward, ROOF_HALF_SPAN)
|
|
399
|
+
.addScaledVector(frame.up, wallHeight + ROOF_EAVE_LIFT - 0.03)
|
|
400
|
+
const offset = positions.length / 3
|
|
401
|
+
positions.push(...left.toArray(), ...ridge.toArray(), ...right.toArray())
|
|
402
|
+
const candidate = ridge.clone().sub(left).cross(right.clone().sub(left))
|
|
403
|
+
if (candidate.dot(normal) >= 0) indices.push(offset, offset + 1, offset + 2)
|
|
404
|
+
else indices.push(offset, offset + 2, offset + 1)
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (indices.length === 0) return
|
|
408
|
+
const geometry = new THREE.BufferGeometry()
|
|
409
|
+
geometry.name = 'sakura-castle-terminal-gable-geometry'
|
|
410
|
+
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3))
|
|
411
|
+
geometry.setIndex(indices)
|
|
412
|
+
geometry.computeVertexNormals()
|
|
413
|
+
const mesh = new THREE.Mesh(geometry, material)
|
|
414
|
+
mesh.name = `${id}-terminal-gables`
|
|
415
|
+
mesh.castShadow = true
|
|
416
|
+
mesh.receiveShadow = true
|
|
417
|
+
mesh.userData.decorative = true
|
|
418
|
+
mesh.userData.visualOnly = true
|
|
419
|
+
root.add(mesh)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function createFaceBandGeometry(
|
|
423
|
+
frames: readonly SakuraCastleWallPathFrame[],
|
|
424
|
+
distances: readonly number[],
|
|
425
|
+
totalLength: number,
|
|
426
|
+
face: -1 | 1,
|
|
427
|
+
bottom: number,
|
|
428
|
+
top: number,
|
|
429
|
+
extendStart: boolean,
|
|
430
|
+
extendEnd: boolean,
|
|
431
|
+
): THREE.BufferGeometry {
|
|
432
|
+
const positions: number[] = []
|
|
433
|
+
const uvs: number[] = []
|
|
434
|
+
const metricUvs: number[] = []
|
|
435
|
+
const indices: number[] = []
|
|
436
|
+
const faceOffset = face * (TRACK_RAIL_WALL_THICKNESS / 2 + 0.006)
|
|
437
|
+
frames.forEach((frame, index) => {
|
|
438
|
+
let facadeOffset = frame.outward.clone().multiplyScalar(faceOffset)
|
|
439
|
+
if (index > 0 && index < frames.length - 1) {
|
|
440
|
+
const incoming = frame.base.clone().sub(frames[index - 1]!.base).setY(0)
|
|
441
|
+
const outgoing = frames[index + 1]!.base.clone().sub(frame.base).setY(0)
|
|
442
|
+
if (incoming.lengthSq() > 0.000_001 && outgoing.lengthSq() > 0.000_001) {
|
|
443
|
+
incoming.normalize()
|
|
444
|
+
outgoing.normalize()
|
|
445
|
+
const incomingOutward = new THREE.Vector3(incoming.z, 0, -incoming.x)
|
|
446
|
+
const outgoingOutward = new THREE.Vector3(outgoing.z, 0, -outgoing.x)
|
|
447
|
+
if (incomingOutward.dot(frame.outward) < 0) incomingOutward.negate()
|
|
448
|
+
if (outgoingOutward.dot(frame.outward) < 0) outgoingOutward.negate()
|
|
449
|
+
const miter = incomingOutward.add(outgoingOutward)
|
|
450
|
+
if (miter.lengthSq() > 0.000_001) {
|
|
451
|
+
miter.normalize()
|
|
452
|
+
const projection = Math.max(0.000_001, Math.abs(miter.dot(outgoingOutward)))
|
|
453
|
+
facadeOffset = miter.multiplyScalar(faceOffset / projection)
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
// Connected wall ends already share the exact canonical connector row.
|
|
458
|
+
// Extending their facade past that row makes two pieces overlap and lets
|
|
459
|
+
// one sill protrude below/around the other. The wrap is only required at
|
|
460
|
+
// a genuinely exposed terminal.
|
|
461
|
+
const terminalExtension = index === 0 && extendStart
|
|
462
|
+
? frame.tangent.clone().normalize().multiplyScalar(-Math.abs(faceOffset))
|
|
463
|
+
: index === frames.length - 1 && extendEnd
|
|
464
|
+
? frame.tangent.clone().normalize().multiplyScalar(Math.abs(faceOffset))
|
|
465
|
+
: new THREE.Vector3()
|
|
466
|
+
for (const height of [bottom, top]) {
|
|
467
|
+
const point = frame.base
|
|
468
|
+
.clone()
|
|
469
|
+
.add(facadeOffset)
|
|
470
|
+
.add(terminalExtension)
|
|
471
|
+
.addScaledVector(frame.up, height - 0.03)
|
|
472
|
+
positions.push(point.x, point.y, point.z)
|
|
473
|
+
uvs.push(distances[index]!, height)
|
|
474
|
+
metricUvs.push(distances[index]!, height, 1, totalLength)
|
|
475
|
+
}
|
|
476
|
+
})
|
|
477
|
+
appendOrientedStripIndices(indices, positions, frames, frame => frame.outward.clone().multiplyScalar(face))
|
|
478
|
+
return createGeometry(positions, uvs, metricUvs, indices, 'sakura-castle-masonry-band-geometry')
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function createRoofSlopeGeometry(
|
|
482
|
+
frames: readonly SakuraCastleWallPathFrame[],
|
|
483
|
+
distances: readonly number[],
|
|
484
|
+
totalLength: number,
|
|
485
|
+
wallHeight: number,
|
|
486
|
+
face: -1 | 1,
|
|
487
|
+
): THREE.BufferGeometry {
|
|
488
|
+
const positions: number[] = []
|
|
489
|
+
const uvs: number[] = []
|
|
490
|
+
const metricUvs: number[] = []
|
|
491
|
+
const indices: number[] = []
|
|
492
|
+
const slopeLength = Math.hypot(ROOF_HALF_SPAN, ROOF_RISE - ROOF_EAVE_LIFT)
|
|
493
|
+
frames.forEach((frame, index) => {
|
|
494
|
+
const eave = frame.base
|
|
495
|
+
.clone()
|
|
496
|
+
.addScaledVector(frame.outward, face * ROOF_HALF_SPAN)
|
|
497
|
+
.addScaledVector(frame.up, wallHeight + ROOF_EAVE_LIFT - 0.03)
|
|
498
|
+
const ridge = frame.base.clone().addScaledVector(frame.up, wallHeight + ROOF_RISE - 0.03)
|
|
499
|
+
for (const [point, crossSlope] of [
|
|
500
|
+
[eave, slopeLength],
|
|
501
|
+
[ridge, 0],
|
|
502
|
+
] as const) {
|
|
503
|
+
positions.push(point.x, point.y, point.z)
|
|
504
|
+
uvs.push(distances[index]!, crossSlope)
|
|
505
|
+
metricUvs.push(distances[index]!, crossSlope, 1, totalLength)
|
|
506
|
+
}
|
|
507
|
+
})
|
|
508
|
+
appendOrientedStripIndices(indices, positions, frames, frame => frame.up)
|
|
509
|
+
return createGeometry(positions, uvs, metricUvs, indices, 'sakura-castle-kawara-slope-geometry')
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Open wall connectors meet on one docking plane. The run ending at that
|
|
514
|
+
* plane owns a tiny visual roof underlap; the run starting there stays exact.
|
|
515
|
+
* This closes transform/curve sampling tolerance without double-owning tiles
|
|
516
|
+
* or changing the canonical wall and collider boundary.
|
|
517
|
+
*/
|
|
518
|
+
function roofFramesWithOwnedConnectedEnd(
|
|
519
|
+
frames: readonly SakuraCastleWallPathFrame[],
|
|
520
|
+
capEnd: boolean,
|
|
521
|
+
ownsConnectedEnd = true,
|
|
522
|
+
): SakuraCastleWallPathFrame[] {
|
|
523
|
+
const owned = frames.map(frame => ({
|
|
524
|
+
base: frame.base.clone(),
|
|
525
|
+
tangent: frame.tangent.clone(),
|
|
526
|
+
up: frame.up.clone(),
|
|
527
|
+
outward: frame.outward.clone(),
|
|
528
|
+
}))
|
|
529
|
+
if (!capEnd && ownsConnectedEnd && owned.length > 0) {
|
|
530
|
+
const end = owned.at(-1)!
|
|
531
|
+
const previous = owned[Math.max(0, owned.length - 2)]!
|
|
532
|
+
const continuation = end.base.clone().sub(previous.base)
|
|
533
|
+
if (continuation.lengthSq() > 0.000_001) {
|
|
534
|
+
end.base.addScaledVector(continuation.normalize(), CONNECTED_ROOF_SEAM_OVERLAP)
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return owned
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function roofFramesWithTJunctionStartOverlap(
|
|
541
|
+
frames: readonly SakuraCastleWallPathFrame[],
|
|
542
|
+
overlapsReceiver: boolean,
|
|
543
|
+
): SakuraCastleWallPathFrame[] {
|
|
544
|
+
const owned = frames.map(frame => ({
|
|
545
|
+
base: frame.base.clone(),
|
|
546
|
+
tangent: frame.tangent.clone(),
|
|
547
|
+
up: frame.up.clone(),
|
|
548
|
+
outward: frame.outward.clone(),
|
|
549
|
+
}))
|
|
550
|
+
if (!overlapsReceiver || owned.length < 2) return owned
|
|
551
|
+
const start = owned[0]!
|
|
552
|
+
const next = owned[1]!
|
|
553
|
+
const continuation = next.base.clone().sub(start.base)
|
|
554
|
+
if (continuation.lengthSq() <= 0.000_001) return owned
|
|
555
|
+
start.base.addScaledVector(
|
|
556
|
+
continuation.normalize(),
|
|
557
|
+
-(TRACK_RAIL_WALL_THICKNESS / 2 + CONNECTED_ROOF_SEAM_OVERLAP),
|
|
558
|
+
)
|
|
559
|
+
return owned
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function createRoofEaveFasciaGeometry(
|
|
563
|
+
frames: readonly SakuraCastleWallPathFrame[],
|
|
564
|
+
distances: readonly number[],
|
|
565
|
+
totalLength: number,
|
|
566
|
+
wallHeight: number,
|
|
567
|
+
face: -1 | 1,
|
|
568
|
+
): THREE.BufferGeometry {
|
|
569
|
+
const positions: number[] = []
|
|
570
|
+
const uvs: number[] = []
|
|
571
|
+
const metricUvs: number[] = []
|
|
572
|
+
const indices: number[] = []
|
|
573
|
+
frames.forEach((frame, index) => {
|
|
574
|
+
const eave = roofSurfacePoint(frame, wallHeight, face, 1)
|
|
575
|
+
for (const drop of [0, 0.075]) {
|
|
576
|
+
const point = eave.clone().addScaledVector(frame.up, -drop)
|
|
577
|
+
positions.push(point.x, point.y, point.z)
|
|
578
|
+
uvs.push(distances[index]!, drop)
|
|
579
|
+
metricUvs.push(distances[index]!, drop, 1, totalLength)
|
|
580
|
+
}
|
|
581
|
+
})
|
|
582
|
+
appendOrientedStripIndices(
|
|
583
|
+
indices,
|
|
584
|
+
positions,
|
|
585
|
+
frames,
|
|
586
|
+
frame => frame.outward.clone().multiplyScalar(face),
|
|
587
|
+
)
|
|
588
|
+
return createGeometry(positions, uvs, metricUvs, indices, 'sakura-castle-kawara-eave-fascia-geometry')
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function appendOrientedStripIndices(
|
|
592
|
+
indices: number[],
|
|
593
|
+
positions: readonly number[],
|
|
594
|
+
frames: readonly SakuraCastleWallPathFrame[],
|
|
595
|
+
desiredNormal: (frame: SakuraCastleWallPathFrame) => THREE.Vector3,
|
|
596
|
+
): void {
|
|
597
|
+
for (let index = 0; index < frames.length - 1; index += 1) {
|
|
598
|
+
const current = index * 2
|
|
599
|
+
const next = current + 2
|
|
600
|
+
const target = desiredNormal(interpolateFrame(frames[index]!, frames[index + 1]!, 0.5)).normalize()
|
|
601
|
+
// Curved and graded strips are non-planar quads. Orient each triangle
|
|
602
|
+
// independently; borrowing the first triangle's winding for the second
|
|
603
|
+
// made alternating wedges disappear under FrontSide culling.
|
|
604
|
+
appendTriangleFacing(indices, positions, current, next, current + 1, target)
|
|
605
|
+
appendTriangleFacing(indices, positions, current + 1, next, next + 1, target)
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function appendTriangleFacing(
|
|
610
|
+
indices: number[],
|
|
611
|
+
positions: readonly number[],
|
|
612
|
+
aIndex: number,
|
|
613
|
+
bIndex: number,
|
|
614
|
+
cIndex: number,
|
|
615
|
+
target: THREE.Vector3,
|
|
616
|
+
): void {
|
|
617
|
+
const a = vectorAt(positions, aIndex)
|
|
618
|
+
const normal = vectorAt(positions, bIndex).sub(a).cross(vectorAt(positions, cIndex).sub(a))
|
|
619
|
+
if (normal.dot(target) >= 0) indices.push(aIndex, bIndex, cIndex)
|
|
620
|
+
else indices.push(aIndex, cIndex, bIndex)
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function createContinuousKawaraRidgeGeometry(
|
|
624
|
+
frames: readonly SakuraCastleWallPathFrame[],
|
|
625
|
+
distances: readonly number[],
|
|
626
|
+
totalLength: number,
|
|
627
|
+
wallHeight: number,
|
|
628
|
+
capStart: boolean,
|
|
629
|
+
capEnd: boolean,
|
|
630
|
+
): THREE.BufferGeometry {
|
|
631
|
+
const positions: number[] = []
|
|
632
|
+
const uvs: number[] = []
|
|
633
|
+
const metricUvs: number[] = []
|
|
634
|
+
const indices: number[] = []
|
|
635
|
+
const radius = 0.075
|
|
636
|
+
const radialSegments = 6
|
|
637
|
+
const circumference = Math.PI * radius * 2
|
|
638
|
+
|
|
639
|
+
frames.forEach((frame, frameIndex) => {
|
|
640
|
+
const center = frame.base.clone().addScaledVector(frame.up, wallHeight + ROOF_RISE - 0.03)
|
|
641
|
+
for (let radialIndex = 0; radialIndex < radialSegments; radialIndex += 1) {
|
|
642
|
+
const angle = (radialIndex / radialSegments) * Math.PI * 2
|
|
643
|
+
const point = center
|
|
644
|
+
.clone()
|
|
645
|
+
.addScaledVector(frame.outward, Math.cos(angle) * radius)
|
|
646
|
+
.addScaledVector(frame.up, Math.sin(angle) * radius)
|
|
647
|
+
positions.push(point.x, point.y, point.z)
|
|
648
|
+
uvs.push(distances[frameIndex]!, radialIndex / radialSegments)
|
|
649
|
+
metricUvs.push(distances[frameIndex]!, (radialIndex / radialSegments) * circumference, 1, totalLength)
|
|
650
|
+
}
|
|
651
|
+
})
|
|
652
|
+
|
|
653
|
+
for (let frameIndex = 0; frameIndex < frames.length - 1; frameIndex += 1) {
|
|
654
|
+
const current = frameIndex * radialSegments
|
|
655
|
+
const next = current + radialSegments
|
|
656
|
+
const frame = interpolateFrame(frames[frameIndex]!, frames[frameIndex + 1]!, 0.5)
|
|
657
|
+
for (let radialIndex = 0; radialIndex < radialSegments; radialIndex += 1) {
|
|
658
|
+
const following = (radialIndex + 1) % radialSegments
|
|
659
|
+
const angle = ((radialIndex + 0.5) / radialSegments) * Math.PI * 2
|
|
660
|
+
const target = frame.outward
|
|
661
|
+
.clone()
|
|
662
|
+
.multiplyScalar(Math.cos(angle))
|
|
663
|
+
.addScaledVector(frame.up, Math.sin(angle))
|
|
664
|
+
.normalize()
|
|
665
|
+
appendTriangleFacing(indices, positions, current + radialIndex, next + radialIndex, current + following, target)
|
|
666
|
+
appendTriangleFacing(indices, positions, current + following, next + radialIndex, next + following, target)
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const appendCap = (frameIndex: number, target: THREE.Vector3): void => {
|
|
671
|
+
const frame = frames[frameIndex]!
|
|
672
|
+
const center = frame.base.clone().addScaledVector(frame.up, wallHeight + ROOF_RISE - 0.03)
|
|
673
|
+
const centerIndex = positions.length / 3
|
|
674
|
+
positions.push(center.x, center.y, center.z)
|
|
675
|
+
uvs.push(distances[frameIndex]!, 0.5)
|
|
676
|
+
metricUvs.push(distances[frameIndex]!, circumference / 2, 1, totalLength)
|
|
677
|
+
const ringStart = frameIndex * radialSegments
|
|
678
|
+
for (let radialIndex = 0; radialIndex < radialSegments; radialIndex += 1) {
|
|
679
|
+
const following = (radialIndex + 1) % radialSegments
|
|
680
|
+
appendTriangleFacing(indices, positions, centerIndex, ringStart + radialIndex, ringStart + following, target)
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if (capStart) appendCap(0, frames[0]!.tangent.clone().negate())
|
|
685
|
+
if (capEnd) appendCap(frames.length - 1, frames.at(-1)!.tangent)
|
|
686
|
+
|
|
687
|
+
return createGeometry(positions, uvs, metricUvs, indices, 'sakura-castle-continuous-kawara-ridge-geometry')
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function appendSweptTimberRail(
|
|
691
|
+
positions: number[],
|
|
692
|
+
uvs: number[],
|
|
693
|
+
metricUvs: number[],
|
|
694
|
+
indices: number[],
|
|
695
|
+
frames: readonly SakuraCastleWallPathFrame[],
|
|
696
|
+
distances: readonly number[],
|
|
697
|
+
totalLength: number,
|
|
698
|
+
face: -1 | 1,
|
|
699
|
+
height: number,
|
|
700
|
+
capStart: boolean,
|
|
701
|
+
capEnd: boolean,
|
|
702
|
+
): void {
|
|
703
|
+
const firstVertex = positions.length / 3
|
|
704
|
+
const halfHeight = 0.0425
|
|
705
|
+
const halfDepth = 0.026
|
|
706
|
+
const faceOffset = face * (TRACK_RAIL_WALL_THICKNESS / 2 + halfDepth)
|
|
707
|
+
const crossSections = [
|
|
708
|
+
[-halfHeight, -halfDepth],
|
|
709
|
+
[halfHeight, -halfDepth],
|
|
710
|
+
[halfHeight, halfDepth],
|
|
711
|
+
[-halfHeight, halfDepth],
|
|
712
|
+
] as const
|
|
713
|
+
|
|
714
|
+
frames.forEach((frame, frameIndex) => {
|
|
715
|
+
const center = frame.base
|
|
716
|
+
.clone()
|
|
717
|
+
.addScaledVector(frame.outward, faceOffset)
|
|
718
|
+
.addScaledVector(frame.up, height - 0.03)
|
|
719
|
+
crossSections.forEach(([vertical, depth], crossIndex) => {
|
|
720
|
+
const point = center.clone().addScaledVector(frame.up, vertical).addScaledVector(frame.outward, depth)
|
|
721
|
+
positions.push(point.x, point.y, point.z)
|
|
722
|
+
uvs.push(distances[frameIndex]!, crossIndex / crossSections.length)
|
|
723
|
+
metricUvs.push(crossIndex / crossSections.length, distances[frameIndex]!, 1, totalLength)
|
|
724
|
+
})
|
|
725
|
+
})
|
|
726
|
+
|
|
727
|
+
for (let frameIndex = 0; frameIndex < frames.length - 1; frameIndex += 1) {
|
|
728
|
+
const current = firstVertex + frameIndex * crossSections.length
|
|
729
|
+
const next = current + crossSections.length
|
|
730
|
+
const frame = interpolateFrame(frames[frameIndex]!, frames[frameIndex + 1]!, 0.5)
|
|
731
|
+
for (let side = 0; side < crossSections.length; side += 1) {
|
|
732
|
+
const following = (side + 1) % crossSections.length
|
|
733
|
+
const [verticalA, depthA] = crossSections[side]!
|
|
734
|
+
const [verticalB, depthB] = crossSections[following]!
|
|
735
|
+
const target = frame.up
|
|
736
|
+
.clone()
|
|
737
|
+
.multiplyScalar(verticalA + verticalB)
|
|
738
|
+
.addScaledVector(frame.outward, depthA + depthB)
|
|
739
|
+
.normalize()
|
|
740
|
+
appendTriangleFacing(indices, positions, current + side, next + side, current + following, target)
|
|
741
|
+
appendTriangleFacing(indices, positions, current + following, next + side, next + following, target)
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const start = firstVertex
|
|
746
|
+
const end = firstVertex + (frames.length - 1) * crossSections.length
|
|
747
|
+
if (capStart) {
|
|
748
|
+
appendTriangleFacing(indices, positions, start, start + 1, start + 2, frames[0]!.tangent.clone().negate())
|
|
749
|
+
appendTriangleFacing(indices, positions, start, start + 2, start + 3, frames[0]!.tangent.clone().negate())
|
|
750
|
+
}
|
|
751
|
+
if (capEnd) {
|
|
752
|
+
appendTriangleFacing(indices, positions, end, end + 2, end + 1, frames.at(-1)!.tangent)
|
|
753
|
+
appendTriangleFacing(indices, positions, end, end + 3, end + 2, frames.at(-1)!.tangent)
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function vectorAt(positions: readonly number[], vertex: number): THREE.Vector3 {
|
|
758
|
+
const offset = vertex * 3
|
|
759
|
+
return new THREE.Vector3(positions[offset]!, positions[offset + 1]!, positions[offset + 2]!)
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function kawaraTilePlacementRuns(
|
|
763
|
+
frames: readonly SakuraCastleWallPathFrame[],
|
|
764
|
+
): SakuraCastleWallPathFrame[][] {
|
|
765
|
+
if (frames.length < 3) return [[...frames]]
|
|
766
|
+
const runs: SakuraCastleWallPathFrame[][] = []
|
|
767
|
+
let current = [frames[0]!]
|
|
768
|
+
// Ordinary bends keep one rotating cadence. Only a near-right-angle authored
|
|
769
|
+
// junction resets placement so no rigid tile bridges across empty space.
|
|
770
|
+
const sharpCornerCosine = Math.cos(THREE.MathUtils.degToRad(78))
|
|
771
|
+
for (let index = 1; index < frames.length - 1; index += 1) {
|
|
772
|
+
const corner = frames[index]!
|
|
773
|
+
current.push(corner)
|
|
774
|
+
const incoming = corner.base.clone().sub(frames[index - 1]!.base)
|
|
775
|
+
const outgoing = frames[index + 1]!.base.clone().sub(corner.base)
|
|
776
|
+
if (
|
|
777
|
+
incoming.lengthSq() > 0.000_001 &&
|
|
778
|
+
outgoing.lengthSq() > 0.000_001 &&
|
|
779
|
+
incoming.normalize().dot(outgoing.normalize()) < sharpCornerCosine
|
|
780
|
+
) {
|
|
781
|
+
runs.push(current)
|
|
782
|
+
current = [corner]
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
current.push(frames.at(-1)!)
|
|
786
|
+
runs.push(current)
|
|
787
|
+
return runs
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function appendKawaraTileMatrices(
|
|
791
|
+
target: THREE.Matrix4[],
|
|
792
|
+
sourceFrames: readonly SakuraCastleWallPathFrame[],
|
|
793
|
+
wallHeight: number,
|
|
794
|
+
face: -1 | 1,
|
|
795
|
+
): void {
|
|
796
|
+
const distances = pointDistances(sourceFrames.map(frame => roofSurfacePoint(frame, wallHeight, face, 1)))
|
|
797
|
+
const totalLength = distances.at(-1) ?? 0
|
|
798
|
+
if (totalLength <= 0.001) return
|
|
799
|
+
// Rigid tiles never straddle independently generated runs. Every run owns
|
|
800
|
+
// one half cadence cell at each terminal, so connected tile edges meet at
|
|
801
|
+
// the canonical seam without either curve projecting a tile backwards into
|
|
802
|
+
// its neighbour. The continuous roof backing and ridge still own the seam.
|
|
803
|
+
const startOffset = 0.5
|
|
804
|
+
const endOffset = 0.5
|
|
805
|
+
const tileCount = Math.max(
|
|
806
|
+
1,
|
|
807
|
+
Math.round(totalLength / KAWARA_TILE_SPACING - startOffset - endOffset + 1),
|
|
808
|
+
)
|
|
809
|
+
const intervalCount = startOffset + tileCount - 1 + endOffset
|
|
810
|
+
const tileArcLength = totalLength / Math.max(0.5, intervalCount)
|
|
811
|
+
for (let tileIndex = 0; tileIndex < tileCount; tileIndex += 1) {
|
|
812
|
+
const centerDistance = (startOffset + tileIndex) * tileArcLength
|
|
813
|
+
const startDistance = Math.max(0, centerDistance - tileArcLength / 2)
|
|
814
|
+
const endDistance = Math.min(totalLength, centerDistance + tileArcLength / 2)
|
|
815
|
+
const startFrame = sampleFrame(sourceFrames, distances, startDistance)
|
|
816
|
+
const endFrame = sampleFrame(sourceFrames, distances, endDistance)
|
|
817
|
+
const middleFrame = sampleFrame(sourceFrames, distances, centerDistance)
|
|
818
|
+
const ridge = roofSurfacePoint(middleFrame, wallHeight, face, 0)
|
|
819
|
+
const eave = roofSurfacePoint(middleFrame, wallHeight, face, 1)
|
|
820
|
+
// Build one orthonormal roof frame from the canonical centre sample.
|
|
821
|
+
// Using the curved surface chord as a matrix axis sheared isolated tiles
|
|
822
|
+
// whenever that chord contained a normal component on the inside of a
|
|
823
|
+
// bend. Those sheared tiles are the apparent upright fins.
|
|
824
|
+
const longitudinal = middleFrame.tangent.clone().normalize()
|
|
825
|
+
const slopeDirection = eave.clone().sub(ridge)
|
|
826
|
+
slopeDirection.addScaledVector(longitudinal, -slopeDirection.dot(longitudinal))
|
|
827
|
+
const slopeLength = slopeDirection.length()
|
|
828
|
+
if (slopeLength <= 0.001) continue
|
|
829
|
+
slopeDirection.normalize()
|
|
830
|
+
let surfaceNormal = new THREE.Vector3().crossVectors(slopeDirection, longitudinal).normalize()
|
|
831
|
+
if (surfaceNormal.dot(middleFrame.up) < 0) {
|
|
832
|
+
longitudinal.negate()
|
|
833
|
+
surfaceNormal = new THREE.Vector3().crossVectors(slopeDirection, longitudinal).normalize()
|
|
834
|
+
}
|
|
835
|
+
const rotation = new THREE.Quaternion().setFromRotationMatrix(
|
|
836
|
+
new THREE.Matrix4().makeBasis(longitudinal, surfaceNormal, slopeDirection),
|
|
837
|
+
)
|
|
838
|
+
|
|
839
|
+
for (let row = 0; row < KAWARA_TILE_ROWS; row += 1) {
|
|
840
|
+
const startFraction = row === 0 ? 0.015 : 0.45
|
|
841
|
+
const endFraction = row === 0 ? 0.56 : 0.985
|
|
842
|
+
const centerFraction = (startFraction + endFraction) / 2
|
|
843
|
+
const startCenter = roofSurfacePoint(startFrame, wallHeight, face, centerFraction)
|
|
844
|
+
const endCenter = roofSurfacePoint(endFrame, wallHeight, face, centerFraction)
|
|
845
|
+
const chord = endCenter.clone().sub(startCenter)
|
|
846
|
+
// Arc length chooses the cadence, but a rigid tile spans the straight
|
|
847
|
+
// 3D chord between its cell boundaries. Giving it the longer arc length
|
|
848
|
+
// makes adjacent instances overlap on the short side of a curve. Do not
|
|
849
|
+
// project the chord onto a rotating frame: that was the steep-slope
|
|
850
|
+
// shrink bug.
|
|
851
|
+
const reachesBeyondRun = centerDistance - tileArcLength / 2 < 0 || centerDistance + tileArcLength / 2 > totalLength
|
|
852
|
+
const tileWidth = reachesBeyondRun
|
|
853
|
+
? tileArcLength
|
|
854
|
+
: Math.max(0.04, Math.min(tileArcLength, chord.length()))
|
|
855
|
+
const rowLength = slopeLength * (endFraction - startFraction)
|
|
856
|
+
const center = roofSurfacePoint(middleFrame, wallHeight, face, centerFraction)
|
|
857
|
+
.addScaledVector(surfaceNormal, 0.012 + row * 0.008)
|
|
858
|
+
const matrix = new THREE.Matrix4().compose(
|
|
859
|
+
center,
|
|
860
|
+
rotation,
|
|
861
|
+
new THREE.Vector3(tileWidth, 1, rowLength),
|
|
862
|
+
)
|
|
863
|
+
target.push(matrix)
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function roofSurfacePoint(
|
|
869
|
+
frame: SakuraCastleWallPathFrame,
|
|
870
|
+
wallHeight: number,
|
|
871
|
+
face: -1 | 1,
|
|
872
|
+
slopeFraction: number,
|
|
873
|
+
): THREE.Vector3 {
|
|
874
|
+
const ridge = frame.base.clone().addScaledVector(frame.up, wallHeight + ROOF_RISE - 0.03)
|
|
875
|
+
const eave = frame.base
|
|
876
|
+
.clone()
|
|
877
|
+
.addScaledVector(frame.outward, face * ROOF_HALF_SPAN)
|
|
878
|
+
.addScaledVector(frame.up, wallHeight + ROOF_EAVE_LIFT - 0.03)
|
|
879
|
+
return ridge.lerp(eave, slopeFraction)
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function pointDistances(points: readonly THREE.Vector3[]): number[] {
|
|
883
|
+
const distances = [0]
|
|
884
|
+
for (let index = 1; index < points.length; index += 1) {
|
|
885
|
+
distances.push(distances[index - 1]! + points[index]!.distanceTo(points[index - 1]!))
|
|
886
|
+
}
|
|
887
|
+
return distances
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function getSharedTimberPostGeometry(): THREE.BoxGeometry {
|
|
891
|
+
if (sharedTimberPostGeometry) return sharedTimberPostGeometry
|
|
892
|
+
sharedTimberPostGeometry = new THREE.BoxGeometry(1, 1, 1)
|
|
893
|
+
sharedTimberPostGeometry.name = 'sakura-castle-timber-post-geometry'
|
|
894
|
+
return sharedTimberPostGeometry
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function getSharedKawaraTileGeometry(): THREE.BufferGeometry {
|
|
898
|
+
if (sharedKawaraTileGeometry) return sharedKawaraTileGeometry
|
|
899
|
+
const positions: number[] = []
|
|
900
|
+
const uvs: number[] = []
|
|
901
|
+
const metricUvs: number[] = []
|
|
902
|
+
const indices: number[] = []
|
|
903
|
+
const acrossSegments = 2
|
|
904
|
+
// Every tile row has the same cross-section and the slope axis is linear,
|
|
905
|
+
// so the old middle row described the exact same surface twice.
|
|
906
|
+
const slopeSegments = 1
|
|
907
|
+
const rowSize = acrossSegments + 1
|
|
908
|
+
const textureFootprint = 0.4
|
|
909
|
+
for (let slopeIndex = 0; slopeIndex <= slopeSegments; slopeIndex += 1) {
|
|
910
|
+
const v = slopeIndex / slopeSegments
|
|
911
|
+
const z = v - 0.5
|
|
912
|
+
for (let acrossIndex = 0; acrossIndex <= acrossSegments; acrossIndex += 1) {
|
|
913
|
+
const u = acrossIndex / acrossSegments
|
|
914
|
+
const x = u - 0.5
|
|
915
|
+
const arch = Math.max(0, Math.cos(x * Math.PI)) * 0.032
|
|
916
|
+
positions.push(x, arch + v * 0.006, z)
|
|
917
|
+
const rotatedU = v
|
|
918
|
+
const rotatedV = 1 - u
|
|
919
|
+
const textureU = 0.5 + (rotatedU - 0.5) * textureFootprint
|
|
920
|
+
const textureV = 0.5 + (rotatedV - 0.5) * textureFootprint
|
|
921
|
+
uvs.push(textureU, textureV)
|
|
922
|
+
// The tile is instanced in its wall-local frame. Mark its authored UVs
|
|
923
|
+
// so the shared material rotates with each instance instead of falling
|
|
924
|
+
// back to one world-projected texture direction.
|
|
925
|
+
metricUvs.push(textureU, textureV, 1, 1)
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
for (let slopeIndex = 0; slopeIndex < slopeSegments; slopeIndex += 1) {
|
|
929
|
+
const currentRow = slopeIndex * rowSize
|
|
930
|
+
const nextRow = currentRow + rowSize
|
|
931
|
+
for (let acrossIndex = 0; acrossIndex < acrossSegments; acrossIndex += 1) {
|
|
932
|
+
const current = currentRow + acrossIndex
|
|
933
|
+
const next = nextRow + acrossIndex
|
|
934
|
+
indices.push(current, next, current + 1, current + 1, next, next + 1)
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
const geometry = new THREE.BufferGeometry()
|
|
938
|
+
geometry.name = 'sakura-castle-kawara-tile-geometry'
|
|
939
|
+
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3))
|
|
940
|
+
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2))
|
|
941
|
+
geometry.setAttribute('themeNetUv', new THREE.Float32BufferAttribute(metricUvs, 4))
|
|
942
|
+
geometry.setIndex(indices)
|
|
943
|
+
geometry.computeVertexNormals()
|
|
944
|
+
geometry.computeBoundingBox()
|
|
945
|
+
geometry.computeBoundingSphere()
|
|
946
|
+
sharedKawaraTileGeometry = geometry
|
|
947
|
+
return sharedKawaraTileGeometry
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function createGeometry(
|
|
951
|
+
positions: number[],
|
|
952
|
+
uvs: number[],
|
|
953
|
+
metricUvs: number[],
|
|
954
|
+
indices: number[],
|
|
955
|
+
name: string,
|
|
956
|
+
): THREE.BufferGeometry {
|
|
957
|
+
const geometry = new THREE.BufferGeometry()
|
|
958
|
+
geometry.name = name
|
|
959
|
+
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3))
|
|
960
|
+
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2))
|
|
961
|
+
geometry.setAttribute('themeNetUv', new THREE.Float32BufferAttribute(metricUvs, 4))
|
|
962
|
+
geometry.setIndex(indices)
|
|
963
|
+
geometry.computeVertexNormals()
|
|
964
|
+
geometry.computeBoundingBox()
|
|
965
|
+
geometry.computeBoundingSphere()
|
|
966
|
+
return geometry
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function pathDistances(frames: readonly SakuraCastleWallPathFrame[]): number[] {
|
|
970
|
+
const distances = [0]
|
|
971
|
+
for (let index = 1; index < frames.length; index += 1) {
|
|
972
|
+
distances.push(distances[index - 1]! + frames[index]!.base.distanceTo(frames[index - 1]!.base))
|
|
973
|
+
}
|
|
974
|
+
return distances
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function sampleFrame(
|
|
978
|
+
frames: readonly SakuraCastleWallPathFrame[],
|
|
979
|
+
distances: readonly number[],
|
|
980
|
+
target: number,
|
|
981
|
+
): SakuraCastleWallPathFrame {
|
|
982
|
+
let segment = 0
|
|
983
|
+
while (segment < distances.length - 2 && distances[segment + 1]! < target) segment += 1
|
|
984
|
+
const length = distances[segment + 1]! - distances[segment]!
|
|
985
|
+
return interpolateFrame(frames[segment]!, frames[segment + 1]!, length <= 0.000_001 ? 0 : (target - distances[segment]!) / length)
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function interpolateFrame(
|
|
989
|
+
start: SakuraCastleWallPathFrame,
|
|
990
|
+
end: SakuraCastleWallPathFrame,
|
|
991
|
+
progress: number,
|
|
992
|
+
): SakuraCastleWallPathFrame {
|
|
993
|
+
const tangent = start.tangent.clone().lerp(end.tangent, progress).normalize()
|
|
994
|
+
const up = start.up.clone().lerp(end.up, progress)
|
|
995
|
+
up.addScaledVector(tangent, -up.dot(tangent)).normalize()
|
|
996
|
+
const outward = new THREE.Vector3().crossVectors(tangent, up).normalize()
|
|
997
|
+
const outwardHint = start.outward.clone().lerp(end.outward, progress)
|
|
998
|
+
if (outward.dot(outwardHint) < 0) outward.negate()
|
|
999
|
+
return { base: start.base.clone().lerp(end.base, progress), tangent, up, outward }
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function verticalFrameQuaternion(frame: SakuraCastleWallPathFrame): THREE.Quaternion {
|
|
1003
|
+
const up = new THREE.Vector3(0, 1, 0)
|
|
1004
|
+
const outward = frame.outward.clone().addScaledVector(up, -frame.outward.dot(up))
|
|
1005
|
+
if (outward.lengthSq() < 0.000_001) outward.crossVectors(frame.tangent, up)
|
|
1006
|
+
outward.normalize()
|
|
1007
|
+
const tangent = new THREE.Vector3().crossVectors(up, outward).normalize()
|
|
1008
|
+
outward.crossVectors(tangent, up).normalize()
|
|
1009
|
+
return new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().makeBasis(tangent, up, outward))
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
function instancedBatch(
|
|
1013
|
+
name: string,
|
|
1014
|
+
geometry: THREE.BufferGeometry,
|
|
1015
|
+
material: THREE.Material,
|
|
1016
|
+
matrices: readonly THREE.Matrix4[],
|
|
1017
|
+
sharedGeometry = false,
|
|
1018
|
+
): THREE.InstancedMesh {
|
|
1019
|
+
const mesh = new THREE.InstancedMesh(geometry, material, matrices.length)
|
|
1020
|
+
mesh.name = name
|
|
1021
|
+
mesh.userData.decorative = true
|
|
1022
|
+
mesh.userData.visualOnly = true
|
|
1023
|
+
mesh.userData.sharedGeometry = sharedGeometry
|
|
1024
|
+
mesh.castShadow = true
|
|
1025
|
+
mesh.receiveShadow = true
|
|
1026
|
+
matrices.forEach((matrix, index) => mesh.setMatrixAt(index, matrix))
|
|
1027
|
+
mesh.instanceMatrix.needsUpdate = true
|
|
1028
|
+
mesh.computeBoundingBox()
|
|
1029
|
+
mesh.computeBoundingSphere()
|
|
1030
|
+
return mesh
|
|
1031
|
+
}
|