Skip to content

Commit c92624a

Browse files
committed
lamborgini example
1 parent 20cf21f commit c92624a

File tree

14 files changed

+37227
-26
lines changed

14 files changed

+37227
-26
lines changed

examples/showcase/app.tsx

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { Environment, Lightformer } from '@react-three/drei'
2+
import { Canvas } from '@react-three/fiber'
3+
import { Effects } from './effect.js'
4+
import { Lamborghini } from './lamborghini.js'
5+
import { RenderText } from './text.js'
6+
7+
export function App() {
8+
return (
9+
<Canvas
10+
gl={{ logarithmicDepthBuffer: true, antialias: false }}
11+
camera={{ position: [300, 0, 0], fov: 90, far: 10000 }}
12+
shadows="soft"
13+
>
14+
<color attach="background" args={['#15151a']} />
15+
<Scene />
16+
</Canvas>
17+
)
18+
}
19+
20+
function Scene() {
21+
return (
22+
<>
23+
<Lamborghini />
24+
<RenderText />
25+
<directionalLight
26+
shadow-camera-far={1000}
27+
shadow-camera-top={1300}
28+
shadow-camera-left={-500}
29+
shadow-camera-bottom={-500}
30+
shadow-camera-right={500}
31+
castShadow
32+
position={[0, 500, 0]}
33+
intensity={0.5}
34+
/>
35+
<mesh
36+
scale={4 / 0.015}
37+
position={[3 / 0.015, -1.161 / 0.015, -1.5 / 0.015]}
38+
rotation={[-Math.PI / 2, 0, Math.PI / 2.5]}
39+
>
40+
<ringGeometry args={[0.9, 1, 4, 1]} />
41+
<meshStandardMaterial color="white" roughness={0.75} />
42+
</mesh>
43+
<mesh
44+
scale={4 / 0.015}
45+
position={[-3 / 0.015, -1.161 / 0.015, -1 / 0.015]}
46+
rotation={[-Math.PI / 2, 0, Math.PI / 2.5]}
47+
>
48+
<ringGeometry args={[0.9, 1, 3, 1]} />
49+
<meshStandardMaterial color="white" roughness={0.75} />
50+
</mesh>
51+
<mesh receiveShadow scale={10000} position-y={-76} rotation={[-Math.PI / 2, 0, 0]}>
52+
<planeGeometry />
53+
<shadowMaterial opacity={0.5} />
54+
</mesh>
55+
{/* We're building a cube-mapped environment declaratively.
56+
Anything you put in here will be filmed (once) by a cubemap-camera
57+
and applied to the scenes environment, and optionally background. */}
58+
<Environment resolution={512}>
59+
<Lightformer intensity={2} rotation-x={Math.PI / 2} position={[0, 4, -9]} scale={[10, 1, 1]} />
60+
<Lightformer intensity={2} rotation-x={Math.PI / 2} position={[0, 4, -6]} scale={[10, 1, 1]} />
61+
<Lightformer intensity={2} rotation-x={Math.PI / 2} position={[0, 4, -3]} scale={[10, 1, 1]} />
62+
<Lightformer intensity={2} rotation-x={Math.PI / 2} position={[0, 4, 0]} scale={[10, 1, 1]} />
63+
<Lightformer intensity={2} rotation-x={Math.PI / 2} position={[0, 4, 3]} scale={[10, 1, 1]} />
64+
<Lightformer intensity={2} rotation-x={Math.PI / 2} position={[0, 4, 6]} scale={[10, 1, 1]} />
65+
<Lightformer intensity={2} rotation-x={Math.PI / 2} position={[0, 4, 9]} scale={[10, 1, 1]} />
66+
<Lightformer intensity={2} rotation-y={Math.PI / 2} position={[-50, 2, 0]} scale={[100, 2, 1]} />
67+
<Lightformer intensity={2} rotation-y={-Math.PI / 2} position={[50, 2, 0]} scale={[100, 2, 1]} />
68+
<Lightformer
69+
form="ring"
70+
color="red"
71+
intensity={10}
72+
scale={2}
73+
position={[10, 5, 10]}
74+
onUpdate={(self) => self.lookAt(0, 0, 0)}
75+
/>
76+
</Environment>
77+
<Effects />
78+
</>
79+
)
80+
}

examples/showcase/effect.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { useLoader, useThree } from '@react-three/fiber'
2+
import { EffectComposer, Bloom, LUT, DepthOfField, Vignette } from '@react-three/postprocessing'
3+
import { LUTCubeLoader } from 'postprocessing'
4+
import { Suspense } from 'react'
5+
import { Texture } from 'three'
6+
import { create } from 'zustand'
7+
8+
export const useDepthOfFieldStore = create(() => ({ focusDistance: 200, focusRange: 0.05 }))
9+
10+
export function Effects() {
11+
const texture = useLoader(LUTCubeLoader, '/F-6800-STD.cube') as Texture
12+
const { focusDistance, focusRange } = useDepthOfFieldStore()
13+
const camera = useThree((s) => s.camera)
14+
return (
15+
<Suspense fallback={null}>
16+
<EffectComposer multisampling={1}>
17+
<DepthOfField
18+
focusDistance={(focusDistance - camera.near) / (camera.far - camera.near)}
19+
focusRange={focusRange}
20+
focalLength={0.02}
21+
bokehScale={5}
22+
/>
23+
<Bloom luminanceThreshold={0.9} mipmapBlur luminanceSmoothing={0} intensity={1} />
24+
<LUT lut={texture} />
25+
<Vignette eskil={false} offset={0.1} darkness={1.1} />
26+
</EffectComposer>
27+
</Suspense>
28+
)
29+
}

examples/showcase/index.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Document</title>
7+
<script async type="module" src="./index.tsx"></script>
8+
</head>
9+
<body style="touch-action: none; margin: 0; position: relative; width: 100dvw; height: 100dvh; overflow: hidden;">
10+
<div id="root" style="position: absolute; inset: 0; display: flex; flex-direction: column;"></div>
11+
</body>
12+
</html>

examples/showcase/index.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { StrictMode } from 'react'
2+
import { createRoot } from 'react-dom/client'
3+
import { App } from './app.js'
4+
5+
createRoot(document.getElementById('root')!).render(
6+
<StrictMode>
7+
<App />
8+
</StrictMode>,
9+
)

examples/showcase/lamborghini.tsx

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import { useGLTF } from '@react-three/drei'
2+
import { applyProps, useThree } from '@react-three/fiber'
3+
import {
4+
action,
5+
doUntil,
6+
lookAt,
7+
mediaFinished,
8+
offsetDistance,
9+
offsetRotation,
10+
parallel,
11+
property,
12+
spring,
13+
springPresets,
14+
timePassed,
15+
transition,
16+
useTimeline,
17+
velocity,
18+
} from '@react-three/timeline'
19+
import { useEffect, useMemo, useRef } from 'react'
20+
import * as THREE from 'three'
21+
import { MeshBVH, StaticGeometryGenerator } from 'three-mesh-bvh'
22+
import { useDepthOfFieldStore } from './effect.js'
23+
import { TextOpacities, TextYOffset } from './text.js'
24+
25+
const NegZ = new THREE.Vector3(0, 0, -1)
26+
const directionHelper = new THREE.Vector3()
27+
const startQuaternionOffset = new THREE.Quaternion().setFromEuler(new THREE.Euler(0, (-20 / 180) * Math.PI, 0))
28+
const endQuaternionoffset = new THREE.Quaternion().setFromEuler(new THREE.Euler(0, (20 / 180) * Math.PI, 0))
29+
30+
/*
31+
Author: Steven Grey (https://sketchfab.com/Steven007)
32+
License: CC-BY-NC-4.0 (http://creativecommons.org/licenses/by-nc/4.0/)
33+
Source: https://sketchfab.com/3d-models/lamborghini-urus-2650599973b649ddb4460ff6c03e4aa2
34+
Title: Lamborghini Urus
35+
*/
36+
export function Lamborghini(props: any) {
37+
const { scene, nodes, materials } = useGLTF('/lambo.glb')
38+
const bvh = useMemo(() => {
39+
const generator = new StaticGeometryGenerator(scene)
40+
return new MeshBVH(generator.generate())
41+
}, [scene])
42+
useEffect(() => scene.traverse((object) => (object.castShadow = true)), [scene])
43+
const camera = useThree((s) => s.camera)
44+
const ref1 = useRef(null)
45+
const ref2 = useRef(null)
46+
useTimeline(
47+
async function* () {
48+
const wheels = ['RR', 'RL', 'FR', 'FL'].map((name) => scene.getObjectByName(name))
49+
const frontWheels = ['FR', 'FL'].map((name) => scene.getObjectByName(name))
50+
while (true) {
51+
camera.position.set(0, 300, 400)
52+
camera.lookAt(new THREE.Vector3(0, -75, 400))
53+
frontWheels.forEach((wheel) => (wheel!.rotation.y = 0))
54+
useDepthOfFieldStore.setState({ focusDistance: 500, focusRange: 1 })
55+
;(camera as THREE.PerspectiveCamera).fov = 45
56+
;(camera as THREE.PerspectiveCamera).updateProjectionMatrix()
57+
TextOpacities.forEach((opacity) => (opacity.value = 0))
58+
TextYOffset.forEach((offset) => (offset.value = 500))
59+
60+
await new Promise((resolve) => window.addEventListener('click', resolve))
61+
const audio = document.createElement('audio')
62+
audio.src = './background.mp3'
63+
document.body.appendChild(audio)
64+
await audio.play()
65+
const boundingBox = new THREE.Box3()
66+
bvh.getBoundingBox(boundingBox)
67+
68+
scene.position.z = -1000
69+
yield* parallel(
70+
'race',
71+
async function* () {
72+
yield* parallel(
73+
'race',
74+
async function* () {
75+
yield* parallel(
76+
'all',
77+
...TextOpacities.map(async function* (opacity, i) {
78+
await timePassed(i * 0.05, 'seconds')
79+
yield* action({
80+
update: [
81+
transition(property(opacity, 'value'), 1, spring(springPresets.wobbly)),
82+
transition(property(TextYOffset[i], 'value'), 0, spring(springPresets.wobbly)),
83+
],
84+
})
85+
}),
86+
)
87+
await timePassed(1, 'seconds')
88+
},
89+
action({
90+
update: [transition(camera.position, [0, 100, 400], velocity(50, 150))],
91+
}),
92+
)
93+
yield* action({
94+
update: [
95+
transition(camera.position, [1000, 0, 0], velocity(200, 200)),
96+
lookAt(camera, scene, spring(springPresets.gentle)),
97+
],
98+
})
99+
},
100+
action({
101+
update: [
102+
transition(
103+
scene.position,
104+
[0, 0, 0],
105+
spring({ ...springPresets.gentle, stiffness: 40, maxVelocity: 250 }),
106+
),
107+
(_, clock) => {
108+
wheels.forEach((wheel) => (wheel!.rotation.x = scene.position.z * Math.PI * 0.0045))
109+
},
110+
],
111+
}),
112+
)
113+
114+
yield* doUntil(mediaFinished(audio), async function* () {
115+
const position = new THREE.Vector3(
116+
Math.random() - 0.5,
117+
Math.random() * 0.6 - 0.1,
118+
Math.random() - 0.5,
119+
).multiplyScalar(1000)
120+
const boundingBoxCenter = boundingBox.getCenter(new THREE.Vector3())
121+
new THREE.Ray(position.clone(), position.clone().negate().add(boundingBoxCenter)).intersectBox(
122+
boundingBox,
123+
position,
124+
)
125+
const point = bvh.closestPointToPoint(position)!.point
126+
const distance = Math.random()
127+
;(camera as THREE.PerspectiveCamera).fov = 25 + 45 * (1 - distance)
128+
;(camera as THREE.PerspectiveCamera).updateProjectionMatrix()
129+
directionHelper.copy(point).negate().add(boundingBoxCenter)
130+
directionHelper.y = Math.min(directionHelper.y, 0)
131+
directionHelper.normalize()
132+
const quaternion = new THREE.Quaternion().setFromUnitVectors(NegZ, directionHelper)
133+
useDepthOfFieldStore.setState({ focusDistance: 50 + distance * 150, focusRange: distance * 0.1 + 0.01 })
134+
const rotateRight = Math.random() > 0.5
135+
frontWheels.forEach((wheel) => (wheel!.rotation.y = Math.random() - 0.5))
136+
yield* action({
137+
update: [
138+
//offsetDistance(ref1.current!, point, 0),
139+
offsetDistance(camera, point, 50 + distance * 500),
140+
offsetRotation(
141+
camera,
142+
point,
143+
quaternion.clone().premultiply(rotateRight ? startQuaternionOffset : endQuaternionoffset),
144+
),
145+
lookAt(camera, point),
146+
],
147+
})
148+
yield* action({
149+
update: [
150+
offsetRotation(
151+
camera,
152+
point,
153+
quaternion.clone().premultiply(rotateRight ? endQuaternionoffset : startQuaternionOffset),
154+
velocity(0.2 + Math.random() * 0.2),
155+
),
156+
lookAt(camera, point),
157+
],
158+
})
159+
})
160+
}
161+
},
162+
[camera],
163+
)
164+
useMemo(() => {
165+
// ⬇⬇⬇ All this is probably better fixed in Blender ...
166+
Object.values(nodes).forEach((node: any) => {
167+
if (node.isMesh) {
168+
// Fix glas, normals look messed up in the original, most likely deformed meshes bc of compression :/
169+
if (node.name.startsWith('glass')) node.geometry.computeVertexNormals()
170+
// Fix logo, too dark
171+
if (node.name === 'silver_001_BreakDiscs_0')
172+
node.material = applyProps(materials.BreakDiscs.clone(), { color: '#ddd' })
173+
}
174+
})
175+
// Fix windows, they have to be inset some more
176+
nodes['glass_003'].scale.setScalar(2.7)
177+
// Fix inner frame, too light
178+
applyProps(materials.FrameBlack, { metalness: 0.75, roughness: 0, color: 'black' })
179+
// Wheels, change color from chrome to black matte
180+
applyProps(materials.Chrome, { metalness: 1, roughness: 0, color: '#333' })
181+
applyProps(materials.BreakDiscs, { metalness: 0.2, roughness: 0.2, color: '#555' })
182+
applyProps(materials.TiresGum, { metalness: 0, roughness: 0.4, color: '#181818' })
183+
applyProps(materials.GreyElements, { metalness: 0, color: '#292929' })
184+
// Make front and tail LEDs emit light
185+
applyProps(materials.emitbrake, { emissiveIntensity: 4.5, toneMapped: false })
186+
applyProps(materials.LightsFrontLed, { emissiveIntensity: 3, toneMapped: false }) // Paint, from yellow to black
187+
;(nodes.yellow_WhiteCar_0 as THREE.Mesh).material = new THREE.MeshPhysicalMaterial({
188+
roughness: 0.3,
189+
metalness: 0.05,
190+
color: '#111',
191+
envMapIntensity: 0.75,
192+
clearcoatRoughness: 0,
193+
clearcoat: 1,
194+
})
195+
}, [nodes, materials])
196+
return (
197+
<>
198+
<primitive object={scene} {...props} />
199+
{/*<mesh ref={ref1} scale={1}>
200+
<sphereGeometry />
201+
<meshBasicMaterial color="red" />
202+
</mesh>
203+
<mesh ref={ref2} scale={1}>
204+
<sphereGeometry />
205+
<meshBasicMaterial color="blue" />
206+
</mesh>*/}
207+
</>
208+
)
209+
}

examples/showcase/package.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"type": "module",
3+
"dependencies": {
4+
"@preact/signals": "^2.3.1",
5+
"@react-three/drei": "10.7.3",
6+
"@react-three/fiber": "^9.2.0",
7+
"@react-three/postprocessing": "3.0.4",
8+
"@react-three/timeline": "workspace:^",
9+
"@react-three/uikit": "^0.8.21",
10+
"@vitejs/plugin-react": "^4.6.0",
11+
"postprocessing": "^6.37.7",
12+
"react": "^19.1.0",
13+
"react-dom": "^19.1.0",
14+
"three": "^0.178.0",
15+
"three-mesh-bvh": "^0.9.1",
16+
"zustand": "^5.0.8"
17+
},
18+
"devDependencies": {
19+
"@types/react": "^19.1.8",
20+
"@types/react-dom": "^19.1.6",
21+
"@types/three": "^0.178.0",
22+
"vite": "^7.0.3"
23+
},
24+
"scripts": {
25+
"dev": "vite"
26+
}
27+
}

0 commit comments

Comments
 (0)