first commit
This commit is contained in:
280
tools/generate_terrain_heightmap.js
Normal file
280
tools/generate_terrain_heightmap.js
Normal 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( );
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user