From 09ba6fc5d47680c3c5a2a98bfb9630239be4079d Mon Sep 17 00:00:00 2001 From: kaj dijkstra Date: Thu, 25 Dec 2025 10:16:42 +0100 Subject: [PATCH] First commit --- heightmap.png | Bin 0 -> 21251 bytes tiled_ground.js | 547 ++++++++++++++++++++++++++++++++++++++++++++++++ tiles.html | 119 +++++++++++ 3 files changed, 666 insertions(+) create mode 100644 heightmap.png create mode 100644 tiled_ground.js create mode 100644 tiles.html diff --git a/heightmap.png b/heightmap.png new file mode 100644 index 0000000000000000000000000000000000000000..4c244fcea430e4ea8f9985a2ff8bff2c94133b73 GIT binary patch literal 21251 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 UPc|(irNPIO$FS}-uAw*n2b<&mOaK4? literal 0 HcmV?d00001 diff --git a/tiled_ground.js b/tiled_ground.js new file mode 100644 index 0000000..8dfe2f5 --- /dev/null +++ b/tiled_ground.js @@ -0,0 +1,547 @@ +import * as THREE from 'https://esm.sh/three@0.160.0'; +import { OrbitControls } from 'https://esm.sh/three@0.160.0/examples/jsm/controls/OrbitControls.js'; +import { EffectComposer } from 'https://esm.sh/three@0.160.0/examples/jsm/postprocessing/EffectComposer.js'; +import { RenderPass } from 'https://esm.sh/three@0.160.0/examples/jsm/postprocessing/RenderPass.js'; +import { UnrealBloomPass } from 'https://esm.sh/three@0.160.0/examples/jsm/postprocessing/UnrealBloomPass.js'; +import { SSAOPass } from 'https://esm.sh/three@0.160.0/examples/jsm/postprocessing/SSAOPass.js'; + + +// === SCENE SETUP === +const scene = new THREE.Scene(); +scene.background = new THREE.Color(0x000000); + +const camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 1000); +camera.position.set(0, 30, 40); +camera.lookAt(0, 0, 0); + +const renderer = new THREE.WebGLRenderer({ antialias: true }); +renderer.setSize(window.innerWidth, window.innerHeight); +renderer.shadowMap.enabled = true; +renderer.toneMapping = THREE.NoToneMapping; +renderer.outputColorSpace = THREE.LinearSRGBColorSpace; +document.body.appendChild(renderer.domElement); + +const controls = new OrbitControls(camera, renderer.domElement); +controls.enableDamping = true; + +// === POSTPROCESSING === +const composer = new EffectComposer(renderer); +composer.addPass(new RenderPass(scene, camera)); +composer.addPass(new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 0.23, 0.4, 0.2)); + +// === LIGHTING === + + + +const light = new THREE.DirectionalLight(0xffffff, 1); +light.position.set(10, 20, 10); +light.castShadow = true; + +light.shadow.mapSize.width = 1024; +light.shadow.mapSize.height = 1024; + +light.shadow.camera.near = 0.5; +light.shadow.camera.far = 100; +light.shadow.camera.left = -50; +light.shadow.camera.right = 50; +light.shadow.camera.top = 50; +light.shadow.camera.bottom = -50; + +scene.add(light); + + +// === MOUSE === +const mouse = new THREE.Vector2(); +const raycaster = new THREE.Raycaster(); +const intersectPoint = new THREE.Vector3(); +const yPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0); +window.addEventListener('mousemove', e => { + mouse.x = (e.clientX / window.innerWidth) * 2 - 1; + mouse.y = -(e.clientY / window.innerHeight) * 2 + 1; +}); + +const ssaoPass = new SSAOPass(scene, camera, window.innerWidth, window.innerHeight); +ssaoPass.kernelRadius = 16; +ssaoPass.minDistance = 0.01; +ssaoPass.maxDistance = 0.2; +ssaoPass.output = SSAOPass.OUTPUT.Default; +//composer.addPass(ssaoPass); + +//composer.addPass(new OutputPass()); + + +//const helper = new THREE.CameraHelper(light.shadow.camera); +//scene.add(helper); + +renderer.shadowMap.enabled = true; +renderer.shadowMap.type = THREE.PCFSoftShadowMap; + +// === HEX TILE SETUP === +const radius = 1; +const hexWidth = 2 * radius; +const hexHeight = Math.sqrt(3) * radius; +const spacing = 0.1; +const gridX = 100; +const gridY = 100; + +const offsetX = -((gridX - 1) * (hexWidth * 0.75 + spacing)) / 2; +const offsetZ = -((gridY - 1) * (hexHeight + spacing)) / 2; + +const hexGeo = new THREE.CylinderGeometry(radius, radius, 1, 6); +hexGeo.rotateY(Math.PI / 6); + +const hexMat = new THREE.MeshStandardMaterial({ + + vertexColors: true // accepted but insufficient alone +}); + +hexMat.onBeforeCompile = function ( shader ) { + + shader.vertexShader = shader.vertexShader.replace( + '#include ', + ` + #include + attribute vec3 instanceColor; + varying vec3 vInstanceColor; + ` + ); + + shader.vertexShader = shader.vertexShader.replace( + '#include ', + ` + #include + vInstanceColor = instanceColor; + ` + ); + + shader.fragmentShader = shader.fragmentShader.replace( + '#include ', + ` + #include + varying vec3 vInstanceColor; + ` + ); + + shader.fragmentShader = shader.fragmentShader.replace( + '#include ', + ` + diffuseColor.rgb *= vInstanceColor; + ` + ); + +}; + +const instCount = gridX * gridY; + + +const instMesh = new THREE.InstancedMesh(hexGeo, hexMat, instCount); +scene.add(instMesh); + +const colorArray = new Float32Array(instCount * 3); + +for (let i = 0; i < instCount; i++) { + colorArray[i * 3 + 0] = 1.0; // R + colorArray[i * 3 + 1] = 1.0; // G + colorArray[i * 3 + 2] = 1.0; // B +} + +const instanceColorAttr = new THREE.InstancedBufferAttribute(colorArray, 3); + +instMesh.geometry.setAttribute("instanceColor", instanceColorAttr); + + +instMesh.castShadow = true; +instMesh.receiveShadow = true; + + +const dummy = new THREE.Object3D(); +const tmpVec = new THREE.Vector3(); + +const hexPositions = []; + +let idx = 0; +for (let y = 0; y < gridY; y++) { + for (let x = 0; x < gridX; x++) { + const px = x * (hexWidth * 0.75 + spacing) + offsetX; + const pz = y * (hexHeight + spacing) + (x % 2) * (hexHeight / 2) + offsetZ; + hexPositions.push({ x: px, z: pz }); + + dummy.position.set(px, 0, pz); + dummy.scale.set(1, 1, 1); + dummy.updateMatrix(); + instMesh.setMatrixAt(idx++, dummy.matrix); + } +} +instMesh.instanceMatrix.needsUpdate = true; + +const hexRadius = 1; + +const effectiveXGap = hexWidth * 0.75 + spacing; +const effectiveZGap = hexHeight + spacing; + +const totalWidth = effectiveXGap * (gridX - 1) + hexWidth; +const totalHeight = effectiveZGap * (gridY - 1) + hexHeight; + + +// === GLOW PLANE === +const glowPlane = new THREE.Mesh( + new THREE.PlaneGeometry(totalWidth - (hexWidth/2), totalHeight - (hexWidth/2)), + new THREE.MeshBasicMaterial({ + color: new THREE.Color(10, 10, 10), + toneMapped: false + }) +); +glowPlane.rotation.x = -Math.PI / 2; +scene.add(glowPlane); + +let currentColorIndex = 0; + +const colorPalette = [ + [1.0, 0.8, 0.0], // Amber + [0.5, 1.0, 0.0], // Lime + [0.0, 1.0, 0.0], // Green + [0.0, 1.0, 0.5], // Spring Green + [0.0, 1.0, 1.0], // Cyan + [0.0, 0.5, 1.0], // Sky Blue + [0.0, 0.0, 1.0], // Blue + [0.5, 0.0, 1.0], // Purple + [1.0, 0.0, 1.0], // Magenta + [1.0, 0.0, 0.5] // Rose +]; + +const panelContainer = document.getElementById("colorPanelContainer"); + +const colorSwatches = []; + +function rgbToCssColor(rgb) { + const [r, g, b] = rgb.map(c => Math.floor(c * 255)); + return `rgb(${r}, ${g}, ${b})`; +} + +function createColorPanels() { + panelContainer.innerHTML = ""; + colorSwatches.length = 0; + + colorPalette.forEach((color, index) => { + const panel = document.createElement("div"); + + panel.className = "color-swatch"; + panel.style.width = "24px"; + panel.style.height = "24px"; + panel.style.borderRadius = "4px"; + panel.style.cursor = "pointer"; + panel.style.backgroundColor = rgbToCssColor(color); + + panel.addEventListener("click", function () { + currentColorIndex = index; + updateColorSwatchSelection(); + }); + + colorSwatches.push(panel); + panelContainer.appendChild(panel); + }); + + updateColorSwatchSelection(); +} + +function updateColorSwatchSelection() { + colorSwatches.forEach((panel, index) => { + panel.style.border = index === currentColorIndex ? "3px solid white" : "1px solid #888"; + }); +} + +createColorPanels(); + +window.addEventListener("wheel", function (event) { + if (event.deltaY > 0) { + currentColorIndex = (currentColorIndex + 1) % colorPalette.length; + } else { + currentColorIndex = (currentColorIndex - 1 + colorPalette.length) % colorPalette.length; + } + updateColorSwatchSelection(); +}); + + +// === HEIGHTMAP === +let heightData = null; +let mapWidth = 0; +let mapHeight = 0; +let heightMapImage = new Image(); +heightMapImage.crossOrigin = ''; +heightMapImage.src = 'heightmap.png'; + +heightMapImage.onload = function () { + const canvas = document.createElement('canvas'); + canvas.width = heightMapImage.width; + canvas.height = heightMapImage.height; + mapWidth = canvas.width; + mapHeight = canvas.height; + + const ctx = canvas.getContext('2d'); + ctx.drawImage(heightMapImage, 0, 0); + const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height).data; + + heightData = new Float32Array(canvas.width * canvas.height); + for (let i = 0; i < canvas.width * canvas.height; i++) { + heightData[i] = imgData[i * 4] / 255; + } +}; + +function sampleHeightFromMap(x, z) { + if (!heightData) return 0; + + const nx = (x + 100) / 200; + const nz = (z + 100) / 200; + + const ix = Math.floor(nx * mapWidth); + const iz = Math.floor(nz * mapHeight); + + if (ix < 0 || ix >= mapWidth || iz < 0 || iz >= mapHeight) return 0; + + return heightData[iz * mapWidth + ix] * 8; +} + +// === INTERACTION LOGIC === +function updateMouseIntersection() { + raycaster.setFromCamera(mouse, camera); + raycaster.ray.intersectPlane(yPlane, intersectPoint); +} + +const storedScales = new Float32Array(instCount).fill(1); + + +let isLeftMouseDown = false; +let isRightMouseDown = false; + +window.addEventListener( 'mousedown', function ( event ) { + + if ( event.button === 0 ) { + isLeftMouseDown = true; + } + + if ( event.button === 2 ) { + isRightMouseDown = true; + } + +} ); + +window.addEventListener( 'mouseup', function ( event ) { + + if ( event.button === 0 ) { + isLeftMouseDown = false; + } + + if ( event.button === 2 ) { + isRightMouseDown = false; + } + +} ); + + +var elevate = true; +/*document.getElementById("toggleMode").addEventListener("click", function () { + elevate = !elevate; + + this.textContent = elevate ? "Return" : "Elevate"; +});*/ + +var restore = false; + +document.getElementById("resetAll").addEventListener("click", function () { + restore = true; +}); + + +controls.enableZoom = false; + +const currentColors = new Array(instCount).fill().map(() => [1, 1, 1]); // white start +const targetColors = new Array(instCount).fill().map(() => [1, 1, 1]); // default: whi +const isHovered = new Array(instCount).fill(false); +let allColorsRestored = true; + +let pulseActive = false; +let pulseStartTime = 0; +const pulseTotalTime = 2.4; // seconds + +document.getElementById("pulseButton").addEventListener("click", function () { + pulseActive = true; + pulseStartTime = performance.now() / 1000; +}); + +function smoothPulse( t ) { + if (t < 0 || t > 1) return 0; + + return Math.pow( 2 * t - 1, 4 ); // fast center, smooth edges +} + +function updateInstancedHeights() { + if (!heightData) return; + let allRestored = true; + let allColorsRestored = true; + + + + for (let i = 0; i < instCount; i++) { + const { x, z } = hexPositions[i]; + tmpVec.set(x, 0, z); + + +const now = performance.now() / 1000; +const pulseTime = now - pulseStartTime; +let pulseOffset = 0; + + + + +if (pulseActive) { + const now = performance.now() / 1000; + const pulseTime = now - pulseStartTime; + const distToCenter = tmpVec.length(); + const waveDelay = distToCenter * 0.05; + const localTime = pulseTime - waveDelay; + const amplitude = 1; + + if (localTime >= 0 && localTime <= pulseTotalTime) { + const normalized = localTime / pulseTotalTime; +const wave = smoothPulse( normalized ); +pulseOffset = wave * amplitude; + + //pulseOffset = wave * 1.5; // adjust amplitude + } +} + +var baseY = storedScales[i]; +var targetY = baseY + pulseOffset; + +if (pulseTime > pulseTotalTime + 2.0) { + pulseActive = false; +} + + + + const d = tmpVec.distanceTo(intersectPoint); + const isHovering = d < 3.5; + + // Elevation logic + //let targetY = storedScales[i]; + + if (elevate) { + if (isHovering) { + const sampled = sampleHeightFromMap(x, z); + const scaled = 1 + sampled * 3; + + if (scaled > targetY) { + targetY = scaled; + } + } + } else { + if (isHovering) { + targetY = 1; + } + } + + if (restore) { + targetY = 1; + + if (Math.abs(storedScales[i] - targetY) > 1.01) { + allRestored = false; + } + } + + // Interpolate elevation + storedScales[i] += (targetY - storedScales[i]) * 0.1; + + // Apply matrix + dummy.position.set(x, 0, z); + dummy.scale.set(1, storedScales[i], 1); + dummy.updateMatrix(); + instMesh.setMatrixAt(i, dummy.matrix); + } + + + const instanceColors = instMesh.geometry.attributes.instanceColor; + const [ r, g, b ] = colorPalette[ currentColorIndex ]; + for (let i = 0; i < instCount; i++) { + const { x, z } = hexPositions[i]; + tmpVec.set(x, 0, z); + + const d = tmpVec.distanceTo(intersectPoint); + + // Mark hover state + if (d < 3.5 && !restore) { + targetColors[i][0] = r; + targetColors[i][1] = g; + targetColors[i][2] = b; + + isHovered[i] = true; + } else { + isHovered[i] = false; + } + + if (restore) { + targetColors[i][0] = 1; + targetColors[i][1] = 1; + targetColors[i][2] = 1; + isHovered[i] = false; + } + + + + // Only update current color if hovered this frame + if (isHovered[i] || restore) { + for (let j = 0; j < 3; j++) { + currentColors[i][j] += (targetColors[i][j] - currentColors[i][j]) * 0.05; + } + } + + const [ cr, cg, cb ] = currentColors[i]; + + if (restore) { + if (Math.abs(cr - 1) > 0.01 || Math.abs(cg - 1) > 0.01 || Math.abs(cb - 1) > 0.01) { + allColorsRestored = false; + } + } + + instanceColors.setXYZ(i, currentColors[i][0], currentColors[i][1], currentColors[i][2]); + } + + instanceColors.needsUpdate = true; + instanceColors.needsUpdate = true; + + + instanceColors.needsUpdate = true; + instMesh.instanceMatrix.needsUpdate = true; + + if (restore && allRestored && allColorsRestored) { + restore = false; + } +} + + + + +// === ANIMATION LOOP === +function animate() { + requestAnimationFrame(animate); + + updateMouseIntersection(); + updateInstancedHeights(); + + controls.update(); + + camera.layers.set(1); + composer.render(); + camera.layers.set(0); + renderer.clearDepth(); + composer.render(); + + +} +animate(); + +window.addEventListener('resize', () => { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); + composer.setSize(window.innerWidth, window.innerHeight); +}); diff --git a/tiles.html b/tiles.html new file mode 100644 index 0000000..103a83f --- /dev/null +++ b/tiles.html @@ -0,0 +1,119 @@ + + + + + Three.js Texture Editor + + + +
+
+ Scroll to switch colors +
+ +
+ +
+ + + + +
+
+ + + +