Open World 3D Prototype
by ChromeBuilder441042 lines16.1 KB๐ ๏ธ Three.js (3D graphics)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Open World 3D Prototype</title>
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
overflow: hidden;
background: #000;
font-family: Arial, sans-serif;
}
#hud {
position: fixed;
top: 20px;
left: 20px;
z-index: 10;
color: white;
background: rgba(0,0,0,.45);
padding: 12px 16px;
border-radius: 10px;
backdrop-filter: blur(8px);
}
#speed {
font-size: 22px;
font-weight: bold;
}
#help {
margin-top: 5px;
font-size: 13px;
opacity: .8;
}
#crosshair {
position: fixed;
left: 50%;
top: 50%;
width: 6px;
height: 6px;
transform: translate(-50%, -50%);
border: 1px solid white;
border-radius: 50%;
z-index: 5;
}
</style>
</head>
<body>
<div id="hud">
<div id="speed">0 km/h</div>
<div id="help">
W/S = accelerate/brake ยท A/D = steer ยท Space = handbrake
</div>
</div>
<div id="crosshair"></div>
<script type="module">
import * as THREE from
"https://cdn.jsdelivr.net/npm/three@0.180.0/build/three.module.js";
import { OrbitControls } from
"https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/controls/OrbitControls.js";
/* =========================================================
ENGINE
========================================================= */
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x87b8e6);
scene.fog = new THREE.FogExp2(
0x87b8e6,
0.0022
);
const camera = new THREE.PerspectiveCamera(
70,
innerWidth / innerHeight,
0.1,
2500
);
camera.position.set(0, 8, 15);
const renderer = new THREE.WebGLRenderer({
antialias: true,
powerPreference: "high-performance"
});
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(
Math.min(devicePixelRatio, 2)
);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type =
THREE.PCFSoftShadowMap;
renderer.outputColorSpace =
THREE.SRGBColorSpace;
renderer.toneMapping =
THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.15;
document.body.appendChild(renderer.domElement);
/* =========================================================
LIGHTING
========================================================= */
const sun = new THREE.DirectionalLight(
0xffffff,
4
);
sun.position.set(
200,
300,
150
);
sun.castShadow = true;
sun.shadow.mapSize.width = 2048;
sun.shadow.mapSize.height = 2048;
sun.shadow.camera.left = -500;
sun.shadow.camera.right = 500;
sun.shadow.camera.top = 500;
sun.shadow.camera.bottom = -500;
scene.add(sun);
const ambient = new THREE.HemisphereLight(
0xbfdcff,
0x35402d,
1.5
);
scene.add(ambient);
/* =========================================================
GROUND
========================================================= */
const groundGeometry =
new THREE.PlaneGeometry(
2000,
2000
);
const groundMaterial =
new THREE.MeshStandardMaterial({
color: 0x385b35,
roughness: 1
});
const ground =
new THREE.Mesh(
groundGeometry,
groundMaterial
);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);
/* =========================================================
ROAD SYSTEM
========================================================= */
function createRoad(
x,
z,
width,
depth
) {
const geometry =
new THREE.BoxGeometry(
width,
0.08,
depth
);
const material =
new THREE.MeshStandardMaterial({
color: 0x252525,
roughness: 0.9
});
const road =
new THREE.Mesh(
geometry,
material
);
road.position.set(
x,
0.04,
z
);
road.receiveShadow = true;
scene.add(road);
}
/* Main streets */
for (
let x = -500;
x <= 500;
x += 100
) {
createRoad(
x,
0,
32,
1000
);
}
for (
let z = -500;
z <= 500;
z += 100
) {
createRoad(
0,
z,
1000,
32
);
}
/* =========================================================
BUILDINGS
========================================================= */
function random(min, max) {
return Math.random() *
(max - min) + min;
}
function createBuilding(x, z) {
const width =
random(20, 55);
const depth =
random(20, 55);
const height =
random(15, 110);
const geometry =
new THREE.BoxGeometry(
width,
height,
depth
);
const material =
new THREE.MeshStandardMaterial({
color: new THREE.Color(
random(.15, .5),
random(.15, .5),
random(.15, .5)
),
roughness: .72,
metalness: .05
});
const building =
new THREE.Mesh(
geometry,
material
);
building.position.set(
x,
height / 2,
z
);
building.castShadow = true;
building.receiveShadow = true;
scene.add(building);
/* rooftop */
const roof =
new THREE.Mesh(
new THREE.BoxGeometry(
width * .85,
1,
depth * .85
),
new THREE.MeshStandardMaterial({
color: 0x333333
})
);
roof.position.set(
x,
height + .5,
z
);
roof.castShadow = true;
scene.add(roof);
}
/* City blocks */
for (
let x = -450;
x <= 450;
x += 70
) {
for (
let z = -450;
z <= 450;
z += 70
) {
/*
Leave space around roads.
*/
if (
Math.abs(x % 100) > 30 &&
Math.abs(z % 100) > 30
) {
createBuilding(
x + random(-15, 15),
z + random(-15, 15)
);
}
}
}
/* =========================================================
TREES
========================================================= */
function createTree(x, z) {
const trunk =
new THREE.Mesh(
new THREE.CylinderGeometry(
1.2,
1.5,
8,
8
),
new THREE.MeshStandardMaterial({
color: 0x60402a
})
);
trunk.position.set(
x,
4,
z
);
trunk.castShadow = true;
scene.add(trunk);
const leaves =
new THREE.Mesh(
new THREE.SphereGeometry(
5,
12,
10
),
new THREE.MeshStandardMaterial({
color: 0x246b2a,
roughness: 1
})
);
leaves.position.set(
x,
10,
z
);
leaves.castShadow = true;
scene.add(leaves);
}
for (
let i = 0;
i < 120;
i++
) {
const x =
random(-480, 480);
const z =
random(-480, 480);
/*
Avoid placing too many trees
directly on major roads.
*/
if (
Math.abs(x % 100) > 20 &&
Math.abs(z % 100) > 20
) {
createTree(x, z);
}
}
/* =========================================================
CAR
========================================================= */
const car =
new THREE.Group();
/* Body */
const body =
new THREE.Mesh(
new THREE.BoxGeometry(
4.2,
1.2,
7
),
new THREE.MeshStandardMaterial({
color: 0x8b1010,
metalness: .65,
roughness: .25
})
);
body.position.y = 1.35;
body.castShadow = true;
car.add(body);
/* Cabin */
const cabin =
new THREE.Mesh(
new THREE.BoxGeometry(
3.4,
1.25,
3.3
),
new THREE.MeshPhysicalMaterial({
color: 0x10151b,
metalness: .1,
roughness: .05,
transmission: .05,
transparent: true,
opacity: .9
})
);
cabin.position.set(
0,
2.15,
-.35
);
cabin.castShadow = true;
car.add(cabin);
/* Wheels */
const wheels = [];
function createWheel(x, z) {
const wheel =
new THREE.Mesh(
new THREE.CylinderGeometry(
.85,
.85,
.55,
20
),
new THREE.MeshStandardMaterial({
color: 0x111111,
roughness: .85
})
);
wheel.rotation.z =
Math.PI / 2;
wheel.position.set(
x,
.85,
z
);
wheel.castShadow = true;
car.add(wheel);
wheels.push(wheel);
}
createWheel(-2.1, -2.3);
createWheel(2.1, -2.3);
createWheel(-2.1, 2.3);
createWheel(2.1, 2.3);
car.position.set(
0,
0,
0
);
scene.add(car);
/* =========================================================
VEHICLE PHYSICS
========================================================= */
let speed = 0;
let steering = 0;
const keys = {};
addEventListener(
"keydown",
e => keys[e.code] = true
);
addEventListener(
"keyup",
e => keys[e.code] = false
);
function updateCar(dt) {
const accelerating =
keys["KeyW"];
const braking =
keys["KeyS"];
const handbrake =
keys["Space"];
/* Acceleration */
if (accelerating) {
speed +=
28 * dt;
}
else if (braking) {
speed -=
20 * dt;
}
else {
/*
Natural rolling resistance.
*/
speed *=
Math.pow(.985, dt * 60);
}
/* Speed limits */
speed =
THREE.MathUtils.clamp(
speed,
-12,
70
);
/* Steering */
let targetSteering = 0;
if (keys["KeyA"])
targetSteering = 1;
if (keys["KeyD"])
targetSteering = -1;
steering +=
(
targetSteering -
steering
) * 8 * dt;
/*
Steering becomes more effective
at higher speeds.
*/
const steeringStrength =
Math.min(
Math.abs(speed) / 20,
1
);
car.rotation.y +=
steering *
steeringStrength *
speed *
.012 *
dt *
60;
/* Handbrake */
if (handbrake) {
speed *=
Math.pow(.92, dt * 60);
}
/* Forward movement */
const direction =
new THREE.Vector3(
0,
0,
-1
);
direction.applyQuaternion(
car.quaternion
);
car.position.addScaledVector(
direction,
speed * dt
);
/* Wheel animation */
wheels.forEach(
wheel => {
wheel.rotation.x +=
speed * dt * 2;
}
);
/*
Keep player inside world.
*/
car.position.x =
THREE.MathUtils.clamp(
car.position.x,
-950,
950
);
car.position.z =
THREE.MathUtils.clamp(
car.position.z,
-950,
950
);
document.getElementById(
"speed"
).textContent =
Math.round(
Math.abs(speed) * 3.6
) + " km/h";
}
/* =========================================================
THIRD PERSON CAMERA
========================================================= */
const cameraOffset =
new THREE.Vector3(
0,
6,
13
);
function updateCamera(dt) {
const desired =
cameraOffset.clone();
desired.applyQuaternion(
car.quaternion
);
desired.add(
car.position
);
camera.position.lerp(
desired,
1 - Math.pow(
.001,
dt
)
);
const lookAt =
car.position.clone();
lookAt.y += 1.5;
camera.lookAt(
lookAt
);
}
/* =========================================================
SIMPLE NPC SYSTEM
========================================================= */
const pedestrians = [];
function createPedestrian() {
const person =
new THREE.Group();
const body =
new THREE.Mesh(
new THREE.CapsuleGeometry(
.45,
1.2,
6,
12
),
new THREE.MeshStandardMaterial({
color:
new THREE.Color(
random(.2, .8),
random(.2, .8),
random(.2, .8)
)
})
);
body.position.y = 1.2;
body.castShadow = true;
person.add(body);
person.position.set(
random(-450, 450),
0,
random(-450, 450)
);
person.userData.direction =
Math.random() *
Math.PI * 2;
person.userData.speed =
random(.8, 2.2);
scene.add(person);
pedestrians.push(person);
}
for (
let i = 0;
i < 80;
i++
) {
createPedestrian();
}
function updatePedestrians(dt) {
pedestrians.forEach(
p => {
const direction =
new THREE.Vector3(
Math.sin(
p.userData.direction
),
0,
Math.cos(
p.userData.direction
)
);
p.position.addScaledVector(
direction,
p.userData.speed * dt
);
/*
Occasionally change direction.
*/
if (
Math.random() < .003
) {
p.userData.direction +=
random(-1, 1);
}
/*
Keep NPCs inside world.
*/
if (
Math.abs(p.position.x) > 480 ||
Math.abs(p.position.z) > 480
) {
p.userData.direction +=
Math.PI;
}
}
);
}
/* =========================================================
DAY/NIGHT SYSTEM
========================================================= */
let worldTime = 12;
function updateTime(dt) {
worldTime +=
dt * .15;
if (worldTime >= 24)
worldTime = 0;
const sunAngle =
(worldTime / 24) *
Math.PI * 2;
sun.position.x =
Math.cos(sunAngle) * 400;
sun.position.y =
Math.sin(sunAngle) * 400;
const daylight =
Math.max(
.08,
Math.sin(sunAngle)
);
sun.intensity =
daylight * 4;
ambient.intensity =
.35 + daylight;
/*
Slightly darken the sky at night.
*/
const sky =
new THREE.Color();
sky.setHSL(
.58,
.55,
.25 + daylight * .35
);
scene.background = sky;
scene.fog.color.copy(
sky
);
}
/* =========================================================
GAME LOOP
========================================================= */
const clock =
new THREE.Clock();
function animate() {
requestAnimationFrame(
animate
);
const dt =
Math.min(
clock.getDelta(),
.05
);
updateCar(dt);
updateCamera(dt);
updatePedestrians(dt);
updateTime(dt);
renderer.render(
scene,
camera
);
}
animate();
/* =========================================================
RESIZE
========================================================= */
addEventListener(
"resize",
() => {
camera.aspect =
innerWidth /
innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(
innerWidth,
innerHeight
);
}
);
</script>
</body>
</html>Game Source: Open World 3D Prototype
Creator: ChromeBuilder44
Libraries: three
Complexity: complex (1042 lines, 16.1 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: open-world-3d-prototype-chromebuilder44" to link back to the original. Then publish at arcadelab.ai/publish.