@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
346 lines (262 loc) • 10.1 kB
JavaScript
import {
ClampToEdgeWrapping,
DoubleSide,
GLSL3,
Matrix4,
NearestFilter,
NoBlending,
RawShaderMaterial,
RGBAIntegerFormat,
UnsignedByteType,
Vector2,
Vector4,
WebGLRenderTarget
} from "three";
import { assert } from "../../../../core/assert.js";
import { array_copy } from "../../../../core/collection/array/array_copy.js";
import { clamp } from "../../../../core/math/clamp.js";
import { max2 } from "../../../../core/math/max2.js";
import { generate_halton_jitter } from "../../../../core/math/random/generate_halton_jitter.js";
import { renderScreenSpace } from "../../render/utils/renderScreenSpace.js";
import { Sampler2D } from "../sampler/Sampler2D.js";
import { VirtualTextureUsage } from "./VirtualTextureUsage.js";
import { fragment, vertex } from "./VirtualTextureUsageShader.js";
const usage_material_uniforms = {
"u_mt_params": { value: new Vector2(0, 1) },
/**
* Format: VT_WIDTH, VT_HEIGHT, VT_TILE_SIZE, VT_ID
* - VT_WIDTH : width of the virtual texture
* - VT_HEIGHT : height of the virtual texture
* - VT_TILE_SIZE : resolution of a single tile
* - VT_ID : multiple different virtual textures may be used, this identifies current texture
*/
"u_mt_tex": { value: new Vector4(1, 1, 1, 3) },
"u_mt_viewport_scale": { value: new Vector2(1, 1) },
"modelViewMatrix": { value: new Matrix4() },
"projectionMatrix": { value: new Matrix4() }
};
const usage_material = new RawShaderMaterial({
uniforms: usage_material_uniforms,
vertexShader: vertex(),
fragmentShader: fragment(),
side: DoubleSide,
glslVersion: GLSL3
});
usage_material.blending = NoBlending;
const clear_usage_material = new RawShaderMaterial({
depthTest: false,
depthWrite: false,
vertexShader: `
precision mediump float;
in vec2 uv;
void main() {
gl_Position = vec4( (uv - 0.5)*2.0, 0.0, 1.0 );
}`,
fragmentShader: `
precision mediump int;
out uvec4 out_decoded;
void main(){
out_decoded = uvec4(0u);
}
`,
glslVersion: GLSL3
});
const scratch_matrix = new Float32Array(16);
export class VirtualTextureUsageUpdater {
#frame_index = 0;
/**
* In order to avoid aliasing due to the fact that we render at a lower resolution, we move the camera slightly each frame to sweep across texels in-between
* @type {Float32Array}
*/
#frame_jitter_offsets = new Float32Array([0, 0]);
#frame_jitter_samples = 1;
#frame_jitter_enabled = false;
get frame_jitter_enabled() {
return this.#frame_jitter_enabled;
}
/**
*
* @param {boolean} v
*/
set frame_jitter_enabled(v) {
this.#frame_jitter_enabled = v;
}
#initialize_frame_jitter() {
this.#frame_jitter_samples = this.#usage_resolution_scale;
this.#frame_jitter_offsets = generate_halton_jitter(this.#frame_jitter_samples);
}
#usage_buffer = new WebGLRenderTarget(1, 1, {
wrapT: ClampToEdgeWrapping,
wrapS: ClampToEdgeWrapping,
minFilter: NearestFilter,
magFilter: NearestFilter,
format: RGBAIntegerFormat,
type: UnsignedByteType,
internalFormat: "RGBA8UI",
anisotropy: 1,
depthBuffer: true,
stencilBuffer: false,
generateMipmaps: false,
});
#usage_pixel_data = Sampler2D.uint8(4, 1, 1);
#viewport_resolution = [1, 1];
/**
*
* @type {number}
*/
#usage_resolution_scale = 32;
#usage_metadata = new VirtualTextureUsage();
#texture_id = 3;
#texture_resolution = 65536;
#tile_resolution = 256;
#max_mip_level = 0;
get texture_resolution() {
return this.#texture_resolution;
}
get tile_resolution() {
return this.#tile_resolution;
}
get max_mip_level() {
return this.#max_mip_level;
}
/**
* Used to fetch higher/lower resolution tiles, negative value will fetch higher resolution tiles, and vice-versa
* @type {number}
*/
#usage_texture_bias = 0;
/**
*
* @param {number} resolution
* @param {number} tile_size
* @param {number} texture_id
*/
setTextureParameters(
resolution,
tile_size,
texture_id
) {
assert.isNonNegativeInteger(resolution,'resolution');
assert.lessThanOrEqual(tile_size, resolution, `tile_size(=${tile_size}) must be <= than the overall texture resolution(=${resolution})`);
this.#texture_id = texture_id;
this.#texture_resolution = resolution;
this.#tile_resolution = tile_size;
this.#max_mip_level = Math.log2(this.#texture_resolution / this.#tile_resolution);
if (!Number.isInteger(this.#max_mip_level)) {
throw new Error(`texture resolution must be a power-of-two multiple of tile resolution`);
}
this.#initialize_usage_uniforms();
this.#usage_metadata.max_mip_level = this.#max_mip_level;
}
get usage_metadata() {
return this.#usage_metadata;
}
get usage_data() {
return this.#usage_pixel_data;
}
constructor() {
const target = this.#usage_buffer;
target.scissorTest = false;
this.#initialize_usage_uniforms();
this.#initialize_frame_jitter();
}
#initialize_usage_uniforms() {
const uniforms = usage_material.uniforms;
const max_mip_level = this.#max_mip_level;
uniforms.u_mt_params.value.set(
this.#usage_texture_bias,
max_mip_level
);
uniforms.u_mt_tex.value.set(
this.#texture_resolution,
this.#texture_resolution,
this.#tile_resolution,
this.#texture_id
);
}
/**
*
* @param {number} x
* @param {number} y
*/
setViewportResolution(x, y) {
assert.isNumber(x, 'x');
assert.isNumber(y, 'y');
assert.notNaN(x, 'x');
assert.notNaN(y, 'y');
const _x = max2(1, Math.floor(x));
const _y = max2(1, Math.floor(y));
const viewport = this.#viewport_resolution;
if (_x === viewport[0] && _y === viewport[1]) {
// no change, do nothing
return;
}
viewport[0] = _x;
viewport[1] = _y;
const usage_resolution_x = clamp(Math.floor(_x / this.#usage_resolution_scale), 1, _x);
const usage_resolution_y = clamp(Math.floor(_y / this.#usage_resolution_scale), 1, _y);
this.#usage_buffer.setSize(usage_resolution_x, usage_resolution_y);
this.#usage_pixel_data.resize(usage_resolution_x, usage_resolution_y, true);
const usage_material_uniforms = usage_material.uniforms;
usage_material_uniforms.u_mt_viewport_scale.value.set(usage_resolution_x / _x, usage_resolution_y / _y);
}
#prepareUsageUniforms(camera) {
const uniforms = usage_material.uniforms;
const usage_buffer = this.#usage_buffer;
const camera_projection_matrix = camera.projectionMatrix.elements;
if (this.#frame_jitter_enabled) {
// apply jitter to projection matrix
const jitter_index = this.#frame_index % this.#frame_jitter_samples;
const jitter_index_2 = jitter_index * 2;
const jitter_offset_x = this.#frame_jitter_offsets[jitter_index_2];
const jitter_offset_y = this.#frame_jitter_offsets[jitter_index_2 + 1];
camera_projection_matrix[8] = jitter_offset_x / usage_buffer.width;
camera_projection_matrix[9] = jitter_offset_y / usage_buffer.height;
}
uniforms.modelViewMatrix.value.multiplyMatrices(camera.matrixWorldInverse, camera.matrixWorld);
uniforms.projectionMatrix.value.copy(camera.projectionMatrix);
}
/**
*
* @param {WebGLRenderer} renderer
* @param {Scene} scene
* @param {Camera} camera
*/
updateUsage(renderer, scene, camera) {
const usage_buffer = this.#usage_buffer;
const camera_projection_matrix = camera.projectionMatrix.elements;
// remember existing state
const _old_render_target = renderer.getRenderTarget();
const _auto_clear = renderer.autoClear;
array_copy(camera_projection_matrix, 0, scratch_matrix, 0, 16);
const _old_material = scene.overrideMaterial;
const _old_camera_matrix_update = camera.matrixAutoUpdate;
camera.matrixAutoUpdate = false;
renderer.autoClear = false;
renderer.setRenderTarget(usage_buffer);
// clear out usage texture
renderScreenSpace(renderer, clear_usage_material);
renderer.clearDepth();
scene.overrideMaterial = usage_material;
this.#prepareUsageUniforms(camera);
renderer.render(scene, camera);
renderer.readRenderTargetPixels(
usage_buffer, 0, 0,
usage_buffer.width, usage_buffer.height,
this.#usage_pixel_data.data
);
// restore state
scene.overrideMaterial = _old_material;
renderer.setRenderTarget(_old_render_target);
renderer.autoClear = _auto_clear;
array_copy(scratch_matrix, 0, camera_projection_matrix, 0, 16);
camera.matrixAutoUpdate = _old_camera_matrix_update;
//
this.#usage_metadata.fromTexture(this.#usage_pixel_data.data);
this.#usage_metadata.promoteAncestors(1);
this.#usage_metadata.sortOccupancyByLOD();
this.#frame_index++;
}
dispose() {
this.#usage_buffer.dispose();
}
}