Skip to content

Commit 0d7457c

Browse files
committed
Implement self-driving car simulation with map integration.
1 parent 7abef6d commit 0d7457c

9 files changed

Lines changed: 958 additions & 16 deletions

File tree

client/.env.template

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1-
# client/.env
2-
VITE_API_BASE_URL=http://localhost:8000
3-
VITE_WS_BASE_URL=ws://localhost:8000
1+
# client/.env
2+
VITE_API_BASE_URL=http://localhost:8000
3+
VITE_WS_BASE_URL=ws://localhost:8000
4+
VITE_GOOGLE_MAPS_API_KEY=xxx

client/index.html

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<head>
44
<meta charset="UTF-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6-
<title>Three ML-Agents - Three.js Implementation of Unity ML-Agents</title>
6+
<title>Three ML-Agents</title>
77
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
88
<style>
99
html,
@@ -22,5 +22,9 @@
2222
<body>
2323
<div id="root"></div>
2424
<script type="module" src="/src/main.jsx"></script>
25+
<script
26+
src="https://maps.googleapis.com/maps/api/js?key=VITE_GOOGLE_MAPS_API_KEY&v=weekly"
27+
async
28+
></script>
2529
</body>
2630
</html>

client/package.json

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,18 @@
1616
"@fontsource/geist": "^5.2.6",
1717
"@geist-ui/core": "^2.3.8",
1818
"@geist-ui/icons": "^1.0.2",
19-
"@react-three/drei": "^9.107.0",
20-
"@react-three/fiber": "^8.16.8",
21-
"@react-three/postprocessing": "2.19.1",
22-
"chart.js": "^4.5.0",
23-
"geist-icons": "^1.2.3",
19+
"@googlemaps/three": "^4.0.13",
20+
"@react-three/drei": "^9.88.2",
21+
"@react-three/fiber": "^8.14.2",
22+
"@react-three/postprocessing": "^2.15.11",
23+
"cesium": "^1.111.0",
24+
"3d-tiles-renderer": "^0.3.1",
25+
"@takram/three-geospatial": "^0.2.0",
26+
"chart.js": "^4.4.0",
27+
"geist-icons": "^1.0.0",
2428
"katex": "^0.16.22",
2529
"onnxruntime-web": "^1.17.0",
26-
"react": "^18.3.0",
30+
"react": "^18.2.0",
2731
"react-chartjs-2": "^5.3.0",
2832
"react-dom": "^18.3.0",
2933
"react-katex": "^3.1.0",

client/pnpm-lock.yaml

Lines changed: 407 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/src/App.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import GliderExample from './examples/Glider.jsx';
1515
import MineCraftExample from './examples/MineCraft.jsx';
1616
import FishExample from './examples/Fish.jsx';
1717
import IntersectionExample from './examples/Intersection.jsx';
18+
import SelfDrivingCarExample from './examples/SelfDrivingCar.jsx';
1819

1920
export default function App() {
2021
return (
@@ -40,6 +41,7 @@ export default function App() {
4041
<Route path="/minecraft" element={<MineCraftExample />} />
4142
<Route path="/fish" element={<FishExample />} />
4243
<Route path="/intersection" element={<IntersectionExample />} />
44+
<Route path="/self-driving-car" element={<SelfDrivingCarExample />} />
4345
<Route path="*" element={<Navigate to="/" replace />} />
4446
</Routes>
4547
</BrowserRouter>

client/src/components/Map.jsx

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import React, { useState, useEffect } from 'react';
2+
import { useThree, useFrame } from '@react-three/fiber';
3+
import * as THREE from 'three';
4+
import { TilesRenderer as TilesRendererImpl, GlobeControls as GlobeControlsImpl } from '3d-tiles-renderer';
5+
import {
6+
GLTFExtensionsPlugin,
7+
GoogleCloudAuthPlugin,
8+
TileCompressionPlugin,
9+
TilesFadePlugin,
10+
UpdateOnChangePlugin
11+
} from '3d-tiles-renderer/plugins';
12+
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
13+
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader';
14+
import { Geodetic, PointOfView, radians } from '@takram/three-geospatial';
15+
16+
const API_KEY = import.meta.env.VITE_GOOGLE_MAPS_API_KEY;
17+
18+
const dracoLoader = new DRACOLoader();
19+
dracoLoader.setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.5.6/');
20+
21+
const Globe = ({ onMapLoaded }) => {
22+
const [tiles, setTiles] = useState(null);
23+
const [controls, setControls] = useState(null);
24+
const { scene, camera, gl } = useThree();
25+
26+
const setInitialCameraPosition = (camera) => {
27+
const longitude = -74.0;
28+
const latitude = 40.75;
29+
const altitude = 1500;
30+
const heading = 0;
31+
const pitch = -45;
32+
33+
const pov = new PointOfView(altitude, radians(heading), radians(pitch));
34+
const geodetic = new Geodetic(radians(longitude), radians(latitude));
35+
36+
pov.decompose(
37+
geodetic.toECEF(),
38+
camera.position,
39+
camera.quaternion,
40+
camera.up
41+
);
42+
43+
camera.updateMatrix();
44+
camera.updateMatrixWorld(true);
45+
};
46+
47+
useEffect(() => {
48+
if (!API_KEY) return;
49+
50+
const tilesUrl = `https://tile.googleapis.com/v1/3dtiles/root.json?key=${API_KEY}`;
51+
const tilesRenderer = new TilesRendererImpl(tilesUrl);
52+
53+
tilesRenderer.setCamera(camera);
54+
tilesRenderer.setResolutionFromRenderer(camera, gl);
55+
56+
setInitialCameraPosition(camera);
57+
58+
const globeControls = new GlobeControlsImpl(scene, camera, gl.domElement, tilesRenderer);
59+
globeControls.enableDamping = true;
60+
globeControls.adjustHeight = false;
61+
globeControls.maxAltitude = Math.PI * 0.55;
62+
63+
const enableAdjustHeight = () => {
64+
globeControls.adjustHeight = true;
65+
globeControls.removeEventListener('start', enableAdjustHeight);
66+
};
67+
globeControls.addEventListener('start', enableAdjustHeight);
68+
69+
const gltfLoader = new GLTFLoader(tilesRenderer.manager);
70+
gltfLoader.setDRACOLoader(dracoLoader);
71+
tilesRenderer.manager.addHandler(/\.gltf$/i, gltfLoader);
72+
tilesRenderer.manager.addHandler(/\.glb$/i, gltfLoader);
73+
74+
try {
75+
const authPlugin = new GoogleCloudAuthPlugin({
76+
apiToken: API_KEY,
77+
autoRefreshToken: true
78+
});
79+
tilesRenderer.registerPlugin(authPlugin);
80+
tilesRenderer.registerPlugin(new GLTFExtensionsPlugin());
81+
tilesRenderer.registerPlugin(new TileCompressionPlugin());
82+
tilesRenderer.registerPlugin(new UpdateOnChangePlugin());
83+
tilesRenderer.registerPlugin(new TilesFadePlugin());
84+
} catch (error) {
85+
console.warn('Could not register plugins:', error);
86+
}
87+
88+
scene.add(tilesRenderer.group);
89+
90+
setTiles(tilesRenderer);
91+
setControls(globeControls);
92+
93+
if (onMapLoaded) {
94+
const ecefToLatLng = (x, y, z) => {
95+
const a = 6378137.0;
96+
const f = 1 / 298.257223563;
97+
const e2 = 2 * f - f * f;
98+
99+
const p = Math.sqrt(x * x + y * y);
100+
const theta = Math.atan2(z * a, p * (1 - f) * a);
101+
102+
const lat = Math.atan2(
103+
z + (e2 * (1 - f) / (1 - e2)) * a * Math.pow(Math.sin(theta), 3),
104+
p - e2 * a * Math.pow(Math.cos(theta), 3)
105+
);
106+
107+
const lng = Math.atan2(y, x);
108+
const N = a / Math.sqrt(1 - e2 * Math.sin(lat) * Math.sin(lat));
109+
const alt = p / Math.cos(lat) - N;
110+
111+
return {
112+
lat: lat * 180 / Math.PI,
113+
lng: lng * 180 / Math.PI,
114+
alt: alt
115+
};
116+
};
117+
const latLngToECEF = (lat, lng, alt = 0) => {
118+
const a = 6378137.0;
119+
const f = 1 / 298.257223563;
120+
const e2 = 2 * f - f * f;
121+
122+
const latRad = lat * Math.PI / 180;
123+
const lngRad = lng * Math.PI / 180;
124+
125+
const N = a / Math.sqrt(1 - e2 * Math.sin(latRad) * Math.sin(latRad));
126+
127+
const x = (N + alt) * Math.cos(latRad) * Math.cos(lngRad);
128+
const y = (N + alt) * Math.cos(latRad) * Math.sin(lngRad);
129+
const z = (N * (1 - e2) + alt) * Math.sin(latRad);
130+
131+
return new THREE.Vector3(x, y, z);
132+
};
133+
onMapLoaded({ latLngToECEF, ecefToLatLng });
134+
}
135+
136+
return () => {
137+
scene.remove(tilesRenderer.group);
138+
tilesRenderer.dispose();
139+
globeControls.dispose();
140+
};
141+
}, [API_KEY, scene, camera, gl]);
142+
143+
useFrame(() => {
144+
if (tiles) {
145+
tiles.update();
146+
}
147+
if (controls) {
148+
controls.update();
149+
}
150+
});
151+
152+
return null;
153+
};
154+
155+
const Map = ({ onMapLoaded }) => {
156+
return <Globe onMapLoaded={onMapLoaded} />;
157+
}
158+
159+
export default Map;

client/src/components/Roads.jsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import React, { useState, useEffect } from 'react';
2+
import { Line } from '@react-three/drei';
3+
import * as THREE from 'three';
4+
5+
const Roads = ({ roadNetwork, coordinateTransformer }) => {
6+
const [lines, setLines] = useState([]);
7+
8+
useEffect(() => {
9+
if (!roadNetwork || !coordinateTransformer) return;
10+
11+
const newLines = roadNetwork.map((road, i) => {
12+
const points = road.map(p => {
13+
const [lat, lng] = p;
14+
return coordinateTransformer.latLngToECEF(lat, lng);
15+
});
16+
return (
17+
<Line
18+
key={i}
19+
points={points}
20+
color="white"
21+
lineWidth={2}
22+
/>
23+
);
24+
});
25+
setLines(newLines);
26+
}, [roadNetwork, coordinateTransformer]);
27+
28+
return <group>{lines}</group>;
29+
};
30+
31+
export default Roads;

client/src/examples/Index.jsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -711,6 +711,55 @@ export default function ExamplesIndex() {
711711
</Card.Footer>
712712
</Card>
713713
</Grid>
714+
<Grid xs={24} sm={16} md={12} lg={8}>
715+
<Card
716+
hoverable
717+
style={{
718+
width: '100%',
719+
backgroundColor: 'rgba(17, 17, 17, 0.8)',
720+
boxShadow: '0 0 20px rgba(0, 255, 255, 0.2)',
721+
backdropFilter: 'blur(10px)',
722+
transition: 'all 0.3s ease',
723+
overflow: 'hidden'
724+
}}
725+
>
726+
<Card.Content style={{ padding: 0 }}>
727+
<Link to="/self-driving-car" style={{ textDecoration: 'none', color: 'inherit' }}>
728+
<div style={{ cursor: 'pointer' }}>
729+
<img
730+
src="/three-mlagents/basic_example.jpg"
731+
alt="Self-Driving Car Example"
732+
style={{
733+
width: '100%',
734+
height: '200px',
735+
objectFit: 'cover',
736+
display: 'block'
737+
}}
738+
/>
739+
</div>
740+
</Link>
741+
</Card.Content>
742+
<Card.Footer style={{ backgroundColor: 'rgba(17, 17, 17, 0.9)', padding: isXs ? '12px' : '16px', display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
743+
<Link to="/self-driving-car" style={{ textDecoration: 'none', color: 'inherit' }}>
744+
<Text h4 style={{ color: '#fff', margin: '0 0 8px 0', cursor: 'pointer', fontSize: isXs ? '1.1rem' : '1.25rem' }}>
745+
Self-Driving Car
746+
</Text>
747+
</Link>
748+
<Text p style={{ color: '#888', margin: '0 0 16px 0', lineHeight: '1.5', fontSize: isXs ? '0.875rem' : '1rem' }}>
749+
A self-driving car learning to navigate a city.
750+
</Text>
751+
<Link to="/self-driving-car" style={{ textDecoration: 'none' }}>
752+
<Button
753+
type="success"
754+
icon={<Play />}
755+
auto
756+
>
757+
Launch Example
758+
</Button>
759+
</Link>
760+
</Card.Footer>
761+
</Card>
762+
</Grid>
714763
</Grid.Container>
715764
</Page.Content>
716765
</Page>

0 commit comments

Comments
 (0)