From 4492b7ace80b37e58e42b3f14516dfdbbd3f3ff5 Mon Sep 17 00:00:00 2001 From: Kamil Krzywda Date: Tue, 6 Oct 2026 19:08:38 +0000 Subject: [PATCH] feat: tank geometry, glass, gravel & lighting (#1, #2) --- src/main.ts | 24 +++----- src/scene/Lighting.ts | 36 +++++++++++ src/scene/Tank.ts | 135 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 179 insertions(+), 16 deletions(-) create mode 100644 src/scene/Lighting.ts create mode 100644 src/scene/Tank.ts diff --git a/src/main.ts b/src/main.ts index 66d397d..56af372 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,6 @@ import * as THREE from "three"; +import { createTank } from "./scene/Tank"; +import { setupLighting } from "./scene/Lighting"; const canvas = document.querySelector("#aquarium"); if (!canvas) { @@ -13,29 +15,19 @@ renderer.setPixelRatio(window.devicePixelRatio); const scene = new THREE.Scene(); scene.background = new THREE.Color(0x0a0a0f); -// Camera +// Camera — frames the tank, positioned slightly above its center const camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 0.1, 100 ); -camera.position.set(0, 0, 8); -camera.lookAt(0, 0, 0); +camera.position.set(0, 1.5, 6); +camera.lookAt(0, 1.2, 0); -// Placeholder: small blue-green box at the origin -const placeholder = new THREE.Mesh( - new THREE.BoxGeometry(1, 1, 1), - new THREE.MeshBasicMaterial({ color: 0x00ffaa }) -); -scene.add(placeholder); - -// Lights (so we can use standard materials later) -const ambient = new THREE.AmbientLight(0xffffff, 0.6); -scene.add(ambient); -const directional = new THREE.DirectionalLight(0xffffff, 1); -directional.position.set(5, 10, 7); -scene.add(directional); +// Scene content +setupLighting(scene, renderer); +createTank(scene); // Resize handling function resize(): void { diff --git a/src/scene/Lighting.ts b/src/scene/Lighting.ts new file mode 100644 index 0000000..2e35eed --- /dev/null +++ b/src/scene/Lighting.ts @@ -0,0 +1,36 @@ +import * as THREE from "three"; + +export function setupLighting( + scene: THREE.Scene, + renderer: THREE.WebGLRenderer +): void { + // Renderer shadow config + renderer.shadowMap.enabled = true; + renderer.shadowMap.type = THREE.PCFSoftShadowMap; + + // Warm point light near top center inside the tank (main, shadow-casting) + const mainLight = new THREE.PointLight(0xffeedd, 2); + mainLight.position.set(0, 2.2, 0); + mainLight.castShadow = true; + mainLight.shadow.mapSize.set(1024, 1024); + mainLight.shadow.bias = -0.002; + scene.add(mainLight); + + // Warm point light offset left-back (fill, no shadow) + const fillLight = new THREE.PointLight(0xffe8cc, 1.2); + fillLight.position.set(-1.2, 2.0, -0.5); + fillLight.castShadow = false; + scene.add(fillLight); + + // Cool dark blue ambient (dark room feel) + const ambient = new THREE.AmbientLight(0x1a2a3a, 0.4); + scene.add(ambient); + + // Subtle directional from above (no shadow — point light handles shadows) + const directional = new THREE.DirectionalLight(0xffffff, 0.3); + directional.position.set(0, 5, 0); + directional.target.position.set(0, 0, 0); + directional.castShadow = false; + scene.add(directional); + scene.add(directional.target); +} diff --git a/src/scene/Tank.ts b/src/scene/Tank.ts new file mode 100644 index 0000000..18ad888 --- /dev/null +++ b/src/scene/Tank.ts @@ -0,0 +1,135 @@ +import * as THREE from "three"; + +// Tank dimensions (units) +const TANK_WIDTH = 4; // x +const TANK_HEIGHT = 2.5; // y +const TANK_DEPTH = 3; // z + +// Tank spans x: -2..2, y: 0..2.5, z: -1.5..1.5 (open top) + +function createGlassMaterial(): THREE.MeshPhysicalMaterial { + return new THREE.MeshPhysicalMaterial({ + transmission: 0.9, + transparent: true, + opacity: 0.3, + color: 0x88ccff, + roughness: 0.05, + metalness: 0, + side: THREE.DoubleSide, + }); +} + +const GRAVEL_COLORS = [ + 0x8b7355, // brown + 0xa0926b, // brown + 0x6b5b45, // dark brown + 0xc4a882, // tan + 0x4a3b2a, // dark + 0x7a7a7a, // grey +]; + +function hexToCss(hex: number): string { + return `#${hex.toString(16).padStart(6, "0")}`; +} + +function createGravelTexture(): THREE.CanvasTexture { + const size = 256; + const canvas = document.createElement("canvas"); + canvas.width = size; + canvas.height = size; + + const ctx = canvas.getContext("2d"); + if (ctx) { + // Base fill (earthy mid-brown) + ctx.fillStyle = hexToCss(0x6b5b45); + ctx.fillRect(0, 0, size, size); + + // ~200 random dots in earthy colors + for (let i = 0; i < 200; i++) { + const color = GRAVEL_COLORS[Math.floor(Math.random() * GRAVEL_COLORS.length)]; + ctx.fillStyle = hexToCss(color); + const radius = 2 + Math.random() * 4; // 2-6px + const x = Math.random() * size; + const y = Math.random() * size; + ctx.beginPath(); + ctx.arc(x, y, radius, 0, Math.PI * 2); + ctx.fill(); + } + } + + return new THREE.CanvasTexture(canvas); +} + +export function createTank(scene: THREE.Scene): { group: THREE.Group } { + const group = new THREE.Group(); + + // Shared glass material for all walls + const glass = createGlassMaterial(); + + // Front wall (z = +1.5, facing the camera) + const front = new THREE.Mesh( + new THREE.PlaneGeometry(TANK_WIDTH, TANK_HEIGHT), + glass + ); + front.position.set(0, TANK_HEIGHT / 2, TANK_DEPTH / 2); + + // Back wall (z = -1.5) + const back = new THREE.Mesh( + new THREE.PlaneGeometry(TANK_WIDTH, TANK_HEIGHT), + glass + ); + back.position.set(0, TANK_HEIGHT / 2, -TANK_DEPTH / 2); + back.rotation.y = Math.PI; + + // Left wall (x = -2) + const left = new THREE.Mesh( + new THREE.PlaneGeometry(TANK_DEPTH, TANK_HEIGHT), + glass + ); + left.position.set(-TANK_WIDTH / 2, TANK_HEIGHT / 2, 0); + left.rotation.y = -Math.PI / 2; + + // Right wall (x = +2) + const right = new THREE.Mesh( + new THREE.PlaneGeometry(TANK_DEPTH, TANK_HEIGHT), + glass + ); + right.position.set(TANK_WIDTH / 2, TANK_HEIGHT / 2, 0); + right.rotation.y = Math.PI / 2; + + // Bottom (y = 0, facing up) + const bottom = new THREE.Mesh( + new THREE.PlaneGeometry(TANK_WIDTH, TANK_DEPTH), + glass + ); + bottom.position.set(0, 0, 0); + bottom.rotation.x = -Math.PI / 2; + + // Thin frame outline around the tank + const frameGeometry = new THREE.EdgesGeometry( + new THREE.BoxGeometry(TANK_WIDTH, TANK_HEIGHT, TANK_DEPTH) + ); + const frame = new THREE.LineSegments( + frameGeometry, + new THREE.LineBasicMaterial({ color: 0x333333 }) + ); + frame.position.set(0, TANK_HEIGHT / 2, 0); + + // Gravel floor, slightly above the bottom glass + const gravel = new THREE.Mesh( + new THREE.PlaneGeometry(TANK_WIDTH, TANK_DEPTH), + new THREE.MeshStandardMaterial({ + map: createGravelTexture(), + roughness: 0.9, + metalness: 0, + }) + ); + gravel.position.set(0, 0.05, 0); + gravel.rotation.x = -Math.PI / 2; + gravel.receiveShadow = true; + + group.add(front, back, left, right, bottom, frame, gravel); + scene.add(group); + + return { group }; +}