// 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( ); }