@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
636 lines (487 loc) • 17.1 kB
JavaScript
import {
ClampToEdgeWrapping,
Color,
DataTexture,
LinearFilter,
Mesh,
NearestFilter,
NoBlending,
Object3D,
OrthographicCamera,
PlaneBufferGeometry,
RedFormat,
RGBAFormat,
Scene,
ShaderMaterial,
SkinnedMesh,
UnsignedByteType,
Vector2,
WebGLRenderTarget
} from "three";
import { assert } from "../../../../../core/assert.js";
import { HashMap } from "../../../../../core/collection/map/HashMap.js";
import { invokeObjectEquals } from "../../../../../core/model/object/invokeObjectEquals.js";
import { invokeObjectHash } from "../../../../../core/model/object/invokeObjectHash.js";
import { ScreenSpaceQuadShader } from "../../../shaders/ScreenSpaceQuadShader.js";
import { three_setSceneAutoUpdate } from "../../../three/three_setSceneAutoUpdate.js";
import { makeHighlightDecodeShader } from "./HighlightDecodeShader.js";
import { makeDilationShader } from "./makeDilationShader.js";
import { makeGaussianBlurShader } from "./makeGaussianBlurShader.js";
import { traverseThreeObject } from "./traverseThreeObject.js";
const material_id_fragment_shader = `
uniform float id;
void main(){
gl_FragColor = vec4(id / 255.0, 0.0, 0.0, 1.0);
}
`;
const material_id_vertex_shader_skinned = `
#include <common>
#include <uv_pars_vertex>
#include <displacementmap_pars_vertex>
#include <morphtarget_pars_vertex>
#include <skinning_pars_vertex>
#include <logdepthbuf_pars_vertex>
#include <clipping_planes_pars_vertex>
void main()
{
#include <uv_vertex>
#include <skinbase_vertex>
#ifdef USE_DISPLACEMENTMAP
#include <beginnormal_vertex>
#include <morphnormal_vertex>
#include <skinnormal_vertex>
#endif
#include <begin_vertex>
#include <morphtarget_vertex>
#include <skinning_vertex>
#include <displacementmap_vertex>
#include <project_vertex>
#include <logdepthbuf_vertex>
#include <clipping_planes_vertex>
}
`;
const material_id_vertex_shader_static = `
void main()
{
vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
gl_Position = projectionMatrix * mvPosition;
}
`;
function makeIdMaterialStatic() {
return new ShaderMaterial({
uniforms: {
id: { value: 0 }
},
vertexShader: material_id_vertex_shader_static,
fragmentShader: material_id_fragment_shader
});
}
/**
*
* @type {WeakMap<Object3D, Object3D>}
*/
const object_id_cache = new WeakMap();
/**
*
* @param {Mesh|SkinnedMesh} object
*/
function obtain_id_mesh(object) {
let mesh = object_id_cache.get(object);
if (mesh === undefined) {
if (object.isSkinnedMesh) {
mesh = new SkinnedMesh(object.geometry, makeIdMaterialSkinned());
} else {
mesh = new Mesh(object.geometry, makeIdMaterialStatic());
}
object_id_cache.set(object, mesh);
}
if (object.isSkinnedMesh) {
mesh.bindMatrix.copy(object.bindMatrix);
mesh.bindMatrixInverse.copy(object.bindMatrixInverse);
mesh.skeleton = object.skeleton;
}
return mesh;
}
function makeIdMaterialSkinned() {
return new ShaderMaterial({
uniforms: {
id: { value: 0 }
},
vertexShader: material_id_vertex_shader_skinned,
fragmentShader: material_id_fragment_shader
});
}
const material_id_skinned = makeIdMaterialSkinned();
material_id_skinned.skinning = true;
const OBJECT_MAX_COUNT = 256;
function makeBlurShader() {
const blurShader = makeGaussianBlurShader({ samples: 5, distance: 5 });
return new ShaderMaterial({
uniforms: {
"tDiffuse": { type: "t", value: null },
"tMask": { type: "t", value: null },
"resolution": { type: "v2", value: new Vector2(800, 600) },
},
blending: NoBlending,
lights: false,
fog: false,
depthTest: false,
depthWrite: false,
transparent: true,
vertexColors: false,
vertexShader: ScreenSpaceQuadShader.vertexShader(),
fragmentShader: blurShader
});
}
const tempColor = new Color();
export class OutlineRenderer {
/**
*
* @constructor
*/
constructor() {
/**
*
* @type {WebGLRenderer}
*/
this.renderer = null;
/**
*
* @type {Camera}
*/
this.camera = null;
/**
*
* @type {Scene}
*/
this.scene = null;
/**
*
* @type {HashMap<HighlightDefinition>}
* @private
*/
this.__definition_dictionary = new HashMap({
keyHashFunction: invokeObjectHash,
keyEqualityFunction: invokeObjectEquals
});
this.__ss_camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);
this.__ss_scene = new Scene();
this.__material_decode = makeHighlightDecodeShader();
this.__material_dilate = makeDilationShader();
this.__material_blur = makeBlurShader();
this.__ss_mesh = new Mesh(new PlaneBufferGeometry(2, 2, 1, 1), this.__material_decode);
this.__ss_scene.add(this.__ss_mesh);
this.__id_scene = new Scene();
three_setSceneAutoUpdate(this.__id_scene, false);
this.__id_scene.matrixAutoUpdate = false;
this.__id_scene.matrixWorldNeedsUpdate = false;
/**
*
* @type {boolean}
*/
this.isTargetClear = true;
this.__textureParametersColor = new DataTexture(
new Uint8Array(4 * OBJECT_MAX_COUNT),
1,
OBJECT_MAX_COUNT,
RGBAFormat,
UnsignedByteType
);
this.__textureParametersColor.wrapT = ClampToEdgeWrapping;
this.__textureParametersColor.wrapS = ClampToEdgeWrapping;
this.__textureParametersColor.minFilter = NearestFilter;
this.__textureParametersColor.magFilter = NearestFilter;
this.__textureParametersColor.generateMipmaps = false;
this.__renderTargetObjectId_t0 = new WebGLRenderTarget(1, 1, {
minFilter: NearestFilter,
magFilter: NearestFilter,
format: RedFormat,
depthBuffer: true,
stencilBuffer: false,
generateMipmaps: false
});
this.__renderTargetObjectId_t1 = new WebGLRenderTarget(1, 1, {
minFilter: NearestFilter,
magFilter: NearestFilter,
format: RedFormat,
depthBuffer: false,
stencilBuffer: false,
generateMipmaps: false
});
this.__renderTargetObjectId_t2 = new WebGLRenderTarget(1, 1, {
minFilter: NearestFilter,
magFilter: NearestFilter,
format: RedFormat,
depthBuffer: false,
stencilBuffer: false,
generateMipmaps: false
});
this.__renderTargetColor = new WebGLRenderTarget(1, 1, {
format: RGBAFormat,
minFilter: LinearFilter,
magFilter: LinearFilter,
depthBuffer: false,
stencilBuffer: false,
generateMipmaps: false
});
this.__renderTargetFinal = new WebGLRenderTarget(1, 1, {
format: RGBAFormat,
minFilter: NearestFilter,
magFilter: NearestFilter,
depthBuffer: false,
stencilBuffer: false,
generateMipmaps: false
});
this.__material_decode.uniforms.tObjects.value = this.__renderTargetObjectId_t0.texture;
this.__material_decode.uniforms.tParameters.value = this.__textureParametersColor;
this.__material_blur.uniforms.tMask.value = this.__renderTargetObjectId_t0.texture;
/**
* Mask will be dilated by this number of pixels before being blurred
* @type {number}
* @private
*/
this.__outline_offset = 3;
/**
*
* @type {number}
* @private
*/
this.__resolution_scale = 1;
/**
*
* @type {HighlightDefinition|null}
* @private
*/
this.__active_definition = null;
}
setScene(scene) {
this.scene = scene;
}
/**
*
* @param {WebGLRenderer} renderer
*/
setWebGLRenderer(renderer) {
this.renderer = renderer;
}
/**
*
* @param {number} x
* @param {number} y
*/
resize(x, y) {
assert.isNonNegativeInteger(x, 'x');
assert.isNonNegativeInteger(y, 'y');
// mark RT for redraw
this.isTargetClear = false;
this.__renderTargetObjectId_t0.setSize(x, y);
this.__renderTargetFinal.setSize(x, y);
const resolution_scale = this.__resolution_scale;
const scaled_res_x = Math.floor(x * resolution_scale);
const scaled_res_y = Math.floor(y * resolution_scale);
this.__renderTargetObjectId_t1.setSize(scaled_res_x, scaled_res_y);
this.__renderTargetObjectId_t2.setSize(scaled_res_x, scaled_res_y);
this.__renderTargetColor.setSize(scaled_res_x, scaled_res_y);
this.__material_decode.uniforms.resolution.value.set(scaled_res_x, scaled_res_y);
this.__material_dilate.uniforms.resolution.value.set(scaled_res_x, scaled_res_y);
this.__material_blur.uniforms.resolution.value.set(scaled_res_x, scaled_res_y);
}
/**
*
* @param {PerspectiveCamera|OrthographicCamera} camera
*/
setCamera(camera) {
this.camera = camera;
}
/**
*
* @param {HighlightRenderGroup} group
* @private
*/
__build_definition_dictionary(group) {
let id_counter = 0;
const dictionary = this.__definition_dictionary;
dictionary.clear();
/**
*
* @type {List<HighlightRenderElement>}
*/
const elements = group.elements;
const element_count = elements.length;
for (let i = 0; i < element_count; i++) {
const renderElement = elements.get(i);
renderElement.merge();
const definition = renderElement.merged;
if (!dictionary.has(definition)) {
dictionary.set(definition, id_counter++);
}
}
}
/**
*
* @param {Object3D} object
* @private
*/
__append_to_id_scene(object) {
if (object.isMesh) {
const mesh = obtain_id_mesh(object);
const parameter_set_id = this.__definition_dictionary.get(this.__active_definition);
mesh.material.uniforms.id.value = parameter_set_id;
mesh.matrixAutoUpdate = false;
mesh.matrixWorldNeedsUpdate = false;
mesh.frustumCulled = false;
mesh.matrix.copy(object.matrix);
mesh.matrixWorld.copy(object.matrixWorld);
mesh.modelViewMatrix.copy(object.modelViewMatrix);
this.__id_scene.children.push(mesh);
}
}
/**
*
* @param {HighlightRenderGroup} group
*/
renderObjectIDs(group) {
this.__current_group = group;
const renderer = this.renderer;
renderer.setRenderTarget(this.__renderTargetObjectId_t0);
renderer.getClearColor(tempColor);
const __clearAlpha = renderer.getClearAlpha();
renderer.setClearColor(0xFFFFFF, 1);
renderer.clearDepth();
renderer.clearColor();
// prepare ID scene
this.__id_scene.children = [];
const elementList = group.elements;
const n = elementList.length;
for (let i = 0; i < n; i++) {
const renderElement = elementList.get(i);
const object = renderElement.object;
// lookup parameter set ID
const definition = renderElement.merged;
this.__active_definition = definition;
traverseThreeObject(object, this.__append_to_id_scene, this);
}
try {
renderer.render(this.__id_scene, this.camera);
} catch (e) {
console.warn(e);
}
renderer.setClearColor(tempColor);
renderer.setClearAlpha(__clearAlpha);
renderer.setRenderTarget(null);
this.__material_decode.uniforms.tObjects.value = this.__renderTargetObjectId_t0.texture;
}
/**
*
* @param {number} thickness
*/
dilateObjectIDs(thickness) {
if (thickness === 0) {
return;
}
const renderer = this.renderer;
let t0, t1;
t0 = this.__renderTargetObjectId_t1;
t1 = this.__renderTargetObjectId_t2;
this.__ss_mesh.material = this.__material_dilate;
renderer.setRenderTarget(t0);
this.__material_dilate.uniforms.tInput.value = this.__renderTargetObjectId_t0.texture;
renderer.render(this.__ss_scene, this.__ss_camera);
for (let i = 1; i < thickness; i++) {
renderer.setRenderTarget(t1);
this.__material_dilate.uniforms.tInput.value = t0.texture;
renderer.render(this.__ss_scene, this.__ss_camera);
// swap render targets
const temp = t0;
t0 = t1;
t1 = temp;
}
this.__material_decode.uniforms.tObjects.value = t0.texture;
renderer.setRenderTarget(null);
}
/**
*
* @param {Material} material
* @param {WebGLRenderTarget} target
* @private
*/
__ss_render_to_target(material, target) {
const renderer = this.renderer;
renderer.setRenderTarget(target);
//record state
const __autoClear = renderer.autoClear;
const __clearAlpha = renderer.getClearAlpha();
renderer.setClearAlpha(0);
renderer.autoClear = false;
renderer.clearColor();
// set decoding material
this.__ss_mesh.material = material;
renderer.render(this.__ss_scene, this.__ss_camera);
//restore state
renderer.autoClear = __autoClear;
renderer.setClearAlpha(__clearAlpha);
renderer.setRenderTarget(null);
}
renderColorPass() {
this.__ss_render_to_target(this.__material_decode, this.__renderTargetColor);
}
/**
* Requires object ID texture as input, produces rendering of outlines
*/
renderMainPass() {
this.__material_blur.uniforms.tDiffuse.value = this.__renderTargetColor.texture;
this.__ss_render_to_target(this.__material_blur, this.__renderTargetFinal);
}
/**
*
* @param {number} id
* @param {HighlightDefinition} definition
* @private
*/
__write_parameters_for_definition(id, definition) {
const texture = this.__textureParametersColor;
const offset = id * 4;
const data = texture.image.data;
const color = definition.color;
data[offset + 0] = Math.round(color.r * 255);
data[offset + 1] = Math.round(color.g * 255);
data[offset + 2] = Math.round(color.b * 255);
data[offset + 3] = Math.round(definition.opacity * 255);
texture.needsUpdate = true;
}
writeParameters() {
this.__definition_dictionary.forEach(this.__write_parameters_for_definition, this);
}
/**
*
* @param {HighlightRenderGroup} group
*/
render(group) {
// prepare definition dictionary
this.__build_definition_dictionary(group);
// prepare parameter lookup texture
this.writeParameters();
// render parameter IDs
this.renderObjectIDs(group);
// dilate parameter ID map by a set number of pixels
this.dilateObjectIDs(this.__outline_offset);
// render out color pass based of parameter texture
this.renderColorPass();
// do the final blur/mask pass
this.renderMainPass();
// mark render target as dirty
this.isTargetClear = false;
}
clearRenderTarget() {
if (this.isTargetClear) {
// already clear
return;
}
const renderer = this.renderer;
const oldRenderTarget = renderer.getRenderTarget();
renderer.setRenderTarget(this.__renderTargetFinal);
renderer.clear(true, false, false);
renderer.setRenderTarget(oldRenderTarget);
this.isTargetClear = true;
}
}