first commit

This commit is contained in:
2025-12-31 14:22:45 +01:00
commit c78a860098
73 changed files with 30137 additions and 0 deletions

View File

@@ -0,0 +1,280 @@
// Simple Node.js (ESM) script to bake a grayscale heightmap PNG
// from the OBJ terrain model: resources/models/Terrain003_1K.obj
//
// Usage (from repo root, after installing pngjs):
// npm install pngjs
// node tools/generate_terrain_heightmap.js
//
// It will write a PNG into: resources/textures/heightmap/Terrain003_1K_Height512.png
import fs from "node:fs";
import path from "node:path";
import { PNG } from "pngjs";
const ROOT_DIR = path.dirname( path.dirname( new URL( import.meta.url ).pathname ) );
const OBJ_PATH = path.join( ROOT_DIR, "resources", "models", "Terrain003_1K.obj" );
const OUT_PATH = path.join( ROOT_DIR, "resources", "textures", "heightmap", "Terrain003_1K_Height512.png" );
const RESOLUTION = 512;
function parseObjVertices( text ) {
const lines = text.split( /\r?\n/ );
const vertices = [];
for ( let i = 0; i < lines.length; i++ ) {
const line = lines[ i ].trim();
if ( line.length === 0 || line.charAt( 0 ) === "#" ) {
continue;
}
const parts = line.split( /\s+/ );
if ( parts[ 0 ] === "v" && parts.length >= 4 ) {
const x = parseFloat( parts[ 1 ] );
const y = parseFloat( parts[ 2 ] );
const z = parseFloat( parts[ 3 ] );
if ( !isNaN( x ) && !isNaN( y ) && !isNaN( z ) ) {
vertices.push( { x, y, z } );
}
}
}
return vertices;
}
function buildHeightmapFromVertices( vertices, resolution ) {
if ( vertices.length === 0 ) {
throw new Error( "No vertices parsed from OBJ." );
}
let minX = Infinity, maxX = -Infinity;
let minY = Infinity, maxY = -Infinity;
let minZ = Infinity, maxZ = -Infinity;
for ( const v of vertices ) {
if ( v.x < minX ) minX = v.x;
if ( v.x > maxX ) maxX = v.x;
if ( v.y < minY ) minY = v.y;
if ( v.y > maxY ) maxY = v.y;
if ( v.z < minZ ) minZ = v.z;
if ( v.z > maxZ ) maxZ = v.z;
}
const spanX = maxX - minX || 1;
const spanZ = maxZ - minZ || 1;
const width = resolution;
const height= resolution;
const heights = new Float32Array( width * height );
const counts = new Uint32Array( width * height );
for ( const v of vertices ) {
const uNorm = ( v.x - minX ) / spanX;
const vNorm = ( v.z - minZ ) / spanZ;
let ix = Math.round( uNorm * ( width - 1 ) );
let iy = Math.round( vNorm * ( height - 1 ) );
if ( ix < 0 ) ix = 0;
if ( ix >= width ) ix = width - 1;
if ( iy < 0 ) iy = 0;
if ( iy >= height ) iy = height - 1;
const idx = iy * width + ix;
heights[ idx ] += v.y;
counts[ idx ]++;
}
for ( let i = 0; i < heights.length; i++ ) {
if ( counts[ i ] > 0 ) {
heights[ i ] /= counts[ i ];
} else {
heights[ i ] = minY;
}
}
const tmp = new Float32Array( width * height );
for ( let pass = 0; pass < 2; pass++ ) {
for ( let y = 0; y < height; y++ ) {
for ( let x = 0; x < width; x++ ) {
let sum = 0;
let weight = 0;
for ( let dy = -1; dy <= 1; dy++ ) {
for ( let dx = -1; dx <= 1; dx++ ) {
const nx = x + dx;
const ny = y + dy;
if ( nx < 0 || ny < 0 || nx >= width || ny >= height ) {
continue;
}
const nIdx = ny * width + nx;
sum += heights[ nIdx ];
weight += 1;
}
}
const idx = y * width + x;
tmp[ idx ] = weight > 0 ? sum / weight : heights[ idx ];
}
}
for ( let i = 0; i < heights.length; i++ ) {
heights[ i ] = tmp[ i ];
}
}
let outMin = Infinity;
let outMax = -Infinity;
for ( let i = 0; i < heights.length; i++ ) {
const h = heights[ i ];
if ( h < outMin ) outMin = h;
if ( h > outMax ) outMax = h;
}
const range = outMax - outMin || 1;
const normalized = new Float32Array( width * height );
for ( let i = 0; i < heights.length; i++ ) {
normalized[ i ] = ( heights[ i ] - outMin ) / range;
}
return { width, height, data: normalized };
}
function writeHeightmapPng( heightmap, outPath ) {
const { width, height, data } = heightmap;
const png = new PNG( { width, height } );
for ( let y = 0; y < height; y++ ) {
for ( let x = 0; x < width; x++ ) {
const idx = y * width + x;
const v = Math.max( 0, Math.min( 1, data[ idx ] ) );
const c = Math.round( v * 255 );
const o = idx * 4;
png.data[ o + 0 ] = c;
png.data[ o + 1 ] = c;
png.data[ o + 2 ] = c;
png.data[ o + 3 ] = 255;
}
}
return new Promise( function( resolve, reject ) {
const stream = fs.createWriteStream( outPath );
stream.on( "finish", resolve );
stream.on( "error", reject );
png.pack().pipe( stream );
} );
}
async function main( ) {
try {
console.log( "Reading OBJ:", OBJ_PATH );
const text = fs.readFileSync( OBJ_PATH, "utf8" );
const vertices = parseObjVertices( text );
console.log( "Parsed vertices:", vertices.length );
const heightmap = buildHeightmapFromVertices( vertices, RESOLUTION );
console.log( "Heightmap built:", heightmap.width, "x", heightmap.height );
await writeHeightmapPng( heightmap, OUT_PATH );
console.log( "Heightmap written to:", OUT_PATH );
} catch ( err ) {
console.error( "Failed to generate heightmap:", err );
process.exitCode = 1;
}
}
if ( import.meta.url === `file://${ process.argv[ 1 ] }` ) {
main( );
}