First commit
This commit is contained in:
547
tiled_ground.js
Normal file
547
tiled_ground.js
Normal file
@@ -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 <common>',
|
||||
`
|
||||
#include <common>
|
||||
attribute vec3 instanceColor;
|
||||
varying vec3 vInstanceColor;
|
||||
`
|
||||
);
|
||||
|
||||
shader.vertexShader = shader.vertexShader.replace(
|
||||
'#include <begin_vertex>',
|
||||
`
|
||||
#include <begin_vertex>
|
||||
vInstanceColor = instanceColor;
|
||||
`
|
||||
);
|
||||
|
||||
shader.fragmentShader = shader.fragmentShader.replace(
|
||||
'#include <common>',
|
||||
`
|
||||
#include <common>
|
||||
varying vec3 vInstanceColor;
|
||||
`
|
||||
);
|
||||
|
||||
shader.fragmentShader = shader.fragmentShader.replace(
|
||||
'#include <color_fragment>',
|
||||
`
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user