PlayT2/t2hmTool.html
2025-01-26 10:37:54 -05:00

471 lines
19 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Babylon.js Terrain Loader</title>
<style>
body {
margin: 0;
overflow: hidden;
}
#renderCanvas {
width: 100vw;
height: 100vh;
touch-action: none;
}
#buttonContainer {
position: absolute;
top: 10px;
left: 10px;
z-index: 10;
display: flex;
flex-direction: column;
gap: 10px;
}
#greyscaleCanvas {
display: block;
position: absolute;
bottom: 10px;
left: 10px;
z-index: 10;
border: 1px solid black;
width: 256px;
height: 256px;
}
</style>
</head>
<body>
<div id="buttonContainer">
<label style="color: white;">Note: Only use 256x256 PNG for import</label>
<label style="color: white;">T2 terrains are 2048m x 2048m</label>
<label style="color: white;">Each pixel is a vertex 8m apart 256 * 8</label>
<input type="file" id="fileInput" accept="image/png" />
<button id="normalizeButton">Normalize Image</button>
<button id="exportTerButton">Export Ter File</button>
<label for="minHeightSlider" style="color: white;">Min Height: <span id="minHeightValue"
style="color: white;">50</span></label>
<input type="range" id="minHeightSlider" min="10" max="1000" value="50" />
<input type="number" id="minHeightInput" min="10" max="1000" value="50" />
<label for="maxHeightSlider" style="color: white;">Max Height: <span id="maxHeightValue"
style="color: white;">200</span></label>
<input type="range" id="maxHeightSlider" min="10" max="1000" value="200" />
<input type="number" id="maxHeightInput" min="10" max="1000" value="200" />
<button id="rotateLeftButton">Rotate Left</button>
<button id="rotateRightButton">Rotate Right</button>
<button id="flipVerticalButton">Flip Vertical</button>
<button id="flipHorizontalButton">Flip Horizontal</button>
<label style="color: white;">Vertical Distance: <span id="distanceValue" style="color: white;">0</span> units</label>
<label style="color: white;">Max Position: <span id="maxPosition" style="color: white;">(0, 0, 0)</span></label>
<label style="color: white;">Min Position: <span id="minPosition" style="color: white;">(0, 0, 0)</span></label>
</div>
<canvas id="renderCanvas"></canvas>
<canvas id="greyscaleCanvas" width="256" height="256"></canvas>
<script src="https://cdn.babylonjs.com/babylon.js"></script>
<script src="https://cdn.babylonjs.com/gui/babylon.gui.min.js"></script>
<script>
const canvas = document.getElementById('renderCanvas');
const engine = new BABYLON.Engine(canvas, true);
const scene = new BABYLON.Scene(engine);
const camera = new BABYLON.ArcRotateCamera("camera1", Math.PI / 2, Math.PI / 3, 2048, BABYLON.Vector3.Zero(), scene);
camera.attachControl(canvas, true);
camera.wheelDeltaPercentage = 0.005;
const light = new BABYLON.HemisphericLight("light1", new BABYLON.Vector3(0.0, 1.0, 0.0), scene);
light.intensity = 0.75;
const size = 256;
const scaleFactor = 8;
let mHeight = [];
let map;
let minHeight = 50;
let maxHeight = 200;
let highestPoint = { x: 0, y: 0, z: 0, height: -Infinity };
let lowestPoint = { x: 0, y: 0, z: 0, height: Infinity };
let exportFileName = 'terrain';
function createTerrain() {
highestPoint = { x: 0, y: 0, z: 0, height: -Infinity };
lowestPoint = { x: 0, y: 0, z: 0, height: Infinity };
// Dispose of the previous terrain mesh if it exists
if (map) {
map.dispose();
}
const mapSubX = size; // Points along the X axis (e.g., 256)
const mapSubZ = size; // Points along the Z axis (e.g., 256)
const vertices = [];
const indices = [];
// Create vertices for the mesh (flipping on X-axis)
for (let l = 0; l < mapSubZ; l++) {
for (let w = 0; w < mapSubX; w++) {
const x = -(w - mapSubX * 0.5) * 8; // Flip the X coordinate
const z = (l - mapSubZ * 0.5) * 8; // Each vertex is 8m apart
const y = mHeight[l * mapSubX + w]; // Use loaded height data
// Store the vertex position
vertices.push(x, y, z);
if (y > highestPoint.height) {
highestPoint = { x, y, z, height: y };
}
if (y < lowestPoint.height) {
lowestPoint = { x, y, z, height: y };
}
}
}
// Create triangles by defining indices (Clockwise winding order)
for (let l = 0; l < mapSubZ - 1; l++) {
for (let w = 0; w < mapSubX - 1; w++) {
const topLeft = l * mapSubX + w;
const topRight = topLeft + 1;
const bottomLeft = (l + 1) * mapSubX + w;
const bottomRight = bottomLeft + 1;
// Adjusted triangle definitions (clockwise winding order)
indices.push(bottomLeft, topRight, topLeft); // First triangle
indices.push(bottomLeft, bottomRight, topRight); // Second triangle
}
}
// Create the mesh using the vertices and indices
const terrainMesh = new BABYLON.Mesh("terrain", scene);
const vertexData = new BABYLON.VertexData();
vertexData.positions = vertices;
vertexData.indices = indices;
// Compute normals for smooth shading
vertexData.normals = [];
BABYLON.VertexData.ComputeNormals(vertexData.positions, vertexData.indices, vertexData.normals);
// Apply the vertex data to the mesh
vertexData.applyToMesh(terrainMesh);
// Create a material with smooth shading
flatMaterial = new BABYLON.StandardMaterial("flatMaterial", scene);
flatMaterial.wireframe = false; // Set to false for smooth shaded view
flatMaterial.diffuseColor = new BABYLON.Color3(0.8, 0.8, 0.8); // Set color to something natural
flatMaterial.specularColor = new BABYLON.Color3(0.0, 0.0, 0.0); // No specular highlights for smooth shading
flatMaterial.emissiveColor = new BABYLON.Color3(0, 0, 0); // No emissive color
// Apply the material to the terrain mesh
terrainMesh.material = flatMaterial;
// Update the map variable to reference the newly created terrain mesh
map = terrainMesh;
generateGreyscaleImage();
visualizeExtremes();
}
function loadPngFile(file) {
exportFileName = file.name.split('.')[0];
const reader = new FileReader();
reader.onload = function (e) {
const img = new Image();
img.onload = function () {
if (img.width !== 256 || img.height !== 256) {
alert("Image must be 256x256 pixels.");
return;
}
const greyscaleCanvas = document.createElement('canvas');
greyscaleCanvas.width = size;
greyscaleCanvas.height = size;
const context = greyscaleCanvas.getContext('2d');
context.drawImage(img, 0, 0, size, size);
const imageData = context.getImageData(0, 0, size, size);
mHeight = [];
for (let i = 0; i < imageData.data.length; i += 4) {
var greenValue = imageData.data[i + 1]; // Use the green channel value
var height = greenValue / 255 * (maxHeight - minHeight) + minHeight;
mHeight.push(height);
}
createTerrain();
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
function rescaleHeightmap() {
const min = Math.min(...mHeight);
const max = Math.max(...mHeight);
const range = maxHeight - minHeight;
for (let i = 0; i < mHeight.length; i++) {
mHeight[i] = ((mHeight[i] - min) / (max - min)) * range + minHeight;
}
createTerrain();
}
function normalizeHeightmap() {
const min = Math.min(...mHeight);
const max = Math.max(...mHeight);
for (let i = 0; i < mHeight.length; i++) {
mHeight[i] = ((mHeight[i] - min) / (max - min)) * 255;
}
rescaleHeightmap();
}
function generateGreyscaleImage() {
const greyscaleCanvas = document.getElementById('greyscaleCanvas');
const context = greyscaleCanvas.getContext('2d');
const imageData = context.createImageData(size, size);
const minHeight = Math.min(...mHeight);
const maxHeight = Math.max(...mHeight);
for (let i = 0; i < mHeight.length; i++) {
const normalizedHeight = (mHeight[i] - minHeight) / (maxHeight - minHeight);
const grayValue = Math.floor(normalizedHeight * 255);
imageData.data[i * 4] = grayValue;
imageData.data[i * 4 + 1] = grayValue;
imageData.data[i * 4 + 2] = grayValue;
imageData.data[i * 4 + 3] = 255;
}
context.putImageData(imageData, 0, 0);
greyscaleCanvas.style.display = 'block';
}
function visualizeExtremes() {
scene.meshes.filter(m => m.name === "extreme_marker" || m.name === "extreme_line").forEach(m => m.dispose());
const highestMarker = BABYLON.MeshBuilder.CreateSphere("extreme_marker", { diameter: 20 }, scene);
highestMarker.position = new BABYLON.Vector3(highestPoint.x, highestPoint.y, highestPoint.z);
highestMarker.material = new BABYLON.StandardMaterial("highMat", scene);
highestMarker.material.diffuseColor = new BABYLON.Color3(1, 0, 0);
const lowestMarker = BABYLON.MeshBuilder.CreateSphere("extreme_marker", { diameter: 20 }, scene);
lowestMarker.position = new BABYLON.Vector3(lowestPoint.x, lowestPoint.y, lowestPoint.z);
lowestMarker.material = new BABYLON.StandardMaterial("lowMat", scene);
lowestMarker.material.diffuseColor = new BABYLON.Color3(0, 0, 1);
const highestMarker2 = BABYLON.MeshBuilder.CreateSphere("extreme_marker", { diameter: 20 }, scene);
highestMarker2.position = new BABYLON.Vector3(0, highestPoint.y, 0);
highestMarker2.material = new BABYLON.StandardMaterial("highMat", scene);
highestMarker2.material.diffuseColor = new BABYLON.Color3(1, 0, 0);
const lowestMarker2 = BABYLON.MeshBuilder.CreateSphere("extreme_marker", { diameter: 20 }, scene);
lowestMarker2.position = new BABYLON.Vector3(0, lowestPoint.y, 0);
lowestMarker2.material = new BABYLON.StandardMaterial("lowMat", scene);
lowestMarker2.material.diffuseColor = new BABYLON.Color3(0, 0, 1);
const line = BABYLON.MeshBuilder.CreateLines("extreme_line", {
points: [
new BABYLON.Vector3(highestPoint.x, highestPoint.y, highestPoint.z),
new BABYLON.Vector3(lowestPoint.x, lowestPoint.y, lowestPoint.z)
]
}, scene);
line.color = new BABYLON.Color3(0, 1, 0);
const line2 = BABYLON.MeshBuilder.CreateLines("extreme_line", {
points: [
new BABYLON.Vector3(0, highestPoint.y, 0),
new BABYLON.Vector3(0, lowestPoint.y, 0)
]
}, scene);
line2.color = new BABYLON.Color3(0, 1, 0);
const verticalDistance = Math.abs(highestPoint.y - lowestPoint.y);
document.getElementById('distanceValue').textContent = verticalDistance.toFixed(2);
document.getElementById('maxPosition').textContent = `(${highestPoint.x.toFixed(2)}, ${highestPoint.z.toFixed(2)}, ${highestPoint.y.toFixed(2)})`;
document.getElementById('minPosition').textContent = `(${lowestPoint.x.toFixed(2)}, ${lowestPoint.z.toFixed(2)}, ${lowestPoint.y.toFixed(2)})`;
}
function rotateLeft() {
const newHeight = new Array(size * size).fill(0);
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
newHeight[(size - x - 1) * size + y] = mHeight[y * size + x];
}
}
mHeight = newHeight;
rescaleHeightmap();
}
function rotateRight() {
const newHeight = new Array(size * size).fill(0);
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
newHeight[x * size + (size - y - 1)] = mHeight[y * size + x];
}
}
mHeight = newHeight;
rescaleHeightmap();
}
function flipVertical() {
const newHeight = new Array(size * size).fill(0);
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
newHeight[(size - y - 1) * size + x] = mHeight[y * size + x];
}
}
mHeight = newHeight;
rescaleHeightmap();
}
function flipHorizontal() {
const newHeight = new Array(size * size).fill(0);
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
newHeight[y * size + (size - x - 1)] = mHeight[y * size + x];
}
}
mHeight = newHeight;
rescaleHeightmap();
}
function writeString(dataView, offset, string) {
dataView.setUint8(offset, string.length);
offset++;
for (let i = 0; i < string.length; i++) {
dataView.setUint8(offset, string.charCodeAt(i));
offset++;
}
return offset;
}
//.ter format
//int8 version number
//height data 256 * 256 1d array of int16 and converted to a float with fixedToFloat or terHeight = (height * 0.03125);
//terrain material map array 256 * 256 1d array of int8, not sure if this is all that important
//texture name array first byte string size then the string with a total of 8 textures
//8 arrays of 256 * 256 of int8 alpha maps for each texture
function exportTer() {
var size = 256;
// /v // terrain size //???? //Mat Names //Texture Map 1 //Texture Map 2
var bufferSize = 1 + ((size * size) *2) + (size * size) + 23 + 23 + 6 + (size * size) + (size * size);
const buffer = new ArrayBuffer(bufferSize);
var dataViewEX = new DataView(buffer);
let off = 0;// skip the version
dataViewEX.setUint8(off, 3);
off++;
for (let i = 0; i < size * size; i++) {
var height = Math.floor(mHeight[i] / 0.03125);
dataViewEX.setUint16(off, height, true); // true for little-endian
off += 2;
}
for (let i = 0; i < size * size; i++) {
dataViewEX.setUint8(off,0);
off++;
}
off = writeString(dataViewEX, off, 'DesertWorld.SandOrange');
off = writeString(dataViewEX, off, 'DesertWorld.RockSmooth');
dataViewEX.setUint8(off, 0);
off++;
dataViewEX.setUint8(off, 0);
off++;
dataViewEX.setUint8(off, 0);
off++;
dataViewEX.setUint8(off, 0);
off++;
dataViewEX.setUint8(off, 0);
off++;
dataViewEX.setUint8(off, 0);
off++;
for (let i = 0; i < size * size; i++) {
dataViewEX.setUint8(off, 255);
off++;
}
for (let i = 0; i < size * size; i++) {
dataViewEX.setUint8(off, 128);
off++;
}
console.log('finaloffset' + off + 'buffer size' + bufferSize);
const blob = new Blob([dataViewEX.buffer], { type: "application/octet-stream" });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = exportFileName + '.ter';
link.click();
}
document.getElementById('exportTerButton').addEventListener('click', exportTer);
document.getElementById('normalizeButton').addEventListener('click', function () {
normalizeHeightmap();
});
document.getElementById('rotateLeftButton').addEventListener('click', function () {
rotateLeft();
});
document.getElementById('rotateRightButton').addEventListener('click', function () {
rotateRight();
});
document.getElementById('flipVerticalButton').addEventListener('click', function () {
flipVertical();
});
document.getElementById('flipHorizontalButton').addEventListener('click', function () {
flipHorizontal();
});
document.getElementById('minHeightSlider').addEventListener('input', function () {
const minHeightValue = document.getElementById('minHeightValue');
minHeight = parseInt(this.value, 10);
minHeightValue.textContent = this.value;
document.getElementById('minHeightInput').value = this.value;
rescaleHeightmap();
});
document.getElementById('maxHeightSlider').addEventListener('input', function () {
const maxHeightValue = document.getElementById('maxHeightValue');
maxHeight = parseInt(this.value, 10);
maxHeightValue.textContent = this.value;
document.getElementById('maxHeightInput').value = this.value;
rescaleHeightmap();
});
document.getElementById('minHeightInput').addEventListener('input', function () {
const minHeightValue = document.getElementById('minHeightValue');
minHeight = parseInt(this.value, 10);
minHeightValue.textContent = this.value;
document.getElementById('minHeightSlider').value = this.value;
rescaleHeightmap();
});
document.getElementById('maxHeightInput').addEventListener('input', function () {
const maxHeightValue = document.getElementById('maxHeightValue');
maxHeight = parseInt(this.value, 10);
maxHeightValue.textContent = this.value;
document.getElementById('maxHeightSlider').value = this.value;
rescaleHeightmap();
});
document.getElementById('fileInput').addEventListener('change', function (event) {
const file = event.target.files[0];
if (file) {
mHeight = [];
if (map) {
map.dispose();
map = null;
}
loadPngFile(file);
} else {
console.error("No file selected.");
}
});
engine.runRenderLoop(function () {
scene.render();
});
window.addEventListener('resize', function () {
engine.resize();
});
</script>
</body>
</html>