@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,257 @@
|
|
|
1
|
+
import RAPIER from '@dimforge/rapier3d-deterministic-compat'
|
|
2
|
+
import type { MarbleSimulationSnapshot, PhysicsCourse } from '@marble.fun/physics-engine'
|
|
3
|
+
import * as THREE from 'three'
|
|
4
|
+
|
|
5
|
+
import { MarbleSphereGeometry } from '../../../three/marble-sphere-geometry'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
import { createLobbyMarble, type LobbyMarble } from '../../lobby/marble-factory'
|
|
9
|
+
import { RACE_STOCK_MARBLES, STOCK_MARBLES } from '../../lobby/lobby-types'
|
|
10
|
+
import { createGlassMarbleMaterial } from './marble-material'
|
|
11
|
+
import { createMarblePhysicsState, type MarblePhysicsState } from './race-physics'
|
|
12
|
+
import type { MarbleAssignment, MarbleTuning } from './race-types'
|
|
13
|
+
import type { TrackCourse } from './track-course'
|
|
14
|
+
import { RigidBodyTransformInterpolator } from './race-transform-interpolation'
|
|
15
|
+
|
|
16
|
+
const TRAIL_POINT_COUNT = 22
|
|
17
|
+
const MARBLE_LABEL_CLEARANCE = 1.12
|
|
18
|
+
|
|
19
|
+
export interface MarbleEntity extends MarblePhysicsState {
|
|
20
|
+
visual: THREE.Group
|
|
21
|
+
visualReady: Promise<void>
|
|
22
|
+
disposeVisual(): void
|
|
23
|
+
label: THREE.Sprite
|
|
24
|
+
trail: THREE.Line
|
|
25
|
+
trailPositions: Float32Array
|
|
26
|
+
transformInterpolator: RigidBodyTransformInterpolator
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function createLabelSprite(assignment: MarbleAssignment): THREE.Sprite {
|
|
30
|
+
const labelText = assignment.label.slice(0, 8).toUpperCase()
|
|
31
|
+
const canvas = document.createElement('canvas')
|
|
32
|
+
canvas.width = 256
|
|
33
|
+
canvas.height = 72
|
|
34
|
+
const context = canvas.getContext('2d')
|
|
35
|
+
|
|
36
|
+
if (!context) {
|
|
37
|
+
throw new Error('Canvas 2D context is unavailable')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
context.font = '700 28px Inter, system-ui, sans-serif'
|
|
41
|
+
const pillWidth = THREE.MathUtils.clamp(Math.ceil(context.measureText(labelText).width + 64), 152, 248)
|
|
42
|
+
const pillX = (canvas.width - pillWidth) / 2
|
|
43
|
+
context.clearRect(0, 0, canvas.width, canvas.height)
|
|
44
|
+
const pillColor = new THREE.Color(assignment.color).multiplyScalar(0.34)
|
|
45
|
+
context.fillStyle = `#${pillColor.getHexString()}`
|
|
46
|
+
context.beginPath()
|
|
47
|
+
context.roundRect(pillX, 7, pillWidth, 58, 29)
|
|
48
|
+
context.fill()
|
|
49
|
+
context.fillStyle = '#f7fafb'
|
|
50
|
+
context.font = '700 28px Inter, system-ui, sans-serif'
|
|
51
|
+
context.textAlign = 'center'
|
|
52
|
+
context.textBaseline = 'middle'
|
|
53
|
+
context.fillText(labelText, canvas.width / 2, 36)
|
|
54
|
+
|
|
55
|
+
const texture = new THREE.CanvasTexture(canvas)
|
|
56
|
+
texture.colorSpace = THREE.SRGBColorSpace
|
|
57
|
+
const material = new THREE.SpriteMaterial({
|
|
58
|
+
map: texture,
|
|
59
|
+
transparent: true,
|
|
60
|
+
depthTest: false,
|
|
61
|
+
depthWrite: false,
|
|
62
|
+
toneMapped: false,
|
|
63
|
+
})
|
|
64
|
+
const sprite = new THREE.Sprite(material)
|
|
65
|
+
sprite.scale.set(2.35, 0.66, 1)
|
|
66
|
+
sprite.renderOrder = 1_000
|
|
67
|
+
|
|
68
|
+
return sprite
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type RaceMarbleVisualStyle = 'default' | 'stock' | 'stock-no-logo'
|
|
72
|
+
|
|
73
|
+
function createDefaultMarbleVisual(assignment: MarbleAssignment, radius: number): THREE.Group {
|
|
74
|
+
const root = new THREE.Group()
|
|
75
|
+
root.name = `${assignment.slotId}-${assignment.label}`
|
|
76
|
+
const shell = new THREE.Mesh(
|
|
77
|
+
new MarbleSphereGeometry(radius, 36, 24),
|
|
78
|
+
createGlassMarbleMaterial(assignment.color, radius),
|
|
79
|
+
)
|
|
80
|
+
shell.castShadow = true
|
|
81
|
+
shell.receiveShadow = true
|
|
82
|
+
root.add(shell)
|
|
83
|
+
|
|
84
|
+
return root
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function createStockMarbleVisual(
|
|
88
|
+
assignment: MarbleAssignment,
|
|
89
|
+
radius: number,
|
|
90
|
+
includeLogo: boolean,
|
|
91
|
+
): { marble: LobbyMarble; visual: THREE.Group } | undefined {
|
|
92
|
+
const slotIndex = Number.parseInt(assignment.slotId.slice(1), 10) - 1
|
|
93
|
+
const spec = RACE_STOCK_MARBLES.find(candidate => candidate.id === assignment.label) ?? STOCK_MARBLES[slotIndex]
|
|
94
|
+
if (!spec) return undefined
|
|
95
|
+
|
|
96
|
+
const marble = createLobbyMarble(
|
|
97
|
+
{
|
|
98
|
+
...spec,
|
|
99
|
+
interactive: false,
|
|
100
|
+
withPodium: false,
|
|
101
|
+
},
|
|
102
|
+
{ includeLogo },
|
|
103
|
+
)
|
|
104
|
+
marble.marble.position.set(0, 0, 0)
|
|
105
|
+
marble.marble.scale.setScalar(1)
|
|
106
|
+
marble.root.scale.setScalar(radius / 0.73)
|
|
107
|
+
marble.root.name = `${assignment.slotId}-${assignment.label}`
|
|
108
|
+
return { marble, visual: marble.root }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function createTrail(color: string): { line: THREE.Line; positions: Float32Array } {
|
|
112
|
+
const positions = new Float32Array(TRAIL_POINT_COUNT * 3)
|
|
113
|
+
const geometry = new THREE.BufferGeometry()
|
|
114
|
+
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3))
|
|
115
|
+
const material = new THREE.LineBasicMaterial({
|
|
116
|
+
color,
|
|
117
|
+
transparent: true,
|
|
118
|
+
opacity: 0.42,
|
|
119
|
+
depthWrite: false,
|
|
120
|
+
})
|
|
121
|
+
const line = new THREE.Line(geometry, material)
|
|
122
|
+
line.frustumCulled = false
|
|
123
|
+
|
|
124
|
+
return { line, positions }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function createMarbleEntity(
|
|
128
|
+
scene: THREE.Object3D,
|
|
129
|
+
world: RAPIER.World,
|
|
130
|
+
course: TrackCourse,
|
|
131
|
+
assignment: MarbleAssignment,
|
|
132
|
+
tuning: MarbleTuning,
|
|
133
|
+
visualStyle: RaceMarbleVisualStyle = 'default',
|
|
134
|
+
): MarbleEntity {
|
|
135
|
+
const physics = createMarblePhysicsState(world, course, assignment, tuning)
|
|
136
|
+
const translation = physics.body.translation()
|
|
137
|
+
const position = new THREE.Vector3(translation.x, translation.y, translation.z)
|
|
138
|
+
const stockVisual =
|
|
139
|
+
visualStyle === 'stock' || visualStyle === 'stock-no-logo'
|
|
140
|
+
? createStockMarbleVisual(assignment, tuning.radius, visualStyle === 'stock')
|
|
141
|
+
: undefined
|
|
142
|
+
const visual = stockVisual?.visual ?? createDefaultMarbleVisual(assignment, tuning.radius)
|
|
143
|
+
const label = createLabelSprite(assignment)
|
|
144
|
+
const { line: trail, positions: trailPositions } = createTrail(assignment.color)
|
|
145
|
+
visual.position.copy(position)
|
|
146
|
+
label.position.copy(position)
|
|
147
|
+
label.position.y += tuning.radius + MARBLE_LABEL_CLEARANCE
|
|
148
|
+
scene.add(trail, visual, label)
|
|
149
|
+
|
|
150
|
+
for (let index = 0; index < TRAIL_POINT_COUNT; index += 1) {
|
|
151
|
+
trailPositions[index * 3] = position.x
|
|
152
|
+
trailPositions[index * 3 + 1] = position.y
|
|
153
|
+
trailPositions[index * 3 + 2] = position.z
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
...physics,
|
|
158
|
+
visual,
|
|
159
|
+
visualReady: stockVisual?.marble.ready ?? Promise.resolve(),
|
|
160
|
+
disposeVisual: () => {
|
|
161
|
+
if (stockVisual) stockVisual.marble.dispose()
|
|
162
|
+
else {
|
|
163
|
+
visual.traverse(object => {
|
|
164
|
+
if (!(object instanceof THREE.Mesh)) return
|
|
165
|
+
object.geometry.dispose()
|
|
166
|
+
const materials = Array.isArray(object.material) ? object.material : [object.material]
|
|
167
|
+
materials.forEach(material => material.dispose())
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
visual.removeFromParent()
|
|
171
|
+
label.material.map?.dispose()
|
|
172
|
+
label.material.dispose()
|
|
173
|
+
label.removeFromParent()
|
|
174
|
+
trail.geometry.dispose()
|
|
175
|
+
const trailMaterials = Array.isArray(trail.material) ? trail.material : [trail.material]
|
|
176
|
+
trailMaterials.forEach(material => material.dispose())
|
|
177
|
+
trail.removeFromParent()
|
|
178
|
+
},
|
|
179
|
+
label,
|
|
180
|
+
trail,
|
|
181
|
+
trailPositions,
|
|
182
|
+
transformInterpolator: new RigidBodyTransformInterpolator(physics.body),
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function captureMarblePhysicsTransform(entity: MarbleEntity): void {
|
|
187
|
+
// Portals are authoritative discontinuities. Snapping render history avoids
|
|
188
|
+
// drawing a one-frame flight across the world between linked endpoints.
|
|
189
|
+
entity.transformInterpolator.capture(entity.body, 2)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Copies an immutable authoritative core snapshot into the runtime's render/query
|
|
194
|
+
* mirror. The mirrored body is never stepped and cannot affect core physics.
|
|
195
|
+
*/
|
|
196
|
+
export function applyMarbleSimulationSnapshot(
|
|
197
|
+
entity: MarbleEntity,
|
|
198
|
+
snapshot: MarbleSimulationSnapshot,
|
|
199
|
+
course: PhysicsCourse,
|
|
200
|
+
captureTransform = true,
|
|
201
|
+
): void {
|
|
202
|
+
entity.body.setTranslation({ x: snapshot.position[0], y: snapshot.position[1], z: snapshot.position[2] }, false)
|
|
203
|
+
entity.body.setRotation(
|
|
204
|
+
{
|
|
205
|
+
x: snapshot.rotation[0],
|
|
206
|
+
y: snapshot.rotation[1],
|
|
207
|
+
z: snapshot.rotation[2],
|
|
208
|
+
w: snapshot.rotation[3],
|
|
209
|
+
},
|
|
210
|
+
false,
|
|
211
|
+
)
|
|
212
|
+
entity.body.setLinvel(
|
|
213
|
+
{
|
|
214
|
+
x: snapshot.linearVelocity[0],
|
|
215
|
+
y: snapshot.linearVelocity[1],
|
|
216
|
+
z: snapshot.linearVelocity[2],
|
|
217
|
+
},
|
|
218
|
+
false,
|
|
219
|
+
)
|
|
220
|
+
entity.body.setAngvel(
|
|
221
|
+
{
|
|
222
|
+
x: snapshot.angularVelocity[0],
|
|
223
|
+
y: snapshot.angularVelocity[1],
|
|
224
|
+
z: snapshot.angularVelocity[2],
|
|
225
|
+
},
|
|
226
|
+
false,
|
|
227
|
+
)
|
|
228
|
+
course.projectPoint(
|
|
229
|
+
{ x: snapshot.position[0], y: snapshot.position[1], z: snapshot.position[2] },
|
|
230
|
+
entity.projection.sampleIndex,
|
|
231
|
+
entity.projection,
|
|
232
|
+
)
|
|
233
|
+
entity.projection.distance = snapshot.route.distance
|
|
234
|
+
course.writePoseAtDistance(snapshot.route.distance, entity.pose)
|
|
235
|
+
entity.progress = snapshot.route.distance
|
|
236
|
+
entity.bestProgress = snapshot.bestProgressMeters
|
|
237
|
+
entity.speed = snapshot.speedMetersPerSecond
|
|
238
|
+
entity.finishedAtTick = snapshot.finishedAtTick
|
|
239
|
+
if (captureTransform) captureMarblePhysicsTransform(entity)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function syncMarbleVisual(entity: MarbleEntity, interpolationAlpha = 1): void {
|
|
243
|
+
entity.transformInterpolator.write(entity.visual, interpolationAlpha)
|
|
244
|
+
entity.label.position.copy(entity.visual.position)
|
|
245
|
+
entity.label.position.y += entity.tuning.radius + MARBLE_LABEL_CLEARANCE
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function updateMarbleTrail(entity: MarbleEntity): void {
|
|
249
|
+
const translation = entity.body.translation()
|
|
250
|
+
entity.trailPositions.copyWithin(0, 3)
|
|
251
|
+
const finalIndex = (TRAIL_POINT_COUNT - 1) * 3
|
|
252
|
+
entity.trailPositions[finalIndex] = translation.x
|
|
253
|
+
entity.trailPositions[finalIndex + 1] = translation.y
|
|
254
|
+
entity.trailPositions[finalIndex + 2] = translation.z
|
|
255
|
+
const attribute = entity.trail.geometry.getAttribute('position')
|
|
256
|
+
attribute.needsUpdate = true
|
|
257
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as THREE from 'three'
|
|
2
|
+
|
|
3
|
+
export function createGlassMarbleMaterial(color: string, radius: number): THREE.MeshPhysicalMaterial {
|
|
4
|
+
const marbleColor = new THREE.Color(color)
|
|
5
|
+
return new THREE.MeshPhysicalMaterial({
|
|
6
|
+
color: marbleColor,
|
|
7
|
+
emissive: marbleColor,
|
|
8
|
+
emissiveIntensity: 0.035,
|
|
9
|
+
metalness: 0,
|
|
10
|
+
roughness: 0.075,
|
|
11
|
+
transmission: 0,
|
|
12
|
+
thickness: radius * 1.4,
|
|
13
|
+
ior: 1.52,
|
|
14
|
+
specularIntensity: 1,
|
|
15
|
+
specularColor: new THREE.Color(0xffffff),
|
|
16
|
+
clearcoat: 1,
|
|
17
|
+
clearcoatRoughness: 0.025,
|
|
18
|
+
envMapIntensity: 1.25,
|
|
19
|
+
})
|
|
20
|
+
}
|