Skip to content

Commit cbbbf57

Browse files
author
Salim Laimeche
committed
feat(demo-maze-3d): replace GLB with procedural low-poly RobotPrefab
RobotPrefab.tsx — fully procedural low-poly robot built from scratch: - Head: cube with visor + glowing cyan eyes - Body: rounded box with chest plate + core light - Arms: segmented cylinders with shoulder joints + hands - Legs: blocky with hip/knee joints + feet - Backpack: vented box - Antenna: rod with pulsing glowing tip - Animations: idle breathing + walking swing - Key glow: octahedron floats above head when key collected Camera adjusted to target spawn point for better framing. Removed robot.glb dependency (no external assets needed). Build passes.
1 parent e504713 commit cbbbf57

4 files changed

Lines changed: 445 additions & 26 deletions

File tree

packages/demo-maze-3d/src/Maze3D.tsx

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { useRef, useMemo } from 'react'
22
import { Canvas, useFrame } from '@react-three/fiber'
3-
import { OrbitControls, Text, useGLTF } from '@react-three/drei'
3+
import { OrbitControls, Text } from '@react-three/drei'
44
import * as THREE from 'three'
55
import { useMazeStore } from './store'
6+
import RobotPrefab from './RobotPrefab'
67
import { MAZE_LAYOUT, CELL_SIZE } from './maze-env'
78

89
function Wall({ x, z }: { x: number; z: number }) {
@@ -87,33 +88,23 @@ function Exit({ x, z }: { x: number; z: number }) {
8788
function Agent() {
8889
const { agentPos, agentAngle, hasKey } = useMazeStore()
8990
const groupRef = useRef<THREE.Group>(null)
90-
const { scene } = useGLTF('./robot.glb')
9191

9292
useFrame(() => {
9393
if (groupRef.current) {
9494
const targetX = agentPos.x * CELL_SIZE
9595
const targetZ = agentPos.z * CELL_SIZE
9696
groupRef.current.position.x += (targetX - groupRef.current.position.x) * 0.2
9797
groupRef.current.position.z += (targetZ - groupRef.current.position.z) * 0.2
98-
// RobotExpressive faces +Z by default; rotate to match maze angle
99-
groupRef.current.rotation.y = -agentAngle + Math.PI
98+
groupRef.current.rotation.y = -agentAngle + Math.PI / 2
10099
}
101100
})
102101

103102
return (
104103
<group
105104
ref={groupRef}
106105
position={[agentPos.x * CELL_SIZE, 0, agentPos.z * CELL_SIZE]}
107-
scale={0.5}
108106
>
109-
<primitive object={scene.clone()} castShadow />
110-
{/* Key glow indicator */}
111-
{hasKey && (
112-
<mesh position={[0, 1.2, 0]}>
113-
<sphereGeometry args={[0.1, 8, 8]} />
114-
<meshStandardMaterial color="#fbbf24" emissive="#f59e0b" emissiveIntensity={2} />
115-
</mesh>
116-
)}
107+
<RobotPrefab hasKey={hasKey} isWalking={true} />
117108
</group>
118109
)
119110
}
@@ -143,18 +134,18 @@ function Scene() {
143134
<Agent />
144135

145136
<OrbitControls
146-
target={[MAZE_LAYOUT[0].length * CELL_SIZE * 0.5, 0, MAZE_LAYOUT.length * CELL_SIZE * 0.5]}
137+
target={[2, 0.5, 2]}
147138
maxPolarAngle={Math.PI / 2.2}
148-
minDistance={5}
149-
maxDistance={25}
139+
minDistance={2}
140+
maxDistance={15}
150141
/>
151142
</>
152143
)
153144
}
154145

155146
export default function Maze3D() {
156147
return (
157-
<Canvas shadows camera={{ position: [5, 8, 12], fov: 50 }}>
148+
<Canvas shadows camera={{ position: [3, 6, 7], fov: 50 }}>
158149
<color attach="background" args={['#020617']} />
159150
<fog attach="fog" args={['#020617', 15, 35]} />
160151
<Scene />
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
import { useRef } from 'react'
2+
import { useFrame } from '@react-three/fiber'
3+
import * as THREE from 'three'
4+
5+
interface RobotPrefabProps {
6+
hasKey?: boolean
7+
isWalking?: boolean
8+
}
9+
10+
/**
11+
* Low-poly robot prefab — built from scratch with Three.js primitives.
12+
* Head: cube with glowing visor
13+
* Body: rounded box with chest plate
14+
* Arms: segmented cylinders with joints
15+
* Legs: blocky with knee joints
16+
* Antenna: rod + glowing tip
17+
* Backpack: vented box
18+
*/
19+
export default function RobotPrefab({ hasKey = false, isWalking = false }: RobotPrefabProps) {
20+
const groupRef = useRef<THREE.Group>(null)
21+
const leftLegRef = useRef<THREE.Group>(null)
22+
const rightLegRef = useRef<THREE.Group>(null)
23+
const leftArmRef = useRef<THREE.Group>(null)
24+
const rightArmRef = useRef<THREE.Group>(null)
25+
const antennaRef = useRef<THREE.Mesh>(null)
26+
27+
useFrame((state) => {
28+
const t = state.clock.elapsedTime
29+
if (isWalking) {
30+
// Walking animation
31+
leftLegRef.current!.rotation.x = Math.sin(t * 8) * 0.4
32+
rightLegRef.current!.rotation.x = Math.sin(t * 8 + Math.PI) * 0.4
33+
leftArmRef.current!.rotation.x = Math.sin(t * 8 + Math.PI) * 0.3
34+
rightArmRef.current!.rotation.x = Math.sin(t * 8) * 0.3
35+
} else {
36+
// Idle breathing
37+
leftLegRef.current!.rotation.x = 0
38+
rightLegRef.current!.rotation.x = 0
39+
leftArmRef.current!.rotation.x = Math.sin(t * 2) * 0.05
40+
rightArmRef.current!.rotation.x = Math.sin(t * 2 + Math.PI) * 0.05
41+
}
42+
// Antenna pulse
43+
if (antennaRef.current) {
44+
const mat = antennaRef.current.material as THREE.MeshStandardMaterial
45+
mat.emissiveIntensity = 1 + Math.sin(t * 4) * 0.5
46+
}
47+
})
48+
49+
const metalDark = '#475569'
50+
const metalLight = '#94a3b8'
51+
const accent = hasKey ? '#fbbf24' : '#3b82f6'
52+
const accentEmissive = hasKey ? '#f59e0b' : '#2563eb'
53+
54+
return (
55+
<group ref={groupRef} scale={0.6}>
56+
{/* === BODY === */}
57+
<mesh position={[0, 0.7, 0]} castShadow>
58+
<boxGeometry args={[0.5, 0.6, 0.35]} />
59+
<meshStandardMaterial color={metalDark} roughness={0.4} metalness={0.6} />
60+
</mesh>
61+
{/* Chest plate */}
62+
<mesh position={[0, 0.75, 0.18]} castShadow>
63+
<boxGeometry args={[0.35, 0.35, 0.05]} />
64+
<meshStandardMaterial color={accent} emissive={accentEmissive} emissiveIntensity={0.3} roughness={0.3} metalness={0.4} />
65+
</mesh>
66+
{/* Chest core light */}
67+
<mesh position={[0, 0.75, 0.21]}>
68+
<circleGeometry args={[0.06, 16]} />
69+
<meshStandardMaterial color="#ffffff" emissive={accentEmissive} emissiveIntensity={2} />
70+
</mesh>
71+
72+
{/* === HEAD === */}
73+
<group position={[0, 1.15, 0]}>
74+
<mesh castShadow>
75+
<boxGeometry args={[0.4, 0.35, 0.4]} />
76+
<meshStandardMaterial color={metalLight} roughness={0.3} metalness={0.5} />
77+
</mesh>
78+
{/* Visor */}
79+
<mesh position={[0, 0.02, 0.2]}>
80+
<boxGeometry args={[0.3, 0.12, 0.03]} />
81+
<meshStandardMaterial color="#0f172a" roughness={0.1} metalness={0.9} />
82+
</mesh>
83+
{/* Eyes */}
84+
<mesh position={[-0.08, 0.02, 0.22]}>
85+
<boxGeometry args={[0.06, 0.06, 0.02]} />
86+
<meshStandardMaterial color="#22d3ee" emissive="#06b6d4" emissiveIntensity={3} />
87+
</mesh>
88+
<mesh position={[0.08, 0.02, 0.22]}>
89+
<boxGeometry args={[0.06, 0.06, 0.02]} />
90+
<meshStandardMaterial color="#22d3ee" emissive="#06b6d4" emissiveIntensity={3} />
91+
</mesh>
92+
{/* Antenna */}
93+
<mesh position={[0.12, 0.22, 0]}>
94+
<cylinderGeometry args={[0.01, 0.01, 0.2, 6]} />
95+
<meshStandardMaterial color={metalDark} />
96+
</mesh>
97+
<mesh ref={antennaRef} position={[0.12, 0.33, 0]}>
98+
<sphereGeometry args={[0.035, 8, 8]} />
99+
<meshStandardMaterial color={accent} emissive={accentEmissive} emissiveIntensity={1} />
100+
</mesh>
101+
</group>
102+
103+
{/* === BACKPACK === */}
104+
<mesh position={[0, 0.75, -0.25]} castShadow>
105+
<boxGeometry args={[0.4, 0.45, 0.15]} />
106+
<meshStandardMaterial color={metalDark} roughness={0.5} metalness={0.5} />
107+
</mesh>
108+
{/* Vents */}
109+
{[-0.1, 0, 0.1].map((x, i) => (
110+
<mesh key={i} position={[x, 0.75, -0.33]}>
111+
<boxGeometry args={[0.06, 0.3, 0.02]} />
112+
<meshStandardMaterial color="#1e293b" />
113+
</mesh>
114+
))}
115+
116+
{/* === LEFT ARM === */}
117+
<group ref={leftArmRef} position={[-0.35, 0.85, 0]}>
118+
{/* Shoulder joint */}
119+
<mesh>
120+
<sphereGeometry args={[0.06, 8, 8]} />
121+
<meshStandardMaterial color={metalLight} />
122+
</mesh>
123+
{/* Upper arm */}
124+
<mesh position={[0, -0.15, 0]} castShadow>
125+
<boxGeometry args={[0.12, 0.25, 0.12]} />
126+
<meshStandardMaterial color={metalDark} />
127+
</mesh>
128+
{/* Forearm */}
129+
<mesh position={[0, -0.35, 0]} castShadow>
130+
<boxGeometry args={[0.1, 0.2, 0.1]} />
131+
<meshStandardMaterial color={metalLight} />
132+
</mesh>
133+
{/* Hand */}
134+
<mesh position={[0, -0.48, 0]}>
135+
<boxGeometry args={[0.08, 0.08, 0.08]} />
136+
<meshStandardMaterial color={accent} emissive={accentEmissive} emissiveIntensity={0.5} />
137+
</mesh>
138+
</group>
139+
140+
{/* === RIGHT ARM === */}
141+
<group ref={rightArmRef} position={[0.35, 0.85, 0]}>
142+
<mesh>
143+
<sphereGeometry args={[0.06, 8, 8]} />
144+
<meshStandardMaterial color={metalLight} />
145+
</mesh>
146+
<mesh position={[0, -0.15, 0]} castShadow>
147+
<boxGeometry args={[0.12, 0.25, 0.12]} />
148+
<meshStandardMaterial color={metalDark} />
149+
</mesh>
150+
<mesh position={[0, -0.35, 0]} castShadow>
151+
<boxGeometry args={[0.1, 0.2, 0.1]} />
152+
<meshStandardMaterial color={metalLight} />
153+
</mesh>
154+
<mesh position={[0, -0.48, 0]}>
155+
<boxGeometry args={[0.08, 0.08, 0.08]} />
156+
<meshStandardMaterial color={accent} emissive={accentEmissive} emissiveIntensity={0.5} />
157+
</mesh>
158+
</group>
159+
160+
{/* === LEFT LEG === */}
161+
<group ref={leftLegRef} position={[-0.12, 0.35, 0]}>
162+
{/* Hip joint */}
163+
<mesh>
164+
<sphereGeometry args={[0.06, 8, 8]} />
165+
<meshStandardMaterial color={metalLight} />
166+
</mesh>
167+
{/* Thigh */}
168+
<mesh position={[0, -0.15, 0]} castShadow>
169+
<boxGeometry args={[0.14, 0.25, 0.14]} />
170+
<meshStandardMaterial color={metalDark} />
171+
</mesh>
172+
{/* Knee */}
173+
<mesh position={[0, -0.3, 0]}>
174+
<sphereGeometry args={[0.05, 8, 8]} />
175+
<meshStandardMaterial color={metalLight} />
176+
</mesh>
177+
{/* Shin */}
178+
<mesh position={[0, -0.42, 0]} castShadow>
179+
<boxGeometry args={[0.12, 0.2, 0.12]} />
180+
<meshStandardMaterial color={metalDark} />
181+
</mesh>
182+
{/* Foot */}
183+
<mesh position={[0, -0.55, 0.04]} castShadow>
184+
<boxGeometry args={[0.14, 0.06, 0.18]} />
185+
<meshStandardMaterial color={metalLight} />
186+
</mesh>
187+
</group>
188+
189+
{/* === RIGHT LEG === */}
190+
<group ref={rightLegRef} position={[0.12, 0.35, 0]}>
191+
<mesh>
192+
<sphereGeometry args={[0.06, 8, 8]} />
193+
<meshStandardMaterial color={metalLight} />
194+
</mesh>
195+
<mesh position={[0, -0.15, 0]} castShadow>
196+
<boxGeometry args={[0.14, 0.25, 0.14]} />
197+
<meshStandardMaterial color={metalDark} />
198+
</mesh>
199+
<mesh position={[0, -0.3, 0]}>
200+
<sphereGeometry args={[0.05, 8, 8]} />
201+
<meshStandardMaterial color={metalLight} />
202+
</mesh>
203+
<mesh position={[0, -0.42, 0]} castShadow>
204+
<boxGeometry args={[0.12, 0.2, 0.12]} />
205+
<meshStandardMaterial color={metalDark} />
206+
</mesh>
207+
<mesh position={[0, -0.55, 0.04]} castShadow>
208+
<boxGeometry args={[0.14, 0.06, 0.18]} />
209+
<meshStandardMaterial color={metalLight} />
210+
</mesh>
211+
</group>
212+
213+
{/* === KEY GLOW (when collected) === */}
214+
{hasKey && (
215+
<mesh position={[0, 1.6, 0]}>
216+
<octahedronGeometry args={[0.08, 0]} />
217+
<meshStandardMaterial color="#fbbf24" emissive="#f59e0b" emissiveIntensity={3} transparent opacity={0.9} />
218+
</mesh>
219+
)}
220+
</group>
221+
)
222+
}

0 commit comments

Comments
 (0)