@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,931 @@
|
|
|
1
|
+
import * as THREE from 'three'
|
|
2
|
+
|
|
3
|
+
import { homepageMarbleGeometry } from '../../three/homepage-marble-geometry'
|
|
4
|
+
import { MarbleSphereGeometry } from '../../three/marble-sphere-geometry'
|
|
5
|
+
|
|
6
|
+
import { VisualCylinderGeometry, VisualTorusGeometry, VisualLatheGeometry, VisualExtrudeGeometry } from '../../three/visual-geometry'
|
|
7
|
+
|
|
8
|
+
import type { MarbleSpec } from './lobby-types'
|
|
9
|
+
|
|
10
|
+
export interface LobbyMarble {
|
|
11
|
+
root: THREE.Group
|
|
12
|
+
marble: THREE.Mesh<THREE.SphereGeometry, THREE.Material>
|
|
13
|
+
ready: Promise<void>
|
|
14
|
+
setFocused(focused: boolean): void
|
|
15
|
+
setBacked(backed: boolean): void
|
|
16
|
+
setRestingY(y: number): void
|
|
17
|
+
setPodiumVisible(visible: boolean): void
|
|
18
|
+
setInFrontOfLens(inFront: boolean): void
|
|
19
|
+
setEntranceReveal(progress: number): void
|
|
20
|
+
setPerformanceMode(enabled: boolean): void
|
|
21
|
+
update(elapsedSeconds: number): void
|
|
22
|
+
dispose(): void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface LobbyMarbleOptions {
|
|
26
|
+
/** Restore original sphere and filament tessellation for the homepage showcase only. */
|
|
27
|
+
homepageGeometry?: boolean
|
|
28
|
+
includeLogo?: boolean
|
|
29
|
+
geometryDetail?: 'default' | 'medium'
|
|
30
|
+
performanceMaterial?: boolean
|
|
31
|
+
tokenMaterial?: THREE.Material
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const LOBBY_MARBLE_RADIUS = 0.73
|
|
35
|
+
export const LOBBY_TOKEN_OUTER_RADIUS = 0.738
|
|
36
|
+
|
|
37
|
+
const sharedMarbleGeometry = new MarbleSphereGeometry(LOBBY_MARBLE_RADIUS, 48, 40)
|
|
38
|
+
sharedMarbleGeometry.userData.shared = true
|
|
39
|
+
const sharedMediumMarbleGeometry = new MarbleSphereGeometry(LOBBY_MARBLE_RADIUS, 32, 24)
|
|
40
|
+
sharedMediumMarbleGeometry.userData.shared = true
|
|
41
|
+
const sharedBubbleRimGeometry = new MarbleSphereGeometry(LOBBY_TOKEN_OUTER_RADIUS, 40, 32)
|
|
42
|
+
sharedBubbleRimGeometry.userData.shared = true
|
|
43
|
+
|
|
44
|
+
interface SharedVeinTextures {
|
|
45
|
+
colorTexture: THREE.CanvasTexture
|
|
46
|
+
roughnessTexture: THREE.CanvasTexture
|
|
47
|
+
ready: Promise<void>
|
|
48
|
+
references: number
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const veinTextureCache = new Map<string, SharedVeinTextures>()
|
|
52
|
+
const GLOSSY_LOGO_COLOR = '#9aa09e'
|
|
53
|
+
|
|
54
|
+
function buildVeinTextures(spec: MarbleSpec, includeLogo: boolean): SharedVeinTextures {
|
|
55
|
+
const canvas = document.createElement('canvas')
|
|
56
|
+
canvas.width = 768
|
|
57
|
+
canvas.height = 384
|
|
58
|
+
const context = canvas.getContext('2d')
|
|
59
|
+
if (!context) throw new Error('Canvas 2D context is unavailable')
|
|
60
|
+
const roughnessCanvas = document.createElement('canvas')
|
|
61
|
+
roughnessCanvas.width = canvas.width
|
|
62
|
+
roughnessCanvas.height = canvas.height
|
|
63
|
+
const roughnessContext = roughnessCanvas.getContext('2d')
|
|
64
|
+
if (!roughnessContext) throw new Error('Canvas 2D context is unavailable')
|
|
65
|
+
roughnessContext.fillStyle = '#303030'
|
|
66
|
+
roughnessContext.fillRect(0, 0, roughnessCanvas.width, roughnessCanvas.height)
|
|
67
|
+
const image = context.createImageData(canvas.width, canvas.height)
|
|
68
|
+
let seed = [...spec.id].reduce((value, character) => value + character.charCodeAt(0) * 97, 17)
|
|
69
|
+
const random = () => {
|
|
70
|
+
seed = (seed * 1664525 + 1013904223) >>> 0
|
|
71
|
+
return seed / 4294967296
|
|
72
|
+
}
|
|
73
|
+
const phase = random() * Math.PI * 2
|
|
74
|
+
|
|
75
|
+
for (let y = 0; y < canvas.height; y += 1) {
|
|
76
|
+
const v = y / canvas.height
|
|
77
|
+
for (let x = 0; x < canvas.width; x += 1) {
|
|
78
|
+
const u = x / canvas.width
|
|
79
|
+
const theta = u * Math.PI * 2
|
|
80
|
+
const cloud =
|
|
81
|
+
Math.sin(theta * 2 + v * 5.2 + phase) * 0.52 +
|
|
82
|
+
Math.sin(theta * 4 - v * 8.5 + phase * 1.8) * 0.28 +
|
|
83
|
+
Math.sin(theta * 9 + v * 15 - phase * 0.7) * 0.13 +
|
|
84
|
+
Math.sin(theta * 17 - v * 23 + phase * 2.4) * 0.06
|
|
85
|
+
const broadCloud =
|
|
86
|
+
Math.sin(theta * 1.15 + v * 3.1 + phase * 0.55) * 0.62 + Math.sin(theta * 0.58 - v * 5.4 + phase * 1.35) * 0.38
|
|
87
|
+
const patchField = broadCloud + Math.sin(theta * 2.35 + v * 2.2 - phase * 0.8) * 0.28
|
|
88
|
+
const darkPatch = THREE.MathUtils.smoothstep(patchField, -0.58, 0.78)
|
|
89
|
+
const value = Math.max(180, Math.min(246, 236 + cloud * 14 - darkPatch * 40))
|
|
90
|
+
const pixel = (y * canvas.width + x) * 4
|
|
91
|
+
image.data[pixel] = value
|
|
92
|
+
image.data[pixel + 1] = Math.min(255, value + 2)
|
|
93
|
+
image.data[pixel + 2] = Math.min(255, value + 1)
|
|
94
|
+
image.data[pixel + 3] = 255
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
context.putImageData(image, 0, 0)
|
|
98
|
+
|
|
99
|
+
interface VeinPoint {
|
|
100
|
+
x: number
|
|
101
|
+
y: number
|
|
102
|
+
}
|
|
103
|
+
const traceCrackPath = (points: VeinPoint[], offsetX = 0) => {
|
|
104
|
+
context.beginPath()
|
|
105
|
+
context.moveTo(points[0]!.x + offsetX, points[0]!.y)
|
|
106
|
+
for (let index = 1; index < points.length; index += 1) {
|
|
107
|
+
const point = points[index]!
|
|
108
|
+
context.lineTo(point.x + offsetX, point.y)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const strokeWrappedPath = (points: VeinPoint[], style: string, width: number, blur = 0) => {
|
|
112
|
+
for (const offsetX of [-canvas.width, 0, canvas.width]) {
|
|
113
|
+
traceCrackPath(points, offsetX)
|
|
114
|
+
context.strokeStyle = style
|
|
115
|
+
context.lineWidth = width
|
|
116
|
+
context.lineCap = 'round'
|
|
117
|
+
context.lineJoin = 'miter'
|
|
118
|
+
context.filter = blur ? `blur(${blur}px)` : 'none'
|
|
119
|
+
context.stroke()
|
|
120
|
+
}
|
|
121
|
+
context.filter = 'none'
|
|
122
|
+
}
|
|
123
|
+
const fillWrappedVariablePath = (points: VeinPoint[], style: string, baseWidth: number, phaseOffset: number) => {
|
|
124
|
+
const edgePoints = points.map((point, index) => {
|
|
125
|
+
const previous = points[Math.max(0, index - 1)]!
|
|
126
|
+
const next = points[Math.min(points.length - 1, index + 1)]!
|
|
127
|
+
const tangentX = next.x - previous.x
|
|
128
|
+
const tangentY = next.y - previous.y
|
|
129
|
+
const tangentLength = Math.max(0.001, Math.hypot(tangentX, tangentY))
|
|
130
|
+
const progress = index / (points.length - 1)
|
|
131
|
+
const taper = Math.sin(progress * Math.PI) ** 0.42
|
|
132
|
+
const variation =
|
|
133
|
+
0.92 +
|
|
134
|
+
Math.sin(progress * Math.PI * 2.4 + phaseOffset) * 0.24 +
|
|
135
|
+
Math.sin(progress * Math.PI * 5.2 - phaseOffset * 0.7) * 0.1
|
|
136
|
+
const halfWidth = baseWidth * Math.max(0.16, taper) * Math.max(0.5, variation) * 0.5
|
|
137
|
+
const normalX = -tangentY / tangentLength
|
|
138
|
+
const normalY = tangentX / tangentLength
|
|
139
|
+
return {
|
|
140
|
+
left: { x: point.x + normalX * halfWidth, y: point.y + normalY * halfWidth },
|
|
141
|
+
right: { x: point.x - normalX * halfWidth, y: point.y - normalY * halfWidth },
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
context.fillStyle = style
|
|
145
|
+
for (const offsetX of [-canvas.width, 0, canvas.width]) {
|
|
146
|
+
context.beginPath()
|
|
147
|
+
context.moveTo(edgePoints[0]!.left.x + offsetX, edgePoints[0]!.left.y)
|
|
148
|
+
for (let index = 1; index < edgePoints.length; index += 1) {
|
|
149
|
+
const edge = edgePoints[index]!
|
|
150
|
+
context.lineTo(edge.left.x + offsetX, edge.left.y)
|
|
151
|
+
}
|
|
152
|
+
for (let index = edgePoints.length - 1; index >= 0; index -= 1) {
|
|
153
|
+
const edge = edgePoints[index]!
|
|
154
|
+
context.lineTo(edge.right.x + offsetX, edge.right.y)
|
|
155
|
+
}
|
|
156
|
+
context.closePath()
|
|
157
|
+
context.fill()
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const veinCount = 7
|
|
162
|
+
for (let veinIndex = 0; veinIndex < veinCount; veinIndex += 1) {
|
|
163
|
+
const points: VeinPoint[] = []
|
|
164
|
+
const pointCount = 9 + Math.floor(random() * 6)
|
|
165
|
+
const crackLength = canvas.width * (0.3 + random() * 0.2)
|
|
166
|
+
const longitudeSlot = (veinIndex / veinCount) * canvas.width
|
|
167
|
+
const crackStartX = longitudeSlot + (random() - 0.5) * canvas.width * 0.14
|
|
168
|
+
let veinY = 62 + random() * (canvas.height - 124)
|
|
169
|
+
let verticalDrift = (random() - 0.5) * 9
|
|
170
|
+
for (let pointIndex = 0; pointIndex < pointCount; pointIndex += 1) {
|
|
171
|
+
verticalDrift = THREE.MathUtils.clamp(verticalDrift + (random() - 0.5) * 7, -14, 14)
|
|
172
|
+
veinY += verticalDrift + (random() - 0.5) * 15
|
|
173
|
+
veinY = Math.max(44, Math.min(canvas.height - 44, veinY))
|
|
174
|
+
points.push({
|
|
175
|
+
x: crackStartX + pointIndex * (crackLength / (pointCount - 1)),
|
|
176
|
+
y: veinY,
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const veinWidth = 1.15 + random() * 1.85
|
|
181
|
+
strokeWrappedPath(points, 'rgba(30, 39, 37, 0.13)', veinWidth * 6.2, 5)
|
|
182
|
+
strokeWrappedPath(points, 'rgba(244, 251, 248, 0.16)', veinWidth * 3.4, 1.5)
|
|
183
|
+
fillWrappedVariablePath(points, 'rgba(45, 56, 53, 0.34)', veinWidth, phase + veinIndex)
|
|
184
|
+
fillWrappedVariablePath(
|
|
185
|
+
points,
|
|
186
|
+
'rgba(255, 255, 255, 0.27)',
|
|
187
|
+
Math.max(0.4, veinWidth * 0.22),
|
|
188
|
+
phase + veinIndex + 0.8,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
for (const branchPointIndex of [Math.floor(pointCount * 0.34), Math.floor(pointCount * 0.64)]) {
|
|
192
|
+
if (random() < 0.34) continue
|
|
193
|
+
const origin = points[branchPointIndex]!
|
|
194
|
+
const branch: VeinPoint[] = [{ ...origin }]
|
|
195
|
+
let branchX = origin.x
|
|
196
|
+
let branchY = origin.y
|
|
197
|
+
const direction = random() > 0.5 ? 1 : -1
|
|
198
|
+
for (let branchIndex = 0; branchIndex < 4; branchIndex += 1) {
|
|
199
|
+
branchX += 13 + random() * 18
|
|
200
|
+
branchY += direction * (7 + random() * 16) + (random() - 0.5) * 8
|
|
201
|
+
branch.push({ x: branchX, y: branchY })
|
|
202
|
+
}
|
|
203
|
+
const branchWidth = 0.6 + random() * 0.9
|
|
204
|
+
fillWrappedVariablePath(branch, 'rgba(48, 59, 56, 0.25)', branchWidth, phase + branchPointIndex)
|
|
205
|
+
fillWrappedVariablePath(branch, 'rgba(250, 255, 253, 0.17)', branchWidth * 0.28, phase + branchPointIndex + 0.65)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const colorTexture = new THREE.CanvasTexture(canvas)
|
|
210
|
+
colorTexture.colorSpace = THREE.SRGBColorSpace
|
|
211
|
+
colorTexture.wrapS = THREE.RepeatWrapping
|
|
212
|
+
colorTexture.wrapT = THREE.RepeatWrapping
|
|
213
|
+
colorTexture.anisotropy = 4
|
|
214
|
+
const roughnessTexture = new THREE.CanvasTexture(roughnessCanvas)
|
|
215
|
+
roughnessTexture.wrapS = THREE.RepeatWrapping
|
|
216
|
+
roughnessTexture.wrapT = THREE.RepeatWrapping
|
|
217
|
+
roughnessTexture.anisotropy = 4
|
|
218
|
+
const ready = includeLogo
|
|
219
|
+
? new Promise<void>(resolve => {
|
|
220
|
+
const centerX = canvas.width * 0.25
|
|
221
|
+
const centerY = canvas.height * 0.5
|
|
222
|
+
const logo = new Image()
|
|
223
|
+
logo.onload = () => {
|
|
224
|
+
const targetSize =
|
|
225
|
+
spec.logo === 'spacex' ? 170 : spec.logo === 'nvidia' ? 140 : spec.logo === 'microsoft' ? 98 : 130
|
|
226
|
+
const ratio = logo.naturalWidth / logo.naturalHeight || 1
|
|
227
|
+
const width = ratio >= 1 ? targetSize : targetSize * ratio
|
|
228
|
+
const height = ratio >= 1 ? targetSize / ratio : targetSize
|
|
229
|
+
const logoCanvas = document.createElement('canvas')
|
|
230
|
+
logoCanvas.width = Math.ceil(width)
|
|
231
|
+
logoCanvas.height = Math.ceil(height)
|
|
232
|
+
const logoContext = logoCanvas.getContext('2d')!
|
|
233
|
+
logoContext.drawImage(logo, 0, 0, width, height)
|
|
234
|
+
logoContext.globalCompositeOperation = 'source-in'
|
|
235
|
+
logoContext.fillStyle = GLOSSY_LOGO_COLOR
|
|
236
|
+
logoContext.fillRect(0, 0, width, height)
|
|
237
|
+
context.drawImage(logoCanvas, centerX - width / 2, centerY - height / 2)
|
|
238
|
+
roughnessContext.save()
|
|
239
|
+
roughnessContext.filter = 'brightness(0)'
|
|
240
|
+
roughnessContext.drawImage(logo, centerX - width / 2, centerY - height / 2, width, height)
|
|
241
|
+
roughnessContext.restore()
|
|
242
|
+
colorTexture.needsUpdate = true
|
|
243
|
+
roughnessTexture.needsUpdate = true
|
|
244
|
+
resolve()
|
|
245
|
+
}
|
|
246
|
+
logo.onerror = () => resolve()
|
|
247
|
+
logo.src = `/images/lobby/${spec.logo}.svg`
|
|
248
|
+
})
|
|
249
|
+
: Promise.resolve()
|
|
250
|
+
return { colorTexture, roughnessTexture, ready, references: 0 }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function acquireVeinTextures(spec: MarbleSpec, includeLogo: boolean): SharedVeinTextures & { release(): void } {
|
|
254
|
+
const key = `${spec.id}:${spec.logo}:${includeLogo ? 'logo' : 'plain'}`
|
|
255
|
+
const resource = veinTextureCache.get(key) ?? buildVeinTextures(spec, includeLogo)
|
|
256
|
+
if (!veinTextureCache.has(key)) veinTextureCache.set(key, resource)
|
|
257
|
+
resource.references += 1
|
|
258
|
+
let released = false
|
|
259
|
+
return {
|
|
260
|
+
...resource,
|
|
261
|
+
release: () => {
|
|
262
|
+
if (released) return
|
|
263
|
+
released = true
|
|
264
|
+
resource.references -= 1
|
|
265
|
+
if (resource.references > 0) return
|
|
266
|
+
veinTextureCache.delete(key)
|
|
267
|
+
resource.colorTexture.dispose()
|
|
268
|
+
resource.roughnessTexture.dispose()
|
|
269
|
+
},
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function createPodium(color: THREE.Color): {
|
|
274
|
+
aura: THREE.Mesh
|
|
275
|
+
glow: THREE.Mesh
|
|
276
|
+
group: THREE.Group
|
|
277
|
+
rings: THREE.Mesh[]
|
|
278
|
+
} {
|
|
279
|
+
const group = new THREE.Group()
|
|
280
|
+
group.position.y = -1.06
|
|
281
|
+
|
|
282
|
+
const base = new THREE.Mesh(
|
|
283
|
+
new VisualLatheGeometry(
|
|
284
|
+
[
|
|
285
|
+
new THREE.Vector2(0, -0.09),
|
|
286
|
+
new THREE.Vector2(0.79, -0.09),
|
|
287
|
+
new THREE.Vector2(0.88, -0.078),
|
|
288
|
+
new THREE.Vector2(0.94, -0.057),
|
|
289
|
+
new THREE.Vector2(0.985, -0.022),
|
|
290
|
+
new THREE.Vector2(0.99, 0.004),
|
|
291
|
+
new THREE.Vector2(0.966, 0.026),
|
|
292
|
+
new THREE.Vector2(0.91, 0.052),
|
|
293
|
+
new THREE.Vector2(0.84, 0.062),
|
|
294
|
+
new THREE.Vector2(0, 0.062),
|
|
295
|
+
],
|
|
296
|
+
128,
|
|
297
|
+
),
|
|
298
|
+
new THREE.MeshPhysicalMaterial({
|
|
299
|
+
color: new THREE.Color(0x080b0a).lerp(color, 0.025),
|
|
300
|
+
emissive: color,
|
|
301
|
+
emissiveIntensity: 0.012,
|
|
302
|
+
metalness: 0.94,
|
|
303
|
+
roughness: 0.24,
|
|
304
|
+
clearcoat: 0.34,
|
|
305
|
+
clearcoatRoughness: 0.19,
|
|
306
|
+
anisotropy: 0.42,
|
|
307
|
+
anisotropyRotation: Math.PI / 2,
|
|
308
|
+
iridescence: 0.12,
|
|
309
|
+
iridescenceIOR: 1.45,
|
|
310
|
+
}),
|
|
311
|
+
)
|
|
312
|
+
group.add(base)
|
|
313
|
+
|
|
314
|
+
const top = new THREE.Mesh(
|
|
315
|
+
new VisualCylinderGeometry(0.79, 0.83, 0.022, 128),
|
|
316
|
+
new THREE.MeshPhysicalMaterial({
|
|
317
|
+
color: 0x010202,
|
|
318
|
+
metalness: 0.86,
|
|
319
|
+
roughness: 0.075,
|
|
320
|
+
clearcoat: 1,
|
|
321
|
+
clearcoatRoughness: 0.035,
|
|
322
|
+
envMapIntensity: 2.2,
|
|
323
|
+
}),
|
|
324
|
+
)
|
|
325
|
+
top.position.y = 0.064
|
|
326
|
+
group.add(top)
|
|
327
|
+
|
|
328
|
+
const edge = new THREE.Mesh(
|
|
329
|
+
new VisualTorusGeometry(0.925, 0.022, 12, 128),
|
|
330
|
+
new THREE.MeshPhysicalMaterial({
|
|
331
|
+
color: new THREE.Color(0x252b29).lerp(color, 0.06),
|
|
332
|
+
metalness: 1,
|
|
333
|
+
roughness: 0.14,
|
|
334
|
+
clearcoat: 0.7,
|
|
335
|
+
clearcoatRoughness: 0.08,
|
|
336
|
+
}),
|
|
337
|
+
)
|
|
338
|
+
edge.rotation.x = Math.PI / 2
|
|
339
|
+
edge.position.y = 0.027
|
|
340
|
+
group.add(edge)
|
|
341
|
+
|
|
342
|
+
const seam = new THREE.Mesh(
|
|
343
|
+
new VisualTorusGeometry(0.805, 0.007, 8, 128),
|
|
344
|
+
new THREE.MeshStandardMaterial({
|
|
345
|
+
color,
|
|
346
|
+
emissive: color,
|
|
347
|
+
emissiveIntensity: 0.72,
|
|
348
|
+
metalness: 0.45,
|
|
349
|
+
roughness: 0.24,
|
|
350
|
+
}),
|
|
351
|
+
)
|
|
352
|
+
seam.rotation.x = Math.PI / 2
|
|
353
|
+
seam.position.y = 0.078
|
|
354
|
+
group.add(seam)
|
|
355
|
+
const undercut = new THREE.Mesh(
|
|
356
|
+
new VisualTorusGeometry(0.86, 0.018, 10, 128),
|
|
357
|
+
new THREE.MeshStandardMaterial({
|
|
358
|
+
color: 0x050706,
|
|
359
|
+
metalness: 0.96,
|
|
360
|
+
roughness: 0.2,
|
|
361
|
+
}),
|
|
362
|
+
)
|
|
363
|
+
undercut.rotation.x = Math.PI / 2
|
|
364
|
+
undercut.position.y = -0.082
|
|
365
|
+
group.add(undercut)
|
|
366
|
+
|
|
367
|
+
const rings = [0.76, 0.94].map((radius, index) => {
|
|
368
|
+
const ring = new THREE.Mesh(
|
|
369
|
+
new VisualTorusGeometry(radius, index ? 0.018 : 0.014, 8, 64),
|
|
370
|
+
new THREE.MeshStandardMaterial({
|
|
371
|
+
color,
|
|
372
|
+
emissive: color,
|
|
373
|
+
emissiveIntensity: 2.1,
|
|
374
|
+
metalness: 0.08,
|
|
375
|
+
roughness: 0.22,
|
|
376
|
+
}),
|
|
377
|
+
)
|
|
378
|
+
ring.rotation.x = Math.PI / 2
|
|
379
|
+
ring.position.y = 0.05 + index * 0.025
|
|
380
|
+
ring.userData.color = color
|
|
381
|
+
ring.userData.inactiveColor = new THREE.Color(0x4a504e)
|
|
382
|
+
ring.userData.inactiveEmissive = new THREE.Color(0x171b1a)
|
|
383
|
+
group.add(ring)
|
|
384
|
+
return ring
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
const glow = new THREE.Mesh(
|
|
388
|
+
new VisualTorusGeometry(0.86, 0.018, 8, 64),
|
|
389
|
+
new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.08 }),
|
|
390
|
+
)
|
|
391
|
+
glow.rotation.x = Math.PI / 2
|
|
392
|
+
glow.position.y = -0.958
|
|
393
|
+
const aura = new THREE.Mesh(
|
|
394
|
+
new VisualTorusGeometry(0.91, 0.045, 12, 64),
|
|
395
|
+
new THREE.MeshBasicMaterial({
|
|
396
|
+
color,
|
|
397
|
+
blending: THREE.AdditiveBlending,
|
|
398
|
+
transparent: true,
|
|
399
|
+
opacity: 0.022,
|
|
400
|
+
depthWrite: false,
|
|
401
|
+
}),
|
|
402
|
+
)
|
|
403
|
+
aura.rotation.x = Math.PI / 2
|
|
404
|
+
aura.position.y = -0.95
|
|
405
|
+
return { aura, glow, group, rings }
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function createFilamentTexture(): THREE.CanvasTexture {
|
|
409
|
+
const canvas = document.createElement('canvas')
|
|
410
|
+
canvas.width = 256
|
|
411
|
+
canvas.height = 256
|
|
412
|
+
const context = canvas.getContext('2d')
|
|
413
|
+
if (!context) throw new Error('Canvas 2D context is unavailable')
|
|
414
|
+
|
|
415
|
+
context.fillStyle = '#8f8f8f'
|
|
416
|
+
context.fillRect(0, 0, canvas.width, canvas.height)
|
|
417
|
+
for (let index = 0; index < 90; index += 1) {
|
|
418
|
+
const y = Math.random() * canvas.height
|
|
419
|
+
const alpha = 0.025 + Math.random() * 0.055
|
|
420
|
+
context.strokeStyle = `rgba(255, 255, 255, ${alpha})`
|
|
421
|
+
context.lineWidth = 0.35 + Math.random() * 1.1
|
|
422
|
+
context.beginPath()
|
|
423
|
+
context.moveTo(-24, y)
|
|
424
|
+
context.bezierCurveTo(64, y + 4, 184, y - 5, 280, y + 2)
|
|
425
|
+
context.stroke()
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const texture = new THREE.CanvasTexture(canvas)
|
|
429
|
+
texture.wrapS = THREE.RepeatWrapping
|
|
430
|
+
texture.wrapT = THREE.RepeatWrapping
|
|
431
|
+
texture.repeat.set(1.3, 5)
|
|
432
|
+
texture.colorSpace = THREE.NoColorSpace
|
|
433
|
+
return texture
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function createTokenFilament(surfaceTexture?: THREE.Texture, simplified = false, homepageGeometry = false): THREE.Group {
|
|
437
|
+
const ExtrudeGeometry = homepageGeometry ? THREE.ExtrudeGeometry : VisualExtrudeGeometry
|
|
438
|
+
const group = new THREE.Group()
|
|
439
|
+
const scale = 0.00505
|
|
440
|
+
const x = (value: number) => (value - 128) * scale
|
|
441
|
+
const y = (value: number) => (128 - value) * scale
|
|
442
|
+
const point = (pointX: number, pointY: number) => [x(pointX), y(pointY)] as const
|
|
443
|
+
const moveTo = (shape: THREE.Shape, [pointX, pointY]: readonly [number, number]) => {
|
|
444
|
+
shape.moveTo(pointX, pointY)
|
|
445
|
+
}
|
|
446
|
+
const curveTo = (
|
|
447
|
+
shape: THREE.Shape,
|
|
448
|
+
[control1X, control1Y]: readonly [number, number],
|
|
449
|
+
[control2X, control2Y]: readonly [number, number],
|
|
450
|
+
[pointX, pointY]: readonly [number, number],
|
|
451
|
+
) => {
|
|
452
|
+
shape.bezierCurveTo(control1X, control1Y, control2X, control2Y, pointX, pointY)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const start = point(48.804, 207.196)
|
|
456
|
+
const end = point(207.196, 48.804)
|
|
457
|
+
const lower = new THREE.Shape()
|
|
458
|
+
moveTo(lower, start)
|
|
459
|
+
curveTo(lower, point(86, 196), point(123, 171), point(147, 144))
|
|
460
|
+
curveTo(lower, point(171, 117), point(178, 62), end)
|
|
461
|
+
curveTo(lower, point(180, 66), point(168, 112), point(142, 137))
|
|
462
|
+
curveTo(lower, point(112, 165), point(83, 190), start)
|
|
463
|
+
lower.closePath()
|
|
464
|
+
|
|
465
|
+
const middle = new THREE.Shape()
|
|
466
|
+
moveTo(middle, start)
|
|
467
|
+
curveTo(middle, point(83, 190), point(112, 165), point(142, 137))
|
|
468
|
+
curveTo(middle, point(168, 112), point(180, 66), end)
|
|
469
|
+
curveTo(middle, point(181, 62), point(156, 91), point(134, 119))
|
|
470
|
+
curveTo(middle, point(112, 148), point(89, 183), start)
|
|
471
|
+
middle.closePath()
|
|
472
|
+
|
|
473
|
+
const upper = new THREE.Shape()
|
|
474
|
+
moveTo(upper, start)
|
|
475
|
+
curveTo(upper, point(89, 183), point(112, 148), point(134, 119))
|
|
476
|
+
curveTo(upper, point(156, 91), point(181, 62), end)
|
|
477
|
+
curveTo(upper, point(171, 48), point(140, 67), point(118, 101))
|
|
478
|
+
curveTo(upper, point(96, 135), point(77, 190), start)
|
|
479
|
+
upper.closePath()
|
|
480
|
+
|
|
481
|
+
const pieces = [
|
|
482
|
+
{ shape: lower, color: '#dcff72', emissive: '#789b26' },
|
|
483
|
+
{ shape: middle, color: '#c9ff3d', emissive: '#6d941f' },
|
|
484
|
+
{ shape: upper, color: '#9fce32', emissive: '#526f1d' },
|
|
485
|
+
] as const
|
|
486
|
+
|
|
487
|
+
pieces.forEach(({ shape, color, emissive }) => {
|
|
488
|
+
const geometry = new ExtrudeGeometry(shape, {
|
|
489
|
+
depth: 0.04,
|
|
490
|
+
bevelEnabled: true,
|
|
491
|
+
bevelSegments: simplified ? 1 : 3,
|
|
492
|
+
bevelSize: 0.006,
|
|
493
|
+
bevelThickness: 0.006,
|
|
494
|
+
curveSegments: simplified ? 24 : 64,
|
|
495
|
+
steps: 1,
|
|
496
|
+
})
|
|
497
|
+
geometry.translate(0, 0, -0.02)
|
|
498
|
+
const positions = geometry.getAttribute('position')
|
|
499
|
+
for (let index = 0; index < positions.count; index += 1) {
|
|
500
|
+
const localX = positions.getX(index)
|
|
501
|
+
const localY = positions.getY(index)
|
|
502
|
+
const alongRibbon = localX + localY
|
|
503
|
+
const acrossRibbon = localY - localX
|
|
504
|
+
positions.setZ(index, positions.getZ(index) + Math.sin(alongRibbon * 3.2) * 0.026 + acrossRibbon * 0.055)
|
|
505
|
+
}
|
|
506
|
+
positions.needsUpdate = true
|
|
507
|
+
geometry.computeVertexNormals()
|
|
508
|
+
const material = simplified
|
|
509
|
+
? new THREE.MeshStandardMaterial({
|
|
510
|
+
color: new THREE.Color(color),
|
|
511
|
+
emissive: new THREE.Color(emissive),
|
|
512
|
+
emissiveIntensity: 0.08,
|
|
513
|
+
roughness: 0.38,
|
|
514
|
+
metalness: 0,
|
|
515
|
+
side: THREE.DoubleSide,
|
|
516
|
+
transparent: true,
|
|
517
|
+
depthWrite: false,
|
|
518
|
+
toneMapped: false,
|
|
519
|
+
})
|
|
520
|
+
: [
|
|
521
|
+
new THREE.MeshPhysicalMaterial({
|
|
522
|
+
color: new THREE.Color(color),
|
|
523
|
+
emissive: new THREE.Color(emissive),
|
|
524
|
+
emissiveIntensity: 0.05,
|
|
525
|
+
roughness: 0.3,
|
|
526
|
+
roughnessMap: surfaceTexture,
|
|
527
|
+
bumpMap: surfaceTexture,
|
|
528
|
+
bumpScale: 0.006,
|
|
529
|
+
metalness: 0,
|
|
530
|
+
clearcoat: 0.42,
|
|
531
|
+
clearcoatRoughness: 0.2,
|
|
532
|
+
sheen: 0.16,
|
|
533
|
+
sheenColor: new THREE.Color('#eaffaa'),
|
|
534
|
+
side: THREE.DoubleSide,
|
|
535
|
+
transparent: true,
|
|
536
|
+
opacity: 1,
|
|
537
|
+
depthTest: true,
|
|
538
|
+
depthWrite: false,
|
|
539
|
+
toneMapped: false,
|
|
540
|
+
}),
|
|
541
|
+
new THREE.MeshBasicMaterial({
|
|
542
|
+
color: new THREE.Color(color),
|
|
543
|
+
side: THREE.DoubleSide,
|
|
544
|
+
transparent: true,
|
|
545
|
+
opacity: 1,
|
|
546
|
+
depthTest: true,
|
|
547
|
+
depthWrite: false,
|
|
548
|
+
toneMapped: false,
|
|
549
|
+
}),
|
|
550
|
+
]
|
|
551
|
+
const piece = new THREE.Mesh(geometry, material)
|
|
552
|
+
piece.renderOrder = 4
|
|
553
|
+
group.add(piece)
|
|
554
|
+
})
|
|
555
|
+
group.rotation.set(-0.08, -0.12, -0.035)
|
|
556
|
+
group.scale.setScalar(1.28)
|
|
557
|
+
group.renderOrder = 4
|
|
558
|
+
return group
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
let sharedPackedFilament: THREE.Group | undefined
|
|
562
|
+
|
|
563
|
+
function createPackedTokenFilament(): THREE.Group {
|
|
564
|
+
if (!sharedPackedFilament) {
|
|
565
|
+
const surfaceTexture = createFilamentTexture()
|
|
566
|
+
surfaceTexture.userData.shared = true
|
|
567
|
+
sharedPackedFilament = createTokenFilament(surfaceTexture)
|
|
568
|
+
sharedPackedFilament.traverse(object => {
|
|
569
|
+
if (!(object instanceof THREE.Mesh)) return
|
|
570
|
+
object.geometry.userData.shared = true
|
|
571
|
+
const materials = Array.isArray(object.material) ? object.material : [object.material]
|
|
572
|
+
materials.forEach(material => {
|
|
573
|
+
material.userData.shared = true
|
|
574
|
+
})
|
|
575
|
+
})
|
|
576
|
+
}
|
|
577
|
+
return sharedPackedFilament.clone(true)
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
export function createLobbyMarble(spec: MarbleSpec, options: LobbyMarbleOptions = {}): LobbyMarble {
|
|
581
|
+
const root = new THREE.Group()
|
|
582
|
+
root.name = `lobby-marble:${spec.id}`
|
|
583
|
+
const color = new THREE.Color(spec.color)
|
|
584
|
+
const isToken = spec.materialStyle === 'marble-token'
|
|
585
|
+
const isPackedToken = spec.materialStyle === 'packed-token'
|
|
586
|
+
const isPackedStock = spec.materialStyle === 'packed-stock'
|
|
587
|
+
const supportsEntranceFade = spec.interactive === true || spec.entranceFade === true
|
|
588
|
+
const ownedTextures: THREE.Texture[] = []
|
|
589
|
+
const releaseSharedResources: Array<() => void> = []
|
|
590
|
+
let ready = Promise.resolve()
|
|
591
|
+
|
|
592
|
+
const marbleMaterial = isToken
|
|
593
|
+
? (options.tokenMaterial ??
|
|
594
|
+
new THREE.MeshPhysicalMaterial({
|
|
595
|
+
color: 0xffffff,
|
|
596
|
+
emissive: 0xffffff,
|
|
597
|
+
emissiveIntensity: 0.002,
|
|
598
|
+
metalness: 0,
|
|
599
|
+
roughness: 0.08,
|
|
600
|
+
clearcoat: 1,
|
|
601
|
+
clearcoatRoughness: 0.04,
|
|
602
|
+
transparent: true,
|
|
603
|
+
opacity: 0.14,
|
|
604
|
+
depthWrite: false,
|
|
605
|
+
}))
|
|
606
|
+
: isPackedToken
|
|
607
|
+
? new THREE.MeshPhysicalMaterial({
|
|
608
|
+
color: 0xf4fff0,
|
|
609
|
+
emissive: 0xd8ffd0,
|
|
610
|
+
emissiveIntensity: 0.008,
|
|
611
|
+
metalness: 0,
|
|
612
|
+
roughness: 0.08,
|
|
613
|
+
clearcoat: 0.9,
|
|
614
|
+
clearcoatRoughness: 0.05,
|
|
615
|
+
envMapIntensity: 0.9,
|
|
616
|
+
transparent: true,
|
|
617
|
+
opacity: 0.24,
|
|
618
|
+
depthWrite: false,
|
|
619
|
+
})
|
|
620
|
+
: (() => {
|
|
621
|
+
const textures = acquireVeinTextures(spec, options.includeLogo !== false)
|
|
622
|
+
const { colorTexture, roughnessTexture } = textures
|
|
623
|
+
ready = textures.ready
|
|
624
|
+
releaseSharedResources.push(textures.release)
|
|
625
|
+
return new THREE.MeshPhysicalMaterial({
|
|
626
|
+
color,
|
|
627
|
+
map: colorTexture,
|
|
628
|
+
emissive: color,
|
|
629
|
+
emissiveIntensity: isPackedStock ? 0.002 : 0.018,
|
|
630
|
+
metalness: 0,
|
|
631
|
+
roughness: isPackedStock ? 0.08 : 0.42,
|
|
632
|
+
roughnessMap: roughnessTexture,
|
|
633
|
+
clearcoat: 1,
|
|
634
|
+
clearcoatRoughness: isPackedStock ? 0.036 : 0.032,
|
|
635
|
+
envMapIntensity: 2.25,
|
|
636
|
+
ior: 1.5,
|
|
637
|
+
specularIntensity: 0.82,
|
|
638
|
+
specularColor: new THREE.Color(0xffffff),
|
|
639
|
+
iridescence: 0.025,
|
|
640
|
+
iridescenceIOR: 1.32,
|
|
641
|
+
iridescenceThicknessRange: [90, 145],
|
|
642
|
+
sheen: 0.035,
|
|
643
|
+
sheenColor: color.clone().lerp(new THREE.Color(0xffffff), 0.35),
|
|
644
|
+
sheenRoughness: 0.2,
|
|
645
|
+
// Reserve marbles switch render paths as they orbit the $MRBL lens:
|
|
646
|
+
// rear marbles stay in the opaque pass while foreground marbles
|
|
647
|
+
// become transparent-pass objects and render later.
|
|
648
|
+
transparent: supportsEntranceFade,
|
|
649
|
+
})
|
|
650
|
+
})()
|
|
651
|
+
|
|
652
|
+
const performanceMaterial =
|
|
653
|
+
options.performanceMaterial && marbleMaterial instanceof THREE.MeshPhysicalMaterial && !isToken && !isPackedToken
|
|
654
|
+
? new THREE.MeshStandardMaterial({
|
|
655
|
+
color: marbleMaterial.color.clone(),
|
|
656
|
+
map: marbleMaterial.map,
|
|
657
|
+
emissive: marbleMaterial.emissive.clone(),
|
|
658
|
+
emissiveIntensity: marbleMaterial.emissiveIntensity,
|
|
659
|
+
metalness: marbleMaterial.metalness,
|
|
660
|
+
roughness: marbleMaterial.roughness,
|
|
661
|
+
roughnessMap: marbleMaterial.roughnessMap,
|
|
662
|
+
transparent: supportsEntranceFade,
|
|
663
|
+
})
|
|
664
|
+
: undefined
|
|
665
|
+
const medium = options.geometryDetail === 'medium'
|
|
666
|
+
const marbleGeometry = options.homepageGeometry
|
|
667
|
+
? homepageMarbleGeometry(LOBBY_MARBLE_RADIUS, medium ? 32 : 48, medium ? 24 : 40)
|
|
668
|
+
: medium
|
|
669
|
+
? sharedMediumMarbleGeometry
|
|
670
|
+
: sharedMarbleGeometry
|
|
671
|
+
const marble: THREE.Mesh<THREE.SphereGeometry, THREE.Material> = new THREE.Mesh(marbleGeometry, marbleMaterial)
|
|
672
|
+
marble.position.set(0, -0.12, -0.18)
|
|
673
|
+
marble.scale.setScalar(spec.size ?? 1)
|
|
674
|
+
root.add(marble)
|
|
675
|
+
let inFrontOfLens = false
|
|
676
|
+
|
|
677
|
+
if (isToken) {
|
|
678
|
+
marble.add(createTokenFilament(undefined, true, options.homepageGeometry))
|
|
679
|
+
const bubbleRim = new THREE.Mesh(
|
|
680
|
+
options.homepageGeometry ? homepageMarbleGeometry(LOBBY_TOKEN_OUTER_RADIUS, 40, 32) : sharedBubbleRimGeometry,
|
|
681
|
+
new THREE.ShaderMaterial({
|
|
682
|
+
vertexShader: `
|
|
683
|
+
varying vec3 vViewNormal;
|
|
684
|
+
varying vec3 vViewDirection;
|
|
685
|
+
void main() {
|
|
686
|
+
vec4 viewPosition = modelViewMatrix * vec4(position, 1.0);
|
|
687
|
+
vViewNormal = normalize(normalMatrix * normal);
|
|
688
|
+
vViewDirection = normalize(-viewPosition.xyz);
|
|
689
|
+
gl_Position = projectionMatrix * viewPosition;
|
|
690
|
+
}
|
|
691
|
+
`,
|
|
692
|
+
fragmentShader: `
|
|
693
|
+
varying vec3 vViewNormal;
|
|
694
|
+
varying vec3 vViewDirection;
|
|
695
|
+
void main() {
|
|
696
|
+
float fresnel = pow(
|
|
697
|
+
1.0 - max(dot(vViewNormal, vViewDirection), 0.0),
|
|
698
|
+
3.2
|
|
699
|
+
);
|
|
700
|
+
gl_FragColor = vec4(vec3(0.94, 1.0, 0.98), fresnel * 0.17);
|
|
701
|
+
}
|
|
702
|
+
`,
|
|
703
|
+
blending: THREE.AdditiveBlending,
|
|
704
|
+
transparent: true,
|
|
705
|
+
depthWrite: false,
|
|
706
|
+
}),
|
|
707
|
+
)
|
|
708
|
+
bubbleRim.renderOrder = 6
|
|
709
|
+
marble.add(bubbleRim)
|
|
710
|
+
} else if (isPackedToken) {
|
|
711
|
+
marble.add(createPackedTokenFilament())
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const podium = spec.withPodium ? createPodium(color) : undefined
|
|
715
|
+
if (podium) root.add(podium.group, podium.glow, podium.aura)
|
|
716
|
+
|
|
717
|
+
if (isToken) {
|
|
718
|
+
const glassKey = new THREE.SpotLight(0xf7fffd, 11, 9, 0.28, 0.92, 1.4)
|
|
719
|
+
glassKey.position.set(-0.65, 3.8, 2.5)
|
|
720
|
+
glassKey.target = marble
|
|
721
|
+
root.add(glassKey)
|
|
722
|
+
const glassEdge = new THREE.PointLight(0xf4f8ff, 1.8, 2.8, 1.7)
|
|
723
|
+
glassEdge.position.set(0.72, 0.42, 1.15)
|
|
724
|
+
root.add(glassEdge)
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const supportsSelection = spec.interactive === true
|
|
728
|
+
const beamOpacity = { value: 0 }
|
|
729
|
+
const beamTime = { value: 0 }
|
|
730
|
+
const beamMaterial = supportsSelection
|
|
731
|
+
? new THREE.ShaderMaterial({
|
|
732
|
+
uniforms: {
|
|
733
|
+
uColor: { value: color },
|
|
734
|
+
uOpacity: beamOpacity,
|
|
735
|
+
uTime: beamTime,
|
|
736
|
+
},
|
|
737
|
+
vertexShader: `
|
|
738
|
+
varying vec2 vUv;
|
|
739
|
+
void main() {
|
|
740
|
+
vUv = uv;
|
|
741
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
742
|
+
}
|
|
743
|
+
`,
|
|
744
|
+
fragmentShader: `
|
|
745
|
+
uniform vec3 uColor;
|
|
746
|
+
uniform float uOpacity;
|
|
747
|
+
uniform float uTime;
|
|
748
|
+
varying vec2 vUv;
|
|
749
|
+
void main() {
|
|
750
|
+
float horizontal = smoothstep(0.0, 0.25, vUv.x)
|
|
751
|
+
* smoothstep(0.0, 0.25, 1.0 - vUv.x);
|
|
752
|
+
float vertical = smoothstep(0.0, 0.12, vUv.y)
|
|
753
|
+
* smoothstep(0.0, 0.18, 1.0 - vUv.y);
|
|
754
|
+
float center = 1.0 - abs(vUv.x - 0.5) * 2.0;
|
|
755
|
+
float haze = 0.86 + sin(vUv.y * 38.0 + uTime * 0.55) * 0.04;
|
|
756
|
+
float alpha = uOpacity * horizontal * vertical * (0.48 + center * 0.52) * haze;
|
|
757
|
+
vec3 neutralLight = mix(vec3(0.75, 1.0, 0.94), uColor, 0.22);
|
|
758
|
+
gl_FragColor = vec4(neutralLight * 1.25, alpha);
|
|
759
|
+
}
|
|
760
|
+
`,
|
|
761
|
+
blending: THREE.AdditiveBlending,
|
|
762
|
+
depthWrite: false,
|
|
763
|
+
side: THREE.DoubleSide,
|
|
764
|
+
transparent: true,
|
|
765
|
+
})
|
|
766
|
+
: undefined
|
|
767
|
+
const spotlightBeam = beamMaterial
|
|
768
|
+
? new THREE.Mesh(new THREE.PlaneGeometry(1.72, 7.5, 1, 16), beamMaterial)
|
|
769
|
+
: undefined
|
|
770
|
+
if (spotlightBeam) {
|
|
771
|
+
spotlightBeam.visible = false
|
|
772
|
+
spotlightBeam.position.set(0, 2.7, -0.12)
|
|
773
|
+
root.add(spotlightBeam)
|
|
774
|
+
}
|
|
775
|
+
const dustPositions = supportsSelection ? new Float32Array(30 * 3) : undefined
|
|
776
|
+
for (let index = 0; dustPositions && index < 30; index += 1) {
|
|
777
|
+
dustPositions[index * 3] = (Math.random() - 0.5) * 1.3
|
|
778
|
+
dustPositions[index * 3 + 1] = Math.random() * 6.8 - 2.7
|
|
779
|
+
dustPositions[index * 3 + 2] = (Math.random() - 0.5) * 0.7
|
|
780
|
+
}
|
|
781
|
+
const dustGeometry = dustPositions ? new THREE.BufferGeometry() : undefined
|
|
782
|
+
dustGeometry?.setAttribute('position', new THREE.BufferAttribute(dustPositions!, 3))
|
|
783
|
+
const dustMaterial = supportsSelection
|
|
784
|
+
? new THREE.PointsMaterial({
|
|
785
|
+
color,
|
|
786
|
+
size: 0.028,
|
|
787
|
+
blending: THREE.AdditiveBlending,
|
|
788
|
+
transparent: true,
|
|
789
|
+
opacity: 0,
|
|
790
|
+
depthWrite: false,
|
|
791
|
+
})
|
|
792
|
+
: undefined
|
|
793
|
+
const selectionDust = dustGeometry && dustMaterial ? new THREE.Points(dustGeometry, dustMaterial) : undefined
|
|
794
|
+
if (selectionDust) {
|
|
795
|
+
selectionDust.visible = false
|
|
796
|
+
selectionDust.position.set(0, 2.7, 0.1)
|
|
797
|
+
root.add(selectionDust)
|
|
798
|
+
}
|
|
799
|
+
let focused = false
|
|
800
|
+
let backed = false
|
|
801
|
+
let restingY = marble.position.y
|
|
802
|
+
let entranceReveal = 1
|
|
803
|
+
|
|
804
|
+
const applyEntranceReveal = () => {
|
|
805
|
+
if (isToken || !supportsEntranceFade) return
|
|
806
|
+
const reveal = THREE.MathUtils.smoothstep(entranceReveal, 0, 1)
|
|
807
|
+
const fading = reveal < 0.999
|
|
808
|
+
for (const material of performanceMaterial ? [marbleMaterial, performanceMaterial] : [marbleMaterial]) {
|
|
809
|
+
material.opacity = reveal
|
|
810
|
+
material.depthWrite = !fading
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
return {
|
|
815
|
+
root,
|
|
816
|
+
marble,
|
|
817
|
+
ready,
|
|
818
|
+
setInFrontOfLens: value => {
|
|
819
|
+
if (isToken || value === inFrontOfLens) return
|
|
820
|
+
inFrontOfLens = value
|
|
821
|
+
const transparent = value || supportsEntranceFade
|
|
822
|
+
if (marbleMaterial.transparent !== transparent) {
|
|
823
|
+
marbleMaterial.transparent = transparent
|
|
824
|
+
marbleMaterial.needsUpdate = true
|
|
825
|
+
}
|
|
826
|
+
marbleMaterial.opacity = 1
|
|
827
|
+
marbleMaterial.depthWrite = true
|
|
828
|
+
applyEntranceReveal()
|
|
829
|
+
marble.renderOrder = value ? 8 : 0
|
|
830
|
+
},
|
|
831
|
+
setEntranceReveal: progress => {
|
|
832
|
+
entranceReveal = THREE.MathUtils.clamp(progress, 0, 1)
|
|
833
|
+
applyEntranceReveal()
|
|
834
|
+
},
|
|
835
|
+
setPerformanceMode: enabled => {
|
|
836
|
+
if (!performanceMaterial) return
|
|
837
|
+
marble.material = enabled ? performanceMaterial : marbleMaterial
|
|
838
|
+
applyEntranceReveal()
|
|
839
|
+
},
|
|
840
|
+
setPodiumVisible: visible => {
|
|
841
|
+
if (!podium) return
|
|
842
|
+
podium.group.visible = visible
|
|
843
|
+
podium.glow.visible = visible
|
|
844
|
+
podium.aura.visible = visible
|
|
845
|
+
},
|
|
846
|
+
setFocused: value => {
|
|
847
|
+
focused = value
|
|
848
|
+
if (value && spotlightBeam && selectionDust) {
|
|
849
|
+
spotlightBeam.visible = true
|
|
850
|
+
selectionDust.visible = true
|
|
851
|
+
}
|
|
852
|
+
},
|
|
853
|
+
setBacked: value => {
|
|
854
|
+
backed = value
|
|
855
|
+
if (value && spotlightBeam && selectionDust) {
|
|
856
|
+
spotlightBeam.visible = true
|
|
857
|
+
selectionDust.visible = true
|
|
858
|
+
}
|
|
859
|
+
},
|
|
860
|
+
setRestingY: value => {
|
|
861
|
+
restingY = value
|
|
862
|
+
if (!backed) marble.position.y = value
|
|
863
|
+
},
|
|
864
|
+
update: elapsedSeconds => {
|
|
865
|
+
const active = focused || backed
|
|
866
|
+
const targetY = restingY + (backed ? 0.58 + Math.sin(elapsedSeconds * 1.65 + root.id) * 0.06 : 0)
|
|
867
|
+
marble.position.y = THREE.MathUtils.lerp(marble.position.y, targetY, 0.18)
|
|
868
|
+
const targetScale = (spec.size ?? 1) * (backed ? 1.16 : 1)
|
|
869
|
+
marble.scale.setScalar(THREE.MathUtils.lerp(marble.scale.x, targetScale, 0.2))
|
|
870
|
+
if (!isToken) marble.rotation.y = Math.sin(elapsedSeconds * 0.42 + root.id) * 0.22
|
|
871
|
+
if (dustMaterial && spotlightBeam && selectionDust) {
|
|
872
|
+
dustMaterial.opacity = THREE.MathUtils.lerp(dustMaterial.opacity, active ? 0.5 : 0, 0.08)
|
|
873
|
+
spotlightBeam.visible = active || beamOpacity.value > 0.001
|
|
874
|
+
selectionDust.visible = active || dustMaterial.opacity > 0.001
|
|
875
|
+
selectionDust.position.y = 2.7 + Math.sin(elapsedSeconds * 0.35 + root.id) * 0.12
|
|
876
|
+
}
|
|
877
|
+
const activeMarbleMaterial = marble.material
|
|
878
|
+
if (activeMarbleMaterial instanceof THREE.MeshStandardMaterial) {
|
|
879
|
+
activeMarbleMaterial.emissiveIntensity = THREE.MathUtils.lerp(
|
|
880
|
+
activeMarbleMaterial.emissiveIntensity,
|
|
881
|
+
active ? 0.12 : isToken ? 0.008 : 0.065,
|
|
882
|
+
0.09,
|
|
883
|
+
)
|
|
884
|
+
}
|
|
885
|
+
if (beamMaterial) {
|
|
886
|
+
beamOpacity.value = THREE.MathUtils.lerp(beamOpacity.value, active ? 0.068 : 0, 0.08)
|
|
887
|
+
beamTime.value = elapsedSeconds
|
|
888
|
+
}
|
|
889
|
+
if (podium) {
|
|
890
|
+
const glowMaterial = podium.glow.material as THREE.MeshBasicMaterial
|
|
891
|
+
const auraMaterial = podium.aura.material as THREE.MeshBasicMaterial
|
|
892
|
+
glowMaterial.opacity = THREE.MathUtils.lerp(glowMaterial.opacity, active ? 0.48 : 0.08, 0.08)
|
|
893
|
+
auraMaterial.opacity = THREE.MathUtils.lerp(
|
|
894
|
+
auraMaterial.opacity,
|
|
895
|
+
active ? 0.17 + Math.sin(elapsedSeconds * 2.1) * 0.025 : 0.022,
|
|
896
|
+
0.08,
|
|
897
|
+
)
|
|
898
|
+
}
|
|
899
|
+
podium?.rings.forEach((ring, index) => {
|
|
900
|
+
const targetY = active ? 0.235 : 0.05 + index * 0.025
|
|
901
|
+
const targetX = Math.PI / 2 + (active ? -0.18 + Math.sin(elapsedSeconds * 1.15) * 0.025 : 0)
|
|
902
|
+
ring.position.y = THREE.MathUtils.lerp(ring.position.y, targetY, 0.17)
|
|
903
|
+
ring.rotation.x = THREE.MathUtils.lerp(ring.rotation.x, targetX, 0.16)
|
|
904
|
+
ring.rotation.z = active
|
|
905
|
+
? -(elapsedSeconds * (0.34 + index * 0.05)) % (Math.PI * 2)
|
|
906
|
+
: THREE.MathUtils.lerp(ring.rotation.z, 0, 0.16)
|
|
907
|
+
const material = ring.material as THREE.MeshStandardMaterial
|
|
908
|
+
material.color.lerp(active ? color : (ring.userData.inactiveColor as THREE.Color), 0.1)
|
|
909
|
+
material.emissive.lerp(active ? color : (ring.userData.inactiveEmissive as THREE.Color), 0.1)
|
|
910
|
+
material.emissiveIntensity = THREE.MathUtils.lerp(material.emissiveIntensity, active ? 2.1 : 0.14, 0.1)
|
|
911
|
+
})
|
|
912
|
+
},
|
|
913
|
+
dispose: () => {
|
|
914
|
+
const geometries = new Set<THREE.BufferGeometry>()
|
|
915
|
+
const materials = new Set<THREE.Material>()
|
|
916
|
+
root.traverse(object => {
|
|
917
|
+
if (!(object instanceof THREE.Mesh || object instanceof THREE.Points)) return
|
|
918
|
+
if (object.geometry.userData.shared !== true) geometries.add(object.geometry)
|
|
919
|
+
const objectMaterials = Array.isArray(object.material) ? object.material : [object.material]
|
|
920
|
+
objectMaterials.forEach(material => {
|
|
921
|
+
if (material.userData.shared !== true) materials.add(material)
|
|
922
|
+
})
|
|
923
|
+
})
|
|
924
|
+
if (performanceMaterial && performanceMaterial.userData.shared !== true) materials.add(performanceMaterial)
|
|
925
|
+
ownedTextures.forEach(texture => texture.dispose())
|
|
926
|
+
releaseSharedResources.forEach(release => release())
|
|
927
|
+
materials.forEach(material => material.dispose())
|
|
928
|
+
geometries.forEach(geometry => geometry.dispose())
|
|
929
|
+
},
|
|
930
|
+
}
|
|
931
|
+
}
|