Skip to content

Instantly share code, notes, and snippets.

@RageshAntonyHM
Created April 10, 2025 18:19
Show Gist options
  • Save RageshAntonyHM/39f541b48f79ad9e47219b6aaacafc30 to your computer and use it in GitHub Desktop.
Save RageshAntonyHM/39f541b48f79ad9e47219b6aaacafc30 to your computer and use it in GitHub Desktop.
THREE.js forest
<!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