Created
April 10, 2025 18:19
-
-
Save RageshAntonyHM/39f541b48f79ad9e47219b6aaacafc30 to your computer and use it in GitHub Desktop.
THREE.js forest
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>THREE.js Forest Lake Cave Scene</title> | |
<style> | |
body { margin: 0; overflow: hidden; background-color: #87CEEB; } /* Sky blue background */ | |
canvas { display: block; } | |
</style> | |
</head> | |
<body> | |
<script type="importmap"> | |
{ | |
"imports": { | |
"three": "https://unpkg.com/[email protected]/build/three.module.js", | |
"three/addons/": "https://unpkg.com/[email protected]/examples/jsm/" | |
} | |
} | |
</script> | |
<script type="module"> | |
import * as THREE from 'three'; | |
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; | |
let scene, camera, renderer, controls, clock; | |
let ground, lake; | |
const trees = []; | |
const rocks = []; | |
function init() { | |
// Basic Setup | |
scene = new THREE.Scene(); | |
scene.background = new THREE.Color(0x87CEEB); // Sky blue | |
scene.fog = new THREE.Fog(0x87CEEB, 50, 200); // Add fog for atmosphere | |
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); | |
camera.position.set(0, 30, 60); | |
renderer = new THREE.WebGLRenderer({ antialias: true }); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
renderer.shadowMap.enabled = true; // Enable shadows | |
renderer.shadowMap.type = THREE.PCFSoftShadowMap; | |
document.body.appendChild(renderer.domElement); | |
controls = new OrbitControls(camera, renderer.domElement); | |
controls.enableDamping = true; | |
controls.dampingFactor = 0.05; | |
controls.maxPolarAngle = Math.PI / 2 - 0.1; // Prevent looking under the ground | |
controls.minDistance = 10; | |
controls.maxDistance = 150; | |
clock = new THREE.Clock(); | |
// Lighting | |
const ambientLight = new THREE.AmbientLight(0xaaaaaa, 0.8); // Softer ambient | |
scene.add(ambientLight); | |
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5); | |
directionalLight.position.set(50, 80, 30); | |
directionalLight.castShadow = true; | |
directionalLight.shadow.mapSize.width = 2048; | |
directionalLight.shadow.mapSize.height = 2048; | |
directionalLight.shadow.camera.near = 0.5; | |
directionalLight.shadow.camera.far = 200; | |
directionalLight.shadow.camera.left = -100; | |
directionalLight.shadow.camera.right = 100; | |
directionalLight.shadow.camera.top = 100; | |
directionalLight.shadow.camera.bottom = -100; | |
directionalLight.shadow.bias = -0.001; // Adjust to prevent shadow acne | |
scene.add(directionalLight); | |
// const shadowHelper = new THREE.CameraHelper( directionalLight.shadow.camera ); // Optional: Visualize shadow frustum | |
// scene.add( shadowHelper ); | |
// Create Scene Elements | |
createGround(); | |
createLake(); | |
createCave(); | |
createForest(80); // Create 80 trees | |
createRocks(50); // Create 50 rocks | |
// Event Listeners | |
window.addEventListener('resize', onWindowResize); | |
// Start Animation Loop | |
animate(); | |
} | |
function createGround() { | |
const groundSize = 200; | |
const segments = 100; | |
const geometry = new THREE.PlaneGeometry(groundSize, groundSize, segments, segments); | |
const material = new THREE.MeshStandardMaterial({ | |
color: 0x228B22, // Forest green | |
roughness: 0.9, | |
metalness: 0.1, | |
// wireframe: true // For debugging terrain | |
}); | |
ground = new THREE.Mesh(geometry, material); | |
ground.rotation.x = -Math.PI / 2; // Rotate to be horizontal | |
ground.receiveShadow = true; | |
// Make terrain bumpy | |
const positions = geometry.attributes.position; | |
const vertex = new THREE.Vector3(); | |
for (let i = 0; i < positions.count; i++) { | |
vertex.fromBufferAttribute(positions, i); | |
// Simple noise - more complex noise (Perlin/Simplex) would be better but adds complexity | |
const distFromCenter = vertex.length(); | |
const noise = Math.sin(vertex.x * 0.1) * Math.cos(vertex.y * 0.1) * 2; | |
const dampening = 1 / (1 + distFromCenter * 0.05); // Less bumpy further out | |
positions.setZ(i, noise * dampening + (Math.random() - 0.5) * 0.5 * dampening); | |
} | |
geometry.computeVertexNormals(); // Recalculate normals after deformation | |
positions.needsUpdate = true; | |
scene.add(ground); | |
} | |
function createLake() { | |
const lakeRadius = 25; | |
const geometry = new THREE.CircleGeometry(lakeRadius, 64); | |
const material = new THREE.MeshStandardMaterial({ | |
color: 0x4682B4, // Steel blue | |
roughness: 0.1, | |
metalness: 0.2, | |
transparent: true, | |
opacity: 0.85, | |
side: THREE.DoubleSide // Render both sides | |
}); | |
lake = new THREE.Mesh(geometry, material); | |
lake.rotation.x = -Math.PI / 2; | |
lake.position.set(-30, 0.1, 0); // Position slightly above ground, adjust coords as needed | |
lake.receiveShadow = true; | |
scene.add(lake); | |
// Simple waves (optional, adds complexity and needs animation) | |
/* | |
lake.userData.vertices = geometry.attributes.position.count; | |
lake.userData.originalPositions = geometry.attributes.position.clone(); | |
*/ | |
} | |
function createCave() { | |
const cavePosition = new THREE.Vector3(40, 5, -30); // Position the cave entrance area | |
const rockMaterial = new THREE.MeshStandardMaterial({ | |
color: 0x888888, // Grey rock | |
roughness: 0.9, | |
metalness: 0.1, | |
}); | |
// Create the main rock face structure around the entrance | |
const numFaceRocks = 15; | |
for (let i = 0; i < numFaceRocks; i++) { | |
const size = Math.random() * 8 + 5; // Random size | |
const rockGeo = createDeformedGeometry(new THREE.SphereGeometry(size, 8, 6), 0.5); // Deform a sphere | |
const rockMesh = new THREE.Mesh(rockGeo, rockMaterial); | |
rockMesh.castShadow = true; | |
rockMesh.receiveShadow = true; | |
// Position rocks to form an arch/opening | |
const angle = (i / numFaceRocks) * Math.PI * 1.5 + Math.PI / 4; // Place in an arc shape | |
const radius = 10 + Math.random() * 3; | |
const xOffset = Math.cos(angle) * radius; | |
const yOffset = Math.sin(angle) * radius * 0.8; // Make it wider than tall | |
const zOffset = (Math.random() - 0.5) * 5; | |
rockMesh.position.set( | |
cavePosition.x + xOffset, | |
cavePosition.y + yOffset + size * 0.3, // Lift slightly based on size | |
cavePosition.z + zOffset | |
); | |
// Random rotation | |
rockMesh.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, Math.random() * Math.PI); | |
scene.add(rockMesh); | |
rocks.push(rockMesh); // Add to rocks array for potential interaction later | |
} | |
// Add a dark plane inside to simulate the cave darkness | |
const darknessGeo = new THREE.PlaneGeometry(15, 12); | |
const darknessMat = new THREE.MeshBasicMaterial({ color: 0x050505, side: THREE.DoubleSide }); // Very dark | |
const darknessMesh = new THREE.Mesh(darknessGeo, darknessMat); | |
darknessMesh.position.set(cavePosition.x, cavePosition.y + 5, cavePosition.z - 3); // Position behind the opening | |
darknessMesh.rotation.y = Math.PI; // Face outwards (though DoubleSide makes it less critical) | |
scene.add(darknessMesh); | |
} | |
function createDeformedGeometry(geometry, magnitude) { | |
const positions = geometry.attributes.position; | |
const vertex = new THREE.Vector3(); | |
const normal = new THREE.Vector3(); | |
for (let i = 0; i < positions.count; i++) { | |
vertex.fromBufferAttribute(positions, i); | |
// Get normal for displacement direction (might not exist on basic geo, calculate if needed) | |
if (geometry.attributes.normal) { | |
normal.fromBufferAttribute(geometry.attributes.normal, i); | |
} else { | |
// Simple approximation: direction from origin for sphere/icosahedron | |
normal.copy(vertex).normalize(); | |
} | |
const offset = normal.multiplyScalar((Math.random() - 0.5) * magnitude); | |
vertex.add(offset); | |
positions.setXYZ(i, vertex.x, vertex.y, vertex.z); | |
} | |
geometry.computeVertexNormals(); // Essential after deforming | |
positions.needsUpdate = true; | |
return geometry; | |
} | |
function createTree(position) { | |
const tree = new THREE.Group(); | |
// Trunk | |
const trunkHeight = Math.random() * 5 + 8; // Random height between 8 and 13 | |
const trunkRadius = trunkHeight * (Math.random() * 0.1 + 0.08); // Radius proportional to height | |
const trunkGeo = new THREE.CylinderGeometry(trunkRadius * 0.7, trunkRadius, trunkHeight, 8); // Tapered slightly | |
const trunkMat = new THREE.MeshStandardMaterial({ | |
color: 0x8B4513, // Saddle brown | |
roughness: 1.0, | |
metalness: 0.0 | |
}); | |
const trunkMesh = new THREE.Mesh(trunkGeo, trunkMat); | |
trunkMesh.castShadow = true; | |
trunkMesh.receiveShadow = true; | |
trunkMesh.position.y = trunkHeight / 2; // Base rests on the ground | |
tree.add(trunkMesh); | |
// Canopy (multiple parts for complexity) | |
const canopyLevels = Math.floor(Math.random() * 3) + 3; // 3 to 5 levels of foliage | |
let currentHeight = trunkHeight; | |
let currentRadius = trunkHeight * 0.6; // Start radius based on height | |
const leafMat = new THREE.MeshStandardMaterial({ | |
color: new THREE.Color(0x00ff00).multiplyScalar(Math.random() * 0.4 + 0.6), // Vary green shade | |
roughness: 0.8, | |
metalness: 0.1 | |
}); | |
for (let i = 0; i < canopyLevels; i++) { | |
const levelRadius = currentRadius * (Math.random() * 0.3 + 0.8); // Vary radius per level | |
const levelHeight = levelRadius * (Math.random() * 0.5 + 0.8); | |
// Use Icosahedron for a more 'clumpy' look than Sphere | |
const foliageGeo = new THREE.IcosahedronGeometry(levelRadius, 1); // Detail 1 for slightly more complexity | |
// Add some slight deformation to foliage clumps | |
createDeformedGeometry(foliageGeo, levelRadius * 0.2); | |
const foliageMesh = new THREE.Mesh(foliageGeo, leafMat); | |
foliageMesh.castShadow = true; | |
// foliageMesh.receiveShadow = true; // Leaves usually don't receive much shadow from above | |
foliageMesh.position.y = currentHeight - levelHeight * 0.2; // Position this level | |
foliageMesh.rotation.set(Math.random() * Math.PI * 0.1, Math.random() * Math.PI, Math.random() * Math.PI * 0.1); // Slight random tilt | |
tree.add(foliageMesh); | |
// Update for next level | |
currentHeight -= levelHeight * 0.6; // Move down for next level, overlap slightly | |
currentRadius *= 0.8; // Shrink radius for upper levels | |
} | |
tree.position.copy(position); | |
// Find ground height at tree position (simple raycast down) - Optional but better placement | |
const raycaster = new THREE.Raycaster(new THREE.Vector3(position.x, 50, position.z), new THREE.Vector3(0, -1, 0)); | |
const intersects = raycaster.intersectObject(ground); | |
if (intersects.length > 0) { | |
tree.position.y = intersects[0].point.y; | |
} | |
scene.add(tree); | |
trees.push(tree); | |
} | |
function createForest(count) { | |
const groundSize = 100; // Area to place trees within | |
const lakePos = lake.position; | |
const lakeRadius = lake.geometry.parameters.radius; | |
const cavePos = new THREE.Vector3(40, 0, -30); // Approximate cave center for exclusion | |
const caveRadius = 20; // Exclusion zone around cave | |
for (let i = 0; i < count; i++) { | |
let placed = false; | |
while (!placed) { | |
const x = (Math.random() - 0.5) * groundSize * 1.8; // Spread them out more | |
const z = (Math.random() - 0.5) * groundSize * 1.8; | |
const distToLake = Math.sqrt(Math.pow(x - lakePos.x, 2) + Math.pow(z - lakePos.z, 2)); | |
const distToCave = Math.sqrt(Math.pow(x - cavePos.x, 2) + Math.pow(z - cavePos.z, 2)); | |
// Check if position is valid (not in lake, not too close to cave) | |
if (distToLake > lakeRadius + 5 && distToCave > caveRadius) { // Add buffer zone | |
createTree(new THREE.Vector3(x, 0, z)); // Y position will be adjusted later | |
placed = true; | |
} | |
} | |
} | |
} | |
function createRocks(count) { | |
const groundSize = 100; | |
const rockMaterial = new THREE.MeshStandardMaterial({ | |
color: 0xaaaaaa, // Grey rock | |
roughness: 0.9, | |
metalness: 0.1, | |
}); | |
const lakePos = lake.position; | |
const lakeRadius = lake.geometry.parameters.radius; | |
const cavePos = new THREE.Vector3(40, 0, -30); // Approximate cave center | |
const cavePlacementRadius = 25; // Place some rocks near cave | |
for (let i = 0; i < count; i++) { | |
const size = Math.random() * 1.5 + 0.5; // Random size 0.5 to 2.0 | |
// Use Icosahedron (low detail) for jagged look, deform it | |
const rockGeo = createDeformedGeometry(new THREE.IcosahedronGeometry(size, 0), size * 0.4); // Detail 0, deform significantly | |
const rockMesh = new THREE.Mesh(rockGeo, rockMaterial); | |
rockMesh.castShadow = true; | |
rockMesh.receiveShadow = true; | |
// Try placing near lake shore or cave, otherwise randomly | |
let x, z; | |
if (Math.random() < 0.3) { // Place near lake | |
const angle = Math.random() * Math.PI * 2; | |
const radius = lakeRadius + Math.random() * 5 + 1; // Just outside lake radius | |
x = lakePos.x + Math.cos(angle) * radius; | |
z = lakePos.z + Math.sin(angle) * radius; | |
} else if (Math.random() < 0.4) { // Place near cave | |
const angle = Math.random() * Math.PI * 2; | |
const radius = cavePlacementRadius * Math.random(); | |
x = cavePos.x + Math.cos(angle) * radius; | |
z = cavePos.z + Math.sin(angle) * radius; | |
} | |
else { // Place randomly | |
x = (Math.random() - 0.5) * groundSize * 1.8; | |
z = (Math.random() - 0.5) * groundSize * 1.8; | |
} | |
// Find ground height (raycast) | |
const raycaster = new THREE.Raycaster(new THREE.Vector3(x, 50, z), new THREE.Vector3(0, -1, 0)); | |
const intersects = raycaster.intersectObject(ground); | |
let y = 0; | |
if (intersects.length > 0) { | |
y = intersects[0].point.y + size * 0.3; // Place slightly embedded | |
} | |
rockMesh.position.set(x, y, z); | |
rockMesh.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, Math.random() * Math.PI); // Random rotation | |
// Avoid placing rocks inside the lake volume itself | |
const distToLakeCenter = Math.sqrt(Math.pow(x - lakePos.x, 2) + Math.pow(z - lakePos.z, 2)); | |
if (distToLakeCenter > lakeRadius) { | |
scene.add(rockMesh); | |
rocks.push(rockMesh); | |
} | |
} | |
} | |
function onWindowResize() { | |
camera.aspect = window.innerWidth / window.innerHeight; | |
camera.updateProjectionMatrix(); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
} | |
function animate() { | |
requestAnimationFrame(animate); | |
const delta = clock.getDelta(); | |
controls.update(); // Only required if controls.enableDamping = true | |
// Optional: Animate lake waves | |
/* | |
if (lake && lake.userData.originalPositions) { | |
const time = clock.getElapsedTime() * 2; | |
const positions = lake.geometry.attributes.position; | |
const originalPos = lake.userData.originalPositions; | |
for (let i = 0; i < positions.count; i++) { | |
const x = originalPos.getX(i); | |
const y = originalPos.getY(i); // Original Y is 0 for CircleGeometry | |
const wave1 = Math.sin(x * 0.5 + time) * 0.1; | |
const wave2 = Math.sin(y * 0.8 + time * 0.8) * 0.08; | |
positions.setZ(i, wave1 + wave2); // Modify Z which becomes world Y after rotation | |
} | |
positions.needsUpdate = true; | |
lake.geometry.computeVertexNormals(); // Update normals for lighting | |
} | |
*/ | |
renderer.render(scene, camera); | |
} | |
init(); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment