// Height-Map based, Bound Box fit, Particle System for pxlNav
// Written by Kevin Edzenga; 2025
import {
Vector3,
NearestFilter,
LinearFilter,
NearestMipmapNearestFilter,
LinearMipmapLinearFilter,
AdditiveBlending
} from "../../../libs/three/three.module.min.js";
import { ParticleBase } from './ParticleBase.js';
import { heightMapVert, heightMapFrag } from './shaders/HeightMap.js';
/**
* Class representing a HeightMap particle system.
* Extends the ParticleBase class.
*
* Access at - `pxlNav.pxlEffects.pxlParticles.HeightMap`
*
* Extends - [ParticleBase]{@link ParticleBase}
*
* @alias pxlParticles/HeightMap
* @class
* @memberof pxlNav.pxlEffects.pxlParticles
* @example
* this.shaderSettings = {
* "vertCount" : 1000,
* "pScale" : 7,
* "pOpacity" : 1.0,
* "proxDist" : 200,
* "atlasRes" : 4,
* "atlasPicks" : [],
* "randomAtlas" : false,
* "additiveBlend" : false,
*
* "jumpHeightMult" : 0,
* "offsetPos" : new Vector3( 0, 0, 0 ),
* "windDir" : new Vector3( 0, 0, 0 ),
*
* "size" : new Vector3( 0, 0, 0 ),
*
* "hasLights" : false,
* "fadeOutScalar" : 1.59 ,
* "wanderInf" : 1.0 ,
* "wanderRate" : 1.0 ,
* "wanderFrequency" : 2.85
* }
*@example
* // HeightMap particle system
* // Generate a system that uses a height map to set the Y position of the particles
* // With a spawn map to determine the density of the particles
* // Set noise and other settings to alter the particles
* import { Object3D } from "three";
* import { pxlEffects } from "pxlNav.esm.js";
* const HeightMap = pxlEffects.pxlParticles.HeightMap;
*
* // You can put this in yuor `fbxPostLoad()` or `build()` function
* fbxPostLoad(){
*
* // Create the HeightMap system
* let heightMapSystem = new HeightMap( this, "heightMap" );
*
* // Set the paths for the height map
* heightMapSystem.setHeightMapPath( "path/to/heightMap.jpg" );
*
* // Set the paths for the spawn map
* heightMapSystem.setSpawnMapPath( "path/to/spawnMap.jpg", 1 ); // 1 for single channel map, 3 for RGB
*
* // Get a copy of the current particle systems settings
* // Update the settings as needed
* let curShaderSettings = heightMapSystem.getSettings();
* curShaderSettings["vertCount"] = 600; // Number of particles
* curShaderSettings["pScale"] = 9; // Scale of the particles
* curShaderSettings["pOpacity"] = 0.8; // Opacity of the particles
* curShaderSettings["proxDist"] = 400; // Proximity distance
* curShaderSettings["additiveBlend"] = true; // Additive blending for the particles
*
* // Create a 3D object to use as a reference for the particle system
* // The position is used for the system's position
* // The scale is used for bounding box size of the system
* let objectRef = new Object3D();
* objectRef.position.set( 0, 0, 0 );
* objectRef.scale.set( 1000, 100, 1000 );
*
* // If you used a 3D object in your FBX file, you can use it to set the size
* // The object should have a userData property with `Scripted`{bool}
* // To set the bounding box size, set your objects scale
* // Or use these user attributes - `SizeX`{num}, `SizeY`{num}, & `SizeZ`{num}
* // let objectRef = this.geoList[ "Scripted" ][ "YouObjectName" ];
*
* heightMapSystem.build( curShaderSettings, objectRef );
*
* }
*/
export class HeightMap extends ParticleBase{
/**
* Creates an instance of HeightMap.
*
* @param {Object} room - The room object.
* @param {string} [systemName='heightMap'] - The name of the particle system.
* @property {Object} room - The room object.
* @property {string} name - The name of the particle system.
* @property {Object} heightMapPath - The path to the height map texture.
* @property {Object} spawnMapPath - The path to the spawn map texture.
* @property {number} spawnMapMode - The number of channels in the spawn map texture.
* @property {Object} material - The material for the particle system.
* @property {Object} shaderSettings - The shader settings for the particle system.
* @property {Array<string>} knownKeys - Known keys for shader settings.
*/
constructor( room=null, systemName='heightMap'){
super( room, systemName );
this.name=systemName;
this.room=room;
this.heightMapPath = null;
this.spawnMapPath = null;
this.spawnMapMode = 1;
this.material = null;
/**
* Shader settings for the height map particles.
* @type {Object}
* @property {number} vertCount - Number of vertices.
* @property {number} pScale - Scale of the particles.
* @property {number} pOpacity - Opacity of the particles.
* @property {number} proxDist - Proximity distance.
* @property {number} atlasRes - Atlas resolution.
* @property {Array} atlasPicks - Atlas picks.
* @property {boolean} randomAtlas - Random atlas flag.
* @property {boolean} additiveBlend - Additive blending flag.
* @property {number} jumpHeightMult - Jump height multiplier
* @property {Vector3} offsetPos - Offset position.
* @property {Vector3} windDir - Wind direction.
* @property {boolean} hasLights - Lights flag.
* @property {number} fadeOutScalar - Fade out scalar.
* @property {number} wanderInf - Wander influence.
* @property {number} wanderRate - Wander rate.
* @property {number} wanderFrequency - Wander frequency.
*/
this.shaderSettings = {
"vertCount" : 1000,
"pScale" : 7,
"pOpacity" : 1.0,
"proxDist" : 200,
"atlasRes" : 4,
"atlasPicks" : [],
"randomAtlas" : false,
"additiveBlend" : false,
"jumpHeightMult" : 0,
"offsetPos" : new Vector3( 0, 0, 0 ),
"windDir" : new Vector3( 0, 0, 0 ),
"size" : new Vector3( 0, 0, 0 ),
"hasLights" : false,
"fadeOutScalar" : 1.59 ,
"wanderInf" : 1.0 ,
"wanderRate" : 1.0 ,
"wanderFrequency" : 2.85
}
/**
* Known keys for shader settings.
* @type {Array<string>}
*/
this.knownKeys = Object.keys( this.shaderSettings );
}
/**
* Sets the path for the height map texture.
* @method
* @memberof pxlParticles/HeightMap
* @param {string} path - The path to the height map texture.
*/
setHeightMapPath( path ){
this.heightMapPath = path;
}
/**
* Sets the path for the spawn map texture and its mode.
* @method
* @memberof pxlParticles/HeightMap
* @param {string} path - The path to the spawn map texture.
* @param {number} [channels=1] - The number of channels in the spawn map texture.
*/
setSpawnMapPath( path, channels=1 ){
this.spawnMapPath = path;
// If the spawnMap includes wind direction data in Green & Blue channels
if( channels == 3 ){
channels = 4;
}
this.spawnMapMode = channels;
}
/**
* Builds the particle system with the given shader settings and object reference.
* @method
* @memberof pxlParticles/HeightMap
* @param {Object} [curShaderSettings={}] - The current shader settings.
* @param {Object} [objectRef=null] - The reference object for positioning and sizing.
* @returns {Object} The particle system added to the scene.
*/
build( curShaderSettings={}, objectRef=null ){
if( curShaderSettings && typeof curShaderSettings === Object ){
let curSettingKeys = Object.keys( curShaderSettings );
this.knownKeys.forEach( key => {
if( curSettingKeys.includes( key ) ){
this.shaderSettings[key] = curShaderSettings[key];
}else{
curShaderSettings[key] = this.shaderSettings[key];
}
});
}
if( curShaderSettings.hasOwnProperty("atlasPicks") ){
this.shaderSettings["atlasPicks"] = curShaderSettings["atlasPicks"];
}else if( !this.shaderSettings["atlasPicks"] || this.shaderSettings["atlasPicks"].length < 1 ){
this.shaderSettings["atlasPicks"] = [
...super.dupeArray([0.0,0.],4), ...super.dupeArray([0.25,0.],4),
...super.dupeArray([0.5,0.0],2), ...super.dupeArray([0.5,0.25],2),
...super.dupeArray([0.5,0.5],2), ...super.dupeArray([0.5,0.75],2),
...super.dupeArray([0.75,0.75],4), ...super.dupeArray([0.75,0.25],3),
...super.dupeArray([0.75,0.25],3)
];
}
this.shaderSettings["hasLights"] = super.hasPointLights();
// -- -- --
let offsetPos = curShaderSettings["offsetPos"];
if( offsetPos && typeof offsetPos === Object && typeof offsetPos !== Vector3 && offsetPos.length === 3 ){
this.shaderSettings["offsetPos"].set( offsetPos[0], offsetPos[1], offsetPos[2] );
}
// -- -- --
let sizeX = 100;
let sizeY = 100;
let sizeZ = 100;
if( objectRef ){
if( objectRef.hasOwnProperty("userData") ){
if( objectRef.userData.hasOwnProperty("SizeX") ){
sizeX = objectRef.userData.SizeX;
}
if( objectRef.userData.hasOwnProperty("SizeY") ){
sizeY = objectRef.userData.SizeY;
}
if( objectRef.userData.hasOwnProperty("SizeZ") ){
sizeZ = objectRef.userData.SizeZ;
}
}else if( objectRef.hasOwnProperty("scale") ){
sizeX = objectRef.scale.x;
sizeY = objectRef.scale.y;
sizeZ = objectRef.scale.z;
}
}
if( curShaderSettings.hasOwnProperty("size") ){
if( curShaderSettings["size"].x > 0 ){
sizeX = curShaderSettings["size"].x;
}
if( curShaderSettings["size"].y > 0 ){
sizeY = curShaderSettings["size"].y;
}
if( curShaderSettings["size"].z > 0 ){
sizeZ = curShaderSettings["size"].z;
}
}
let tankSize = new Vector3( sizeX, sizeY, sizeZ );
// -- -- --
let dustUniforms={
heightMap:{ type:"t", value: null },
spawnMap:{ type:"t", value: null },
tankSize:{ type:"v", value: tankSize },
atlasTexture:{ type:"t", value: null },
atlasAlphaTexture:{type:"t", value: null },
noiseTexture:{ type:"t", value: null },
time:{ type:"f", value: this.room.msRunner },
pointScale:{ type:"f", value: this.pscale },
intensity:{ type:"f", value:1.0 },
rate:{ type:"f", value:.035 },
positionOffset:{ type:"v", value:this.shaderSettings["offsetPos"] },
windDir:{ type:"v", value:this.shaderSettings["windDir"] }
};
//let mtl = this.pxlFile.pxlShaderBuilder( snowUniforms, snowFallVert( true ), snowFallVert() );
let mtl = this.room.pxlFile.pxlShaderBuilder( dustUniforms, heightMapVert( this.shaderSettings ), heightMapFrag( this.hasAlphaMap ) );
mtl.transparent=true;
if( this.hasAlphaMap ){
mtl.uniforms.atlasTexture.value = this.room.pxlUtils.loadTexture( this.atlasPath, 4, {"magFilter":NearestFilter, "minFilter":NearestMipmapNearestFilter} );
if( this.atlasAlphaPath ){
mtl.uniforms.atlasAlphaTexture.value = this.room.pxlUtils.loadTexture( this.atlasAlphaPath, 1, {"magFilter":NearestFilter, "minFilter":NearestMipmapNearestFilter} );
}
}else{
mtl.uniforms.atlasTexture.value = this.room.pxlUtils.loadTexture( this.atlasAlphaPath, 4, {"magFilter":NearestFilter, "minFilter":NearestMipmapNearestFilter} );
}
if( this.heightMapPath ){ // RGB Height Map
mtl.uniforms.heightMap.value = this.room.pxlUtils.loadTexture( this.heightMapPath, 4, {"magFilter":LinearFilter, "minFilter":LinearMipmapLinearFilter} );
}
if( this.spawnMapPath ){ // A or RGB Spawn Map
mtl.uniforms.spawnMap.value = this.room.pxlUtils.loadTexture( this.spawnMapPath, this.spawnMapMode, {"magFilter":NearestFilter, "minFilter":NearestMipmapNearestFilter} );
}
if( this.shaderSettings["additiveBlend"] ){
mtl.blending=AdditiveBlending;
}
mtl.uniforms.noiseTexture.value = this.room.softNoiseTexture;
mtl.depthTest=true;
mtl.depthWrite=false;
this.room.materialList[ this.name ]=mtl;
this.material = mtl;
let pSystem = super.addToScene();
pSystem.position.set( objectRef.position.x, objectRef.position.y, objectRef.position.z );
pSystem.userData["tankRes"] = tankSize;
return pSystem;
}
}