mirror of
https://github.com/ChocoTaco1/PlayT2.git
synced 2026-01-19 17:44:45 +00:00
471 lines
19 KiB
HTML
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> |