UNPKG

playcanvas

Version:

PlayCanvas WebGL game engine

292 lines (289 loc) 15.5 kB
import { Color } from '../../core/math/color.js'; import { Entity } from '../../framework/entity.js'; import { BlendState } from '../../platform/graphics/blend-state.js'; import { CULLFACE_NONE, FILTER_LINEAR, FILTER_LINEAR_MIPMAP_LINEAR, ADDRESS_CLAMP_TO_EDGE, PIXELFORMAT_SRGBA8, BLENDEQUATION_ADD, BLENDMODE_SRC_ALPHA, BLENDMODE_ONE_MINUS_SRC_ALPHA } from '../../platform/graphics/constants.js'; import { DepthState } from '../../platform/graphics/depth-state.js'; import { RenderTarget } from '../../platform/graphics/render-target.js'; import { Texture } from '../../platform/graphics/texture.js'; import { drawQuadWithShader } from '../../scene/graphics/quad-render-utils.js'; import { QuadRender } from '../../scene/graphics/quad-render.js'; import { StandardMaterialOptions } from '../../scene/materials/standard-material-options.js'; import { StandardMaterial } from '../../scene/materials/standard-material.js'; import { shaderChunks } from '../../scene/shader-lib/chunks/chunks.js'; import { createShaderFromCode } from '../../scene/shader-lib/utils.js'; /** * @import { AppBase } from '../../framework/app-base.js' * @import { Layer } from "../../scene/layer.js" */ // Fragment shader which works on a source image containing objects rendered using a constant color. // The shader removes the original object color and outputs outline color only. var shaderOutlineExtendPS = "\n\n varying vec2 vUv0;\n\n uniform vec2 uOffset;\n uniform float uSrcMultiplier;\n uniform sampler2D source;\n\n void main(void)\n {\n vec4 pixel;\n vec4 texel = texture2D(source, vUv0);\n vec4 firstTexel = texel;\n float diff = texel.a * uSrcMultiplier;\n\n pixel = texture2D(source, vUv0 + uOffset * -2.0);\n texel = max(texel, pixel);\n diff = max(diff, length(firstTexel.rgb - pixel.rgb));\n\n pixel = texture2D(source, vUv0 + uOffset * -1.0);\n texel = max(texel, pixel);\n diff = max(diff, length(firstTexel.rgb - pixel.rgb));\n\n pixel = texture2D(source, vUv0 + uOffset * 1.0);\n texel = max(texel, pixel);\n diff = max(diff, length(firstTexel.rgb - pixel.rgb));\n\n pixel = texture2D(source, vUv0 + uOffset * 2.0);\n texel = max(texel, pixel);\n diff = max(diff, length(firstTexel.rgb - pixel.rgb));\n\n gl_FragColor = vec4(texel.rgb, min(diff, 1.0));\n }\n"; var _tempFloatArray = new Float32Array(2); var _tempColor = new Color(); /** * Class responsible for rendering color outlines around objects in the scene. * * @category Graphics */ class OutlineRenderer { /** * Destroy the outline renderer and its resources. */ destroy() { var _this_quadRenderer; this.whiteTex.destroy(); this.whiteTex = null; this.outlineCameraEntity.destroy(); this.outlineCameraEntity = null; this.rt.destroyTextureBuffers(); this.rt.destroy(); this.rt = null; this.tempRt.destroyTextureBuffers(); this.tempRt.destroy(); this.tempRt = null; this.app.scene.off('postrender', this.postRender); (_this_quadRenderer = this.quadRenderer) == null ? void 0 : _this_quadRenderer.destroy(); this.quadRenderer = null; } getMeshInstances(entity, recursive) { var meshInstances = []; var renders = recursive ? entity.findComponents('render') : entity.render ? [ entity.render ] : []; renders.forEach((render)=>{ if (render.meshInstances) { meshInstances.push(...render.meshInstances); } }); var models = recursive ? entity.findComponents('model') : entity.model ? [ entity.model ] : []; models.forEach((model)=>{ if (model.meshInstances) { meshInstances.push(...model.meshInstances); } }); return meshInstances; } /** * Add an entity to the outline renderer. * * @param {Entity} entity - The entity to add. All MeshInstance of the entity and its * descendants will be added. * @param {Color} color - The color of the outline. * @param {boolean} [recursive] - Whether to add MeshInstances of the entity's descendants. * Defaults to true. */ addEntity(entity, color, recursive) { if (recursive === void 0) recursive = true; var meshInstances = this.getMeshInstances(entity, recursive); // update all materials meshInstances.forEach((meshInstance)=>{ if (meshInstance.material instanceof StandardMaterial) { var outlineShaderPass = this.outlineShaderPass; meshInstance.material.onUpdateShader = (options)=>{ if (options.pass === outlineShaderPass) { // custom shader for the outline shader pass, renders single color meshes using emissive color var opts = new StandardMaterialOptions(); opts.defines = options.defines; opts.opacityMap = options.opacityMap; opts.opacityMapUv = options.opacityMapUv; opts.opacityMapChannel = options.opacityMapChannel; opts.opacityMapTransform = options.opacityMapTransform; opts.opacityVertexColor = options.opacityVertexColor; opts.opacityVertexColorChannel = options.opacityVertexColorChannel; opts.litOptions.vertexColors = options.litOptions.vertexColors; opts.litOptions.alphaTest = options.litOptions.alphaTest; opts.litOptions.skin = options.litOptions.skin; opts.litOptions.batch = options.litOptions.batch; opts.litOptions.useInstancing = options.litOptions.useInstancing; opts.litOptions.useMorphPosition = options.litOptions.useMorphPosition; opts.litOptions.useMorphNormal = options.litOptions.useMorphNormal; opts.litOptions.useMorphTextureBasedInt = options.litOptions.useMorphTextureBasedInt; return opts; } return options; }; // set emissive color override for the outline shader pass only _tempColor.linear(color); var colArray = new Float32Array([ _tempColor.r, _tempColor.g, _tempColor.b ]); meshInstance.setParameter('material_emissive', colArray, 1 << this.outlineShaderPass); meshInstance.setParameter('texture_emissiveMap', this.whiteTex, 1 << this.outlineShaderPass); } }); this.renderingLayer.addMeshInstances(meshInstances, true); } /** * Remove an entity from the outline renderer. * * @param {Entity} entity - The entity to remove. * @param {boolean} [recursive] - Whether to add MeshInstances of the entity's descendants. * Defaults to true. */ removeEntity(entity, recursive) { if (recursive === void 0) recursive = true; var meshInstances = this.getMeshInstances(entity, recursive); this.renderingLayer.removeMeshInstances(meshInstances); meshInstances.forEach((meshInstance)=>{ if (meshInstance.material instanceof StandardMaterial) { meshInstance.material.onUpdateShader = null; meshInstance.deleteParameter('material_emissive'); } }); } removeAllEntities() { this.renderingLayer.clearMeshInstances(); } blendOutlines() { // blend in the outlines texture on top of the rendering var device = this.app.graphicsDevice; device.scope.resolve('source').setValue(this.rt.colorBuffer); device.setDepthState(DepthState.NODEPTH); device.setCullMode(CULLFACE_NONE); device.setBlendState(this.blendState); this.quadRenderer.render(); } onPostRender() { // when the outline camera has rendered the outline objects to the texture, process the texture // to generate the outline effect var device = this.app.graphicsDevice; var uOffset = device.scope.resolve('uOffset'); var uColorBuffer = device.scope.resolve('source'); var uSrcMultiplier = device.scope.resolve('uSrcMultiplier'); var { rt, tempRt, shaderExtend } = this; var { width, height } = rt; // horizontal extend pass _tempFloatArray[0] = 1.0 / width / 2.0; _tempFloatArray[1] = 0; uOffset.setValue(_tempFloatArray); uColorBuffer.setValue(rt.colorBuffer); uSrcMultiplier.setValue(0.0); drawQuadWithShader(device, tempRt, shaderExtend); // vertical extend pass _tempFloatArray[0] = 0; _tempFloatArray[1] = 1.0 / height / 2.0; uOffset.setValue(_tempFloatArray); uColorBuffer.setValue(tempRt.colorBuffer); uSrcMultiplier.setValue(1.0); drawQuadWithShader(device, rt, shaderExtend); } createRenderTarget(name, width, height, depth) { // Create texture render target with specified resolution and mipmap generation var texture = new Texture(this.app.graphicsDevice, { name: name, width: width, height: height, format: PIXELFORMAT_SRGBA8, mipmaps: false, addressU: ADDRESS_CLAMP_TO_EDGE, addressV: ADDRESS_CLAMP_TO_EDGE, minFilter: FILTER_LINEAR_MIPMAP_LINEAR, magFilter: FILTER_LINEAR }); // render target return new RenderTarget({ colorBuffer: texture, depth: depth, flipY: this.app.graphicsDevice.isWebGPU }); } updateRenderTarget(sceneCamera) { var _sceneCamera_renderTarget, _sceneCamera_renderTarget1; var _sceneCamera_renderTarget_width; // main camera resolution var width = (_sceneCamera_renderTarget_width = (_sceneCamera_renderTarget = sceneCamera.renderTarget) == null ? void 0 : _sceneCamera_renderTarget.width) != null ? _sceneCamera_renderTarget_width : this.app.graphicsDevice.width; var _sceneCamera_renderTarget_height; var height = (_sceneCamera_renderTarget_height = (_sceneCamera_renderTarget1 = sceneCamera.renderTarget) == null ? void 0 : _sceneCamera_renderTarget1.height) != null ? _sceneCamera_renderTarget_height : this.app.graphicsDevice.height; var outlineCamera = this.outlineCameraEntity.camera; if (!outlineCamera.renderTarget || outlineCamera.renderTarget.width !== width || outlineCamera.renderTarget.height !== height) { this.rt.resize(width, height); this.tempRt.resize(width, height); } } /** * Update the outline renderer. Should be called once per frame. * * @param {Entity} sceneCameraEntity - The camera used to render the scene, which is used to provide * the camera properties to the outline rendering camera. * @param {Layer} blendLayer - The layer in which the outlines should be rendered. * @param {boolean} blendLayerTransparent - Whether the blend layer is transparent. */ frameUpdate(sceneCameraEntity, blendLayer, blendLayerTransparent) { var sceneCamera = sceneCameraEntity.camera; this.updateRenderTarget(sceneCamera); // function called before the scene camera renders a layer var evt = this.app.scene.on('prerender:layer', (cameraComponent, layer, transparent)=>{ if (sceneCamera === cameraComponent && transparent === blendLayerTransparent && layer === blendLayer) { this.blendOutlines(); evt.off(); } }); // copy the transform this.outlineCameraEntity.setLocalPosition(sceneCameraEntity.getPosition()); this.outlineCameraEntity.setLocalRotation(sceneCameraEntity.getRotation()); // copy other properties from the scene camera var outlineCamera = this.outlineCameraEntity.camera; outlineCamera.projection = sceneCamera.projection; outlineCamera.horizontalFov = sceneCamera.horizontalFov; outlineCamera.fov = sceneCamera.fov; outlineCamera.orthoHeight = sceneCamera.orthoHeight; outlineCamera.nearClip = sceneCamera.nearClip; outlineCamera.farClip = sceneCamera.farClip; } /** * Create a new OutlineRenderer. * * @param {AppBase} app - The application. * @param {Layer} [renderingLayer] - A layer used internally to render the outlines. If not * provided, the renderer will use the 'Immediate' layer. This needs to be supplied only if the * 'Immediate' layer is not present in the scene. * @param {number} [priority] - The priority of the camera rendering the outlines. Should be * smaller value than the priority of the scene camera, to be updated first. Defaults to -1. */ constructor(app, renderingLayer, priority = -1){ this.app = app; this.renderingLayer = renderingLayer != null ? renderingLayer : app.scene.layers.getLayerByName('Immediate'); this.rt = this.createRenderTarget('OutlineTexture', 1, 1, true); // camera which renders the outline texture this.outlineCameraEntity = new Entity('OutlineCamera'); this.outlineCameraEntity.addComponent('camera', { layers: [ this.renderingLayer.id ], priority: priority, clearColor: new Color(0, 0, 0, 0), renderTarget: this.rt }); // custom shader pass for the outline camera this.outlineShaderPass = this.outlineCameraEntity.camera.setShaderPass('OutlineShaderPass'); // function called after the camera has rendered the outline objects to the texture this.postRender = (cameraComponent)=>{ if (this.outlineCameraEntity.camera === cameraComponent) { this.onPostRender(); } }; app.scene.on('postrender', this.postRender); // add the camera to the scene this.app.root.addChild(this.outlineCameraEntity); // temporary render target for intermediate steps this.tempRt = this.createRenderTarget('OutlineTempTexture', 1, 1, false); this.blendState = new BlendState(true, BLENDEQUATION_ADD, BLENDMODE_SRC_ALPHA, BLENDMODE_ONE_MINUS_SRC_ALPHA); var device = this.app.graphicsDevice; this.shaderExtend = createShaderFromCode(device, shaderChunks.fullscreenQuadVS, shaderOutlineExtendPS, 'OutlineExtendShader'); this.shaderBlend = createShaderFromCode(device, shaderChunks.fullscreenQuadVS, shaderChunks.outputTex2DPS, 'OutlineBlendShader'); this.quadRenderer = new QuadRender(this.shaderBlend); this.whiteTex = new Texture(device, { name: 'OutlineWhiteTexture', width: 1, height: 1, format: PIXELFORMAT_SRGBA8, mipmaps: false }); var pixels = this.whiteTex.lock(); pixels.set(new Uint8Array([ 255, 255, 255, 255 ])); this.whiteTex.unlock(); } } export { OutlineRenderer };