UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

636 lines (487 loc) • 17.1 kB
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; } }