UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

266 lines (204 loc) • 9.13 kB
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); } }