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> ```