import {onChange, serialize} from 'ts-browser-helpers' import { BasicDepthPacking, Color, Euler, LinearFilter, MeshDepthMaterial, NoBlending, NoColorSpace, OrthographicCamera, RGBAFormat, UnsignedByteType, Vector3, WebGLRenderTarget, } from 'three' import {BaseGroundPlugin} from '../base/BaseGroundPlugin' import {GBufferRenderPass} from '../../postprocessing' import {ThreeViewer} from '../../viewer' import {IRenderTarget} from '../../rendering' import {uiPanelContainer, uiSlider, uiToggle} from 'uiconfig.js' import {HVBlurHelper} from '../../three/utils/HVBlurHelper' import {shaderReplaceString} from '../../utils' @uiPanelContainer('Contact Shadow Ground') export class ContactShadowGroundPlugin extends BaseGroundPlugin { static readonly PluginType = 'ContactShadowGroundPlugin' @uiToggle('Contact Shadows') @onChange(ContactShadowGroundPlugin.prototype.refresh) @serialize() contactShadows = true @uiSlider('Shadow Scale', [0, 2]) @serialize() @onChange(ContactShadowGroundPlugin.prototype._refreshShadowCameraFrustum) shadowScale = 1 @uiSlider('Shadow Height', [0, 20]) @serialize() @onChange(ContactShadowGroundPlugin.prototype._refreshShadowCameraFrustum) shadowHeight = 5 @uiSlider('Blur Amount', [0, 10]) @serialize() @onChange(ContactShadowGroundPlugin.prototype._setDirty) blurAmount = 1 shadowCamera = new OrthographicCamera(-1, 1, 1, -1, 0.001, this.shadowHeight) private _depthPass?: GBufferRenderPass<'contactShadowGround', WebGLRenderTarget> private _blurHelper?: HVBlurHelper constructor() { super() this._refreshShadowCameraFrustum = this._refreshShadowCameraFrustum.bind(this) this.refresh = this.refresh.bind(this) } onAdded(viewer: ThreeViewer): void { const target = viewer.renderManager.createTarget({ type: UnsignedByteType, format: RGBAFormat, colorSpace: NoColorSpace, size: {width: 512, height: 512}, generateMipmaps: false, depthBuffer: true, minFilter: LinearFilter, magFilter: LinearFilter, // isAntialiased: this._viewer.isAntialiased, }) target.texture.name = 'groundContactDepthTexture' // https://github.com/mrdoob/three.js/blob/master/examples/webgl_shadow_contact.html const material = new MeshDepthMaterial({ // depthPacking: RGBADepthPacking, // todo depthPacking: BasicDepthPacking, transparent: false, blending: NoBlending, }) material.onBeforeCompile = (shader) => { shader.uniforms.opacity.value = 1. shader.fragmentShader = shaderReplaceString(shader.fragmentShader, 'gl_FragColor = vec4( vec3( 1.0 - fragCoordZ ), opacity );', 'gl_FragColor = vec4( vec3( 1.0 - fragCoordZ ), 1.0 );', // 'gl_FragColor = vec4( vec3( 0.0 ), ( 1.0 - fragCoordZ ) * darkness );', ) } this._depthPass = new GBufferRenderPass('contactShadowGround', target, material, new Color(0, 0, 0), 0) this._blurHelper = new HVBlurHelper(viewer) super.onAdded(viewer) } onRemove(viewer: ThreeViewer): void { const target = this._depthPass?.target if (target) this._viewer?.renderManager.disposeTarget(target) this._depthPass?.dispose() this._depthPass = undefined this._blurHelper?.dispose() this._blurHelper = undefined return super.onRemove(viewer) } // todo: dispose target, material, pass and stuff protected _postFrame() { super._postFrame() if (!this._viewer) return } protected _preRender() { super._preRender() if (!this._viewer || !this._depthPass || !this._blurHelper) return this._depthPass.scene = this._viewer.scene this._depthPass.camera = this.shadowCamera this._depthPass.render(this._viewer.renderManager.renderer, null) const blurTarget = this._viewer.renderManager.getTempTarget({ type: UnsignedByteType, format: RGBAFormat, colorSpace: NoColorSpace, size: {width: 1024, height: 1024}, generateMipmaps: false, depthBuffer: false, minFilter: LinearFilter, magFilter: LinearFilter, // isAntialiased: this._viewer.isAntialiased, }) this._blurHelper.blur(this._depthPass.target.texture, this._depthPass.target, blurTarget, this.blurAmount / 256) this._blurHelper.blur(this._depthPass.target.texture, this._depthPass.target, blurTarget, 0.4 * this.blurAmount / 256) this._viewer.renderManager.releaseTempTarget(blurTarget) } protected _refreshTransform() { super._refreshTransform() if (!this._mesh) return if (!this._viewer) return this.shadowCamera.position.copy(this._mesh.getWorldPosition(new Vector3())) this.shadowCamera.setRotationFromEuler(new Euler(Math.PI / 2., 0, 0)) this.shadowCamera.updateMatrixWorld() this._refreshShadowCameraFrustum() this._mesh.scale.y = -this.size } private _refreshShadowCameraFrustum() { if (!this.shadowCamera) return this.shadowCamera.left = -this.size / (2 * this.shadowScale) this.shadowCamera.right = this.size / (2 * this.shadowScale) this.shadowCamera.top = this.size / (2 * this.shadowScale) this.shadowCamera.bottom = -this.size / (2 * this.shadowScale) this.shadowCamera.far = this.shadowHeight this.shadowCamera.updateProjectionMatrix() this._setDirty() } private _setDirty() { this._viewer?.setDirty() } protected _removeMaterial() { if (!this._material) return // todo: remove map or render target thats assigned super._removeMaterial() } public refresh(): void { if (!this._viewer) return // todo: shadow enabled check super.refresh() } protected _refreshMaterial(): boolean { if (!this._viewer) return false const isNewMaterial = super._refreshMaterial() if (!this._material) return isNewMaterial this._material.alphaMap = this._depthPass?.target.texture || null if (isNewMaterial) { this._material.roughness = 1 this._material.metalness = 0 this._material.color.set(0x111111) this._material.transparent = true this._material.userData.ssreflDisabled = true // todo: unset this in remove material. this._material.userData.ssreflNonPhysical = false // this._material.materialObject.userData.inverseAlphaMap = false // this must be false, if getting inverted colors, check clear color of gbuffer render pass. } return isNewMaterial } }