Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added ui/public/scene3d/car-front.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added ui/public/scene3d/car-glass.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added ui/public/scene3d/car-paint.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added ui/public/scene3d/car-rear.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added ui/public/scene3d/drive-building.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added ui/public/scene3d/drive-city.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added ui/public/scene3d/drive-coast.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added ui/public/scene3d/drive-road.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added ui/public/scene3d/drive-tunnel.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 23 additions & 12 deletions ui/src/features/scene3d/Scene3DStage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { TextureLoader } from 'three'
import { GLTFLoader, type GLTF } from 'three/addons/loaders/GLTFLoader.js'
import { adoptCafeMaps, loadCafeMaps } from './cafeSet.ts'
import { syncDressing } from './dressing.ts'
import { adoptDriveMaps, isDriveDressing, loadDriveMaps } from './driveSet.ts'
import {
applyLight,
catalogFromClips,
Expand Down Expand Up @@ -162,20 +163,30 @@ export const Scene3DStage = forwardRef<Scene3DStageHandle, Props>(function Scene
useEffect(() => {
const world = worldRef.current
if (!world) return
if (document.dressing !== 'cafe') {
world.dressingReady = true
if (document.dressing === 'cafe') {
world.dressingReady = false
syncDressing(world, 'cafe')
let gone = false
void loadCafeMaps().then(maps => {
if (!adoptCafeMaps(maps, () => !gone && worldRef.current === world)) return
syncDressing(world, 'cafe', { cafe: maps })
world.dressingReady = true
})
return () => { gone = true }
}
if (isDriveDressing(document.dressing)) {
world.dressingReady = false
syncDressing(world, document.dressing)
return
let gone = false
void loadDriveMaps().then(maps => {
if (!adoptDriveMaps(maps, () => !gone && worldRef.current === world)) return
syncDressing(world, document.dressing, { drive: maps })
world.dressingReady = true
})
return () => { gone = true }
}
world.dressingReady = false
syncDressing(world, 'cafe')
let gone = false
void loadCafeMaps().then(maps => {
if (!adoptCafeMaps(maps, () => !gone && worldRef.current === world)) return
syncDressing(world, 'cafe', maps)
world.dressingReady = true
})
return () => { gone = true }
world.dressingReady = true
syncDressing(world, document.dressing)
}, [document.dressing])

useEffect(() => {
Expand Down
2 changes: 1 addition & 1 deletion ui/src/features/scene3d/Scene3DWorkspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { applyScene3DTemplate, patchScene3DSlot, SCENE3D_TEMPLATES, type Scene3D
import type { Scene3DCameraFamily, Scene3DClipCatalogEntry, Scene3DDocument, Scene3DLoop, Scene3DSlot } from './types.ts'
import { documentFromWorld3DRequest, listenForWorld3DWorkflow } from './world3dAgent.ts'

const FAMILIES = ['establishment', 'orbit', 'follow', 'pursuit', 'side', 'front', 'product', 'reveal', 'encounter', 'musical'] as const satisfies readonly Scene3DCameraFamily[]
const FAMILIES = ['establishment', 'orbit', 'follow', 'pursuit', 'side', 'front', 'chase', 'hood', 'wing', 'product', 'reveal', 'encounter', 'musical'] as const satisfies readonly Scene3DCameraFamily[]

type Props = {
width: number
Expand Down
14 changes: 14 additions & 0 deletions ui/src/features/scene3d/camera.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ export function cameraLookAtTime(
duration: number,
slots: readonly Scene3DSlot[] = [],
): Vec3 {
if (camera.family === 'chase' || camera.family === 'hood' || camera.family === 'wing') {
return camera.family === 'hood' ? [0, 0.82, -14] : [0, 0.7, -0.55]
}
if (camera.family === 'follow' || camera.family === 'pursuit' || camera.family === 'side' || camera.family === 'front') {
return slotLook(slots, 'subject_1', camera.look)
}
Expand Down Expand Up @@ -98,6 +101,17 @@ export function cameraEyeAtTime(
const musicalTurns = camera.family === 'musical' ? turns * 2 : turns
return orbitEye(look, radius, productHeight, s * musicalTurns * Math.PI * 2)
}
if (camera.family === 'chase') {
const sway = Math.sin(sceneSeconds * 1.35) * 0.16
return [0.28 + sway, 1.18, 5.45]
}
if (camera.family === 'hood') {
const bump = Math.sin(sceneSeconds * 10) * 0.03
return [0, 1.04 + bump, -0.85]
}
if (camera.family === 'wing') {
return [5.15, 0.95, 1.65]
}
if (camera.family === 'front') {
return [look[0], look[1] - 0.08, look[2] + 4.05]
}
Expand Down
74 changes: 74 additions & 0 deletions ui/src/features/scene3d/carMesh.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import {
BoxGeometry,
CylinderGeometry,
Group,
Mesh,
MeshStandardMaterial,
type Texture,
} from 'three'

export type CarMaps = {
paint: Texture | null
glass: Texture | null
front: Texture | null
rear: Texture | null
}

function skin(map: Texture | null, color: number, extra?: { roughness?: number; metalness?: number; emissive?: number }) {
return new MeshStandardMaterial({
map: map ?? undefined,
color: map ? 0xffffff : color,
roughness: extra?.roughness ?? 0.42,
metalness: extra?.metalness ?? 0.55,
emissive: extra?.emissive ?? 0x000000,
emissiveIntensity: extra?.emissive ? 0.85 : 0,
})
}

function wheel(): Mesh {
const mesh = new Mesh(
new CylinderGeometry(0.33, 0.33, 0.22, 14),
new MeshStandardMaterial({ color: 0x111115, roughness: 0.92 }),
)
mesh.rotation.z = Math.PI / 2
return mesh
}

export function buildCar(maps: CarMaps): { root: Group; wheels: Mesh[] } {
const root = new Group()
const paint = skin(maps.paint, 0x8a1518, { metalness: 0.72, roughness: 0.34 })
const glass = skin(maps.glass, 0x151820, { metalness: 0.8, roughness: 0.12 })
const front = skin(maps.front, 0x8a1518, { metalness: 0.5, roughness: 0.4 })
const rear = skin(maps.rear, 0x8a1518, { metalness: 0.5, roughness: 0.4 })
const body = new Mesh(new BoxGeometry(1.86, 0.5, 4.05), [paint, paint, paint, paint, rear, front])
body.position.set(0, 0.46, 0)
root.add(body)
const cabin = new Mesh(new BoxGeometry(1.48, 0.34, 1.52), glass)
cabin.position.set(0, 0.84, 0.38)
root.add(cabin)
const shield = new Mesh(new BoxGeometry(1.42, 0.03, 0.98), glass)
shield.rotation.x = 0.58
shield.position.set(0, 0.76, -0.38)
root.add(shield)
const spoiler = new Mesh(new BoxGeometry(1.5, 0.05, 0.28), paint)
spoiler.position.set(0, 0.78, 1.85)
root.add(spoiler)
const lamp = new MeshStandardMaterial({ color: 0xfff1c8, emissive: 0xffe8a8, emissiveIntensity: 1.2, roughness: 0.25 })
const tail = new MeshStandardMaterial({ color: 0xff2a2a, emissive: 0xff1a1a, emissiveIntensity: 0.9, roughness: 0.35 })
for (const x of [-0.62, 0.62]) {
const head = new Mesh(new BoxGeometry(0.22, 0.1, 0.08), lamp)
head.position.set(x, 0.48, -2.02)
root.add(head)
const back = new Mesh(new BoxGeometry(0.28, 0.1, 0.06), tail)
back.position.set(x, 0.5, 2.02)
root.add(back)
}
const wheels: Mesh[] = []
for (const [x, z] of [[-0.82, -1.22], [0.82, -1.22], [-0.82, 1.22], [0.82, 1.22]]) {
const item = wheel()
item.position.set(x, 0.33, z)
root.add(item)
wheels.push(item)
}
return { root, wheels }
}
1 change: 1 addition & 0 deletions ui/src/features/scene3d/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export function parseScene3DDocument(raw: unknown): Scene3DDocument | null {
? value.templateId as Scene3DTemplateId
: 'two-shot'
const dressing = value.dressing === 'street' || value.dressing === 'space' || value.dressing === 'treadmill' || value.dressing === 'cafe'
|| value.dressing === 'drive-city' || value.dressing === 'drive-coast' || value.dressing === 'drive-tunnel'
? value.dressing
: undefined
return { ...value, slots, templateId, dressing } as Scene3DDocument
Expand Down
21 changes: 18 additions & 3 deletions ui/src/features/scene3d/dressing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
type Object3D,
} from 'three'
import { cafeGroup, type CafeMaps } from './cafeSet.ts'
import { driveGroup, isDriveDressing, type DriveMaps } from './driveSet.ts'
import { clearDrive } from './driveMotion.ts'
import type { GpuWorld } from './gpu.ts'
import type { Scene3DDressing } from './types.ts'

Expand Down Expand Up @@ -60,11 +62,24 @@ function spaceGroup(): Object3D {
return root
}

export function syncDressing(world: GpuWorld, kind: Scene3DDressing | undefined, maps?: CafeMaps) {
export function syncDressing(
world: GpuWorld,
kind: Scene3DDressing | undefined,
maps?: { cafe?: CafeMaps; drive?: DriveMaps },
) {
dropDressing(world)
world.floor.visible = kind !== 'space' && kind !== 'treadmill' && kind !== 'cafe'
clearDrive(world)
world.floor.visible = kind !== 'space' && kind !== 'treadmill' && kind !== 'cafe' && !isDriveDressing(kind)
if (kind === 'street') world.dressing = streetGroup()
if (kind === 'space') world.dressing = spaceGroup()
if (kind === 'cafe') world.dressing = cafeGroup(maps ?? { facade: null, floor: null, back: null })
if (kind === 'cafe') world.dressing = cafeGroup(maps?.cafe ?? { facade: null, floor: null, back: null })
if (isDriveDressing(kind)) {
const built = driveGroup(kind, maps?.drive ?? { paint: null, glass: null, front: null, rear: null, road: null, building: null })
world.dressing = built.root
world.driveWheels = built.wheels
world.driveRoad = built.roadMap
world.driveMovers = built.movers
world.driveSpeed = 0.2
}
if (world.dressing) world.scene.add(world.dressing)
}
27 changes: 27 additions & 0 deletions ui/src/features/scene3d/driveMotion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { wrapUnit } from './backdrop.ts'
import type { GpuWorld } from './gpu.ts'

function wrapSpan(value: number, min: number, max: number) {
const span = max - min
if (span <= 1e-6) return min
return min + wrapUnit((value - min) / span) * span
}

export function clearDrive(world: GpuWorld) {
world.driveWheels = []
world.driveRoad = null
world.driveMovers = []
world.driveSpeed = 0
}

export function paintDrive(world: GpuWorld, sceneSeconds: number, speed: number) {
const pace = Number.isFinite(speed) ? Math.abs(speed) : 0
if (!pace || (!world.driveWheels.length && !world.driveRoad && !world.driveMovers.length)) return
if (world.driveRoad) world.driveRoad.offset.y = wrapUnit(sceneSeconds * pace)
const roll = sceneSeconds * pace * 88
for (const wheel of world.driveWheels) wheel.rotation.x = roll
for (const item of world.driveMovers) {
const base = Number(item.userData.baseZ) || 0
item.position.z = wrapSpan(base + sceneSeconds * pace * 32, -24, 24)
}
}
Loading
Loading