@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
266 lines (204 loc) • 9.13 kB
JavaScript
import { assert } from "../../../../core/assert.js";
import { v2_distance } from "../../../../core/geom/vec2/v2_distance.js";
import { clamp } from "../../../../core/math/clamp.js";
import { clamp01 } from "../../../../core/math/clamp01.js";
import { inverseLerp } from "../../../../core/math/inverseLerp.js";
import { max2 } from "../../../../core/math/max2.js";
import { seededRandom } from "../../../../core/math/random/seededRandom.js";
import LinearModifier from "../../../../core/model/stat/LinearModifier.js";
import TransitionFunctions from "../../../../engine/animation/TransitionFunctions.js";
import { TerrainLayer } from "../../../../engine/ecs/terrain/ecs/layers/TerrainLayer.js";
import Terrain from "../../../../engine/ecs/terrain/ecs/Terrain.js";
import { CellFilterLiteralFloat } from "../../../filtering/numeric/CellFilterLiteralFloat.js";
import { MarkerNodeAction } from "../MarkerNodeAction.js";
export class MarkerNodeActionPaintTerrain extends MarkerNodeAction {
/**
*
* @CellFilter
*/
weight = CellFilterLiteralFloat.ONE;
/**
* What to paint
* @type {TerrainLayerDescription}
*/
layer = null;
/**
* Actual layer index, will be taken from the terrain upon initialization
* @type {number}
* @private
*/
_layer_index = -1;
/**
*
* @type {SplatMapping}
* @private
*/
_splat_map = null;
/**
*
* @type {Terrain}
* @private
*/
_terrain = null;
/**
* How far does the area extend from the marker's shape, has both % and constant component
* Can grow or shrink the area
* @type {LinearModifier}
*/
dilation = LinearModifier.CONSTANT_ZERO;
/**
* Function to apply smoothing to the area that falls into "edge"
* @type {function(number): number}
*/
weight_edge_easing_function = TransitionFunctions.EaseInOut;
/**
* How wide is the edge, computed from the effective region including padding, must produce a non-negative number
* @type {LinearModifier}
*/
weight_edge_easing = LinearModifier.CONSTANT_ZERO;
/**
*
* @type {function():number}
* @private
*/
_random;
/**
*
* @param {LinearModifier} dilation
* @param {function(number):number} border_blending
* @param {LinearModifier} border
* @param {CellFilter} weight
* @param {TerrainLayerDescription} layer
* @returns {MarkerNodeActionPaintTerrain}
*/
static from({
dilation = LinearModifier.CONSTANT_ZERO,
border_blending = TransitionFunctions.EaseInOut,
border = LinearModifier.CONSTANT_ZERO,
weight = CellFilterLiteralFloat.ONE,
layer
}) {
assert.defined(layer, 'layer');
assert.notNull(layer, 'layer');
assert.equal(layer.isTerrainLayerDescription, true, 'layer.isTerrainLayerDescription !== true');
const r = new MarkerNodeActionPaintTerrain();
r.dilation = dilation;
r.weight_edge_easing = border;
r.weight_edge_easing_function = border_blending;
r.weight = weight;
r.layer = layer;
return r;
}
/**
*
* @param {number} x
* @param {number} y
* @param {MarkerNode} node
* @param {GridData} grid
* @param {number} radius_outer
* @param {number} radius_inner
* @returns {number}
* @private
*/
_getPointWeight(x, y, node, grid, radius_outer, radius_inner) {
// compute distance from node center
const distance_from_center = v2_distance(node.position.x, node.position.y, x, y);
if (distance_from_center > radius_outer) {
// too far away
return 0;
}
const weight_sampled = this.weight.execute(grid, x, y, 0);
// compute position in the padding region, where 0 is at the outer edge of the padding, and 1 is at the
const edge_normalized_position = clamp01(inverseLerp(radius_inner, radius_outer, distance_from_center));
// apply edge smoothing
const edge_weight_bias = this.weight_edge_easing_function(1 - edge_normalized_position);
return weight_sampled * edge_weight_bias;
}
_getLayerIndex() {
if (this._layer_index === -1) {
// bind terrain layer
const layers = this._terrain.layers;
const layer_descriptor = this.layer;
this._layer_index = layers.findIndexByDiffuseTextureURL(layer_descriptor.diffuse);
if (this._layer_index !== -1) {
// check to see if size matches
const actual_layer = layers.get(this._layer_index);
if (!actual_layer.size.equals(layer_descriptor.size)) {
console.warn(`Actual terrain layer with texture '${layer_descriptor.diffuse}' has size ${actual_layer.size}, instead of desired ${layer_descriptor.size}. Ignoring this difference, layer will still be used`);
}
} else {
// layer not found, let's create it
this._layer_index = this._terrain.addLayer(TerrainLayer.fromDescription(layer_descriptor));
}
}
return this._layer_index;
}
execute(grid, ecd, node) {
const _splat_layer_index = this._getLayerIndex();
const splat = this._splat_map;
const splat_resolution_x = splat.size.x;
const splat_resolution_y = splat.size.y;
// compute scale between grid and splat
const splat_scale_x = splat_resolution_x / grid.width;
const splat_scale_y = splat_resolution_y / grid.height;
const splat_to_grid_x = 1 / splat_scale_x;
const splat_to_grid_y = 1 / splat_scale_y;
const radius_outer = node.size * (1 + this.dilation.a) + this.dilation.b;
const edge_width = node.size * this.weight_edge_easing.a + this.weight_edge_easing.b;
const radius_inner = radius_outer - edge_width;
// we shift to the center of the grid cell
const node_p_x = node.position.x + splat_to_grid_x;
const node_p_y = node.position.y + splat_to_grid_y;
// compute affected region in Splat space
const x0 = Math.floor((node_p_x - radius_outer) * splat_scale_x + 0.5);
const x1 = Math.floor((node_p_x + radius_outer) * splat_scale_x + 0.5);
const y0 = Math.ceil((node_p_y - radius_outer) * splat_scale_y + 0.5);
const y1 = Math.ceil((node_p_y + radius_outer) * splat_scale_y + 0.5);
const layer_count = splat.depth;
const splat_data = splat.weightData;
// execute blending
for (let y = y0; y <= y1; y++) {
for (let x = x0; x <= x1; x++) {
// convert position into grid coordinates
const grid_x = (x - 0.5) * splat_to_grid_x - splat_to_grid_x;
const grid_y = (y - 0.5) * splat_to_grid_y - splat_to_grid_y;
const weight = this._getPointWeight(grid_x, grid_y, node, grid, radius_outer, radius_inner);
if (weight === 0) {
continue;
}
const weight_scaled = weight * 255;
// paint
const weight_reciprocal_multiplier = max2(0, 1 - weight);
for (let layer_index = 0; layer_index < layer_count; layer_index++) {
const layer_address = splat_resolution_x * splat_resolution_y * layer_index;
const texel_address = layer_address + y * splat_resolution_x + x;
const source_value = splat_data[texel_address];
let destination_value;
if (_splat_layer_index === layer_index) {
// the channel that we are stamping
destination_value = source_value + weight_scaled;
} else {
// other layers get erased by proportional amount
destination_value = source_value * weight_reciprocal_multiplier;
}
splat_data[texel_address] = clamp(destination_value, 0, 255);
}
}
}
// mark splat map for update
splat.weightTexture.needsUpdate = true;
}
initialize(grid, ecd, seed) {
// find layer index
const terrain = ecd.getAnyComponent(Terrain);
if (terrain.component === null) {
throw new Error('No terrain present');
}
this._terrain = terrain.component;
// bind splats
this._splat_map = terrain.component.splat;
this._layer_index = -1;
this._random = seededRandom(seed);
this.weight.ensureInitialized(grid, seed);
}
}