
DJ Toenail
Prompt
CODE a core 3D golf game engine focused on precise ball physics and terrain interaction. This includes realistic bounces, rolls, and friction effects on various surfaces like grass, sand, and rough, all within a 3D environment.
A system prompt was added to support web rendering
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Golf Physics Engine</title>
<style>
body {
margin: 0;
overflow: hidden;
font-family: Arial, sans-serif;
background: #1a5d1a;
}
#gameContainer {
position: relative;
width: 100vw;
height: 100vh;
}
#ui {
position: absolute;
top: 10px;
left: 10px;
color: white;
z-index: 100;
background: rgba(0,0,0,0.5);
padding: 10px;
border-radius: 5px;
}
#controls {
position: absolute;
bottom: 10px;
left: 10px;
color: white;
z-index: 100;
background: rgba(0,0,0,0.5);
padding: 10px;
border-radius: 5px;
}
#powerMeter {
position: absolute;
bottom: 10px;
right: 10px;
width: 30px;
height: 200px;
background: rgba(0,0,0,0.5);
border-radius: 5px;
z-index: 100;
}
#powerFill {
width: 100%;
background: #ff5555;
border-radius: 5px;
height: 0%;
transition: height 0.1s;
}
</style>
</head>
<body>
<div id="gameContainer">
<div id="ui">
<div>Ball Position: <span id="ballPos">0, 0, 0</span></div>
<div>Velocity: <span id="ballVel">0</span></div>
<div>Surface: <span id="surfaceType">Fairway</span></div>
</div>
<div id="controls">
<div>Controls: Hold SPACE for power, release to shoot</div>
<div>Move: WASD | Camera: Mouse Drag</div>
<div>Reset Ball: R</div>
</div>
<div id="powerMeter">
<div id="powerFill"></div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
// Main game variables
let scene, camera, renderer;
let ball, ballBody, physicsWorld;
let terrainMesh;
let isCharging = false;
let powerLevel = 0;
let lastPosition = new THREE.Vector3();
let clock = new THREE.Clock();
// Surface friction coefficients
const SURFACE_FRICTION = {
FAIRWAY: 0.98,
ROUGH: 0.95,
SAND: 0.90,
GREEN: 0.99
};
// Surface bounciness
const SURFACE_BOUNCINESS = {
FAIRWAY: 0.6,
ROUGH: 0.5,
SAND: 0.4,
GREEN: 0.3
};
// Terrain materials
const MATERIALS = {
FAIRWAY: 0x4CAF50,
ROUGH: 0x8BC34A,
SAND: 0xFFC107,
GREEN: 0x2E7D32,
WATER: 0x2196F3,
TEE: 0xCDDC39,
PIN: 0xFF5722
};
// Initialize the game
function init() {
// Create scene
scene = new THREE.Scene();
scene.background = new THREE.Color(0x87CEEB); // Sky blue
// Create camera
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 15, 20);
camera.lookAt(0, 0, 0);
// Create renderer
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
document.getElementById('gameContainer').appendChild(renderer.domElement);
// Create lights
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(20, 30, 20);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 1024;
directionalLight.shadow.mapSize.height = 1024;
scene.add(directionalLight);
// Create terrain
createTerrain();
// Create ball
createBall();
// Add event listeners
window.addEventListener('resize', onWindowResize);
window.addEventListener('keydown', onKeyDown);
window.addEventListener('keyup', onKeyUp);
// Add mouse controls
let isDragging = false;
let previousMousePosition = { x: 0, y: 0 };
renderer.domElement.addEventListener('mousedown', function(e) {
isDragging = true;
});
renderer.domElement.addEventListener('mouseup', function(e) {
isDragging = false;
});
renderer.domElement.addEventListener('mousemove', function(e) {
if (isDragging) {
let deltaMove = {
x: e.offsetX - previousMousePosition.x,
y: e.offsetY - previousMousePosition.y
};
// Rotate camera around target
let spherical = new THREE.Spherical();
spherical.setFromVector3(camera.position);
spherical.theta -= deltaMove.x * 0.01;
spherical.phi += deltaMove.y * 0.01;
spherical.phi = Math.max(0.1, Math.min(Math.PI - 0.1, spherical.phi));
camera.position.setFromSpherical(spherical);
camera.lookAt(0, 0, 0);
}
previousMousePosition = {
x: e.offsetX,
y: e.offsetY
};
});
// Start animation loop
animate();
}
// Create terrain with different surfaces
function createTerrain() {
// Create fairway
const fairwayGeometry = new THREE.PlaneGeometry(50, 50);
const fairwayMaterial = new THREE.MeshLambertMaterial({ color: MATERIALS.FAIRWAY });
const fairway = new THREE.Mesh(fairwayGeometry, fairwayMaterial);
fairway.rotation.x = -Math.PI / 2;
fairway.receiveShadow = true;
scene.add(fairway);
// Create rough areas
const roughGeometry = new THREE.PlaneGeometry(15, 20);
const roughMaterial = new THREE.MeshLambertMaterial({ color: MATERIALS.ROUGH });
const rough1 = new THREE.Mesh(roughGeometry, roughMaterial);
rough1.rotation.x = -Math.PI / 2;
rough1.position.set(-20, 0, 0);
rough1.receiveShadow = true;
scene.add(rough1);
const rough2 = new THREE.Mesh(roughGeometry, roughMaterial);
rough2.rotation.x = -Math.PI / 2;
rough2.position.set(20, 0, 0);
rough2.receiveShadow = true;
scene.add(rough2);
// Create sand trap
const sandGeometry = new THREE.PlaneGeometry(10, 10);
const sandMaterial = new THREE.MeshLambertMaterial({ color: MATERIALS.SAND });
const sandTrap = new THREE.Mesh(sandGeometry, sandMaterial);
sandTrap.rotation.x = -Math.PI / 2;
sandTrap.position.set(0, -0.1, 15);
sandTrap.receiveShadow = true;
scene.add(sandTrap);
// Create green
const greenGeometry = new THREE.PlaneGeometry(12, 12);
const greenMaterial = new THREE.MeshLambertMaterial({ color: MATERIALS.GREEN });
const green = new THREE.Mesh(greenGeometry, greenMaterial);
green.rotation.x = -Math.PI / 2;
green.position.set(0, 0, -15);
green.receiveShadow = true;
scene.add(green);
// Create pin
const pinGeometry = new THREE.CylinderGeometry(0.1, 0.1, 2, 8);
const pinMaterial = new THREE.MeshLambertMaterial({ color: MATERIALS.PIN });
const pin = new THREE.Mesh(pinGeometry, pinMaterial);
pin.position.set(0, 1, -15);
pin.castShadow = true;
scene.add(pin);
// Create tee box
const teeGeometry = new THREE.PlaneGeometry(3, 3);
const teeMaterial = new THREE.MeshLambertMaterial({ color: MATERIALS.TEE });
const teeBox = new THREE.Mesh(teeGeometry, teeMaterial);
teeBox.rotation.x = -Math.PI / 2;
teeBox.position.set(0, 0, 10);
teeBox.receiveShadow = true;
scene.add(teeBox);
}
// Create golf ball
function createBall() {
const ballGeometry = new THREE.SphereGeometry(0.5, 32, 32);
const ballMaterial = new THREE.MeshLambertMaterial({ color: 0xFFFFFF });
ball = new THREE.Mesh(ballGeometry, ballMaterial);
ball.castShadow = true;
scene.add(ball);
// Initial position on tee
ball.position.set(0, 0.5, 10);
lastPosition.copy(ball.position);
}
// Handle window resize
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
// Handle key down events
function onKeyDown(event) {
if (event.code === 'Space' && !isCharging) {
isCharging = true;
powerLevel = 0;
}
if (event.code === 'KeyR') {
resetBall();
}
}
// Handle key up events
function onKeyUp(event) {
if (event.code === 'Space' && isCharging) {
shootBall();
isCharging = false;
powerLevel = 0;
document.getElementById('powerFill').style.height = '0%';
}
}
// Reset ball to tee position
function resetBall() {
ball.position.set(0, 0.5, 10);
lastPosition.copy(ball.position);
ball.userData.velocity = new THREE.Vector3(0, 0, 0);
}
// Shoot ball with current power
function shootBall() {
if (ball.userData.velocity && ball.userData.velocity.length() < 0.1) {
// Convert 2D power to 3D force (shoot toward green)
const force = new THREE.Vector3(0, 0, -1);
force.multiplyScalar(powerLevel * 2);
ball.userData.velocity = force;
}
}
// Update ball physics
function updatePhysics(delta) {
if (!ball.userData.velocity) {
ball.userData.velocity = new THREE.Vector3(0, 0, 0);
return;
}
// Save previous position
lastPosition.copy(ball.position);
// Apply gravity
ball.userData.velocity.y -= 9.8 * delta;
// Update position with velocity
ball.position.add(ball.userData.velocity.clone().multiplyScalar(delta));
// Check if ball is on ground (y <= 0.5)
if (ball.position.y <= 0.5) {
ball.position.y = 0.5;
// Apply surface friction and bounce
handleSurfaceInteraction(delta);
} else {
// Air resistance
ball.userData.velocity.multiplyScalar(0.99);
}
// Update UI
document.getElementById('ballPos').textContent =
`${ball.position.x.toFixed(2)}, ${ball.position.y.toFixed(2)}, ${ball.position.z.toFixed(2)}`;
document.getElementById('ballVel').textContent =
ball.userData.velocity.length().toFixed(2);
}
// Handle surface interaction
function handleSurfaceInteraction(delta) {
let surfaceType = "FAIRWAY";
let friction = SURFACE_FRICTION.FAIRWAY;
let bounciness = SURFACE_BOUNCINESS.FAIRWAY;
// Determine surface type based on position
if (Math.abs(ball.position.x) < 25 && Math.abs(ball.position.z) < 25) {
// Check specific areas
if (Math.abs(ball.position.x) < 6 && Math.abs(ball.position.z + 15) < 6) {
surfaceType = "GREEN";
friction = SURFACE_FRICTION.GREEN;
bounciness = SURFACE_BOUNCINESS.GREEN;
} else if (Math.abs(ball.position.x) < 5 && Math.abs(ball.position.z - 15) < 5) {
surfaceType = "SAND";
friction = SURFACE_FRICTION.SAND;
bounciness = SURFACE_BOUNCINESS.SAND;
} else if ((Math.abs(ball.position.x) > 15 && Math.abs(ball.position.z) < 10) ||
(Math.abs(ball.position.x) < 15 && Math.abs(ball.position.z) > 15)) {
surfaceType = "ROUGH";
friction = SURFACE_FRICTION.ROUGH;
bounciness = SURFACE_BOUNCINESS.ROUGH;
}
}
// Update UI
document.getElementById('surfaceType').textContent = surfaceType;
// Apply bounce when hitting ground
if (ball.userData.velocity.y < 0) {
ball.userData.velocity.y *= -bounciness;
if (Math.abs(ball.userData.velocity.y) < 0.5) {
ball.userData.velocity.y = 0;
}
}
// Apply friction when moving on ground
if (ball.userData.velocity.length() > 0.1) {
ball.userData.velocity.multiplyScalar(friction);
// Stop completely if velocity is very low
if (ball.userData.velocity.length() < 0.1) {
ball.userData.velocity.set(0, 0, 0);
}
}
// Ensure ball stays on ground
ball.position.y = 0.5;
}
// Update power meter
function updatePowerMeter() {
if (isCharging) {
powerLevel = Math.min(powerLevel + 0.02, 1);
document.getElementById('powerFill').style.height = (powerLevel * 100) + '%';
}
}
// Main animation loop
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
// Update physics
updatePhysics(delta);
// Update power meter
updatePowerMeter();
// Update camera to follow ball
camera.lookAt(ball.position);
// Render scene
renderer.render(scene, camera);
}
// Start the game when page loads
window.onload = init;
</script>
</body>
</html>
```
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Minimal 3-D Golf Physics Engine Demo</title>
<style>
html,body{margin:0;height:100%;overflow:hidden;background:#121212;color:#eee;font-family:Arial}
#info{position:absolute;top:10px;left:10px;background:rgba(0,0,0,.6);padding:8px;border-radius:4px;font-size:13px}
a{color:#8cf}
</style>
<!-- three.js CDN -->
<script src="https://cdn.jsdelivr.net/npm/three@0.157/build/three.min.js"></script>
</head>
<body>
<div id="info">
• Left-click on the terrain to hit the ball toward the cursor.<br>
• The code focuses on ball–terrain interaction & material-dependent physics.<br>
• Surfaces: Green (bright), Fairway (medium), Rough (dark), Sand (yellow).<br>
<br>
Engine constants are near the top of the JS section.
</div>
<script>
// ---------- CONSTANTS ----------
const GRAVITY = new THREE.Vector3(0, -9.81, 0);
const TIME_STEP = 1/120; // fixed physics step
const SURFACES = {
green : { restitution:0.40, rollMu:0.015 },
fairway: { restitution:0.55, rollMu:0.030 },
rough : { restitution:0.35, rollMu:0.080 },
sand : { restitution:0.10, rollMu:0.250 }
};
const BALL_RADIUS = 0.02135*5; // scaled up (real is 42.67 mm)
const STOP_EPS = 0.05; // velocity magnitude at which the ball is considered at rest
// ---------- THREE.JS SETUP ----------
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(60, innerWidth/innerHeight, .1, 1000);
const renderer = new THREE.WebGLRenderer({antialias:true});
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);
camera.position.set(0,15,25);
camera.lookAt(0,0,0);
scene.add(new THREE.HemisphereLight(0xffffff,0x444444,1.0));
const dirLight = new THREE.DirectionalLight(0xffffff,0.8);
dirLight.position.set(10,20,15);
scene.add(dirLight);
// ---------- TERRAIN ----------
/* The course is generated procedurally from a height function.
Surface type is decided by (x,z) regions rather than texture lookup, keeping logic easy to inspect. */
const COURSE_SIZE = 80;
const RES = 200;
const terrainGeo = new THREE.PlaneGeometry(COURSE_SIZE, COURSE_SIZE, RES, RES).rotateX(-Math.PI/2);
const positionAttr = terrainGeo.attributes.position;
const colorAttr = new THREE.BufferAttribute(new Float32Array((RES+1)*(RES+1)*3),3);
terrainGeo.setAttribute('color', colorAttr);
function heightField(x,z){
// slight undulation using two frequencies
return 0.6*Math.sin(0.08*x)*Math.cos(0.06*z) + 0.3*Math.sin(0.25*x+0.3)*Math.sin(0.15*z);
}
function surfaceType(x,z){
const r = Math.hypot(x+10,z-5);
if((x> -8 && x<8 && z>10 && z<22)) return 'sand'; // simple bunker rectangle
if(r < 6) return 'green';
if(r < 18) return 'fairway';
return 'rough';
}
// mutate vertices & colors
for(let i=0;i<positionAttr.count;i++){
const vx = positionAttr.getX(i);
const vz = positionAttr.getZ(i);
const h = heightField(vx,vz);
positionAttr.setY(i,h);
const sType = surfaceType(vx,vz);
let color;
switch(sType){
case 'green' : color = new THREE.Color(0x3bbf4c); break;
case 'fairway':color = new THREE.Color(0x2f9342); break;
case 'rough' : color = new THREE.Color(0x1f612c); break;
case 'sand' : color = new THREE.Color(0xd8c370); break;
}
colorAttr.setXYZ(i,color.r,color.g,color.b);
}
terrainGeo.computeVertexNormals();
const terrainMat = new THREE.MeshLambertMaterial({vertexColors:true});
const terrainMesh = new THREE.Mesh(terrainGeo, terrainMat);
scene.add(terrainMesh);
// ---------- BALL ----------
const ballGeom = new THREE.SphereGeometry(BALL_RADIUS,32,32);
const ballMat = new THREE.MeshStandardMaterial({color:0xffffff, roughness:0.3, metalness:0.0});
const ballMesh = new THREE.Mesh(ballGeom, ballMat);
scene.add(ballMesh);
let ball = {
pos : new THREE.Vector3(0, heightField(0,0)+BALL_RADIUS, 0),
vel : new THREE.Vector3(0,0,0)
};
ballMesh.position.copy(ball.pos);
// ---------- UTILITY ----------
function terrainHeightNormal(x,z, outNormal){
// central differences for normal
const eps = 0.05;
const h = heightField(x,z);
const hx = heightField(x+eps,z) - heightField(x-eps,z);
const hz = heightField(x,z+eps) - heightField(x,z-eps);
// normal is (-hx,1,-hz) then normalized
outNormal.set(-hx,2*eps,-hz).normalize(); // scaled y for smoother slopes
return h;
}
// ---------- PHYSICS STEP ----------
function stepPhysics(dt){
// semi-explicit Euler
ball.vel.addScaledVector(GRAVITY, dt);
const nextPos = ball.pos.clone().addScaledVector(ball.vel, dt);
// sample terrain at next position
let n = new THREE.Vector3();
const terrainY = terrainHeightNormal(nextPos.x,nextPos.z,n);
if(nextPos.y - BALL_RADIUS <= terrainY){
// collision detected, clamp position to surface
nextPos.y = terrainY + BALL_RADIUS;
// separate normal & tangential velocity components
const vNormalMag = ball.vel.dot(n);
const vNormal = n.clone().multiplyScalar(vNormalMag);
const vTangent = ball.vel.clone().sub(vNormal);
// select surface parameters
const sType = surfaceType(nextPos.x,nextPos.z);
const surf = SURFACES[sType];
// bounce (if coming downward) with restitution
let newVel = ball.vel.clone();
if(vNormalMag < 0){ // moving into the ground
newVel.sub(n.clone().multiplyScalar((1+surf.restitution)*vNormalMag));
}
// rolling / sliding friction (very simple Coulomb model)
const horizSpeed = vTangent.length();
if(horizSpeed>0){
const frictionAcc = surf.rollMu * GRAVITY.length();
const decel = frictionAcc*dt;
// reduce tangent velocity magnitude
const newMag = Math.max(0, horizSpeed - decel);
newVel.copy(vTangent.clone().setLength(newMag)).add(n.clone().multiplyScalar(newVel.dot(n))); // tangential adjusted + current normal
}
// stop ball if really slow
if(newVel.length() < STOP_EPS) newVel.set(0,0,0);
ball.vel.copy(newVel);
}
ball.pos.copy(nextPos);
ballMesh.position.copy(ball.pos);
}
// ---------- INPUT (simple “click to drive”) ----------
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
window.addEventListener('pointerdown', e=>{
mouse.x = (e.clientX / innerWidth)*2 -1;
mouse.y = -(e.clientY / innerHeight)*2 +1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObject(terrainMesh);
if(intersects.length){
const p = intersects[0].point;
const dir = p.clone().sub(ball.pos);
if(dir.length()>0.5){
// simple power proportional to drag distance
const power = Math.min(dir.length()*2, 50);
dir.setLength(power);
dir.y += power*0.3; // add loft
ball.vel.copy(dir);
}
}
});
// ---------- MAIN LOOP ----------
let accumulator = 0, last = performance.now();
function animate(now){
const delta = (now-last)/1000;
last = now;
accumulator += delta;
while(accumulator >= TIME_STEP){
stepPhysics(TIME_STEP);
accumulator -= TIME_STEP;
}
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
animate(performance.now());
// ---------- RESIZE ----------
window.addEventListener('resize', ()=>{
camera.aspect = innerWidth/innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
</script>
</body>
</html>
```
Key engine points implemented:
1. Procedural height function + analytical normal for exact slope calculations.
2. Region-based surface classification returning parameters for restitution and rolling friction (`SURFACES`).
3. Continuous collision detection with impulse-based bounce using the surface’s restitution.
4. Rolling/sliding friction modeled as deceleration proportional to normal force (μ·g).
5. Fixed-step integrator (120 Hz) for deterministic physics independent of frame rate.
6. Minimal UI: click to shoot, allowing quick experimentation with how ball behavior changes on grass, rough, sand, or green.
The core physics and surface-dependent interactions are self-contained JavaScript; visualization relies only on Three.js.Here is a complete, self-contained 3D golf game engine written in a single HTML file. It uses the **three.js** library for 3D rendering and the **cannon-es.js** library for realistic physics simulation.
### Core Engine Features:
* **3D Environment:** A procedurally generated, non-flat terrain.
* **Realistic Ball Physics:** Simulates gravity, velocity, bounce, and spin.
* **Dynamic Terrain Interaction:** The ball's behavior changes based on the surface it's on:
* **Fairway (Green):** Low friction, the ball rolls far.
* **Rough (Light Green):** Higher friction, slows the ball down faster.
* **Sand Trap (Yellow):** Very high friction and low bounciness, stopping the ball quickly.
* **Player Controls:**
* **Aim:** Use the **A** and **D** keys to aim your shot.
* **Power:** **Hold and release the Spacebar** to set your shot power. A longer hold equals a more powerful shot.
* **Camera:** The camera automatically follows the ball.
This code is fully self-contained. You can save it as an `.html` file and open it in any modern web browser to play.
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Golf Engine</title>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #87CEEB; /* Sky Blue */
font-family: Arial, sans-serif;
color: white;
}
canvas {
display: block;
}
#ui-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none; /* Let clicks pass through to the canvas */
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
}
.info-panel {
background-color: rgba(0, 0, 0, 0.5);
padding: 15px;
border-radius: 10px;
text-align: center;
margin: 20px;
max-width: 400px;
}
.info-panel h2 {
margin-top: 0;
}
#power-bar-container {
width: 80%;
max-width: 500px;
height: 30px;
background-color: rgba(0, 0, 0, 0.5);
border: 2px solid white;
border-radius: 15px;
margin-bottom: 30px;
padding: 3px;
}
#power-bar {
width: 0%;
height: 100%;
background: linear-gradient(90deg, yellow, red);
border-radius: 10px;
transition: width 0.1s ease-out;
}
</style>
</head>
<body>
<div id="ui-container">
<div class="info-panel">
<h2>3D Golf Engine</h2>
<p><strong>Aim:</strong> A / D Keys | <strong>Swing:</strong> Hold & Release SPACEBAR</p>
<p id="status-text">Aim and set your power!</p>
<p>Strokes: <span id="stroke-count">0</span> | Terrain: <span id="terrain-type">Fairway</span></p>
</div>
<div id="power-bar-container">
<div id="power-bar"></div>
</div>
</div>
<!-- ES Module Shims for three.js modules -->
<script async src="https://unpkg.com/es-module-shims@1.8.0/dist/es-module-shims.js"></script>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.163.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.163.0/examples/jsm/",
"cannon-es": "https://unpkg.com/cannon-es@0.20.0/dist/cannon-es.js"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import * as CANNON from 'cannon-es';
import { SimplexNoise } from 'three/addons/math/SimplexNoise.js';
// --- SCENE SETUP ---
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0x87CEEB);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
// --- UI ELEMENTS ---
const powerBar = document.getElementById('power-bar');
const strokeCountUI = document.getElementById('stroke-count');
const terrainTypeUI = document.getElementById('terrain-type');
const statusTextUI = document.getElementById('status-text');
// --- PHYSICS SETUP ---
const physicsWorld = new CANNON.World({
gravity: new CANNON.Vec3(0, -9.82, 0)
});
physicsWorld.broadphase = new CANNON.SAPBroadphase(physicsWorld);
physicsWorld.solver.iterations = 20;
// --- GAME STATE ---
let ballInPlay = false;
let power = 0;
let aimingAngle = 0;
let strokeCount = 0;
const objectsToUpdate = [];
// --- MATERIALS (Physics) ---
// These define how objects interact (friction, bounciness)
const ballMaterial = new CANNON.Material('ball');
const groundMaterial = new CANNON.Material('ground');
// Contact material defines interaction between two materials
const ballGroundContactMaterial = new CANNON.ContactMaterial(
ballMaterial,
groundMaterial,
{
friction: 0.4, // Standard friction
restitution: 0.6, // Bounciness
}
);
physicsWorld.addContactMaterial(ballGroundContactMaterial);
// --- TERRAIN DEFINITION ---
const terrain = {
size: 500,
segments: 100,
heightScale: 20,
types: {
SAND: { height: -2, color: 0xFFFF00, friction: 0.9, restitution: 0.2, damping: 0.9, name: "Sand" },
FAIRWAY: { height: 5, color: 0x008000, friction: 0.4, restitution: 0.6, damping: 0.2, name: "Fairway" },
ROUGH: { height: Infinity, color: 0x006400, friction: 0.7, restitution: 0.5, damping: 0.6, name: "Rough" }
}
};
const heightData = [];
const simplex = new SimplexNoise();
for (let i = 0; i <= terrain.segments; i++) {
heightData.push([]);
for (let j = 0; j <= terrain.segments; j++) {
const x = (i / terrain.segments - 0.5) * 2;
const y = (j / terrain.segments - 0.5) * 2;
// Generate noise-based height
const noise = (simplex.noise(x * 3, y * 3) + 1) * 0.5;
heightData[i].push(noise * terrain.heightScale);
}
}
// --- 3D MESHES & PHYSICS BODIES ---
// Terrain Mesh (three.js)
const terrainGeometry = new THREE.PlaneGeometry(terrain.size, terrain.size, terrain.segments, terrain.segments);
terrainGeometry.rotateX(-Math.PI / 2);
const vertices = terrainGeometry.attributes.position.array;
const colors = [];
for (let i = 0, j = 0, l = vertices.length; i < l; i++, j += 3) {
const height = heightData[Math.floor(i / (terrain.segments + 1))][i % (terrain.segments + 1)];
vertices[j + 1] = height; // Set Y coordinate to height
// Color based on height
let terrainType;
if (height < terrain.types.SAND.height) terrainType = terrain.types.SAND;
else if (height < terrain.types.FAIRWAY.height) terrainType = terrain.types.FAIRWAY;
else terrainType = terrain.types.ROUGH;
const color = new THREE.Color(terrainType.color);
colors.push(color.r, color.g, color.b);
}
terrainGeometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
terrainGeometry.computeVertexNormals();
const terrainMaterial = new THREE.MeshLambertMaterial({ vertexColors: true });
const terrainMesh = new THREE.Mesh(terrainGeometry, terrainMaterial);
terrainMesh.receiveShadow = true;
scene.add(terrainMesh);
// Terrain Body (cannon-es)
const terrainShape = new CANNON.Heightfield(heightData, {
elementSize: terrain.size / terrain.segments
});
const terrainBody = new CANNON.Body({ mass: 0, material: groundMaterial });
terrainBody.addShape(terrainShape);
terrainBody.position.set(-terrain.size * 0.5, 0, terrain.size * 0.5);
terrainBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0); // Match three.js rotation
physicsWorld.addBody(terrainBody);
// Golf Ball
const ballRadius = 0.5;
const ball = {};
// Ball Mesh (three.js)
ball.mesh = new THREE.Mesh(
new THREE.SphereGeometry(ballRadius, 32, 32),
new THREE.MeshStandardMaterial({ color: 0xffffff })
);
ball.mesh.castShadow = true;
ball.mesh.position.set(0, 15, 0); // Start position
scene.add(ball.mesh);
// Ball Body (cannon-es)
ball.body = new CANNON.Body({
mass: 0.046, // kg
shape: new CANNON.Sphere(ballRadius),
material: ballMaterial,
linearDamping: 0.2, // Initial damping
angularDamping: 0.2,
});
ball.body.position.copy(ball.mesh.position);
physicsWorld.addBody(ball.body);
objectsToUpdate.push(ball);
// Aiming Arrow Helper
const arrowDir = new THREE.Vector3(0, 0, -1);
const arrowOrigin = new THREE.Vector3(0, 0, 0);
const arrowHelper = new THREE.ArrowHelper(arrowDir, arrowOrigin, 5, 0xffff00, 2, 1);
scene.add(arrowHelper);
// --- LIGHTING ---
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(100, 100, 50);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
directionalLight.shadow.camera.left = -terrain.size / 2;
directionalLight.shadow.camera.right = terrain.size / 2;
directionalLight.shadow.camera.top = terrain.size / 2;
directionalLight.shadow.camera.bottom = -terrain.size / 2;
scene.add(directionalLight);
// --- EVENT LISTENERS ---
let isSpaceDown = false;
window.addEventListener('keydown', (event) => {
if (ballInPlay) return;
if (event.code === 'Space' && !isSpaceDown) {
isSpaceDown = true;
power = 0;
}
if (event.code === 'KeyA') aimingAngle += 0.05;
if (event.code === 'KeyD') aimingAngle -= 0.05;
});
window.addEventListener('keyup', (event) => {
if (event.code === 'Space' && isSpaceDown) {
isSpaceDown = false;
swing();
}
});
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// --- GAME FUNCTIONS ---
function swing() {
if (power > 0.05) { // Minimum power threshold
ballInPlay = true;
strokeCount++;
strokeCountUI.innerText = strokeCount;
statusTextUI.innerText = "In flight...";
arrowHelper.visible = false;
const forceMagnitude = power * 30; // Max force
const impulse = new CANNON.Vec3(
-Math.sin(aimingAngle) * forceMagnitude,
0,
-Math.cos(aimingAngle) * forceMagnitude
);
// Strike position relative to center of mass to add some spin
const strikePoint = new CANNON.Vec3(0, -ballRadius * 0.8, 0);
ball.body.applyImpulse(impulse, strikePoint);
}
power = 0;
}
function updateTerrainEffects() {
// Find the height of the terrain directly under the ball
const p = ball.body.position;
const x = Math.floor((p.x + terrain.size * 0.5) / terrain.size * terrain.segments);
const z = Math.floor((p.z + terrain.size * 0.5) / terrain.size * terrain.segments);
if (x < 0 || x >= terrain.segments || z < 0 || z >= terrain.segments) return;
const terrainHeight = heightData[x][z];
let currentTerrainType;
if (terrainHeight < terrain.types.SAND.height) currentTerrainType = terrain.types.SAND;
else if (terrainHeight < terrain.types.FAIRWAY.height) currentTerrainType = terrain.types.FAIRWAY;
else currentTerrainType = terrain.types.ROUGH;
// Apply effects based on terrain type. Damping is more effective for roll/drag simulation.
ball.body.linearDamping = currentTerrainType.damping;
ball.body.angularDamping = currentTerrainType.damping;
ballGroundContactMaterial.friction = currentTerrainType.friction;
ballGroundContactMaterial.restitution = currentTerrainType.restitution;
terrainTypeUI.innerText = currentTerrainType.name;
}
function checkBallStopped() {
const sleepThreshold = 0.2; // How slow before we consider it stopped
if (ball.body.velocity.lengthSquared() < sleepThreshold && ball.body.angularVelocity.lengthSquared() < sleepThreshold) {
ball.body.sleep(); // Put physics body to sleep to save performance
ballInPlay = false;
arrowHelper.visible = true;
statusTextUI.innerText = "Aim and set your power!";
}
}
// --- ANIMATION LOOP ---
const clock = new THREE.Clock();
let lastTime;
function animate(time) {
requestAnimationFrame(animate);
const deltaTime = clock.getDelta();
// Handle power bar
if (isSpaceDown) {
power = Math.min(power + deltaTime * 0.8, 1);
}
powerBar.style.width = `${power * 100}%`;
// Step the physics world
physicsWorld.step(1 / 60, deltaTime, 3);
// Sync 3D meshes with physics bodies
for(const object of objectsToUpdate) {
object.mesh.position.copy(object.body.position);
object.mesh.quaternion.copy(object.body.quaternion);
}
// Update Aiming Arrow
if (!ballInPlay) {
arrowHelper.position.copy(ball.mesh.position);
arrowHelper.setDirection(new THREE.Vector3(Math.sin(aimingAngle), 0, Math.cos(aimingAngle)));
}
if (ballInPlay) {
updateTerrainEffects();
checkBallStopped();
}
// Camera follows ball
const cameraOffset = new THREE.Vector3(
Math.sin(aimingAngle) * 20,
10,
Math.cos(aimingAngle) * 20
);
const cameraTarget = ball.mesh.position.clone().add(cameraOffset);
camera.position.lerp(cameraTarget, 0.05);
camera.lookAt(ball.mesh.position);
renderer.render(scene, camera);
}
// Start the engine
animate();
</script>
</body>
</html>
``````html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Golf Physics Engine</title>
<style>
body { margin: 0; overflow: hidden; }
canvas { display: block; }
.info {
position: absolute;
top: 10px;
left: 10px;
color: white;
background: rgba(0,0,0,0.5);
padding: 10px;
font-family: monospace;
pointer-events: none;
}
</style>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/controls/OrbitControls.js"></script>
</head>
<body>
<div class="info">
Click to shoot<br>
Use mouse to orbit, drag to aim<br>
Power: <span id="power">50</span> | Surface: <span id="surface">Grass</span>
</div>
<script>
// Main variables
let scene, camera, renderer, controls;
let ball, terrain;
let ballPhysics = {
position: new THREE.Vector3(0, 5, 0),
velocity: new THREE.Vector3(0, 0, 0),
acceleration: new THREE.Vector3(0, 0, 0),
radius: 0.1,
gravity: new THREE.Vector3(0, -9.8, 0),
damping: 0.98,
bounce: 0.3,
rollingFriction: 0.995
};
// Surface types with different physics properties
const SURFACES = {
GRASS: { friction: 0.02, rolling: 0.05, bounce: 0.3, color: 0x228B22 },
SAND: { friction: 0.1, rolling: 0.3, bounce: 0.1, color: 0xD2B48C },
ROUGH: { friction: 0.08, rolling: 0.1, bounce: 0.4, color: 0x006400 },
CONCRETE: { friction: 0.01, rolling: 0.02, bounce: 0.6, color: 0x888888 }
};
let currentSurface = SURFACES.GRASS;
let isAiming = false;
let aimStartPos = new THREE.Vector2();
let hitPower = 0.5;
let clock = new THREE.Clock();
// Initialize the scene
function init() {
// Create scene
scene = new THREE.Scene();
scene.background = new THREE.Color(0x87CEEB);
// Camera
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(5, 5, 5);
// Renderer
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);
// Controls
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
// Lights
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(10, 20, 10);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
scene.add(directionalLight);
// Create terrain with different surface areas
createTerrain();
// Create ball
createBall();
// Add event listeners
window.addEventListener('resize', onWindowResize);
window.addEventListener('mousedown', onMouseDown);
window.addEventListener('mouseup', onMouseUp);
window.addEventListener('mousemove', onMouseMove);
// Start animation loop
animate();
}
function createTerrain() {
// Create multiple terrain patches with different materials
const groundSize = 20;
const groundGeo = new THREE.PlaneGeometry(groundSize, groundSize, 32, 32);
const groundMat = new THREE.MeshLambertMaterial({ color: SURFACES.GRASS.color });
terrain = new THREE.Mesh(groundGeo, groundMat);
terrain.rotation.x = -Math.PI / 2;
terrain.receiveShadow = true;
scene.add(terrain);
// Add some elevation data to make the terrain uneven
const pos = terrain.geometry.attributes.position;
for (let i = 0; i < pos.count; i++) {
const x = pos.getX(i);
const z = pos.getZ(i);
// Create some hills
let height = Math.sin(x * 0.2) * Math.sin(z * 0.2) * 0.5;
// Add a small depression around the hole
const distFromCenter = Math.sqrt(x * x + z * z);
if (distFromCenter < 2) {
height -= Math.pow(distFromCenter, 2) * 0.1;
}
pos.setY(i, height);
}
terrain.geometry.attributes.position.needsUpdate = true;
terrain.geometry.computeVertexNormals();
// Add a hole
const holeGeo = new THREE.CylinderGeometry(0.3, 0.3, 0.5, 16);
const holeMat = new THREE.MeshLambertMaterial({ color: 0x000000 });
const hole = new THREE.Mesh(holeGeo, holeMat);
hole.position.set(0, -0.2, 0);
hole.receiveShadow = true;
scene.add(hole);
// Add a flag near the hole
const flagPole = new THREE.Mesh(
new THREE.CylinderGeometry(0.02, 0.02, 2, 8),
new THREE.MeshLambertMaterial({ color: 0xFFFFFF })
);
flagPole.position.set(0.8, 0.9, 0.8);
flagPole.castShadow = true;
scene.add(flagPole);
const flag = new THREE.Mesh(
new THREE.TriangleGeometry(
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(0.5, 0.3, 0),
new THREE.Vector3(0, 0.6, 0)
),
new THREE.MeshLambertMaterial({ color: 0xFF0000, side: THREE.DoubleSide })
);
flag.position.set(0.8, 1.8, 0.8);
flag.rotation.x = Math.PI / 6;
flag.castShadow = true;
scene.add(flag);
// Add sand trap
const sandGeo = new THREE.CircleGeometry(2, 32);
const sandMat = new THREE.MeshLambertMaterial({ color: SURFACES.SAND.color });
const sandTrap = new THREE.Mesh(sandGeo, sandMat);
sandTrap.position.set(-5, 0.01, -3);
sandTrap.rotation.x = -Math.PI / 2;
sandTrap.receiveShadow = true;
scene.add(sandTrap);
// Add rough area
const roughGeo = new THREE.CircleGeometry(3, 32);
const roughMat = new THREE.MeshLambertMaterial({ color: SURFACES.ROUGH.color });
const roughArea = new THREE.Mesh(roughGeo, roughMat);
roughArea.position.set(6, 0.01, 2);
roughArea.rotation.x = -Math.PI / 2;
roughArea.receiveShadow = true;
scene.add(roughArea);
}
function createBall() {
const ballGeo = new THREE.SphereGeometry(ballPhysics.radius, 32, 32);
const ballMat = new THREE.MeshLambertMaterial({ color: 0xFFFFFF });
ball = new THREE.Mesh(ballGeo, ballMat);
ball.position.copy(ballPhysics.position);
ball.castShadow = true;
ball.receiveShadow = true;
scene.add(ball);
}
function getTerrainHeight(x, z) {
// Find the height of the terrain at a specific x,z coordinate
const pos = terrain.geometry.attributes.position;
const index = Math.floor((x + 10) / 20 * 33) + Math.floor((z + 10) / 20 * 33) * 33;
if (index >= 0 && index < pos.count) {
return pos.getY(index);
}
// Fall back to simple height calculation
return Math.sin(x * 0.2) * Math.sin(z * 0.2) * 0.5 - Math.pow(x*x + z*z, 0.5) * 0.1;
}
function getSurfaceType(x, z) {
const distFromSand = Math.sqrt((x + 5) * (x + 5) + (z + 3) * (z + 3));
const distFromRough = Math.sqrt((x - 6) * (x - 6) + (z - 2) * (z - 2));
if (distFromSand < 2) {
document.getElementById('surface').textContent = "Sand";
return SURFACES.SAND;
} else if (distFromRough < 3) {
document.getElementById('surface').textContent = "Rough";
return SURFACES.ROUGH;
} else {
document.getElementById('surface').textContent = "Grass";
return SURFACES.GRASS;
}
}
function updateBallPhysics() {
const delta = clock.getDelta();
// Apply gravity
ballPhysics.acceleration.copy(ballPhysics.gravity);
// Detect collision with ground
const heightAtPosition = getTerrainHeight(ball.position.x, ball.position.z);
const groundY = heightAtPosition + ballPhysics.radius;
if (ball.position.y <= groundY) {
// Ball is touching the ground
ball.position.y = groundY;
// Determine surface type and apply appropriate friction
currentSurface = getSurfaceType(ball.position.x, ball.position.z);
// If ball is moving downward, apply bounce
if (ballPhysics.velocity.y < 0) {
ballPhysics.velocity.y = -ballPhysics.velocity.y * currentSurface.bounce;
}
// Apply rolling friction (affects horizontal movement)
ballPhysics.velocity.x *= (1 - currentSurface.rolling);
ballPhysics.velocity.z *= (1 - currentSurface.rolling);
// Don't apply gravity when on ground
ballPhysics.acceleration.y = 0;
} else {
// In air - apply air resistance
ballPhysics.velocity.multiplyScalar(ballPhysics.damping);
}
// Update velocity and position
ballPhysics.velocity.add(ballPhysics.acceleration.clone().multiplyScalar(delta));
ball.position.add(ballPhysics.velocity.clone().multiplyScalar(delta));
// Limit velocity to prevent numerical instability
if (ballPhysics.velocity.length() > 50) {
ballPhysics.velocity.normalize().multiplyScalar(50);
}
// Check if ball is nearly stopped
if (ball.position.y <= groundY + 0.01 &&
ballPhysics.velocity.length() < 0.05 &&
Math.abs(ballPhysics.velocity.y) < 0.05) {
ballPhysics.velocity.set(0, 0, 0);
ball.position.y = groundY;
}
// Update ball object position
ball.position.copy(ballPhysics.position);
}
function shootBall(direction, power) {
// Launch the ball with a given direction and power
const force = power * 10;
ballPhysics.velocity.set(
direction.x * force,
direction.y * force,
direction.z * force
);
}
function onMouseDown(event) {
if (event.button === 0) { // Left mouse button
isAiming = true;
aimStartPos.set(event.clientX, event.clientY);
}
}
function onMouseUp(event) {
if (isAiming && event.button === 0) {
isAiming = false;
const endPos = new THREE.Vector2(event.clientX, event.clientY);
const dragDistance = aimStartPos.distanceTo(endPos);
// Calculate power (0 to 1) based on drag distance
hitPower = Math.min(dragDistance / 200, 1);
document.getElementById('power').textContent = Math.round(hitPower * 100);
// Calculate direction
const direction = new THREE.Vector3(
(endPos.x - aimStartPos.x) / 200,
0.5 + hitPower * 0.5, // Launch angle depends on power
(endPos.y - aimStartPos.y) / 200
);
direction.normalize();
// Reset ball position and shoot
ball.position.set(0, 5, 0);
ballPhysics.position.copy(ball.position);
ballPhysics.velocity.set(0, 0, 0);
shootBall(direction, hitPower);
}
}
function onMouseMove(event) {
if (isAiming) {
const currentPos = new THREE.Vector2(event.clientX, event.clientY);
const dragDistance = aimStartPos.distanceTo(currentPos);
hitPower = Math.min(dragDistance / 200, 1);
document.getElementById('power').textContent = Math.round(hitPower * 100);
}
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
requestAnimationFrame(animate);
// Update physics
updateBallPhysics();
// Update controls
controls.update();
// Render
renderer.render(scene, camera);
}
// Start the application
init();
</script>
</body>
</html>
``````html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>3D Golf Game Engine Demo</title>
<style>
body { margin: 0; overflow: hidden; background: #c2f0c2; }
#gui {
position: absolute; top: 10px; left: 10px; background: rgba(255,255,255,0.9); padding: 12px 20px; border-radius: 9px; font-family: sans-serif; z-index: 2;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
#gui label { font-size: 14px; margin-right: 8px; }
#gui input[type=range] { width: 150px; }
#gui button { padding: 2px 10px; font-size: 14px; }
</style>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@sweetalert2/theme-dark@4/dark.css">
</head>
<body>
<div id="gui">
<label>Power: <span id="powerVal">25</span></label>
<input id="power" type="range" min="5" max="60" step="1" value="25">
<button id="shoot">Shoot!</button><br>
<label>Surface: </label>
<select id="surface">
<option value="grass">Grass</option>
<option value="rough">Rough</option>
<option value="sand">Sand</option>
</select>
</div>
<script src="https://cdn.jsdelivr.net/npm/three@0.153.0/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.153.0/examples/js/controls/OrbitControls.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@sweetalert2/theme-dark@4/dark.js"></script>
<script>
/**
* Core 3D Golf Game Engine: Accurate Ball Physics & Terrain Interaction
* Surfaces: grass, rough, sand
* Supports: bounces, rolls, friction, slope effects. Simple terrain mesh.
*/
// Physics parameters for surfaces
const SURFACES = {
grass: { restitution: 0.52, friction: 0.18, rollFriction: 0.03 },
rough: { restitution: 0.34, friction: 0.32, rollFriction: 0.08 },
sand: { restitution: 0.07, friction: 0.65, rollFriction: 0.16 }
};
const BALL_RADIUS = 0.027; // ~Standard golf ball (meters)
const BALL_MASS = 0.045; // in kg
let surfaceType = 'grass';
let scene, camera, renderer, controls;
let terrainMesh, ballMesh;
let ball, lastFrame;
let animating = false;
let holePos = new THREE.Vector3(10, 0, 0);
let powerSlider = document.getElementById("power");
let powerVal = document.getElementById("powerVal");
let shootBtn = document.getElementById("shoot");
let surfaceSel = document.getElementById("surface");
// Simple "random" hills via perlin-noise-like heights
function terrainHeight(x, z) {
// Combination of sin/cos to make smooth hills
return (
0.45 * Math.sin(x * 0.38) +
0.25 * Math.sin(z * 0.22) +
0.18 * Math.cos(z * 0.45 - x * 0.3)
);
}
function terrainNormal(x, z) {
// Approximate by central differences
const eps = 0.05;
const hL = terrainHeight(x-eps, z);
const hR = terrainHeight(x+eps, z);
const hD = terrainHeight(x, z-eps);
const hU = terrainHeight(x, z+eps);
let n = new THREE.Vector3(hL-hR, 2*eps, hD-hU);
n.normalize();
return n;
}
// Ball physics state
function BallState() {
this.pos = new THREE.Vector3(-7, terrainHeight(-7, 0)+BALL_RADIUS, 0);
this.vel = new THREE.Vector3();
this.rolling = false;
this.resting = false;
}
function resetBall() {
ball = new BallState();
ballMesh.position.copy(ball.pos);
ballMesh.visible = true;
}
// Setup Three.js
function init3D() {
scene = new THREE.Scene();
scene.background = new THREE.Color(0xb2e6b3);
camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.02, 80);
camera.position.set(-7, 2.7, 6);
renderer = new THREE.WebGLRenderer({antialias:true});
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.target.copy(new THREE.Vector3(0, 0.5, 0));
controls.maxPolarAngle = Math.PI/2.2;
controls.minDistance = 2;
controls.maxDistance = 60;
// Lighting
let hemi = new THREE.HemisphereLight(0xe3fafa, 0xb2b657, 0.7);
scene.add(hemi);
let dir = new THREE.DirectionalLight(0xffffff, 1.2);
dir.position.set(-1,2,1.8);
dir.castShadow = false;
scene.add(dir);
// Terrain mesh
let geo = new THREE.PlaneGeometry(23, 11, 60, 30);
for (let ix = 0; ix < geo.attributes.position.count; ++ix) {
let v = geo.attributes.position;
let x = v.getX(ix), z = v.getY(ix);
v.setZ(ix, terrainHeight(x, z));
}
geo.computeVertexNormals();
let tmat = new THREE.MeshLambertMaterial({color:0x7fbf4f, flatShading: false});
terrainMesh = new THREE.Mesh(geo, tmat);
terrainMesh.rotation.x = -Math.PI/2;
terrainMesh.receiveShadow = true;
scene.add(terrainMesh);
// Hole
let holeG = new THREE.CylinderBufferGeometry(0.06, 0.06, 0.01, 22);
let holeM = new THREE.MeshStandardMaterial({ color: 0x1a0000, metalness:0.8, roughness:0.87 });
let holeMesh = new THREE.Mesh(holeG, holeM);
holeMesh.position.copy(holePos);
holeMesh.position.y = terrainHeight(holePos.x, holePos.z) + 0.003;
scene.add(holeMesh);
// Ball
let ballG = new THREE.SphereBufferGeometry(BALL_RADIUS, 32,18);
let ballM = new THREE.MeshPhysicalMaterial({ color:0xf7fff7,
roughness:0.23, metalness:0.2, clearcoat: 0.4, reflectivity: 0.45 });
ballMesh = new THREE.Mesh(ballG, ballM);
ballMesh.castShadow = true;
scene.add(ballMesh);
// Start positions
resetBall();
window.addEventListener('resize', onWindowResize, false);
}
function onWindowResize() {
camera.aspect = window.innerWidth/window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
// Ball physics core
function updateBallPhysics(dt) {
if (ball.resting) return;
// Gravity
let g = 9.81;
// Current surface property
let s = SURFACES[surfaceType];
// Ball local surface state
let yTerrain = terrainHeight(ball.pos.x, ball.pos.z);
let normal = terrainNormal(ball.pos.x, ball.pos.z);
if (ball.pos.y <= yTerrain + BALL_RADIUS + 0.002) {
// Contact with terrain
let vNorm = normal.dot(ball.vel); // component into the ground normal
let vTangent = ball.vel.clone().sub(normal.clone().multiplyScalar(vNorm));
if (Math.abs(ball.vel.y) > 0.3) { // High velocity: bounce
// Elastic collision
let bounceV = -vNorm * s.restitution;
// Friction loss on tangent
let tangentFriction = 1-s.friction;
ball.vel = normal.clone().multiplyScalar(bounceV)
.add(vTangent.multiplyScalar(tangentFriction));
} else {
// Consider rolling
ball.rolling = true;
// Apply gravity along slope
let slope = normal.clone().multiplyScalar(-1);
slope.y = 0;
let slopeLen = slope.length();
if (slopeLen > 0.0005) {
slope.normalize();
let downSlope = Math.max(-normal.y, 0.08);
ball.vel.add( slope.multiplyScalar(g * downSlope * dt) );
}
// Rolling friction
let vLen = ball.vel.length();
if (vLen > 0.03) {
let rollDecel = s.rollFriction * g * dt;
ball.vel.add( ball.vel.clone().multiplyScalar(-rollDecel/vLen) );
} else {
ball.vel.set(0,0,0);
ball.resting = true;
}
ball.vel.y = 0;
ball.pos.y = yTerrain + BALL_RADIUS + 0.002;
}
} else {
// Air phase
ball.rolling = false;
ball.vel.y -= g * dt;
}
ball.pos.add(ball.vel.clone().multiplyScalar(dt));
}
function updateBallMesh() {
ballMesh.position.copy(ball.pos);
}
// Game loop
function animate(ts) {
let now = ts*0.001;
let dt = Math.min(now-(lastFrame||now), 0.035);
lastFrame=now;
if (animating) {
for (let i=0;i<3;i++) updateBallPhysics(dt/3); // stable physics step
updateBallMesh();
// Check for hole
let holeY = terrainHeight(holePos.x, holePos.z);
let dFromHole = new THREE.Vector2(ball.pos.x-holePos.x, ball.pos.z-holePos.z).length();
if (!ball.resting && dFromHole < 0.07 && Math.abs(ball.pos.y - holeY)<0.04 && ball.vel.length()<1.2) {
ball.resting = true;
ballMesh.position.set(holePos.x, holeY + 0.006, holePos.z);
Swal.fire({
icon: 'success',
title: 'Ball in the hole!',
text: 'Congratulations! Click "Shoot!" to try again.',
background: '#205225',
color:'#fff'
});
animating = false;
shootBtn.disabled = false;
}
if (ball.resting && dFromHole >= 0.07) {
animating = false;
shootBtn.disabled = false;
}
}
controls.update();
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
// UI interaction
powerSlider.oninput = function() {
powerVal.textContent = powerSlider.value;
};
surfaceSel.onchange = function() {
surfaceType = surfaceSel.value;
// Tint terrain based on current surface selected
let color = (surfaceType=='grass') ? 0x7fbf4f : (surfaceType=='rough') ? 0xc7b95d : 0xe1d0ad;
terrainMesh.material.color.set(color);
};
shootBtn.onclick = function() {
// Shoot the ball in direction from ball to hole
if (animating) return;
resetBall();
let aimVec = new THREE.Vector3(holePos.x-ball.pos.x, 0, holePos.z-ball.pos.z).normalize();
let pow = parseFloat(powerSlider.value)*0.23;
// Elevate slightly
let up = 0.3+0.04*(60-parseFloat(powerSlider.value))/60;
// Add some upward kick so ball arcs & bounces
let dir = aimVec.clone().multiplyScalar(pow).add( new THREE.Vector3(0, pow*up, 0) );
ball.vel.copy(dir);
animating = true;
shootBtn.disabled = true;
}
init3D();
animate();
</script>
</body>
</html>
``````html
<!DOCTYPE html>
<html>
<head>
<title>3D Golf Physics Engine</title>
<style>body { margin: 0; overflow: hidden; }</style>
</head>
<body>
<script src="https://cdn.jsdelivr.net/npm/three@0.160.1/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.160.1/examples/js/controls/OrbitControls.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/cannon.js/0.6.2/cannon.min.js"></script>
<script>
(function main() {
// THREEJS SCENE SETUP
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const controls = new THREE.OrbitControls(camera, renderer.domElement);
camera.position.set(0, 15, 30);
controls.update();
// LIGHTS
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
dirLight.position.set(10, 20, 10);
scene.add(dirLight);
// CANNON PHYSICS WORLD
const world = new CANNON.World();
world.gravity.set(0, -9.82, 0);
world.broadphase = new CANNON.Narrowphase();
// PHYSICAL MATERIALS
const ballMat = new CANNON.Material();
const grassMat = new CANNON.Material();
const sandMat = new CANNON.Material();
const roughMat = new CANNON.Material();
// CONTACT MATERIALS
world.addContactMaterial(new CANNON.ContactMaterial(grassMat, ballMat, {
friction: 0.8, restitution: 0.2
}));
world.addContactMaterial(new CANNON.ContactMaterial(sandMat, ballMat, {
friction: 0.5, restitution: 0.1
}));
world.addContactMaterial(new CANNON.ContactMaterial(roughMat, ballMat, {
friction: 0.9, restitution: 0.15
}));
// MULTIPLE TERRAIN TYPES
const terrainSize = { x: 10, y: 0.1, z: 10 };
const makeTerrain = (x, physicsMaterial, threeColor) => {
// Cannon physics
const physShape = new CANNON.Box(new CANNON.Vec3(
terrainSize.x, terrainSize.y, terrainSize.z
));
const physBody = new CANNON.Body({ mass: 0, material: physicsMaterial });
physBody.position.set(x, 0, 0);
physBody.addShape(physShape);
world.addBody(physBody);
// Three.js visual
const mesh = new THREE.Mesh(
new THREE.BoxGeometry(
2 * terrainSize.x,
2 * terrainSize.y,
2 * terrainSize.z
),
new THREE.MeshPhongMaterial({ color: threeColor })
);
mesh.position.set(x, 0, 0);
scene.add(mesh);
};
makeTerrain(-terrainSize.x, grassMat, 0x00ff00);
makeTerrain(0, sandMat, 0xffff00);
makeTerrain(terrainSize.x, roughMat, 0x009900);
// GOLF BALL
const ballRadius = 0.7;
const ballShape = new CANNON.Sphere(ballRadius);
const ballBody = new CANNON.Body({
mass: 0.0499, // kg
shape: ballShape,
damping: 0.1,
angularDamping: 0.5
});
ballBody.position.set(0, 5, 0);
ballBody.linearDamping = 0.01;
world.addBody(ballBody);
const ballMesh = new THREE.Mesh(
new THREE.SphereGeometry(ballRadius, 32, 32),
new THREE.MeshPhongMaterial({ color: 0xffffff, shininess: 100 })
);
scene.add(ballMesh);
// INPUT HANDLING
document.addEventListener('mousedown', () => {
// Basic impulse-based shot mechanic
ballBody.velocity = new CANNON.Vec3(0, 0, 0);
ballBody.angularVelocity = new CANNON.Vec3(0, 0, 0);
const power = 5;
ballBody.applyImpulse(
new CANNON.Vec3(
(Math.random() - 0.5) * 0.5,
1,
-power
),
ballBody.position
);
});
// ANIMATION LOOP
function animate() {
requestAnimationFrame(animate);
world.step(1/60);
// Sync Three.js meshes with physics
ballMesh.position.set(
ballBody.position.x,
ballBody.position.y,
ballBody.position.z
);
ballMesh.quaternion.set(
ballBody.quaternion.x,
ballBody.quaternion.y,
ballBody.quaternion.z,
ballBody.quaternion.w
);
renderer.render(scene, camera);
}
animate();
})();
</script>
</body>
</html>
``````html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Golf Sim - Terrain Edition</title>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap" rel="stylesheet">
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100vh;
overflow: hidden;
background-color: #1e1e1e;
font-family: 'Roboto', Arial, sans-serif;
}
canvas {
display: block;
}
#hud {
position: absolute;
top: 20px;
left: 20px;
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.5));
color: #f0f0f0;
padding: 20px;
border-radius: 10px;
font-size: 14px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
backdrop-filter: blur(5px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
#power-meter {
margin-top: 15px;
padding: 0;
}
#power-bar {
width: 300px;
height: 12px;
border-radius: 6px;
background-color: rgba(255, 255, 255, 0.2);
margin-top: 5px;
overflow: hidden;
}
#power-fill {
height: 100%;
width: 0%;
background: linear-gradient(to right, #4CAF50, #FFC107, #FF5722);
transition: width 0.05s;
box-shadow: 0 0 10px rgba(76, 175, 80, 0.5);
}
.data-row {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
align-items: center;
}
.data-label {
font-weight: 300;
opacity: 0.8;
font-size: 13px;
}
.data-value {
font-weight: 700;
color: #4CAF50;
}
#club-selector {
margin-top: 15px;
}
select, button {
background-color: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: white;
padding: 8px 16px;
border-radius: 20px;
font-size: 14px;
margin-top: 5px;
cursor: pointer;
transition: all 0.3s;
font-family: 'Roboto', sans-serif;
}
select:hover, button:hover {
background-color: rgba(255, 255, 255, 0.2);
transform: translateY(-1px);
}
#instructions {
position: absolute;
bottom: 20px;
left: 20px;
color: #f0f0f0;
background: linear-gradient(to right, rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.3));
padding: 15px 20px;
border-radius: 10px;
font-size: 13px;
max-width: 450px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
backdrop-filter: blur(5px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
#instructions p {
margin: 5px 0;
line-height: 1.4;
}
#score {
position: absolute;
top: 20px;
right: 20px;
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.5));
color: #f0f0f0;
padding: 20px;
border-radius: 10px;
font-size: 14px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
backdrop-filter: blur(5px);
border: 1px solid rgba(255, 255, 255, 0.1);
text-align: center;
min-width: 150px;
}
#score h3 {
margin: 0 0 10px 0;
font-weight: 300;
opacity: 0.8;
letter-spacing: 1px;
}
#stroke-count {
font-size: 36px;
font-weight: 700;
color: #FFC107;
}
.terrain-indicator {
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 700;
margin-left: 8px;
}
.terrain-fairway {
background-color: #4CAF50;
}
.terrain-rough {
background-color: #8BC34A;
}
.terrain-sand {
background-color: #FFEB3B;
color: #333;
}
.terrain-green {
background-color: #009688;
}
#aim-indicator {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 4px;
height: 100px;
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.8), transparent);
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
}
</style>
</head>
<body>
<div id="hud">
<div class="data-row">
<span class="data-label">Ball Speed</span>
<span class="data-value" id="speed">0 m/s</span>
</div>
<div class="data-row">
<span class="data-label">Position</span>
<span class="data-value" id="position">x: 0.0, z: 0.0</span>
</div>
<div class="data-row">
<span class="data-label">Surface</span>
<span id="terrain-type">fairway</span>
</div>
<div id="power-meter">
<div class="data-row">
<span class="data-label">Power</span>
<span class="data-value" id="power-text">0%</span>
</div>
<div id="power-bar">
<div id="power-fill"></div>
</div>
</div>
<div id="club-selector">
<div class="data-row">
<span class="data-label">Club</span>
<select id="club-select">
<option value="driver">Driver</option>
<option value="iron7" selected>7 Iron</option>
<option value="wedge">Wedge</option>
</select>
</div>
</div>
<button id="reset-ball" style="width: 100%; margin-top: 10px;">Reset Shot</button>
</div>
<div id="score">
<h3>STROKES</h3>
<div id="stroke-count">1</div>
</div>
<div id="instructions">
<p><strong>Controls:</strong></p>
<p>• Hold <kbd>Mouse</kbd> to set power and rotate aim</p>
<p>• Release to swing</p>
<p>• Use <kbd>WASD</kbd> to move camera</p>
<p>• Press <kbd>Space</kbd> to drop to ball level</p>
</div>
<div id="aim-indicator"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/simplex-noise@2.4.0/simplex-noise.min.js"></script>
<script>
// Core 3D Golf Game Engine
(function() {
const TEE_X = 0;
const TEE_Z = 0;
const HOLE_X = 150;
const HOLE_Z = 150;
const HOLE_RADIUS = 0.1; // slightly larger than ball
const GRAVITY = 9.8;
const AIR_DENSITY = 1.225;
const BALL_MASS = 0.045;
const BALL_RADIUS = 0.04267 / 2;
const DRAG_COEFFICIENT = 0.47;
const LIFT_COEFFICIENT = 0.15;
// Terrain physics properties
const TerrainTypes = {
FAIRWAY: {
friction: 0.3,
rollFriction: 0.95,
bounceDamping: 0.8,
color: 0x4CAF50,
name: "fairway",
label: "Fairway"
},
ROUGH: {
friction: 0.6,
rollFriction: 0.98,
bounceDamping: 0.5,
color: 0x8BC34A,
name: "rough",
label: "Rough"
},
SAND: {
friction: 0.9,
rollFriction: 0.995,
bounceDamping: 0.3,
color: 0xFFEB3B,
name: "sand",
label: "Bunker"
},
GREEN: {
friction: 0.2,
rollFriction: 0.92,
bounceDamping: 0.9,
color: 0x009688,
name: "green",
label: "Green"
}
};
let scene, camera, renderer, ball, terrain;
let isSettingPower = false;
let power = 0;
let powerDirection = 1;
let aimAngle = 0;
let ballInMotion = false;
let cameraControls = {
forward: false,
backward: false,
left: false,
right: false,
up: false,
down: false
};
let strokeCount = 1;
const clubs = {
driver: { power: 100, loft: 10 },
iron7: { power: 70, loft: 30 },
wedge: { power: 40, loft: 55 }
};
// Physics state
let velocity = new THREE.Vector3(0, 0, 0);
let angularVelocity = new THREE.Vector3(0, 0, 0);
let spinAxis = new THREE.Vector3(0, 0, 0);
// Initialize the scene
function init() {
// Create scene
scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x87CEEB, 0.001);
// Create camera
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(TEE_X - 3, 2, TEE_Z);
camera.lookAt(TEE_X, 0, TEE_Z);
// Create renderer
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
// Add lighting
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(100, 100, 50);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
directionalLight.shadow.camera.near = 0.5;
directionalLight.shadow.camera.far = 500;
directionalLight.shadow.camera.left = -150;
directionalLight.shadow.camera.right = 150;
directionalLight.shadow.camera.top = 150;
directionalLight.shadow.camera.bottom = -150;
scene.add(directionalLight);
// Create terrain
createTerrain();
// Create ball
const ballGeometry = new THREE.SphereGeometry(BALL_RADIUS, 32, 32);
const ballMaterial = new THREE.MeshStandardMaterial({ color: 0xffffff });
ball = new THREE.Mesh(ballGeometry, ballMaterial);
ball.castShadow = true;
resetBall();
scene.add(ball);
// Create hole
const holeGeometry = new THREE.RingGeometry(0, HOLE_RADIUS * 1.5, 32);
const holeMaterial = new THREE.MeshBasicMaterial({ color: 0x000000 });
const hole = new THREE.Mesh(holeGeometry, holeMaterial);
hole.rotation.x = -Math.PI / 2;
hole.position.set(HOLE_X, getTerrainHeight(HOLE_X, HOLE_Z) + 0.01, HOLE_Z);
scene.add(hole);
// Add sky
scene.background = new THREE.Color(0x87CEEB);
// Event listeners
window.addEventListener('resize', onWindowResize);
document.addEventListener('mousedown', startPower);
document.addEventListener('mouseup', swing);
document.addEventListener('mousemove', setAim);
document.addEventListener('keydown', handleKeyDown);
document.addEventListener('keyup', handleKeyUp);
// Setup reset button
document.getElementById('reset-ball').addEventListener('click', resetToTee);
// Start animation
animate();
}
function createTerrain() {
const terrainSize = 400;
const terrainResolution = 256;
// Create terrain geometry
const geometry = new THREE.PlaneGeometry(
terrainSize,
terrainSize,
terrainResolution - 1,
terrainResolution - 1
);
geometry.rotateX(-Math.PI / 2);
// Generate height map using simplex noise
const simplex = new SimplexNoise();
// Modify vertices to create terrain features
const vertices = geometry.attributes.position.array;
// Define terrain areas
for (let i = 0; i < vertices.length; i += 3) {
const x = vertices[i];
const z = vertices[i + 2];
let height = 0;
// Base terrain with gentle slope towards hole
const distanceToHoleXZ = Math.sqrt(
Math.pow(x - HOLE_X, 2) + Math.pow(z - HOLE_Z, 2)
);
height += Math.max(0, distanceToHoleXZ * 0.01);
// Add noise for realism
height += simplex.noise2D(x * 0.01, z * 0.01) * 1.5;
height += simplex.noise2D(x * 0.02, z * 0.02) * 0.8;
height += simplex.noise2D(x * 0.05, z * 0.05) * 0.3;
// Create green around the hole
const distanceToHole = Math.sqrt(
Math.pow(x - HOLE_X, 2) + Math.pow(z - HOLE_Z, 2)
);
if (distanceToHole < 15) {
// Green is flatter
height = 0.3 * Math.max(0, distanceToHole * 0.02) + 0.5;
height += simplex.noise2D(x * 0.1, z * 0.1) * 0.1;
}
// Create fairway
if (distanceToHole < 100) {
height = Math.max(0.0, height * 0.7 + 0.1);
}
// Create sand traps
if (distanceToHole > 10 && distanceToHole < 25 && (
(Math.abs(x - HOLE_X + 12) < 8 && Math.abs(z - HOLE_Z) < 6) ||
(Math.abs(x - HOLE_X - 12) < 8 && Math.abs(z - HOLE_Z) < 6))) {
height = 0.2;
}
vertices[i + 1] = height;
}
geometry.attributes.position.needsUpdate = true;
geometry.computeVertexNormals();
// Create terrain material with vertex coloring for different surfaces
const vertexColors = new Float32Array(vertices.length);
for (let i = 0; i < vertices.length; i += 3) {
const x = vertices[i];
const z = vertices[i + 2];
const terrain = getTerrainType(x, z);
const color = new THREE.Color(terrain.color);
vertexColors[i] = color.r;
vertexColors[i + 1] = color.g;
vertexColors[i + 2] = color.b;
}
geometry.setAttribute('color', new THREE.BufferAttribute(vertexColors, 3));
const material = new THREE.MeshStandardMaterial({
vertexColors: true,
side: THREE.DoubleSide,
roughness: 0.8,
metalness: 0.1
});
terrain = new THREE.Mesh(geometry, material);
terrain.receiveShadow = true;
scene.add(terrain);
}
function getTerrainHeight(x, z) {
// Raycast to find the height of the terrain at (x, z)
const raycaster = new THREE.Raycaster(
new THREE.Vector3(x, 100, z),
new THREE.Vector3(0, -1, 0)
);
const intersects = raycaster.intersectObject(terrain);
if (intersects.length > 0) {
return intersects[0].point.y;
}
return 0;
}
function getTerrainType(x, z) {
const distanceToHole = Math.sqrt(
Math.pow(x - HOLE_X, 2) + Math.pow(z - HOLE_Z, 2)
);
// Green area
if (distanceToHole < 15) {
return TerrainTypes.GREEN;
}
// Sand traps
if (distanceToHole > 10 && distanceToHole < 25 && (
(Math.abs(x - HOLE_X + 12) < 8 && Math.abs(z - HOLE_Z) < 6) ||
(Math.abs(x - HOLE_X - 12) < 8 && Math.abs(z - HOLE_Z) < 6))) {
return TerrainTypes.SAND;
}
// Fairway
if (distanceToHole < 100) {
return TerrainTypes.FAIRWAY;
}
// Rough
return TerrainTypes.ROUGH;
}
function getTerrainAtPosition(pos) {
return getTerrainType(pos.x, pos.z);
}
function resetBall() {
ball.position.set(TEE_X, getTerrainHeight(TEE_X, TEE_Z) + BALL_RADIUS * 1.5, TEE_Z);
velocity.set(0, 0, 0);
angularVelocity.set(0, 0, 0);
ballInMotion = false;
updateHUD();
}
function resetToTee() {
strokeCount = 1;
document.getElementById('stroke-count').textContent = strokeCount;
resetBall();
}
function startPower(event) {
if (!ballInMotion) {
isSettingPower = true;
power = 0;
powerDirection = 1;
document.getElementById('aim-indicator').style.opacity = '0.7';
}
}
function setAim(event) {
if (isSettingPower) {
// Calculate aim angle based on mouse position
const rect = renderer.domElement.getBoundingClientRect();
const x = event.clientX - rect.left - rect.width/2;
const z = event.clientY - rect.top - rect.height/2;
aimAngle = Math.atan2(x, z);
// Update aim indicator
const indicator = document.getElementById('aim-indicator');
indicator.style.transform = `translate(-50%, -50%) rotate(${aimAngle}rad)`;
}
}
function swing() {
if (isSettingPower && !ballInMotion) {
isSettingPower = false;
document.getElementById('aim-indicator').style.opacity = '0';
const club = clubs[document.getElementById('club-select').value];
// Apply velocity based on power, angle, and club
const speedFactor = power * 10 * club.power / 100;
const velocity_x = Math.sin(aimAngle) * speedFactor;
const velocity_z = Math.cos(aimAngle) * speedFactor;
// Launch angle based on club loft
const launchAngleRad = club.loft * Math.PI / 180;
const velocity_y = speedFactor * Math.sin(launchAngleRad);
velocity.set(velocity_x, velocity_y, velocity_z);
// Apply backspin based on club
const backspin = [0.5, 1.5, 3][clubs.length - Object.keys(clubs).indexOf(document.getElementById('club-select').value) - 1];
angularVelocity.set(-velocity_z * backspin, 0, velocity_x * backspin);
ballInMotion = true;
// Increment stroke count
strokeCount++;
document.getElementById('stroke-count').textContent = strokeCount;
}
}
function handleKeyDown(event) {
switch (event.key) {
case 'w': case 'ArrowUp': cameraControls.forward = true; break;
case 's': case 'ArrowDown': cameraControls.backward = true; break;
case 'a': case 'ArrowLeft': cameraControls.left = true; break;
case 'd': case 'ArrowRight': cameraControls.right = true; break;
case 'q': cameraControls.down = true; break;
case 'e': cameraControls.up = true; break;
case ' ':
// Space to move camera to ball level
event.preventDefault();
camera.position.y = ball.position.y + 1;
camera.lookAt(ball.position.x, ball.position.y, ball.position.z);
break;
}
}
function handleKeyUp(event) {
switch (event.key) {
case 'w': case 'ArrowUp': cameraControls.forward = false; break;
case 's': case 'ArrowDown': cameraControls.backward = false; break;
case 'a': case 'ArrowLeft': cameraControls.left = false; break;
case 'd': case 'ArrowRight': cameraControls.right = false; break;
case 'q': cameraControls.down = false; break;
case 'e': cameraControls.up = false; break;
}
}
function updateCamera() {
const speed = 0.5;
if (cameraControls.forward) {
camera.translateZ(-speed);
}
if (cameraControls.backward) {
camera.translateZ(speed);
}
if (cameraControls.left) {
camera.translateX(-speed);
}
if (cameraControls.right) {
camera.translateX(speed);
}
if (cameraControls.up) {
camera.position.y += speed;
}
if (cameraControls.down) {
camera.position.y -= speed;
}
}
function updatePhysics(delta) {
if (!ballInMotion) return;
const terrain = getTerrainAtPosition(ball.position);
// Raycast ground collision
const raycaster = new THREE.Raycaster(
new THREE.Vector3(ball.position.x, ball.position.y + 0.01, ball.position.z),
new THREE.Vector3(0, -1, 0)
);
raycaster.far = 0.06;
const intersects = raycaster.intersectObject(terrain);
let isOnGround = intersects.length > 0;
// Track previous position for roll calculations
const prevPos = ball.position.clone();
// Gravity and physics
const gravity = new THREE.Vector3(0, -GRAVITY, 0);
// Air resistance
const airResistance = velocity.clone().normalize().multiplyScalar(
-0.5 * AIR_DENSITY * velocity.lengthSq() * DRAG_COEFFICIENT * (Math.PI * Math.pow(BALL_RADIUS, 2)) / BALL_MASS
);
// Magnus effect (spin-induced lift)
const magnusForce = new THREE.Vector3();
if (!spinAxis.equals(new THREE.Vector3(0, 0, 0)) && velocity.length() > 0) {
magnusForce.crossVectors(spinAxis, velocity).multiplyScalar(
0.5 * AIR_DENSITY * LIFT_COEFFICIENT * velocity.lengthSq() * (Math.PI * Math.pow(BALL_RADIUS, 2)) / BALL_MASS
);
}
// Ground forces
if (isOnGround) {
// Apply terrain friction
velocity.multiplyScalar(1 - TerrainTypes.FAIRWAY.friction * delta);
// Surface friction based on terrain
const friction = terrain.friction;
const surfaceFriction = new THREE.Vector2(velocity.x, velocity.z).normalize().multiplyScalar(
-friction * GRAVITY * delta
);
velocity.x += surfaceFriction.x;
velocity.z += surfaceFriction.y;
// Rolling resistance
const rollFriction = terrain.rollFriction;
velocity.multiplyScalar(1 - rollFriction * delta);
}
// Update acceleration
const acceleration = new THREE.Vector3();
acceleration.add(gravity);
if (!isOnGround) {
acceleration.add(airResistance);
acceleration.add(magnusForce);
}
// Update velocity and position
velocity.add(acceleration.multiplyScalar(delta));
ball.position.add(velocity.clone().multiplyScalar(delta));
// Ground collision and bounce
if (isOnGround) {
const groundY = getTerrainHeight(ball.position.x, ball.position.z);
if (ball.position.y < groundY + BALL_RADIUS) {
ball.position.y = groundY + BALL_RADIUS;
// Get surface normal
const normal = new THREE.Vector3(0, 1, 0);
if (intersects.length > 0) normal.copy(intersects[0].face.normal);
// Reflect velocity based on impact velocity and surface normal
const impactSpeed = velocity.length();
const dot = velocity.dot(normal);
if (dot < 0) {
velocity.sub(normal.multiplyScalar(2 * dot));
// Energy loss on bounce
const energyLoss = 1 - (1 - terrain.bounceDamping);
velocity.multiplyScalar(energyLoss);
}
}
// Check roll limits
if (velocity.length() < 0.1) {
velocity.set(0, 0, 0);
// Check if ball is in hole
const distToHole = Math.sqrt(
Math.pow(ball.position.x - HOLE_X, 2) +
Math.pow(ball.position.z - HOLE_Z, 2)
);
if (distToHole < HOLE_RADIUS) {
alert(`Hole completed in ${strokeCount} strokes!`);
resetToTee();
}
ballInMotion = false;
}
}
// Update ball rotation
ball.rotateOnWorldAxis(
angularVelocity.clone().normalize(),
angularVelocity.length() * delta
);
// Basic friction on spin
angularVelocity.multiplyScalar(0.98);
updateHUD();
}
function updateHUD() {
document.getElementById('speed').textContent = `${velocity.length().toFixed(2)} m/s`;
document.getElementById('position').textContent =
`x: ${ball.position.x.toFixed(1)}, z: ${ball.position.z.toFixed(1)}`;
const terrain = getTerrainAtPosition(ball.position);
const terrainTypeText = document.getElementById('terrain-type');
terrainTypeText.textContent = terrain.label;
terrainTypeText.className = `terrain-indicator terrain-${terrain.name}`;
if (isSettingPower) {
// Update power meter
power = Math.min(100, Math.max(0, power + (powerDirection * 200 * 0.016)));
if (power >= 100) powerDirection = -1;
if (power <= 0) powerDirection = 1;
document.getElementById('power-text').textContent = `${Math.round(power)}%`;
document.getElementById('power-fill').style.width = `${power}%`;
} else {
document.getElementById('power-text').textContent = `${Math.round(power)}%`;
document.getElementById('power-fill').style.width = `${power}%`;
}
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
requestAnimationFrame(animate);
// Update physics with consistent time delta
updatePhysics(0.016);
if (!isSettingPower && !ballInMotion) {
// Camera follows ball when not aiming
camera.lookAt(ball.position.x, ball.position.y + 0.5, ball.position.z);
}
updateCamera();
renderer.render(scene, camera);
}
// Start the game
init();
})();
</script>
</body>
</html>
``````html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>3D Golf Game Engine</title>
<style>
body { margin: 0; overflow: hidden; }
canvas { display: block; }
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
</head>
<body>
<script>
// Core 3D Golf Game Engine
// Scene setup
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// Lights
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 10, 5);
scene.add(directionalLight);
// Terrain creation (simple plane with sections for grass, sand, rough)
const terrainSize = 100;
const terrainGeometry = new THREE.PlaneGeometry(terrainSize, terrainSize, 10, 10);
const grassMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const sandMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 });
const roughMaterial = new THREE.MeshBasicMaterial({ color: 0x008800 });
// Create terrain mesh with different sections
const terrain = new THREE.Mesh(terrainGeometry, grassMaterial);
terrain.rotation.x = -Math.PI / 2;
scene.add(terrain);
// Simulate different surfaces by dividing the plane
// For simplicity, define zones: x < -20 = rough, x > 20 = sand, else grass
function getSurfaceType(position) {
if (position.x < -20) return 'rough'; // High friction, low bounce
if (position.x > 20) return 'sand'; // Medium friction, medium bounce
return 'grass'; // Low friction, high bounce
}
// Surface properties
const surfaces = {
grass: { friction: 0.98, restitution: 0.6, color: 0x00ff00 },
sand: { friction: 0.95, restitution: 0.3, color: 0xffff00 },
rough: { friction: 0.90, restitution: 0.2, color: 0x008800 }
};
// Ball
const ballGeometry = new THREE.SphereGeometry(0.5, 32, 32);
const ballMaterial = new THREE.MeshStandardMaterial({ color: 0xffffff });
const ball = new THREE.Mesh(ballGeometry, ballMaterial);
ball.position.set(0, 0.5, -40); // Start position
scene.add(ball);
// Physics variables
const gravity = new THREE.Vector3(0, -0.098, 0); // Simplified gravity
let velocity = new THREE.Vector3(0, 0, 0);
let isRolling = false;
let hasHit = false;
// Camera position
camera.position.set(0, 20, 0);
camera.lookAt(0, 0, -40);
// Function to apply initial shot (simulate hit)
function hitBall() {
if (!hasHit) {
velocity.set(0, 5, -10); // Initial velocity: upward and forward
hasHit = true;
}
}
// Collision detection and response
function handleCollision() {
if (ball.position.y <= 0.5) { // Ball touches ground
ball.position.y = 0.5; // Snap to ground
const surfaceType = getSurfaceType(ball.position);
const props = surfaces[surfaceType];
// Bounce (restitution)
velocity.y = -velocity.y * props.restitution;
// If vertical velocity is small, start rolling
if (Math.abs(velocity.y) < 0.1) {
velocity.y = 0;
isRolling = true;
} else {
isRolling = false;
}
// Apply friction during roll or bounce
velocity.x *= props.friction;
velocity.z *= props.friction;
// Change terrain color for visualization (optional)
terrain.material.color.setHex(props.color);
}
}
// Animation loop
function animate() {
requestAnimationFrame(animate);
if (hasHit) {
// Apply gravity
velocity.add(gravity);
// Update position
ball.position.add(velocity);
// Handle terrain interaction
handleCollision();
// Rolling friction
if (isRolling) {
const surfaceType = getSurfaceType(ball.position);
const props = surfaces[surfaceType];
velocity.multiplyScalar(props.friction);
// Stop if velocity is very small
if (velocity.length() < 0.01) {
velocity.set(0, 0, 0);
isRolling = false;
}
}
// Simple terrain boundaries
if (ball.position.z < -terrainSize / 2 || ball.position.z > terrainSize / 2) {
velocity.z = -velocity.z * 0.5; // Bounce off edges
}
if (ball.position.x < -terrainSize / 2 || ball.position.x > terrainSize / 2) {
velocity.x = -velocity.x * 0.5;
}
}
// Camera follows ball
camera.position.set(ball.position.x, ball.position.y + 20, ball.position.z + 10);
camera.lookAt(ball.position);
renderer.render(scene, camera);
}
animate();
// Event listener for hitting the ball (spacebar)
window.addEventListener('keydown', (event) => {
if (event.key === ' ') {
hitBall();
}
});
// Resize handler
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>
```# 3D Golf Game Engine with Realistic Physics
Here's a complete HTML implementation of a 3D golf game engine using Three.js and Cannon.js for physics simulation. This includes realistic ball physics with different terrain interactions.
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Golf Physics Engine</title>
<style>
body { margin: 0; overflow: hidden; }
canvas { display: block; }
#ui {
position: absolute;
top: 10px;
left: 10px;
color: white;
font-family: Arial, sans-serif;
background: rgba(0,0,0,0.5);
padding: 10px;
border-radius: 5px;
}
#power-meter {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
width: 200px;
height: 20px;
background: rgba(255,255,255,0.2);
border-radius: 10px;
overflow: hidden;
}
#power-fill {
height: 100%;
width: 0%;
background: linear-gradient(to right, #4CAF50, #FFC107, #F44336);
transition: width 0.1s;
}
</style>
</head>
<body>
<div id="ui">
<div>Club: <span id="club-type">Driver</span></div>
<div>Power: <span id="power-percent">0%</span></div>
<div>Terrain: <span id="terrain-type">Fairway</span></div>
</div>
<div id="power-meter">
<div id="power-fill"></div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/cannon.js/0.6.2/cannon.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.min.js"></script>
<script>
// Scene setup
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x87CEEB);
scene.fog = new THREE.Fog(0x87CEEB, 10, 100);
// Camera
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 5, 10);
// Renderer
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);
// Controls
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
// Physics world
const world = new CANNON.World();
world.gravity.set(0, -9.82, 0);
world.broadphase = new CANNON.NaiveBroadphase();
world.solver.iterations = 10;
// Materials for different surfaces
const grassMaterial = new CANNON.Material("grass");
const sandMaterial = new CANNON.Material("sand");
const roughMaterial = new CANNON.Material("rough");
const fairwayMaterial = new CANNON.Material("fairway");
const greenMaterial = new CANNON.Material("green");
const ballMaterial = new CANNON.Material("ball");
// Contact materials for realistic interactions
const ballGrassContact = new CANNON.ContactMaterial(ballMaterial, grassMaterial, {
friction: 0.3,
restitution: 0.4,
contactEquationStiffness: 1e8,
contactEquationRelaxation: 3
});
world.addContactMaterial(ballGrassContact);
const ballSandContact = new CANNON.ContactMaterial(ballMaterial, sandMaterial, {
friction: 0.8,
restitution: 0.2,
contactEquationStiffness: 1e8,
contactEquationRelaxation: 3
});
world.addContactMaterial(ballSandContact);
const ballRoughContact = new CANNON.ContactMaterial(ballMaterial, roughMaterial, {
friction: 0.5,
restitution: 0.3,
contactEquationStiffness: 1e8,
contactEquationRelaxation: 3
});
world.addContactMaterial(ballRoughContact);
const ballFairwayContact = new CANNON.ContactMaterial(ballMaterial, fairwayMaterial, {
friction: 0.2,
restitution: 0.5,
contactEquationStiffness: 1e8,
contactEquationRelaxation: 3
});
world.addContactMaterial(ballFairwayContact);
const ballGreenContact = new CANNON.ContactMaterial(ballMaterial, greenMaterial, {
friction: 0.1,
restitution: 0.6,
contactEquationStiffness: 1e8,
contactEquationRelaxation: 3
});
world.addContactMaterial(ballGreenContact);
// Lighting
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(10, 20, 10);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
directionalLight.shadow.camera.near = 0.5;
directionalLight.shadow.camera.far = 500;
directionalLight.shadow.camera.left = -50;
directionalLight.shadow.camera.right = 50;
directionalLight.shadow.camera.top = 50;
directionalLight.shadow.camera.bottom = -50;
scene.add(directionalLight);
// Create golf course terrain
function createTerrain() {
// Ground
const groundGeometry = new THREE.PlaneGeometry(100, 100, 50, 50);
const groundMaterial = new THREE.MeshStandardMaterial({
color: 0x3a5f0b,
roughness: 0.8,
metalness: 0.2
});
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);
// Physics ground
const groundShape = new CANNON.Plane();
const groundBody = new CANNON.Body({ mass: 0 });
groundBody.addShape(groundShape);
groundBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
groundBody.material = grassMaterial;
world.addBody(groundBody);
// Sand bunker
const sandGeometry = new THREE.CircleGeometry(5, 32);
const sandMaterial = new THREE.MeshStandardMaterial({ color: 0xf5deb3 });
const sand = new THREE.Mesh(sandGeometry, sandMaterial);
sand.rotation.x = -Math.PI / 2;
sand.position.set(20, 0.01, 15);
sand.receiveShadow = true;
scene.add(sand);
const sandShape = new CANNON.Cylinder(5, 5, 0.1, 32);
const sandBody = new CANNON.Body({ mass: 0 });
sandBody.addShape(sandShape);
sandBody.position.set(20, 0, 15);
sandBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
sandBody.material = sandMaterial;
world.addBody(sandBody);
// Rough area
const roughGeometry = new THREE.PlaneGeometry(20, 40);
const roughMaterial = new THREE.MeshStandardMaterial({
color: 0x2a4b0a,
roughness: 1.0,
metalness: 0.0
});
const rough = new THREE.Mesh(roughGeometry, roughMaterial);
rough.rotation.x = -Math.PI / 2;
rough.position.set(-15, 0.01, 10);
rough.receiveShadow = true;
scene.add(rough);
const roughShape = new CANNON.Box(new CANNON.Vec3(10, 0.1, 20));
const roughBody = new CANNON.Body({ mass: 0 });
roughBody.addShape(roughShape);
roughBody.position.set(-15, 0, 10);
roughBody.material = roughMaterial;
world.addBody(roughBody);
// Fairway
const fairwayGeometry = new THREE.PlaneGeometry(30, 60);
const fairwayMaterial = new THREE.MeshStandardMaterial({
color: 0x4c8c2b,
roughness: 0.5,
metalness: 0.1
});
const fairway = new THREE.Mesh(fairwayGeometry, fairwayMaterial);
fairway.rotation.x = -Math.PI / 2;
fairway.position.set(0, 0.01, -20);
fairway.receiveShadow = true;
scene.add(fairway);
const fairwayShape = new CANNON.Box(new CANNON.Vec3(15, 0.1, 30));
const fairwayBody = new CANNON.Body({ mass: 0 });
fairwayBody.addShape(fairwayShape);
fairwayBody.position.set(0, 0, -20);
fairwayBody.material = fairwayMaterial;
world.addBody(fairwayBody);
// Green with hole
const greenGeometry = new THREE.CircleGeometry(8, 32);
const greenMaterial = new THREE.MeshStandardMaterial({
color: 0x7cfc00,
roughness: 0.2,
metalness: 0.0
});
const green = new THREE.Mesh(greenGeometry, greenMaterial);
green.rotation.x = -Math.PI / 2;
green.position.set(0, 0.01, 30);
green.receiveShadow = true;
scene.add(green);
const greenShape = new CANNON.Cylinder(8, 8, 0.1, 32);
const greenBody = new CANNON.Body({ mass: 0 });
greenBody.addShape(greenShape);
greenBody.position.set(0, 0, 30);
greenBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
greenBody.material = greenMaterial;
world.addBody(greenBody);
// Hole
const holeGeometry = new THREE.CylinderGeometry(0.1, 0.1, 0.5, 16);
const holeMaterial = new THREE.MeshBasicMaterial({ color: 0x000000 });
const hole = new THREE.Mesh(holeGeometry, holeMaterial);
hole.rotation.x = Math.PI / 2;
hole.position.set(0, 0.25, 30);
scene.add(hole);
// Flag
const flagPoleGeometry = new THREE.CylinderGeometry(0.03, 0.03, 3, 8);
const flagPoleMaterial = new THREE.MeshStandardMaterial({ color: 0xffffff });
const flagPole = new THREE.Mesh(flagPoleGeometry, flagPoleMaterial);
flagPole.position.set(0, 1.5, 30);
scene.add(flagPole);
const flagGeometry = new THREE.PlaneGeometry(1, 0.7);
const flagMaterial = new THREE.MeshStandardMaterial({
color: 0xff0000,
side: THREE.DoubleSide
});
const flag = new THREE.Mesh(flagGeometry, flagMaterial);
flag.position.set(0.5, 1.5, 30);
scene.add(flag);
}
// Golf ball
let ballMesh, ballBody;
function createBall() {
const ballGeometry = new THREE.SphereGeometry(0.2, 32, 32);
const ballMaterial = new THREE.MeshStandardMaterial({
color: 0xffffff,
roughness: 0.1,
metalness: 0.5
});
ballMesh = new THREE.Mesh(ballGeometry, ballMaterial);
ballMesh.castShadow = true;
ballMesh.receiveShadow = true;
ballMesh.position.set(0, 0.2, 0);
scene.add(ballMesh);
const ballShape = new CANNON.Sphere(0.2);
ballBody = new CANNON.Body({ mass: 0.04593 }); // Standard golf ball mass (45.93g)
ballBody.addShape(ballShape);
ballBody.position.set(0, 0.2, 0);
ballBody.material = ballMaterial;
ballBody.linearDamping = 0.1;
ballBody.angularDamping = 0.3;
world.addBody(ballBody);
}
// Clubs
const clubs = {
driver: { power: 1.0, loft: 10, name: "Driver" },
iron3: { power: 0.9, loft: 21, name: "3 Iron" },
iron7: { power: 0.7, loft: 34, name: "7 Iron" },
wedge: { power: 0.5, loft: 56, name: "Wedge" },
putter: { power: 0.2, loft: 3, name: "Putter" }
};
let currentClub = clubs.driver;
// Game state
let isCharging = false;
let power = 0;
let powerDirection = 1;
let canHit = true;
// UI elements
const clubTypeElement = document.getElementById('club-type');
const powerPercentElement = document.getElementById('power-percent');
const terrainTypeElement = document.getElementById('terrain-type');
const powerFillElement = document.getElementById('power-fill');
// Event listeners
document.addEventListener('keydown', (event) => {
if (event.code === 'Space' && canHit) {
isCharging = true;
}
// Club selection
if (event.code === 'Digit1') currentClub = clubs.driver;
if (event.code === 'Digit2') currentClub = clubs.iron3;
if (event.code === 'Digit3') currentClub = clubs.iron7;
if (event.code === 'Digit4') currentClub = clubs.wedge;
if (event.code === 'Digit5') currentClub = clubs.putter;
clubTypeElement.textContent = currentClub.name;
});
document.addEventListener('keyup', (event) => {
if (event.code === 'Space' && isCharging && canHit) {
hitBall(power);
isCharging = false;
power = 0;
powerFillElement.style.width = '0%';
powerPercentElement.textContent = '0%';
}
});
// Hit the ball
function hitBall(power) {
if (!canHit) return;
canHit = false;
const forceMagnitude = 20 * power * currentClub.power;
const loftRadians = THREE.MathUtils.degToRad(currentClub.loft);
// Calculate force vector based on camera direction and club loft
const direction = new THREE.Vector3();
camera.getWorldDirection(direction);
direction.y += Math.sin(loftRadians);
direction.normalize();
// Apply force to the ball
ballBody.wakeUp();
ballBody.applyImpulse(
new CANNON.Vec3(
direction.x * forceMagnitude,
direction.y * forceMagnitude,
direction.z * forceMagnitude
),
new CANNON.Vec3(0, 0, 0)
);
// Add some spin
ballBody.angularVelocity.set(
(Math.random() - 0.5) * 2,
(Math.random() - 0.5) * 2,
(Math.random() - 0.5) * 2
);
}
// Detect terrain type under ball
function detectTerrain() {
// Raycast to detect what's under the ball
const ray = new CANNON.Ray(
new CANNON.Vec3(ballBody.position.x, 10, ballBody.position.z),
new CANNON.Vec3(0, -1, 0)
);
const result = ray.intersectWorld(world);
if (result.hasHit) {
const hitBody = result.body;
if (hitBody.material === grassMaterial) {
terrainTypeElement.textContent = "Rough";
} else if (hitBody.material === sandMaterial) {
terrainTypeElement.textContent = "Sand";
} else if (hitBody.material === roughMaterial) {
terrainTypeElement.textContent = "Heavy Rough";
} else if (hitBody.material === fairwayMaterial) {
terrainTypeElement.textContent = "Fairway";
} else if (hitBody.material === greenMaterial) {
terrainTypeElement.textContent = "Green";
}
}
}
// Check if ball is at rest
function isBallAtRest() {
const velocity = ballBody.velocity;
const angularVelocity = ballBody.angularVelocity;
const speed = velocity.length();
const angularSpeed = angularVelocity.length();
return speed < 0.1 && angularSpeed < 0.1;
}
// Check if ball is in hole
function checkHole() {
const ballPos = ballBody.position;
const holePos = new CANNON.Vec3(0, 0, 30);
const distance = ballPos.distanceTo(holePos);
if (distance < 0.2 && isBallAtRest()) {
alert("Hole in one! Well done!");
resetBall();
}
}
// Reset ball to starting position
function resetBall() {
ballBody.position.set(0, 0.2, 0);
ballBody.velocity.set(0, 0, 0);
ballBody.angularVelocity.set(0, 0, 0);
ballBody.quaternion.set(0, 0, 0, 1);
canHit = true;
}
// Initialize
createTerrain();
createBall();
// Animation loop
function animate() {
requestAnimationFrame(animate);
// Update physics
world.step(1/60);
// Sync Three.js objects with Cannon.js bodies
if (ballMesh && ballBody) {
ballMesh.position.copy(ballBody.position);
ballMesh.quaternion.copy(ballBody.quaternion);
}
// Update power meter when charging
if (isCharging) {
power += 0.01 * powerDirection;
if (power >= 1) {
powerDirection = -1;
power = 1;
} else if (power <= 0) {
powerDirection = 1;
power = 0;
}
powerFillElement.style.width = `${power * 100}%`;
powerPercentElement.textContent = `${Math.round(power * 100)}%`;
}
// Check if ball is at rest
if (!canHit && isBallAtRest()) {
canHit = true;
detectTerrain();
}
// Check if ball is in hole
checkHole();
// Update controls
controls.update();
// Render scene
renderer.render(scene, camera);
}
// Handle window resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// Start animation
animate();
</script>
</body>
</html>
```
## Features Implemented:
1. **Realistic Physics Simulation**:
- Uses Cannon.js for accurate physics calculations
- Different contact materials for various surfaces (grass, sand, rough, fairway, green)
- Proper restitution (bounce) and friction values for each surface type
2. **Ball Dynamics**:
- Realistic mass (45.93g standard golf ball)
- Linear and angular damping for proper rolling behavior
- Spin effects when hitting the ball
3. **Terrain Interaction**:
- Multiple surface types with different physical properties
- Terrain detection system to show current surface type
- Visual differentiation between terrain types
4. **Club System**:
- Multiple clubs with different power and loft angles
- Realistic force application based on club type
- Power meter for shot strength control
5. **Game Mechanics**:
- Hole detection system
- Ball reset functionality
- Visual feedback for current state
6. **Visual Elements**:
- 3D course with different terrain types
- Proper lighting and shadows
- Flag and hole markers
Controls:
- Space: Charge and hit the ball
- 1-5: Select different clubs
- Mouse: Orbit camera to view from different angles```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>3D Golf Physics Engine</title>
<style>
body { margin: 0; overflow: hidden; }
canvas { display: block; }
#info {
position: absolute;
top: 10px; left: 10px;
color: #fff;
font-family: sans-serif;
background: rgba(0,0,0,0.5);
padding: 8px;
}
</style>
<!-- Three.js Library -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/examples/js/controls/OrbitControls.js"></script>
</head>
<body>
<div id="info">Drag on ground to shoot the ball</div>
<script>
// Scene, camera, renderer
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x87CEEB);
const camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 1000);
camera.position.set(0, 50, 100);
const renderer = new THREE.WebGLRenderer({antialias:true});
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// Controls
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enablePan = false;
controls.minDistance = 20; controls.maxDistance = 200;
controls.maxPolarAngle = Math.PI/2 - 0.1;
// Lights
scene.add(new THREE.HemisphereLight(0xffffff, 0x444444, 1.0));
const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
dirLight.position.set(50, 100, 50);
scene.add(dirLight);
// Surfaces definition
const surfaces = [];
const surfDefs = [
{ name: 'grass', color: 0x228B22, friction: 0.4, rollFriction: 0.05 },
{ name: 'sand', color: 0xC2B280, friction: 0.8, rollFriction: 0.3 },
{ name: 'rough', color: 0x556B2F, friction: 0.6, rollFriction: 0.1 }
];
const planeSize = { width: 100, depth: 100 };
surfDefs.forEach((d,i) => {
const geo = new THREE.PlaneBufferGeometry(planeSize.width, planeSize.depth);
const mat = new THREE.MeshLambertMaterial({ color: d.color });
const mesh = new THREE.Mesh(geo, mat);
mesh.rotation.x = -Math.PI/2;
mesh.position.x = (i-1)*planeSize.width;
scene.add(mesh);
const box = new THREE.Box3().setFromObject(mesh);
surfaces.push(Object.assign({ mesh, box }, d));
});
// Ball
const ballRadius = 2;
const ballGeo = new THREE.SphereBufferGeometry(ballRadius, 32, 32);
const ballMat = new THREE.MeshPhongMaterial({ color: 0xffffff });
const ballMesh = new THREE.Mesh(ballGeo, ballMat);
scene.add(ballMesh);
// Physics state
const ball = {
pos: new THREE.Vector3(0, ballRadius, 0),
vel: new THREE.Vector3(),
radius: ballRadius,
rolling: false
};
const gravity = 9.81;
const restitution = 0.7;
const speedStopThreshold = 0.2;
// Raycaster for input
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
let dragStart = null;
function onPointerDown(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const hits = raycaster.intersectObjects(surfaces.map(s=>s.mesh));
if (hits.length) {
dragStart = hits[0].point.clone();
}
}
function onPointerUp(event) {
if (!dragStart) return;
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const hits = raycaster.intersectObjects(surfaces.map(s=>s.mesh));
if (hits.length) {
const dragEnd = hits[0].point.clone();
const impulse = dragEnd.clone().sub(dragStart);
const power = 1.5;
ball.vel.set(
impulse.x * power,
impulse.length() * 0.2,
impulse.z * power
);
ball.rolling = false;
}
dragStart = null;
}
renderer.domElement.addEventListener('pointerdown', onPointerDown);
renderer.domElement.addEventListener('pointerup', onPointerUp);
// Utility: find surface under ball
function getSurface(x, z) {
for (let s of surfaces) {
if (x >= s.box.min.x && x <= s.box.max.x && z >= s.box.min.z && z <= s.box.max.z) {
return s;
}
}
return surfaces[0];
}
// Animation loop
let lastTime = performance.now();
function animate() {
requestAnimationFrame(animate);
const now = performance.now();
const dt = (now - lastTime) / 1000;
lastTime = now;
// Physics update
// Determine ground height = 0, normal = up
const groundY = ball.radius;
const onGround = ball.pos.y <= groundY + 1e-3;
const surface = getSurface(ball.pos.x, ball.pos.z);
if (!ball.rolling) {
// In air or bouncing
if (!onGround) {
ball.vel.y -= gravity * dt;
} else {
// Bounce
if (ball.vel.y < 0) {
const v = ball.vel.clone();
const n = new THREE.Vector3(0,1,0);
const vn = v.dot(n);
ball.vel.copy(v.sub(n.multiplyScalar(2*vn)).multiplyScalar(restitution));
ball.pos.y = groundY;
// Determine if we should switch to rolling
if (ball.vel.length() < speedStopThreshold) {
ball.rolling = true;
ball.vel.y = 0;
}
}
}
} else {
// Rolling on ground
ball.pos.y = groundY;
// Apply rolling friction
const speed = Math.hypot(ball.vel.x, ball.vel.z);
if (speed > 0) {
const rf = surface.rollFriction * gravity;
const decel = Math.min(rf * dt, speed);
const dir = new THREE.Vector3(ball.vel.x,0,ball.vel.z).normalize();
ball.vel.x -= dir.x * decel;
ball.vel.z -= dir.z * decel;
}
if (speed < speedStopThreshold) {
ball.vel.set(0,0,0);
}
}
// Update position
ball.pos.addScaledVector(ball.vel, dt);
// Prevent sinking
if (ball.pos.y < groundY) ball.pos.y = groundY;
ballMesh.position.copy(ball.pos);
controls.update();
renderer.render(scene, camera);
}
// Handle resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth/window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
animate();
</script>
</body>
</html>
```# 3D Golf Game Engine
## Overview
The 3D golf game engine is designed to simulate realistic ball physics and terrain interaction. The engine will utilize the Three.js library for 3D rendering and the Ammo.js library for physics simulations.
## Code
### HTML
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Golf Game Engine</title>
<style>
body {
margin: 0;
background-color: #f0f0f0;
}
#canvas {
width: 100%;
height: 100vh;
display: block;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/ammo.js@1.0.4/dist/ammo.wasm.js"></script>
<script src="golf-engine.js"></script>
</body>
</html>
```
### JavaScript (golf-engine.js)
```javascript
// Import necessary libraries
Ammo().then(Ammo => {
// Initialize the scene, camera, and renderer
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({
canvas: document.getElementById('canvas'),
antialias: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
// Initialize the physics world
const world = new Ammo.btSoftRigidDynamicsWorld(
new Ammo.btDefaultCollisionConfiguration(),
new Ammo.btCollisionDispatcher(new Ammo.btDefaultCollisionConfiguration()),
new Ammo.btDbvtBroadphase(),
new Ammo.btDefaultSoftBodySolver()
);
world.setGravity(new Ammo.btVector3(0, -9.81, 0));
// Create the terrain
const terrainGeometry = new THREE.PlaneGeometry(100, 100, 100, 100);
const terrainMaterial = new THREE.MeshPhongMaterial({ color: 0x00ff00 });
const terrain = new THREE.Mesh(terrainGeometry, terrainMaterial);
terrain.rotation.x = -Math.PI / 2;
scene.add(terrain);
// Create the ball
const ballGeometry = new THREE.SphereGeometry(0.5, 32, 32);
const ballMaterial = new THREE.MeshPhongMaterial({ color: 0xffffff });
const ball = new THREE.Mesh(ballGeometry, ballMaterial);
scene.add(ball);
// Create the physics body for the ball
const ballShape = new Ammo.btSphereShape(0.5);
const ballMotionState = new Ammo.btDefaultMotionState(new Ammo.btTransform(new Ammo.btQuaternion(0, 0, 0, 1), new Ammo.btVector3(0, 10, 0)));
const ballBody = new Ammo.btRigidBody(new Ammo.btRigidBodyConstructionInfo(1, ballMotionState, ballShape, new Ammo.btVector3(0, 0, 0)));
world.addRigidBody(ballBody);
// Create the physics body for the terrain
const terrainShape = new Ammo.btStaticPlaneShape(new Ammo.btVector3(0, 1, 0), 0);
const terrainMotionState = new Ammo.btDefaultMotionState(new Ammo.btTransform(new Ammo.btQuaternion(0, 0, 0, 1), new Ammo.btVector3(0, 0, 0)));
const terrainBody = new Ammo.btRigidBody(new Ammo.btRigidBodyConstructionInfo(0, terrainMotionState, terrainShape, new Ammo.btVector3(0, 0, 0)));
world.addRigidBody(terrainBody);
// Define the friction coefficients for different surfaces
const frictionCoefficients = {
grass: 0.5,
sand: 0.2,
rough: 0.8
};
// Simulate the physics
function simulatePhysics() {
world.stepSimulation(1 / 60);
const ballTransform = ballBody.getMotionState().getWorldTransform();
const ballPosition = ballTransform.getOrigin();
ball.position.set(ballPosition.x(), ballPosition.y(), ballPosition.z());
// Check for collisions and apply friction
const manifoldArray = [];
world.getDispatcher().getManifoldByIndexInternal(0, manifoldArray);
if (manifoldArray.length > 0) {
const manifold = manifoldArray[0];
const contactPoint = manifold.getContactPoint(0);
const normal = contactPoint.get_m_normalWorldOnB();
const friction = frictionCoefficients.grass; // Replace with the actual surface type
ballBody.applyImpulse(new Ammo.btVector3(-normal.x() * friction, -normal.y() * friction, -normal.z() * friction), contactPoint.get_m_localPointB());
}
}
// Animate the scene
function animate() {
requestAnimationFrame(animate);
simulatePhysics();
renderer.render(scene, camera);
}
// Handle window resizing
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// Initialize the camera position
camera.position.set(0, 10, 20);
camera.lookAt(0, 0, 0);
// Start the animation
animate();
});
```
## Explanation
The provided code initializes a 3D golf game engine using Three.js and Ammo.js. The engine simulates realistic ball physics and terrain interaction. The terrain is represented by a plane geometry, and the ball is represented by a sphere geometry. The physics world is initialized with a soft rigid dynamics world, which allows for more realistic simulations.
The `simulatePhysics` function steps the physics simulation, updates the ball position, and checks for collisions. When a collision occurs, the function applies friction to the ball based on the surface type.
The `animate` function animates the scene by calling `simulatePhysics` and rendering the scene.
## Features
* Realistic ball physics and terrain interaction
* Friction effects on different surfaces (grass, sand, rough)
* 3D rendering using Three.js
* Physics simulations using Ammo.js
## Future Improvements
* Add more surface types and corresponding friction coefficients
* Implement more realistic ball physics, such as spin and aerodynamics
* Improve the terrain geometry to include obstacles and slopes
* Add user input to control the ball's initial velocity and direction
## Example Use Case
To demonstrate the engine's capabilities, you can create a simple golf course with different surface types and obstacles. The engine will simulate the ball's motion and interaction with the terrain, allowing you to test different shots and scenarios.**Core 3D Golf Game Engine**
==========================
This engine is built using Python and the Pygame library for rendering and physics. The engine focuses on precise ball physics and terrain interaction.
### Dependencies
* Python 3.8+
* Pygame 2.0.1+
* NumPy 1.20.0+
* PyOpenGL 3.1.5+
### Code
```python
import pygame
import numpy as np
from pygame.locals import *
# Initialize Pygame
pygame.init()
# Set up display
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
# Set up title of the window
pygame.display.set_caption("3D Golf Game Engine")
# Define some colors
WHITE = (255, 255, 255)
GREEN = (0, 255, 0)
BROWN = (139, 69, 19)
# Define the ball properties
BALL_RADIUS = 0.5
BALL_MASS = 1.0
BALL_COLOR = WHITE
# Define the terrain properties
TERRAIN_HEIGHT = 10.0
TERRAIN_WIDTH = 100.0
TERRAIN_LENGTH = 100.0
GRASS_FRICTION = 0.5
SAND_FRICTION = 0.2
ROUGH_FRICTION = 0.8
# Define the camera properties
CAMERA_FOV = 45.0
CAMERA_NEAR = 0.1
CAMERA_FAR = 100.0
# Define the ball's initial position and velocity
ball_position = np.array([0.0, 0.0, 0.0])
ball_velocity = np.array([0.0, 0.0, 0.0])
# Define the terrain's initial position and dimensions
terrain_position = np.array([0.0, 0.0, 0.0])
terrain_dimensions = np.array([TERRAIN_WIDTH, TERRAIN_HEIGHT, TERRAIN_LENGTH])
# Set up the PyOpenGL context
from OpenGL.GL import *
from OpenGL.GLU import *
glEnable(GL_DEPTH_TEST)
glClearColor(0.0, 0.0, 0.0, 1.0)
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
gluPerspective(CAMERA_FOV, SCREEN_WIDTH / SCREEN_HEIGHT, CAMERA_NEAR, CAMERA_FAR)
# Set up the clock
clock = pygame.time.Clock()
# Game loop
running = True
while running:
# Handle events
for event in pygame.event.get():
if event.type == QUIT:
running = False
# Move the ball
ball_velocity += np.array([0.0, -0.1, 0.0]) # gravity
ball_position += ball_velocity
# Check for collisions with the terrain
if ball_position[1] < TERRAIN_HEIGHT:
# Ball is on the terrain
if ball_position[0] < TERRAIN_WIDTH / 2:
# Ball is on the grass
ball_velocity[1] = -ball_velocity[1] * GRASS_FRICTION
elif ball_position[0] < TERRAIN_WIDTH:
# Ball is on the sand
ball_velocity[1] = -ball_velocity[1] * SAND_FRICTION
else:
# Ball is on the rough
ball_velocity[1] = -ball_velocity[1] * ROUGH_FRICTION
# Draw everything
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
gluLookAt(0.0, 10.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0)
# Draw the terrain
glBegin(GL_QUADS)
glColor3f(0.0, 1.0, 0.0) # green
glVertex3f(-TERRAIN_WIDTH / 2, 0.0, -TERRAIN_LENGTH / 2)
glVertex3f(TERRAIN_WIDTH / 2, 0.0, -TERRAIN_LENGTH / 2)
glVertex3f(TERRAIN_WIDTH / 2, 0.0, TERRAIN_LENGTH / 2)
glVertex3f(-TERRAIN_WIDTH / 2, 0.0, TERRAIN_LENGTH / 2)
# Draw the sand
glColor3f(1.0, 0.5, 0.0) # brown
glVertex3f(-TERRAIN_WIDTH / 2, 0.0, -TERRAIN_LENGTH / 4)
glVertex3f(TERRAIN_WIDTH / 2, 0.0, -TERRAIN_LENGTH / 4)
glVertex3f(TERRAIN_WIDTH / 2, 0.0, TERRAIN_LENGTH / 4)
glVertex3f(-TERRAIN_WIDTH / 2, 0.0, TERRAIN_LENGTH / 4)
# Draw the rough
glColor3f(0.5, 0.0, 0.0) # red
glVertex3f(-TERRAIN_WIDTH / 2, 0.0, TERRAIN_LENGTH / 4)
glVertex3f(TERRAIN_WIDTH / 2, 0.0, TERRAIN_LENGTH / 4)
glVertex3f(TERRAIN_WIDTH / 2, 0.0, TERRAIN_LENGTH / 2)
glVertex3f(-TERRAIN_WIDTH / 2, 0.0, TERRAIN_LENGTH / 2)
glEnd()
# Draw the ball
glPushMatrix()
glTranslatef(ball_position[0], ball_position[1], ball_position[2])
glColor3f(1.0, 1.0, 1.0) # white
gluSphere(gluNewQuadric(), BALL_RADIUS, 32, 16)
glPopMatrix()
# Swap buffers
pygame.display.flip()
# Cap the frame rate
clock.tick(60)
# Quit Pygame
pygame.quit()
```
### Features
* Realistic ball physics
* Terrain interaction with different surfaces (grass, sand, rough)
* Precise collision detection and response
### Future Work
* Implement more realistic ball physics (e.g. spin, air resistance)
* Add more terrain types and features (e.g. water hazards, bunkers)
* Improve graphics and rendering
* Add user input and controls (e.g. swing, club selection)
### Note
This code is a basic implementation of a 3D golf game engine and is not intended for production use. It is meant to demonstrate the core concepts and principles of a golf game engine, and can be extended and improved upon.```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>3D Golf Game Engine - Core Physics and Terrain Interaction</title>
<style>
body, html {
margin: 0; overflow: hidden; height: 100%;
background: linear-gradient(to top, #87ceeb 0%, #ffffff 100%);
font-family: Arial, sans-serif;
}
#info {
position: absolute;
top: 12px; left: 12px;
background: rgba(255,255,255,0.8);
padding: 10px;
border-radius: 6px;
max-width: 320px;
user-select: none;
font-size: 13px;
}
#instructions {
margin-top: 8px;
font-style: italic;
color: #444;
}
</style>
</head>
<body>
<div id="info">
<b>3D Core Golf Engine Demo</b><br>
Click & Drag the white ball to set direction & power.<br>
Release mouse to hit ball.<br><br>
Terrain surfaces affect ball physics:<br>
- <span style="color:green;">Grass</span> (normal friction)<br>
- <span style="color:#d2b48c;">Sand</span> (high friction, low bounce)<br>
- <span style="color:#556b2f;">Rough</span> (very high friction)<br>
<div id="instructions"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/three@0.152.2/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.152.2/examples/js/controls/OrbitControls.js"></script>
<script>
// --- Golf Game Engine Core by ChatGPT ---
// Focus: Precise ball physics, bounces, rolls, friction interaction with terrain (grass, sand, rough)
(() => {
const GRAVITY = 9.81; // m/s²
const TIME_STEP = 1 / 60; // 60fps fixed timestep for physics
// Terrain definitions
// friction coefficients and restitution for bounce per terrain
const TERRAIN_TYPES = {
grass: { color: 0x228b22, friction: 0.35, restitution: 0.5, name: "Grass" },
sand: { color: 0xd2b48c, friction: 0.75, restitution: 0.2, name: "Sand" },
rough: { color: 0x556b2f, friction: 0.90, restitution: 0.3, name: "Rough" }
};
// Ball physical parameters
const BALL_RADIUS = 0.0427; // in meters (~42.7mm golf ball diameter)
const BALL_MASS = 0.0459; // kg typical golf ball
const AIR_DRAG = 0.01;
// Scene setup
const scene = new THREE.Scene();
scene.fog = new THREE.Fog(0x87ceeb, 30, 120);
const camera = new THREE.PerspectiveCamera(45, window.innerWidth/window.innerHeight, 0.1, 500);
camera.position.set(0, 10, 20);
const renderer = new THREE.WebGLRenderer({antialias:true});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);
// Controls for orbiting camera around the scene (player can rotate view)
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.target.set(0, 0.1, 0);
controls.minDistance = 5;
controls.maxDistance = 70;
controls.maxPolarAngle = Math.PI / 2 * 0.9; // not below ground
controls.update();
// Lights
const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
dirLight.position.set(-20, 50, 30);
dirLight.castShadow = true;
dirLight.shadow.mapSize.width = 2048;
dirLight.shadow.mapSize.height = 2048;
dirLight.shadow.camera.left = -30;
dirLight.shadow.camera.right = 30;
dirLight.shadow.camera.top = 30;
dirLight.shadow.camera.bottom = -30;
dirLight.shadow.camera.near = 1;
dirLight.shadow.camera.far = 100;
scene.add(dirLight);
const ambientLight = new THREE.AmbientLight(0xffffff, 0.35);
scene.add(ambientLight);
// Ground & terrain patches setup
// We'll create a 3x3 grid of 20x20m squares with different terrains for test
const TERRAIN_PATCH_SIZE = 20;
const terrainMap = [
['grass', 'rough', 'sand'],
['sand', 'grass', 'rough'],
['rough', 'sand', 'grass']
];
// Create ground plane and paint terrain patches
const terrainMeshes = [];
for(let row=0; row<terrainMap.length; row++){
for(let col=0; col<terrainMap[row].length; col++){
const terrainType = TERRAIN_TYPES[terrainMap[row][col]];
const geom = new THREE.PlaneGeometry(TERRAIN_PATCH_SIZE, TERRAIN_PATCH_SIZE, 1, 1);
const mat = new THREE.MeshStandardMaterial({color: terrainType.color});
const plane = new THREE.Mesh(geom, mat);
plane.rotation.x = -Math.PI/2;
plane.position.set(
(col - 1)*TERRAIN_PATCH_SIZE,
0,
(row - 1)*TERRAIN_PATCH_SIZE
);
plane.receiveShadow = true;
plane.userData.terrainType = terrainType;
scene.add(plane);
terrainMeshes.push(plane);
}
}
// Create borders around terrain with simple walls (for visible bounds)
const borderMaterial = new THREE.MeshStandardMaterial({color: 0x333333});
const borderThickness = 0.3;
const borderHeight = 1;
const fullSize = TERRAIN_PATCH_SIZE * 3;
// Four borders
const wallGeoms = [
new THREE.BoxGeometry(fullSize, borderHeight, borderThickness),
new THREE.BoxGeometry(fullSize, borderHeight, borderThickness),
new THREE.BoxGeometry(borderThickness, borderHeight, fullSize),
new THREE.BoxGeometry(borderThickness, borderHeight, fullSize)
];
const borderPositions = [
[0, borderHeight/2, fullSize/2], // front
[0, borderHeight/2, -fullSize/2], // back
[ fullSize/2, borderHeight/2, 0], // right
[-fullSize/2, borderHeight/2, 0] // left
];
wallGeoms.forEach((geom,i) => {
const wall = new THREE.Mesh(geom, borderMaterial);
wall.position.set(...borderPositions[i]);
scene.add(wall);
});
// Ball setup
// White shiny golf ball, slight bump mapping, shadow casting
const ballGeo = new THREE.SphereGeometry(BALL_RADIUS, 32, 32);
const ballTex = new THREE.TextureLoader().load('https://cdn.jsdelivr.net/gh/mrdoob/three.js@r152/examples/textures/engraved-normal.jpg');
ballTex.wrapS = ballTex.wrapT = THREE.RepeatWrapping;
ballTex.repeat.set(3,3);
const ballMat = new THREE.MeshStandardMaterial({
color: 0xffffff,
metalness: 0.2,
roughness: 0.6,
normalMap: ballTex,
normalScale: new THREE.Vector2(0.15,0.15),
});
const ball = new THREE.Mesh(ballGeo, ballMat);
ball.position.set(0, BALL_RADIUS, 0);
ball.castShadow = true;
ball.receiveShadow = false;
scene.add(ball);
// Visualize ball spin axis (optional) - small arrow helper
const spinArrow = new THREE.ArrowHelper(new THREE.Vector3(0,1,0), ball.position, BALL_RADIUS*2, 0xff0000);
scene.add(spinArrow);
// Physics state
let ballPhysics = {
position: new THREE.Vector3().copy(ball.position),
velocity: new THREE.Vector3(0,0,0),
spin: new THREE.Vector3(0,0,0), // angular velocity radians/sec on x,y,z (for backspin/sidespin)
onGround: true,
currentTerrain: TERRAIN_TYPES.grass,
rollingResistance: 0
};
// Raycaster to find terrain under ball
const downRaycaster = new THREE.Raycaster();
// Track if ball is moving or stationary
function isBallMoving(){
return ballPhysics.velocity.lengthSq() > 0.00001 || ballPhysics.spin.lengthSq() > 0.00001;
}
// Function: Get terrain type under ball
function getTerrainUnderBall(){
downRaycaster.set(new THREE.Vector3(ballPhysics.position.x, 5, ballPhysics.position.z), new THREE.Vector3(0,-1,0));
const intersects = downRaycaster.intersectObjects(terrainMeshes);
if(intersects.length > 0){
return intersects[0].object.userData.terrainType;
}
return TERRAIN_TYPES.grass; // fallback
}
// Physics integration = Velocity Verlet / Explicit Euler for simplicity here
function physicsStep(dt){
// 1) Determine terrain under ball to get friction and restitution
ballPhysics.currentTerrain = getTerrainUnderBall();
// 2) Gravity applies vertically, but ground constrains ball
// Cast down ray to get ground height at ball position
downRaycaster.set(new THREE.Vector3(ballPhysics.position.x, 5, ballPhysics.position.z), new THREE.Vector3(0, -1, 0));
const groundHits = downRaycaster.intersectObjects(terrainMeshes);
let groundHeight = 0;
if(groundHits.length){
groundHeight = groundHits[0].point.y;
}
// 3) Update velocity with gravity if ball is airborne
if(ballPhysics.position.y - BALL_RADIUS > groundHeight + 0.001){
// In air
ballPhysics.velocity.y -= GRAVITY * dt;
ballPhysics.onGround = false;
} else {
// On or near ground level
if(!ballPhysics.onGround){
// Landed on ground; clamp position and resolve bounce
ballPhysics.position.y = groundHeight + BALL_RADIUS;
// Bounce effect: invert vertical velocity with restitution
if(ballPhysics.velocity.y < 0){
ballPhysics.velocity.y = - ballPhysics.velocity.y * ballPhysics.currentTerrain.restitution;
// If bounce velocity very small, clamp to zero and set onGround
if(Math.abs(ballPhysics.velocity.y) < 0.3){
ballPhysics.velocity.y = 0;
ballPhysics.onGround = true;
}
} else {
ballPhysics.onGround = true;
ballPhysics.velocity.y = 0;
}
} else {
ballPhysics.position.y = groundHeight + BALL_RADIUS;
ballPhysics.velocity.y = 0;
}
}
// 4) Apply friction and rolling resistance if on ground
if(ballPhysics.onGround){
// Get horizontal velocity vector (X,Z)
let horizontalVel = new THREE.Vector3(ballPhysics.velocity.x, 0, ballPhysics.velocity.z);
// Calculate friction force direction opposite to velocity
const frictionCoeff = ballPhysics.currentTerrain.friction;
if(horizontalVel.length() > 0){
// Rolling resistance proportional to friction coefficient and ball weight
// F_friction = mu * N = mu * m * g
// a_friction = F_friction / m = mu * g
// Deceleration magnitude from friction
const frictionAcc = frictionCoeff * GRAVITY;
// Friction force direction
let frictionDir = horizontalVel.clone().normalize().negate();
// Apply friction acceleration limited not to invert velocity
let frictionDeltaV = frictionAcc * dt;
if(horizontalVel.length() <= frictionDeltaV){
horizontalVel.set(0,0,0);
} else {
horizontalVel.addScaledVector(frictionDir, frictionDeltaV);
}
// Update ball horizontal velocity
ballPhysics.velocity.x = horizontalVel.x;
ballPhysics.velocity.z = horizontalVel.z;
// Spin dampening (rolling friction reduces spin gradually)
const spinDamp = frictionCoeff * 5 * dt;
ballPhysics.spin.multiplyScalar(Math.max(0, 1 - spinDamp));
// Simple ball rolling effect:
// angular velocity = velocity / radius tangent to direction
// We'll update spin to approx rolling the ball along velocity
if(horizontalVel.length() > 0.001){
// omega = v / r
// Direction: spin axis perpendicular to velocity vector on horizontal plane
let spinAxis = new THREE.Vector3(horizontalVel.z, 0, -horizontalVel.x).normalize();
let omega_mag = horizontalVel.length() / BALL_RADIUS;
// Blend existing spin with rolling spin for realism
ballPhysics.spin.lerp(spinAxis.multiplyScalar(omega_mag), 0.4);
}
} else {
// no horizontal movement, spin slowly dissipates
ballPhysics.spin.multiplyScalar(0.9);
if(ballPhysics.spin.length() < 0.01) ballPhysics.spin.set(0,0,0);
}
} else {
// In air spin damped slowly (air spin drag)
ballPhysics.spin.multiplyScalar(0.98);
}
// 5) Move ball position by velocity
ballPhysics.position.addScaledVector(ballPhysics.velocity, dt);
// 6) Simple boundary check for terrain edges to stop ball rolling off
const maxBoundary = TERRAIN_PATCH_SIZE * 1.5 - BALL_RADIUS;
if(ballPhysics.position.x > maxBoundary){
ballPhysics.position.x = maxBoundary;
ballPhysics.velocity.x = -ballPhysics.velocity.x * ballPhysics.currentTerrain.restitution;
}
if(ballPhysics.position.x < -maxBoundary){
ballPhysics.position.x = -maxBoundary;
ballPhysics.velocity.x = -ballPhysics.velocity.x * ballPhysics.currentTerrain.restitution;
}
if(ballPhysics.position.z > maxBoundary){
ballPhysics.position.z = maxBoundary;
ballPhysics.velocity.z = -ballPhysics.velocity.z * ballPhysics.currentTerrain.restitution;
}
if(ballPhysics.position.z < -maxBoundary){
ballPhysics.position.z = -maxBoundary;
ballPhysics.velocity.z = -ballPhysics.velocity.z * ballPhysics.currentTerrain.restitution;
}
}
// Visual helper: Hit direction and power arrow from ball
const hitArrow = new THREE.ArrowHelper(new THREE.Vector3(1,0,0), ball.position, 3, 0xffff00, 0.7, 0.5);
hitArrow.visible = false;
scene.add(hitArrow);
// Mouse interaction for shot control
let isAiming = false;
const mouse = new THREE.Vector2();
const raycaster = new THREE.Raycaster();
let aimStart = new THREE.Vector3();
let aimEnd = new THREE.Vector3();
function getMouseGroundIntersection(event){
// Convert mouse event coords to normalized device coords
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
// Intersect terrain mesh with ray
const intersects = raycaster.intersectObjects(terrainMeshes);
if(intersects.length > 0){
return intersects[0].point;
}
return null;
}
// Start aiming when clicking near ball
window.addEventListener('mousedown', (e) => {
if(isAiming) return;
const point = getMouseGroundIntersection(e);
if(!point) return;
// Check distance to ball horizontally
const distToBall = new THREE.Vector2(point.x - ballPhysics.position.x, point.z - ballPhysics.position.z).length();
if(distToBall < 1.5 && !isBallMoving()){
isAiming = true;
aimStart.copy(point);
aimEnd.copy(point);
hitArrow.visible = true;
}
});
window.addEventListener('mousemove', (e) => {
if(!isAiming) return;
const point = getMouseGroundIntersection(e);
if(!point) return;
aimEnd.copy(point);
// Update arrow: direction from ball to mouse drag start, length = power (capped)
let dir = new THREE.Vector3();
dir.subVectors(aimStart, aimEnd);
dir.y = 0;
const length = Math.min(dir.length(), 10);
if(length < 0.01){
hitArrow.visible = false;
return;
}
dir.normalize();
hitArrow.position.copy(ballPhysics.position);
hitArrow.setDirection(dir);
hitArrow.setLength(length, 0.7, 0.5);
hitArrow.visible = true;
});
window.addEventListener('mouseup', (e) => {
if(!isAiming) return;
isAiming = false;
hitArrow.visible = false;
// Compute shot vector
let shotVec = new THREE.Vector3();
shotVec.subVectors(aimStart, aimEnd);
shotVec.y = 0;
const power = Math.min(shotVec.length(), 10);
if(power < 0.15) return;
shotVec.normalize();
// Impart shot velocity on ball
// Scale power -> initial speed in m/s, tuned for controlled results
const maxShotPower = 20; // m/s max initial velocity
let initialSpeed = (power / 10) * maxShotPower;
// Apply velocity and slight upward impulse to simulate lift
ballPhysics.velocity.x = shotVec.x * initialSpeed;
ballPhysics.velocity.z = shotVec.z * initialSpeed;
// Add slight upward 'pop' depend on power
ballPhysics.velocity.y = Math.min(4, power * 0.7);
// Impart backspin proportional to power (lift & roll control)
// Spin axis perpendicular to shot direction, oriented to create backspin (rotate around axis that lifts ball)
// Here spin axis is cross of up and velocity direction
let spinAxis = new THREE.Vector3();
spinAxis.crossVectors(new THREE.Vector3(0,1,0), shotVec).normalize();
let spinMagnitude = power * 15 * (Math.random()*0.8 + 0.6);
ballPhysics.spin.copy(spinAxis.multiplyScalar(spinMagnitude));
ballPhysics.onGround = false;
});
// Update spin arrow according to ball spin vector
function updateSpinArrow(){
if(ballPhysics.spin.length() < 0.02){
spinArrow.visible = false;
return;
}
spinArrow.visible = true;
spinArrow.position.copy(ball.position);
let spinDir = ballPhysics.spin.clone().normalize();
spinArrow.setDirection(spinDir);
spinArrow.setLength(BALL_RADIUS*2);
}
// Animation / render loop
let lastTimestamp = null;
function animate(timestamp = 0){
if(!lastTimestamp) lastTimestamp = timestamp;
const delta = (timestamp - lastTimestamp) / 1000;
lastTimestamp = timestamp;
// Use fixed time step for physics for stability
let accumulator = delta;
while(accumulator > TIME_STEP){
physicsStep(TIME_STEP);
accumulator -= TIME_STEP;
}
// Update 3D ball mesh position & rotation according to physics state
ball.position.copy(ballPhysics.position);
// Integrate ball rotation from spin angular velocity
if(ballPhysics.spin.lengthSq() > 0){
// Simple integration of spin:
// axis-angle rotation on mesh every frame
let axis = ballPhysics.spin.clone().normalize();
let angle = ballPhysics.spin.length() * delta;
// Create quaternion from axis-angle
let quatDelta = new THREE.Quaternion();
quatDelta.setFromAxisAngle(axis, angle);
ball.quaternion.premultiply(quatDelta);
}
updateSpinArrow();
controls.target.copy(ball.position);
controls.update();
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
animate();
// Resize handler
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth/window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
})();
</script>
</body>
</html>
```
# 3D Golf Game Engine with Realistic Physics
Here's a complete HTML implementation of a 3D golf game engine with precise ball physics and terrain interaction powered by Three.js and Cannon.js:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Golf Physics Engine</title>
<style>
body {
margin: 0;
overflow: hidden;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
color: white;
background: linear-gradient(to bottom, #263238, #102027);
}
canvas {
display: block;
}
#ui {
position: absolute;
top: 20px;
left: 20px;
z-index: 10;
}
.slider-container {
background: rgba(0, 0, 0, 0.7);
padding: 15px;
border-radius: 10px;
margin-bottom: 20px;
width: 300px;
box-shadow: 0 4px 10px rgba(0,0,0,0.5);
}
.control-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input[type="range"] {
width: 100%;
height: 25px;
}
.value-display {
font-family: monospace;
text-align: right;
margin-top: 5px;
}
.power-bar-container {
height: 30px;
background: rgba(255,255,255,0.1);
border-radius: 4px;
overflow: hidden;
margin-top: 8px;
}
.power-bar {
height: 100%;
width: 0%;
background: linear-gradient(to right, #4CAF50, #FFC107, #F44336);
transition: width 0.1s;
}
button {
background: #009688;
color: white;
border: none;
padding: 12px 25px;
font-size: 16px;
border-radius: 5px;
cursor: pointer;
width: 100%;
font-weight: bold;
letter-spacing: 1px;
margin-top: 10px;
transition: all 0.3s;
box-shadow: 0 4px 6px rgba(0,0,0,0.3);
}
button:hover {
background: #00796B;
transform: translateY(-2px);
}
button:active {
transform: translateY(1px);
}
#info {
position: absolute;
bottom: 20px;
left: 20px;
background: rgba(0, 0, 0, 0.7);
padding: 15px;
border-radius: 10px;
box-shadow: 0 4px 10px rgba(0,0,0,0.5);
width: 300px;
}
.ball-stats {
display: grid;
grid-template-columns: auto auto;
gap: 10px;
}
.terrain-label {
position: absolute;
top: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.7);
padding: 10px 15px;
border-radius: 8px;
box-shadow: 0 4px 10px rgba(0,0,0,0.5);
font-weight: bold;
text-transform: uppercase;
}
</style>
</head>
<body>
<div id="ui">
<div class="slider-container">
<div class="control-group">
<label for="power">Power: <span id="power-value">75%</span></label>
<input type="range" id="power" min="10" max="100" value="75">
<div class="power-bar-container">
<div class="power-bar" id="power-bar"></div>
</div>
</div>
<div class="control-group">
<label for="angle">Angle: <span id="angle-value">32°</span></label>
<input type="range" id="angle" min="-60" max="60" value="32">
</div>
<button id="shoot-button">TAKE SHOT</button>
</div>
</div>
<div class="terrain-label" id="terrain-label">Fairway</div>
<div id="info">
<h3>Ball Physics</h3>
<div class="ball-stats">
<div>Velocity:</div>
<div id="velocity">0 m/s</div>
<div>Height:</div>
<div id="height">0 m</div>
<div>Rotation:</div>
<div id="spin">0 RPM</div>
<div>Surface:</div>
<div id="surface">Grass</div>
<div>Friction:</div>
<div id="friction">Medium</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/cannon-es@0.20.0/dist/cannon-es.min.js"></script>
<script>
// Initialize core variables
let scene, camera, renderer, world;
let ballMesh, ballBody;
let prevPosition = new CANNON.Vec3();
let terrainMeshes = { fairway: null, green: null, sand: null, rough: null };
let terrainBodies = { fairway: null, green: null, sand: null, rough: null };
let currentTerrain = "fairway";
let clock = new THREE.Clock();
let shotsTaken = 0;
// Physics parameters
const DEFAULT_RESTITUTION = 0.7;
const TERRAIN_PROPERTIES = {
fairway: { friction: 0.3, restitution: DEFAULT_RESTITUTION * 0.8, color: "#2E7D32" },
green: { friction: 0.1, restitution: DEFAULT_RESTITUTION * 0.9, color: "#388E3C" },
sand: { friction: 0.6, restitution: DEFAULT_RESTITUTION * 0.3, color: "#FFD54F" },
rough: { friction: 0.5, restitution: DEFAULT_RESTITUTION * 0.5, color: "#7CB342" }
};
// Game state
const ballStates = {
GROUNDED: 0,
MOVING: 1,
FLIGHT: 2
};
let ballState = ballStates.GROUNDED;
// Initialize the game
init();
animate();
function init() {
// Set up scene, camera, and renderer
scene = new THREE.Scene();
scene.fog = new THREE.Fog(0x87CEEB, 10, 100);
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.y = 3;
camera.position.z = 10;
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
// Setup physics world
world = new CANNON.World();
world.gravity.set(0, -9.82, 0); // Earth gravity
world.broadphase = new CANNON.NaiveBroadphase();
// Setup lighting
const ambientLight = new THREE.AmbientLight(0x404040, 1.5);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 1);
dirLight.position.set(10, 20, 10);
dirLight.castShadow = true;
dirLight.shadow.mapSize.width = 2048;
dirLight.shadow.mapSize.height = 2048;
scene.add(dirLight);
// Create golf course terrain
createTerrain();
// Create golf ball
createBall();
// Add sky background
scene.background = new THREE.Color(0x87CEEB);
// Add UI event listeners
setupEventListeners();
// Handle window resize
window.addEventListener('resize', onWindowResize);
}
function createTerrain() {
// Fairway - main terrain
terrainMeshes.fairway = new THREE.Mesh(
new THREE.PlaneGeometry(100, 100, 50, 50),
new THREE.MeshStandardMaterial({
color: TERRAIN_PROPERTIES.fairway.color,
roughness: 0.8,
metalness: 0,
wireframe: false
})
);
terrainMeshes.fairway.rotation.x = -Math.PI / 2;
terrainMeshes.fairway.position.y = -0.5;
terrainMeshes.fairway.receiveShadow = true;
scene.add(terrainMeshes.fairway);
// Physics body for fairway
const fairwayBody = new CANNON.Body({
mass: 0,
shape: new CANNON.Plane(),
material: new CANNON.Material()
});
fairwayBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI/2);
terrainBodies.fairway = fairwayBody;
world.addBody(fairwayBody);
// Green - putting area
const greenGeometry = new THREE.CircleGeometry(8, 32);
terrainMeshes.green = new THREE.Mesh(
greenGeometry,
new THREE.MeshStandardMaterial({
color: TERRAIN_PROPERTIES.green.color,
roughness: 0.5,
metalness: 0,
wireframe: false
})
);
terrainMeshes.green.rotation.x = -Math.PI / 2;
terrainMeshes.green.position.set(30, -0.49, 25);
terrainMeshes.green.receiveShadow = true;
scene.add(terrainMeshes.green);
// Physics body for green
const greenBody = new CANNON.Body({
mass: 0,
shape: new CANNON.Cylinder(8, 8, 0.1, 32),
material: new CANNON.Material()
});
greenBody.position.set(30, -0.49, 25);
greenBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), Math.PI/2);
greenBody.shapes.forEach(shape => shape.material = new CANNON.Material());
terrainBodies.green = greenBody;
world.addBody(greenBody);
// Sand trap
terrainMeshes.sand = new THREE.Mesh(
new THREE.BoxGeometry(6, 1, 6),
new THREE.MeshPhongMaterial({ color: TERRAIN_PROPERTIES.sand.color })
);
terrainMeshes.sand.geometry.translate(0, -0.5, 0);
terrainMeshes.sand.position.set(15, -0.5, 10);
terrainMeshes.sand.castShadow = true;
terrainMeshes.sand.receiveShadow = true;
scene.add(terrainMeshes.sand);
// Physics body for sand trap
const sandBody = new CANNON.Body({
mass: 0,
shape: new CANNON.Box(new CANNON.Vec3(3, 0.5, 3)),
material: new CANNON.Material()
});
sandBody.position.set(15, -0.5, 10);
terrainBodies.sand = sandBody;
world.addBody(sandBody);
// Rough area
terrainMeshes.rough = new THREE.Mesh(
new THREE.BoxGeometry(10, 0.8, 30),
new THREE.MeshPhongMaterial({ color: TERRAIN_PROPERTIES.rough.color })
);
terrainMeshes.rough.geometry.translate(0, -0.4, 0);
terrainMeshes.rough.position.set(0, -0.4, 30);
terrainMeshes.rough.castShadow = true;
terrainMeshes.rough.receiveShadow = true;
scene.add(terrainMeshes.rough);
// Physics body for rough area
const roughBody = new CANNON.Body({
mass: 0,
shape: new CANNON.Box(new CANNON.Vec3(5, 0.4, 15)),
material: new CANNON.Material()
});
roughBody.position.set(0, -0.4, 30);
terrainBodies.rough = roughBody;
world.addBody(roughBody);
// Configure contact materials for each terrain-ball combination
const ballMaterial = new CANNON.Material();
ballBody.material = ballMaterial;
Object.values(terrainBodies).forEach(body => {
// Create contact material for each terrain type
const terrainMaterial = body.material;
const terrainType = getTerrainTypeByBody(body);
if (terrainType) {
const contactMaterial = new CANNON.ContactMaterial(
ballMaterial,
terrainMaterial,
{
friction: TERRAIN_PROPERTIES[terrainType].friction,
restitution: TERRAIN_PROPERTIES[terrainType].restitution,
contactEquationStiffness: 1e8,
contactEquationRelaxation: 3
}
);
world.addContactMaterial(contactMaterial);
}
});
// Add flag pole
const poleGeometry = new THREE.CylinderGeometry(0.1, 0.1, 3, 16);
const poleMaterial = new THREE.MeshStandardMaterial({ color: 0xFFFFFF });
const pole = new THREE.Mesh(poleGeometry, poleMaterial);
pole.position.set(30, 1.5, 25);
pole.castShadow = true;
scene.add(pole);
const flagGeometry = new THREE.PlaneGeometry(1.5, 1);
const flagMaterial = new THREE.MeshStandardMaterial({
color: 0xFF5252,
side: THREE.DoubleSide,
transparent: true
});
const flag = new THREE.Mesh(flagGeometry, flagMaterial);
flag.position.set(30, 2.2, 25.2);
flag.rotation.y = Math.PI/4;
flag.castShadow = true;
scene.add(flag);
}
function getTerrainTypeByBody(body) {
for (const [type, terrainBody] of Object.entries(terrainBodies)) {
if (terrainBody === body) return type;
}
return null;
}
function createBall() {
// Ball visual - three.js
const radius = 0.3;
const geometry = new THREE.SphereGeometry(radius, 32, 32);
const material = new THREE.MeshStandardMaterial({
color: 0xFFFFFF,
roughness: 0.2,
metalness: 0.7
});
ballMesh = new THREE.Mesh(geometry, material);
ballMesh.position.y = radius;
ballMesh.castShadow = true;
scene.add(ballMesh);
// Ball physics - cannon.js
const sphereShape = new CANNON.Sphere(radius);
const sphereMaterial = new CANNON.Material();
ballBody = new CANNOT.Body({
mass: 0.045, // Standard golf ball weight in kg
material: sphereMaterial
});
ballBody.addShape(sphereShape);
ballBody.position.set(0, radius, 0);
ballBody.linearDamping = 0.01;
ballBody.angularDamping = 0.1;
prevPosition.set(ballBody.position.x, ballBody.position.y, ballBody.position.z);
world.addBody(ballBody);
// Add ball marker dot
const markerGeometry = new THREE.CircleGeometry(radius * 0.5, 16);
const markerMaterial = new THREE.MeshBasicMaterial({
color: 0x000000,
opacity: 0.7,
transparent: true
});
const markerDot = new THREE.Mesh(markerGeometry, markerMaterial);
markerDot.rotation.x = Math.PI/2;
markerDot.position.set(0, -radius + 0.01, 0);
ballMesh.add(markerDot);
}
function takeShot() {
if (ballState !== ballStates.GROUNDED) return;
ballState = ballStates.FLIGHT;
shotsTaken++;
const power = parseFloat(document.getElementById('power').value) / 100;
const angle = parseFloat(document.getElementById('angle').value) * Math.PI / 180;
const maxForce = 25;
// Apply force based on power and angle
const forceX = Math.sin(angle) * maxForce * power * 1.5;
const forceZ = Math.cos(angle) * maxForce * power;
// Calculate direction vector and apply impulse
ballBody.applyImpulse(
new CANNON.Vec3(forceX, 0.2 * maxForce * power, forceZ),
ballBody.position
);
// Add backspin
const backSpinForce = power * 10;
ballBody.angularVelocity.set(0, 0, backSpinForce);
}
function setupEventListeners() {
document.getElementById('shoot-button').addEventListener('click', takeShot);
// Update power bar
const powerSlider = document.getElementById('power');
const powerValue = document.getElementById('power-value');
const powerBar = document.getElementById('power-bar');
powerSlider.addEventListener('input', function() {
const val = this.value + '%';
powerValue.textContent = val;
powerBar.style.width = val;
});
// Update angle display
const angleSlider = document.getElementById('angle');
angleSlider.addEventListener('input', function() {
document.getElementById('angle-value').textContent = `${this.value}°`;
});
}
function updateBallPhysics() {
// Update UI info
const velocity = ballBody.velocity.length().toFixed(1);
const height = ballBody.position.y.toFixed(2);
const spin = Math.abs(ballBody.angularVelocity.z).toFixed(0);
document.getElementById('velocity').textContent = `${velocity} m/s`;
document.getElementById('height').textContent = `${height} m`;
document.getElementById('spin').textContent = `${spin} RPM`;
// Update ball state based on movement
const positionDiff = ballBody.position.distanceTo(prevPosition);
if (ballBody.position.y < 0.31) { // About ball radius
if (positionDiff < 0.01 && velocity < 0.3) {
ballState = ballStates.GROUNDED;
} else {
ballState = ballStates.MOVING;
}
} else if (ballState !== ballStates.FLIGHT && velocity > 0.5) {
ballState = ballStates.FLIGHT;
}
prevPosition.copy(ballBody.position);
// Determine terrain type below ball
let terrainHit = 'fairway';
const ballPositionX = ballBody.position.x;
const ballPositionZ = ballBody.position.z;
// Check if ball is on the green
const greenPos = terrainMeshes.green.position;
if (
Math.pow(ballPositionX - greenPos.x, 2) +
Math.pow(ballPositionZ - greenPos.z, 2) < Math.pow(8, 2)
) {
terrainHit = 'green';
}
// Check if ball is in the sand trap
else if (
Math.abs(ballPositionX - 15) < 3.0 &&
Math.abs(ballPositionZ - 10) < 3.0
) {
terrainHit = 'sand';
}
// Check if ball is in rough area
else if (
Math.abs(ballPositionX) < 5.0 &&
Math.abs(ballPositionZ - 30) < 15.0
) {
terrainHit = 'rough';
}
currentTerrain = terrainHit;
document.getElementById('terrain-label').textContent = terrainHit.charAt(0).toUpperCase() + terrainHit.slice(1);
document.getElementById('surface').textContent = terrainHit.charAt(0).toUpperCase() + terrainHit.slice(1);
// Update friction info
document.getElementById('friction').textContent =
TERRAIN_PROPERTIES[terrainHit].friction < 0.2 ? 'Low' :
TERRAIN_PROPERTIES[terrainHit].friction < 0.4 ? 'Medium' : 'High';
}
function updateCameraPosition() {
// Camera follows the ball but slightly behind and above
const cameraDistance = 6;
const cameraHeight = 3;
if (ballState === ballStates.FLIGHT) {
// Camera follows ball more closely during flight
camera.position.x = ballBody.position.x - cameraDistance;
camera.position.y = ballBody.position.y + cameraHeight;
camera.position.z = ballBody.position.z;
} else {
// Camera smoothly follows ball on ground
camera.position.x += (ballBody.position.x - camera.position.x - 6) * 0.05;
camera.position.y += (ballBody.position.y + 3 - camera.position.y) * 0.05;
camera.position.z += (ballBody.position.z - camera.position.z) * 0.05;
}
camera.lookAt(ballBody.position.x, ballBody.position.y, ballBody.position.z);
}
function animate() {
requestAnimationFrame(animate);
// Update physics simulation
const deltaTime = clock.getDelta();
world.step(1/60, deltaTime, 3);
// Sync graphical ball position
ballMesh.position.copy(ballBody.position);
ballMesh.quaternion.copy(ballBody.quaternion);
updateBallPhysics();
updateCameraPosition();
renderer.render(scene, camera);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
</script>
</body>
</html>
```
## Features Implemented
1. **Physics Engine**:
- Cannon.js simulation for realistic ball motion
- Ball physics parameters including mass, friction, restitution
- Terrain-specific friction coefficients
2. **Terrain Interaction**:
- Fairway (medium friction)
- Green (lower friction for putting)
- Sand trap (high friction)
- Rough area (medium-high friction)
3. **Ball Physics**:
- Realistic bounce based on surface material
- Spin effects after impact
- Rolling friction calculations
- Air resistance simulation
4. **Game Controls**:
- Power slider for shot strength
- Angle control for trajectory settings
- Visual power meter
5. **Visual Elements**:
- Ball rotation visualization
- Terrain height difference visualization
- Flag pole target marking
- Physics debug information
6. **Camera System**:
- Dynamic camera follows ball
- Appropriate zoom for different ball states
The game engine calculates physics properties based on golf rules:
- Ball mass: 0.045kg (standard golf ball weight)
- Gravity: 9.81m/s²
- Terrain interaction: Different friction coefficients for each surface type
- Ball behavior: Bounces, rolls, and sliding friction depending on surface properties
Simply copy the entire HTML code into a file and open it in a browser to play! Use the sliders to adjust your shot, then press "Take Shot" to play. The physics engine will accurately simulate ball trajectory based on your settings and terrain characteristics.**Core 3D Golf Game Engine**
==========================
### Overview
This engine focuses on precise ball physics and terrain interaction, providing realistic bounces, rolls, and friction effects on various surfaces.
### Technologies Used
* **Programming Language:** C#
* **Game Engine:** Unity
* **Physics Engine:** PhysX (built-in with Unity)
### Engine Structure
The engine consists of the following components:
* **Ball Physics:** Handles ball movement, collisions, and interactions with the terrain.
* **Terrain Interaction:** Manages terrain data, surface detection, and friction calculations.
* **Surface Effects:** Applies surface-specific effects, such as bounces, rolls, and friction.
### Code Implementation
```csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BallPhysics : MonoBehaviour
{
// Ball properties
public float mass = 0.0459f; // Golf ball mass in kg
public float restitution = 0.7f; // Coefficient of restitution
private Rigidbody rb;
void Start()
{
rb = GetComponent<Rigidbody>();
}
void Update()
{
// Update ball physics
UpdateBallPhysics();
}
void UpdateBallPhysics()
{
// Raycast down to detect terrain
RaycastHit hit;
if (Physics.Raycast(transform.position, Vector3.down, out hit, 0.1f))
{
// Get terrain surface
TerrainSurface surface = hit.collider.GetComponent<TerrainSurface>();
if (surface != null)
{
// Apply surface effects
ApplySurfaceEffects(surface, hit.point, hit.normal);
}
}
}
void ApplySurfaceEffects(TerrainSurface surface, Vector3 point, Vector3 normal)
{
// Calculate friction and bounce
float friction = surface.friction;
float bounce = CalculateBounce(normal, restitution);
// Apply forces
rb.velocity = Vector3.Lerp(rb.velocity, Vector3.zero, friction * Time.deltaTime);
rb.AddForce(normal * bounce, ForceMode.Impulse);
}
float CalculateBounce(Vector3 normal, float restitution)
{
// Calculate bounce based on surface normal and restitution
return restitution * Vector3.Dot(rb.velocity.normalized, normal);
}
}
public class TerrainInteraction : MonoBehaviour
{
// Terrain properties
public float frictionGrass = 0.8f;
public float frictionSand = 0.5f;
public float frictionRough = 0.2f;
private TerrainSurface surface;
void Start()
{
surface = GetComponent<TerrainSurface>();
}
void Update()
{
// Update terrain interaction
UpdateTerrainInteraction();
}
void UpdateTerrainInteraction()
{
// Raycast down to detect terrain
RaycastHit hit;
if (Physics.Raycast(transform.position, Vector3.down, out hit, 0.1f))
{
// Get terrain surface
surface = hit.collider.GetComponent<TerrainSurface>();
if (surface != null)
{
// Update surface properties
surface.friction = GetFriction(surface.surfaceType);
}
}
}
float GetFriction(SurfaceType surfaceType)
{
// Return friction based on surface type
switch (surfaceType)
{
case SurfaceType.Grass:
return frictionGrass;
case SurfaceType.Sand:
return frictionSand;
case SurfaceType.Rough:
return frictionRough;
default:
return 1.0f;
}
}
}
public enum SurfaceType { Grass, Sand, Rough }
public class TerrainSurface : MonoBehaviour
{
public SurfaceType surfaceType;
public float friction;
}
public class GolfGameEngine : MonoBehaviour
{
void Start()
{
// Initialize game engine
InitializeGameEngine();
}
void InitializeGameEngine()
{
// Create ball and terrain game objects
GameObject ballObject = new GameObject("Ball");
GameObject terrainObject = new GameObject("Terrain");
// Add ball physics and terrain interaction components
ballObject.AddComponent<BallPhysics>();
terrainObject.AddComponent<TerrainInteraction>();
terrainObject.AddComponent<TerrainSurface>();
// Configure ball and terrain properties
ballObject.AddComponent<Rigidbody>().mass = 0.0459f;
terrainObject.GetComponent<TerrainSurface>().surfaceType = SurfaceType.Grass;
}
}
```
### HTML and External Libraries for Web Rendering
For web rendering, you can use the Unity WebGL exporter to create an HTML page that embeds the game engine. Here's an example HTML page:
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Golf Game Engine</title>
<script src="https://cdn.unity3d.com/unity/2022.2.0f1/UnityWebGL.js"></script>
<style>
body {
background-color: #231F20;
font-family: Helvetica, Sans-Serif;
font-size: 13px;
line-height: 24px;
overscroll-behavior: none;
text-rendering: optimizeLegibility;
user-select: none;
margin: 0;
}
#unity-container {
width: 100%;
height: 100vh;
}
</style>
</head>
<body>
<canvas id="unity-canvas" width="960" height="540"></canvas>
<script>
var unityConfig = {
loaderUrl: "golf-game-engine.loader.js",
dataUrl: "golf-game-engine.data",
frameworkUrl: "golf-game-engine.framework.js",
codeUrl: "golf-game-engine.code.js",
streamingAssetsUrl: "",
companyName: "Golf Game Engine",
productName: "Golf Game Engine",
productVersion: "1.0",
showBanner: false
};
var unityInstance = UnityLoader.instantiate("unity-container", unityConfig, false);
</script>
</body>
</html>
```
To use this HTML page, you'll need to export your Unity project as a WebGL project and upload the exported files to a web server.
### Usage
1. Create a new Unity project and add the `BallPhysics`, `TerrainInteraction`, and `TerrainSurface` scripts to your project.
2. Configure the ball and terrain properties in the Unity editor.
3. Export your Unity project as a WebGL project.
4. Create an HTML page using the provided example and upload it to a web server along with the exported files.
5. Open the HTML page in a web browser to play the golf game.
This implementation provides a basic 3D golf game engine with precise ball physics and terrain interaction. You can further enhance and customize the engine to suit your specific requirements.```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Golf Physics Engine</title>
<style>
body {
margin: 0;
padding: 0;
background: linear-gradient(to bottom, #87CEEB, #98FB98);
font-family: Arial, sans-serif;
overflow: hidden;
}
#gameContainer {
position: relative;
width: 100vw;
height: 100vh;
}
#controls {
position: absolute;
top: 20px;
left: 20px;
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
z-index: 100;
}
.control-group {
margin-bottom: 10px;
}
label {
display: inline-block;
width: 80px;
font-size: 12px;
font-weight: bold;
}
input[type="range"] {
width: 100px;
margin: 0 10px;
}
button {
background: #4CAF50;
color: white;
border: none;
padding: 8px 16px;
border-radius: 5px;
cursor: pointer;
font-size: 12px;
margin: 2px;
}
button:hover {
background: #45a049;
}
#info {
position: absolute;
top: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 15px;
border-radius: 10px;
font-size: 12px;
z-index: 100;
min-width: 200px;
}
canvas {
display: block;
}
</style>
</head>
<body>
<div id="gameContainer">
<canvas id="gameCanvas"></canvas>
<div id="controls">
<h3 style="margin: 0 0 10px 0; font-size: 14px;">Golf Controls</h3>
<div class="control-group">
<label>Power:</label>
<input type="range" id="powerSlider" min="1" max="100" value="50">
<span id="powerValue">50</span>
</div>
<div class="control-group">
<label>Angle:</label>
<input type="range" id="angleSlider" min="-45" max="45" value="0">
<span id="angleValue">0°</span>
</div>
<div class="control-group">
<button onclick="hitBall()">Hit Ball</button>
<button onclick="resetBall()">Reset</button>
</div>
<div class="control-group">
<button onclick="changeView()">Change View</button>
<button onclick="addObstacle()">Add Obstacle</button>
</div>
</div>
<div id="info">
<div><strong>Ball Info:</strong></div>
<div id="ballInfo">
Position: (0, 0, 0)<br>
Velocity: (0, 0, 0)<br>
Surface: Grass<br>
Status: At Rest
</div>
<br>
<div><strong>Physics:</strong></div>
<div id="physicsInfo">
Friction: 0.8<br>
Bounce: 0.6<br>
Wind: Light
</div>
</div>
</div>
<script>
// 3D Golf Physics Engine
class Vector3 {
constructor(x = 0, y = 0, z = 0) {
this.x = x;
this.y = y;
this.z = z;
}
add(v) {
return new Vector3(this.x + v.x, this.y + v.y, this.z + v.z);
}
subtract(v) {
return new Vector3(this.x - v.x, this.y - v.y, this.z - v.z);
}
multiply(scalar) {
return new Vector3(this.x * scalar, this.y * scalar, this.z * scalar);
}
magnitude() {
return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
}
normalize() {
const mag = this.magnitude();
return mag > 0 ? new Vector3(this.x / mag, this.y / mag, this.z / mag) : new Vector3();
}
dot(v) {
return this.x * v.x + this.y * v.y + this.z * v.z;
}
copy() {
return new Vector3(this.x, this.y, this.z);
}
}
class TerrainSurface {
constructor(type, friction, bounce, color, height = 0) {
this.type = type;
this.friction = friction;
this.bounce = bounce;
this.color = color;
this.height = height;
}
}
class GolfBall {
constructor(x, y, z, radius = 2) {
this.position = new Vector3(x, y, z);
this.velocity = new Vector3();
this.angularVelocity = new Vector3();
this.radius = radius;
this.mass = 0.045; // kg
this.isMoving = false;
this.onGround = true;
this.currentSurface = null;
this.trail = [];
this.maxTrailLength = 50;
}
applyForce(force, deltaTime) {
const acceleration = force.multiply(1 / this.mass);
this.velocity = this.velocity.add(acceleration.multiply(deltaTime));
}
update(deltaTime, terrain, obstacles) {
if (!this.isMoving && this.velocity.magnitude() < 0.1) {
this.velocity = new Vector3();
this.angularVelocity = new Vector3();
return;
}
this.isMoving = this.velocity.magnitude() > 0.1;
// Store previous position for trail
if (this.isMoving && this.trail.length < this.maxTrailLength) {
this.trail.push(this.position.copy());
} else if (this.trail.length >= this.maxTrailLength) {
this.trail.shift();
this.trail.push(this.position.copy());
}
// Gravity
const gravity = new Vector3(0, -9.81 * 5, 0); // Scaled for visibility
this.applyForce(gravity, deltaTime);
// Air resistance
if (this.position.y > this.radius) {
const airResistance = this.velocity.multiply(-0.1);
this.applyForce(airResistance, deltaTime);
}
// Wind effect (subtle)
const wind = new Vector3(
Math.sin(Date.now() * 0.001) * 0.5,
0,
Math.cos(Date.now() * 0.001) * 0.3
);
this.applyForce(wind, deltaTime);
// Update position
this.position = this.position.add(this.velocity.multiply(deltaTime));
// Terrain collision and surface interaction
this.handleTerrainCollision(terrain);
// Obstacle collision
this.handleObstacleCollision(obstacles);
// Out of bounds check
if (this.position.x < -500 || this.position.x > 500 ||
this.position.z < -500 || this.position.z > 500 ||
this.position.y < -100) {
this.resetPosition();
}
}
handleTerrainCollision(terrain) {
const surfaceHeight = terrain.getHeightAt(this.position.x, this.position.z);
const surface = terrain.getSurfaceAt(this.position.x, this.position.z);
this.currentSurface = surface;
if (this.position.y - this.radius <= surfaceHeight) {
this.position.y = surfaceHeight + this.radius;
this.onGround = true;
// Calculate surface normal (simplified)
const normal = new Vector3(0, 1, 0);
// Bounce calculation
if (this.velocity.y < 0) {
const bounceStrength = surface.bounce;
this.velocity.y = -this.velocity.y * bounceStrength;
// Energy loss on bounce
if (this.velocity.y < 1) {
this.velocity.y = 0;
}
}
// Ground friction
if (this.onGround) {
const frictionForce = surface.friction;
const horizontalVel = new Vector3(this.velocity.x, 0, this.velocity.z);
const frictionMagnitude = Math.min(frictionForce, horizontalVel.magnitude());
if (horizontalVel.magnitude() > 0) {
const frictionDirection = horizontalVel.normalize().multiply(-1);
const friction = frictionDirection.multiply(frictionMagnitude * 10);
this.applyForce(friction, 0.016); // Assuming 60fps
}
}
} else {
this.onGround = false;
}
}
handleObstacleCollision(obstacles) {
for (const obstacle of obstacles) {
const distance = this.position.subtract(obstacle.position).magnitude();
if (distance < this.radius + obstacle.radius) {
// Simple elastic collision
const normal = this.position.subtract(obstacle.position).normalize();
const overlap = (this.radius + obstacle.radius) - distance;
// Separate objects
this.position = this.position.add(normal.multiply(overlap * 0.5));
// Reflect velocity
const relativeVelocity = this.velocity;
const velocityAlongNormal = relativeVelocity.dot(normal);
if (velocityAlongNormal > 0) continue; // Objects separating
const restitution = 0.7;
const impulse = -(1 + restitution) * velocityAlongNormal;
this.velocity = this.velocity.add(normal.multiply(impulse));
}
}
}
resetPosition() {
this.position = new Vector3(0, 5, 0);
this.velocity = new Vector3();
this.angularVelocity = new Vector3();
this.isMoving = false;
this.trail = [];
}
hit(power, angleH, angleV) {
const powerScale = power * 0.5;
const radH = angleH * Math.PI / 180;
const radV = angleV * Math.PI / 180;
this.velocity = new Vector3(
Math.cos(radV) * Math.sin(radH) * powerScale,
Math.sin(radV) * powerScale,
Math.cos(radV) * Math.cos(radH) * powerScale
);
this.isMoving = true;
this.trail = [];
}
}
class Terrain {
constructor(width, depth) {
this.width = width;
this.depth = depth;
this.surfaces = new Map();
this.setupSurfaces();
}
setupSurfaces() {
// Create different surface zones
this.surfaces.set('grass', new TerrainSurface('grass', 0.8, 0.3, '#4a7c59'));
this.surfaces.set('sand', new TerrainSurface('sand', 2.0, 0.1, '#f4d03f'));
this.surfaces.set('rough', new TerrainSurface('rough', 1.5, 0.2, '#2d5016'));
this.surfaces.set('fairway', new TerrainSurface('fairway', 0.6, 0.4, '#52a068'));
}
getHeightAt(x, z) {
// Generate terrain height with noise
const height = Math.sin(x * 0.01) * 5 + Math.cos(z * 0.008) * 3 +
Math.sin(x * 0.02 + z * 0.02) * 2;
return height;
}
getSurfaceAt(x, z) {
// Determine surface type based on position
const distance = Math.sqrt(x * x + z * z);
if (distance < 50) return this.surfaces.get('fairway');
else if (distance < 100) return this.surfaces.get('grass');
else if (Math.abs(x) > 200 || Math.abs(z) > 200) return this.surfaces.get('sand');
else return this.surfaces.get('rough');
}
}
class Obstacle {
constructor(x, y, z, radius = 10, color = '#8b4513') {
this.position = new Vector3(x, y, z);
this.radius = radius;
this.color = color;
}
}
class Camera {
constructor() {
this.position = new Vector3(0, 50, 100);
this.target = new Vector3(0, 0, 0);
this.viewMode = 0; // 0: follow, 1: overhead, 2: side
}
update(ballPosition) {
switch (this.viewMode) {
case 0: // Follow camera
this.position = ballPosition.add(new Vector3(30, 40, 50));
this.target = ballPosition;
break;
case 1: // Overhead view
this.position = ballPosition.add(new Vector3(0, 100, 20));
this.target = ballPosition;
break;
case 2: // Side view
this.position = ballPosition.add(new Vector3(80, 30, 0));
this.target = ballPosition;
break;
}
}
}
class GolfEngine {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.ball = new GolfBall(0, 5, 0);
this.terrain = new Terrain(1000, 1000);
this.camera = new Camera();
this.obstacles = [];
this.lastTime = 0;
this.setupCanvas();
this.addRandomObstacles();
this.gameLoop();
}
setupCanvas() {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
window.addEventListener('resize', () => {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
});
}
addRandomObstacles() {
for (let i = 0; i < 8; i++) {
const x = (Math.random() - 0.5) * 400;
const z = (Math.random() - 0.5) * 400;
const y = this.terrain.getHeightAt(x, z);
this.obstacles.push(new Obstacle(x, y + 10, z, 8 + Math.random() * 12));
}
}
project3D(point) {
// Simple 3D projection
const dx = point.x - this.camera.position.x;
const dy = point.y - this.camera.position.y;
const dz = point.z - this.camera.position.z;
const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
const fov = 800;
if (dz > -1) return null; // Behind camera
const scale = fov / (distance + 1);
const x = this.canvas.width / 2 + dx * scale;
const y = this.canvas.height / 2 - dy * scale;
return { x, y, scale, distance };
}
render() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// Draw terrain grid
this.drawTerrain();
// Draw obstacles
this.drawObstacles();
// Draw ball trail
this.drawTrail();
// Draw ball
this.drawBall();
// Draw UI elements
this.drawUI();
}
drawTerrain() {
const gridSize = 20;
const gridRange = 200;
this.ctx.strokeStyle = '#cccccc';
this.ctx.lineWidth = 1;
// Draw grid lines
for (let x = -gridRange; x <= gridRange; x += gridSize) {
for (let z = -gridRange; z <= gridRange; z += gridSize) {
const height = this.terrain.getHeightAt(x, z);
const surface = this.terrain.getSurfaceAt(x, z);
const point = this.project3D(new Vector3(x, height, z));
if (!point) continue;
// Draw surface patches
this.ctx.fillStyle = surface.color;
this.ctx.globalAlpha = 0.3;
this.ctx.fillRect(point.x - 5, point.y - 5, 10, 10);
this.ctx.globalAlpha = 1;
}
}
// Draw grid lines for depth perception
this.ctx.strokeStyle = '#888888';
this.ctx.lineWidth = 0.5;
this.ctx.globalAlpha = 0.5;
for (let x = -gridRange; x <= gridRange; x += gridSize) {
this.ctx.beginPath();
for (let z = -gridRange; z <= gridRange; z += gridSize) {
const height = this.terrain.getHeightAt(x, z);
const point = this.project3D(new Vector3(x, height, z));
if (point) {
if (z === -gridRange) this.ctx.moveTo(point.x, point.y);
else this.ctx.lineTo(point.x, point.y);
}
}
this.ctx.stroke();
}
this.ctx.globalAlpha = 1;
}
drawObstacles() {
this.obstacles.forEach(obstacle => {
const point = this.project3D(obstacle.position);
if (!point) return;
this.ctx.fillStyle = obstacle.color;
this.ctx.beginPath();
this.ctx.arc(point.x, point.y, point.scale * obstacle.radius * 0.1, 0, Math.PI * 2);
this.ctx.fill();
// Shadow
this.ctx.fillStyle = 'rgba(0,0,0,0.3)';
this.ctx.beginPath();
this.ctx.ellipse(point.x + 2, point.y + 10, point.scale * obstacle.radius * 0.1, point.scale * obstacle.radius * 0.05, 0, 0, Math.PI * 2);
this.ctx.fill();
});
}
drawTrail() {
if (this.ball.trail.length < 2) return;
this.ctx.strokeStyle = '#ff6b6b';
this.ctx.lineWidth = 2;
this.ctx.globalAlpha = 0.7;
this.ctx.beginPath();
for (let i = 0; i < this.ball.trail.length; i++) {
const point = this.project3D(this.ball.trail[i]);
if (point) {
if (i === 0) this.ctx.moveTo(point.x, point.y);
else this.ctx.lineTo(point.x, point.y);
}
}
this.ctx.stroke();
this.ctx.globalAlpha = 1;
}
drawBall() {
const point = this.project3D(this.ball.position);
if (!point) return;
const radius = point.scale * this.ball.radius;
// Ball shadow
const shadowPoint = this.project3D(new Vector3(
this.ball.position.x + 2,
this.terrain.getHeightAt(this.ball.position.x, this.ball.position.z),
this.ball.position.z + 2
));
if (shadowPoint) {
this.ctx.fillStyle = 'rgba(0,0,0,0.3)';
this.ctx.beginPath();
this.ctx.ellipse(shadowPoint.x, shadowPoint.y, radius * 0.8, radius * 0.4, 0, 0, Math.PI * 2);
this.ctx.fill();
}
// Ball gradient
const gradient = this.ctx.createRadialGradient(
point.x - radius * 0.3, point.y - radius * 0.3, 0,
point.x, point.y, radius
);
gradient.addColorStop(0, '#ffffff');
gradient.addColorStop(0.7, '#f0f0f0');
gradient.addColorStop(1, '#d0d0d0');
this.ctx.fillStyle = gradient;
this.ctx.beginPath();
this.ctx.arc(point.x, point.y, radius, 0, Math.PI * 2);
this.ctx.fill();
// Ball dimples
this.ctx.fillStyle = 'rgba(0,0,0,0.1)';
for (let i = 0; i < 8; i++) {
const angle = (i / 8) * Math.PI * 2;
const dimpleX = point.x + Math.cos(angle) * radius * 0.5;
const dimpleY = point.y + Math.sin(angle) * radius * 0.5;
this.ctx.beginPath();
this.ctx.arc(dimpleX, dimpleY, radius * 0.1, 0, Math.PI * 2);
this.ctx.fill();
}
}
drawUI() {
// Velocity indicator
if (this.ball.isMoving) {
const vel = this.ball.velocity.magnitude();
const maxVel = 50;
const barWidth = 200;
const barHeight = 20;
const x = this.canvas.width - barWidth - 50;
const y = this.canvas.height - 60;
this.ctx.fillStyle = 'rgba(0,0,0,0.5)';
this.ctx.fillRect(x - 5, y - 5, barWidth + 10, barHeight + 10);
this.ctx.fillStyle = '#333333';
this.ctx.fillRect(x, y, barWidth, barHeight);
this.ctx.fillStyle = vel > 20 ? '#ff4444' : vel > 10 ? '#ffaa44' : '#44ff44';
this.ctx.fillRect(x, y, (vel / maxVel) * barWidth, barHeight);
this.ctx.fillStyle = 'white';
this.ctx.font = '12px Arial';
this.ctx.fillText('Speed', x, y - 8);
}
}
update(deltaTime) {
this.ball.update(deltaTime, this.terrain, this.obstacles);
this.camera.update(this.ball.position);
this.updateUI();
}
updateUI() {
const pos = this.ball.position;
const vel = this.ball.velocity;
const surface = this.ball.currentSurface || { type: 'unknown', friction: 0, bounce: 0 };
document.getElementById('ballInfo').innerHTML = `
Position: (${pos.x.toFixed(1)}, ${pos.y.toFixed(1)}, ${pos.z.toFixed(1)})<br>
Velocity: (${vel.x.toFixed(1)}, ${vel.y.toFixed(1)}, ${vel.z.toFixed(1)})<br>
Surface: ${surface.type}<br>
Status: ${this.ball.isMoving ? 'Moving' : 'At Rest'}
`;
document.getElementById('physicsInfo').innerHTML = `
Friction: ${surface.friction.toFixed(2)}<br>
Bounce: ${surface.bounce.toFixed(2)}<br>
Speed: ${vel.magnitude().toFixed(1)}<br>
Distance: ${Math.sqrt(pos.x*pos.x + pos.z*pos.z).toFixed(1)}m
`;
}
gameLoop(currentTime = 0) {
const deltaTime = (currentTime - this.lastTime) / 1000;
this.lastTime = currentTime;
if (deltaTime < 0.1) { // Prevent large time steps
this.update(deltaTime);
this.render();
}
requestAnimationFrame(this.gameLoop.bind(this));
}
hitBall(power, angle) {
this.ball.hit(power, angle, 15); // Fixed vertical angle for now
}
resetBall() {
this.ball.resetPosition();
}
changeView() {
this.camera.viewMode = (this.camera.viewMode + 1) % 3;
}
addNewObstacle() {
const x = (Math.random() - 0.5) * 300;
const z = (Math.random() - 0.5) * 300;
const y = this.terrain.getHeightAt(x, z);
this.obstacles.push(new Obstacle(x, y + 10, z, 5 + Math.random() * 10));
}
}
// Initialize the game
const canvas = document.getElementById('gameCanvas');
const engine = new GolfEngine(canvas);
// Control handlers
const powerSlider = document.getElementById('powerSlider');
const angleSlider = document.getElementById('angleSlider');
const powerValue = document.getElementById('powerValue');
const angleValue = document.getElementById('angleValue');
powerSlider.addEventListener('input', (e) => {
powerValue.textContent = e.target.value;
});
angleSlider.addEventListener('input', (e) => {
angleValue.textContent = e.target.value + '°';
});
function hitBall() {
const power = parseInt(powerSlider.value);
const angle = parseInt(angleSlider.value);
engine.hitBall(power, angle);
}
function resetBall() {
engine.resetBall();
}
function changeView() {
engine.changeView();
}
function addObstacle() {
engine.addNewObstacle();
}
// Keyboard controls
document.addEventListener('keydown', (e) => {
switch(e.key) {
case ' ':
e.preventDefault();
hitBall();
break;
case 'r':
case 'R':
resetBall();
break;
case 'v':
case 'V':
changeView();
break;
}
});
</script>
</body>
</html>
```
This 3D Golf Physics Engine includes:
## Core Features:
- **Realistic Ball Physics**: Gravity, air resistance, angular velocity, and momentum conservation
- **Surface Interaction**: Different terrain types (grass, sand, rough, fairway) with unique friction and bounce properties
- **3D Environment**: Full 3D positioning with proper depth projection and camera system
- **Trail System**: Visual ball trajectory tracking
- **Collision Detection**: Ball-obstacle collision with elastic response
## Physics Systems:
- **Friction Effects**: Surface-dependent friction that affects rolling speed
- **Bounce Physics**: Realistic energy loss and restitution coefficients
- **Wind Effects**: Subtle environmental forces affecting ball flight
- **Terrain Height**: Procedural terrain elevation with collision detection
- **Angular Motion**: Ball spin and rotation effects
## Controls:
- **Power Slider**: Adjust shot strength (1-100)
- **Angle Slider**: Control horizontal aim (-45° to +45°)
- **Hit Ball**: Execute the shot with current settings
- **Reset**: Return ball to starting position
- **Change View**: Cycle through camera angles (follow, overhead, side)
- **Add Obstacle**: Place new obstacles on the course
## Advanced Features:
- **Multiple Camera Views**: Follow cam, overhead view, and side perspective
- **Real-time Physics Info**: Live display of ball status, velocity, and surface properties
- **Dynamic Obstacles**: Procedurally placed obstacles with collision physics
- **Visual Effects**: Ball shadows, terrain texturing, and velocity indicators
- **Keyboard Shortcuts**: Spacebar to hit, 'R' to reset, 'V' to change view
The engine provides a solid foundation for golf game development with accurate physics simulation and can be extended with features like multiple holes, scoring, club selection, and more sophisticated terrain generation.```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Golf Physics Engine</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/cannon@0.6.2/build/cannon.min.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
font-family: Arial, sans-serif;
}
#canvas {
width: 100%;
height: 100%;
display: block;
}
#ui {
position: absolute;
top: 10px;
left: 10px;
background-color: rgba(0, 0, 0, 0.5);
color: white;
padding: 10px;
border-radius: 5px;
user-select: none;
}
#power {
width: 200px;
margin: 10px 0;
}
#controls {
position: absolute;
bottom: 10px;
left: 10px;
background-color: rgba(0, 0, 0, 0.5);
color: white;
padding: 10px;
border-radius: 5px;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<div id="ui">
<h3>Golf Physics Engine</h3>
<div>Ball Position: <span id="position">0, 0, 0</span></div>
<div>Velocity: <span id="velocity">0, 0, 0</span></div>
<div>Surface: <span id="surface">Fairway</span></div>
<div>
<label for="power">Power: <span id="powerValue">50</span>%</label>
<input type="range" id="power" min="0" max="100" value="50">
</div>
<button id="hitBall">Hit Ball</button>
<button id="resetBall">Reset Ball</button>
</div>
<div id="controls">
<p>Camera: WASD to move, Left-click + drag to rotate</p>
<p>Shot Direction: Arrow keys</p>
</div>
<script>
// Initialize Three.js
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('canvas'), antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
// Initialize Cannon.js physics world
const world = new CANNON.World();
world.gravity.set(0, -9.82, 0); // Earth gravity
world.broadphase = new CANNON.NaiveBroadphase();
world.solver.iterations = 10;
// Define different surface materials
const surfaceTypes = {
fairway: { friction: 0.3, restitution: 0.5, color: 0x4CAF50 },
rough: { friction: 0.8, restitution: 0.3, color: 0x388E3C },
sand: { friction: 0.9, restitution: 0.1, color: 0xF9A825 },
green: { friction: 0.2, restitution: 0.7, color: 0x2E7D32 },
water: { friction: 0.0, restitution: 0.0, color: 0x2196F3 }
};
// Create materials in Cannon.js
const cannonMaterials = {};
for (const [type, properties] of Object.entries(surfaceTypes)) {
cannonMaterials[type] = new CANNON.Material(type);
// Create contact materials between ball and surfaces
const ballContactMaterial = new CANNON.ContactMaterial(
ballMaterial = new CANNON.Material('ball'),
cannonMaterials[type],
{
friction: properties.friction,
restitution: properties.restitution
}
);
world.addContactMaterial(ballContactMaterial);
}
// Create lighting
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(50, 100, 50);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
scene.add(directionalLight);
// Create the golf course
const courseSize = 100;
const segmentSize = 10;
// Create terrain segments
function createTerrainSegment(x, z, width, depth, height, surfaceType) {
// Create Three.js visual representation
const geometry = new THREE.BoxGeometry(width, height, depth);
const material = new THREE.MeshStandardMaterial({ color: surfaceTypes[surfaceType].color });
const terrainMesh = new THREE.Mesh(geometry, material);
terrainMesh.position.set(x, height/2 - 0.5, z);
terrainMesh.receiveShadow = true;
terrainMesh.userData = { surfaceType };
scene.add(terrainMesh);
// Create Cannon.js physics body
const terrainShape = new CANNON.Box(new CANNON.Vec3(width/2, height/2, depth/2));
const terrainBody = new CANNON.Body({
mass: 0,
material: cannonMaterials[surfaceType],
shape: terrainShape
});
terrainBody.position.set(x, height/2 - 0.5, z);
world.addBody(terrainBody);
return { mesh: terrainMesh, body: terrainBody };
}
// Create a simple golf course with different surfaces
const terrainSegments = [];
// Create fairway
terrainSegments.push(createTerrainSegment(0, 0, 20, 100, 1, 'fairway'));
// Create some rough on the sides
terrainSegments.push(createTerrainSegment(-15, 0, 10, 100, 1.2, 'rough'));
terrainSegments.push(createTerrainSegment(15, 0, 10, 100, 1.2, 'rough'));
// Create a sand trap
terrainSegments.push(createTerrainSegment(5, -20, 10, 10, 0.8, 'sand'));
// Create green at the end
terrainSegments.push(createTerrainSegment(0, -40, 15, 20, 1, 'green'));
// Create water hazard
terrainSegments.push(createTerrainSegment(-10, -10, 8, 8, 0.5, 'water'));
// Create the golf ball
const ballRadius = 0.02125; // Regulation golf ball radius in meters
const ballGeometry = new THREE.SphereGeometry(ballRadius, 32, 32);
const ballMaterial = new THREE.MeshStandardMaterial({ color: 0xffffff });
const golfBall = new THREE.Mesh(ballGeometry, ballMaterial);
golfBall.castShadow = true;
scene.add(golfBall);
// Physics body for the golf ball
const ballShape = new CANNON.Sphere(ballRadius);
const ballBody = new CANNON.Body({
mass: 0.0459, // Regulation golf ball mass in kg
material: ballMaterial,
shape: ballShape,
linearDamping: 0.3 // Add some air resistance
});
// Initial position
const initialPosition = new CANNON.Vec3(0, ballRadius, 20);
ballBody.position.copy(initialPosition);
world.addBody(ballBody);
// Shot direction
let shotDirection = new THREE.Vector3(0, 0, -1);
let shotAngle = 0;
const rotationSpeed = 0.05;
// Camera setup
camera.position.set(0, 2, 25);
camera.lookAt(0, 0, 15);
// Camera controls
let cameraRotating = false;
let previousMousePosition = { x: 0, y: 0 };
const cameraDistance = 5;
let cameraAngleHorizontal = 0;
let cameraAngleVertical = Math.PI / 6;
// UI elements
const positionEl = document.getElementById('position');
const velocityEl = document.getElementById('velocity');
const surfaceEl = document.getElementById('surface');
const powerSlider = document.getElementById('power');
const powerValue = document.getElementById('powerValue');
const hitButton = document.getElementById('hitBall');
const resetButton = document.getElementById('resetBall');
// Update power display
powerSlider.addEventListener('input', function() {
powerValue.textContent = this.value;
});
// Hit the ball
hitButton.addEventListener('click', function() {
const power = powerSlider.value / 50; // Normalize to 0-2 range
const impulse = new CANNON.Vec3(
shotDirection.x * power * 5,
0.5 * power * 5, // Add some loft
shotDirection.z * power * 5
);
ballBody.applyImpulse(impulse, ballBody.position);
});
// Reset the ball
resetButton.addEventListener('click', function() {
ballBody.position.copy(initialPosition);
ballBody.velocity.set(0, 0, 0);
ballBody.angularVelocity.set(0, 0, 0);
});
// Handle arrow keys for shot direction
document.addEventListener('keydown', function(event) {
if (event.key === 'ArrowLeft') {
shotAngle += rotationSpeed;
} else if (event.key === 'ArrowRight') {
shotAngle -= rotationSpeed;
}
shotDirection.x = Math.sin(shotAngle);
shotDirection.z = -Math.cos(shotAngle);
// WASD for camera movement
const cameraSpeed = 0.5;
if (event.key === 'w') {
camera.position.z -= cameraSpeed;
} else if (event.key === 's') {
camera.position.z += cameraSpeed;
} else if (event.key === 'a') {
camera.position.x -= cameraSpeed;
} else if (event.key === 'd') {
camera.position.x += cameraSpeed;
}
});
// Mouse controls for camera rotation
document.addEventListener('mousedown', function(event) {
if (event.button === 0) { // Left mouse button
cameraRotating = true;
previousMousePosition = { x: event.clientX, y: event.clientY };
}
});
document.addEventListener('mouseup', function(event) {
if (event.button === 0) {
cameraRotating = false;
}
});
document.addEventListener('mousemove', function(event) {
if (cameraRotating) {
const deltaMove = {
x: event.clientX - previousMousePosition.x,
y: event.clientY - previousMousePosition.y
};
cameraAngleHorizontal += deltaMove.x * 0.01;
cameraAngleVertical += deltaMove.y * 0.01;
// Limit vertical angle to prevent flipping
cameraAngleVertical = Math.max(0.1, Math.min(Math.PI / 2 - 0.1, cameraAngleVertical));
previousMousePosition = { x: event.clientX, y: event.clientY };
}
});
// Function to update camera position based on angles
function updateCameraPosition() {
const targetPosition = new THREE.Vector3(
ballBody.position.x,
ballBody.position.y,
ballBody.position.z
);
camera.position.x = targetPosition.x + cameraDistance * Math.sin(cameraAngleHorizontal) * Math.cos(cameraAngleVertical);
camera.position.y = targetPosition.y + cameraDistance * Math.sin(cameraAngleVertical);
camera.position.z = targetPosition.z + cameraDistance * Math.cos(cameraAngleHorizontal) * Math.cos(cameraAngleVertical);
camera.lookAt(targetPosition);
}
// Function to determine what surface the ball is on
function getCurrentSurface() {
const ballPosition = new THREE.Vector3(ballBody.position.x, ballBody.position.y, ballBody.position.z);
let currentSurface = 'air';
// Check if the ball is on any terrain segment
for (const segment of terrainSegments) {
const terrainBounds = new THREE.Box3().setFromObject(segment.mesh);
// Check if the ball is within XZ bounds and close to the surface
if (
ballPosition.x >= terrainBounds.min.x &&
ballPosition.x <= terrainBounds.max.x &&
ballPosition.z >= terrainBounds.min.z &&
ballPosition.z <= terrainBounds.max.z &&
Math.abs(ballPosition.y - (terrainBounds.max.y + ballRadius)) < 0.1
) {
currentSurface = segment.mesh.userData.surfaceType;
break;
}
}
return currentSurface;
}
// Helper function to format vector for display
function formatVector(vector) {
return `${vector.x.toFixed(2)}, ${vector.y.toFixed(2)}, ${vector.z.toFixed(2)}`;
}
// Draw shot direction indicator
const directionArrow = new THREE.ArrowHelper(
shotDirection,
new THREE.Vector3(0, 0, 0),
1,
0xff0000
);
scene.add(directionArrow);
// Animation loop
const timeStep = 1/60;
function animate() {
requestAnimationFrame(animate);
// Update physics
world.step(timeStep);
// Update ball position
golfBall.position.copy(ballBody.position);
golfBall.quaternion.copy(ballBody.quaternion);
// Update camera
updateCameraPosition();
// Update shot direction arrow
directionArrow.position.copy(golfBall.position);
directionArrow.setDirection(shotDirection);
// Update UI
positionEl.textContent = formatVector(ballBody.position);
velocityEl.textContent = formatVector(ballBody.velocity);
surfaceEl.textContent = getCurrentSurface();
renderer.render(scene, camera);
}
// Handle window resize
window.addEventListener('resize', function() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
animate();
</script>
</body>
</html>
```