First commit
This commit is contained in:
BIN
heightmap.png
Normal file
BIN
heightmap.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
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);
|
||||||
|
});
|
||||||
119
tiles.html
Normal file
119
tiles.html
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Three.js Texture Editor</title>
|
||||||
|
<style>
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
position: relative;
|
||||||
|
font-family: "Segoe UI", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
#colorPanelContainer {
|
||||||
|
position: absolute;
|
||||||
|
top: 42px;
|
||||||
|
left: 12px;
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px;
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
border-radius: 6px;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-swatch {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border: 2px solid white;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-swatch.selected {
|
||||||
|
outline: 2px solid yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 6px 14px;
|
||||||
|
background: linear-gradient(to bottom, #f0f0f0, #dcdcdc);
|
||||||
|
border: 1px solid #999;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
box-shadow: 0 1px 2px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: linear-gradient(to bottom, #ffffff, #e0e0e0);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="ui-overlay">
|
||||||
|
<div id="colorHint" style="
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
left: 10px;
|
||||||
|
color: white;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
background: rgba(0,0,0,0.6);
|
||||||
|
padding: 4px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
">
|
||||||
|
Scroll to switch colors
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="colorPanelContainer"></div>
|
||||||
|
|
||||||
|
<div class="button-group">
|
||||||
|
<button id="resetAll">Reset All</button>
|
||||||
|
|
||||||
|
<button id="pulseButton">Pulse</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module" src="./tiled_ground.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user