| @@ -29,9 +29,9 @@ async function init() { | |||
| viewer.scene.mainCamera.userData.maxFarPlane = 10 | |||
| viewer.scene.refreshScene() | |||
| const depthTarget = depthPlugin.getTarget() | |||
| const depthTarget = depthPlugin.target | |||
| if (!depthTarget) { | |||
| throw new Error('depthPlugin.getTarget() returned undefined') | |||
| throw new Error('depthPlugin.target returned undefined') | |||
| } | |||
| // to render depth buffer to screen, uncomment this line: | |||
| @@ -42,7 +42,7 @@ async function init() { | |||
| targetPreview.addTarget(()=>depthTarget, 'depth') | |||
| createSimpleButtons({ | |||
| ['Toggle depth rendering']: () => { | |||
| ['Toggle Depth rendering']: () => { | |||
| viewer.renderManager.screenPass.overrideReadBuffer = | |||
| viewer.renderManager.screenPass.overrideReadBuffer ? null : depthTarget | |||
| viewer.setDirty() | |||
| @@ -216,6 +216,7 @@ | |||
| <h2 class="category">Plugins</h2> | |||
| <ul> | |||
| <li><a href="./depth-buffer-plugin/">Depth Buffer Plugin </a></li> | |||
| <li><a href="./normal-buffer-plugin/">Normal Buffer Plugin </a></li> | |||
| <li><a href="./render-target-preview/">Render Target Preview </a></li> | |||
| <li><a href="./dropzone-plugin/">Dropzone (Drag & Drop) </a></li> | |||
| </ul> | |||
| @@ -0,0 +1,34 @@ | |||
| <!DOCTYPE html> | |||
| <html lang="en"> | |||
| <head> | |||
| <meta charset="UTF-8"> | |||
| <title>Depth Buffer Plugin</title> | |||
| <!-- Import maps polyfill --> | |||
| <!-- Remove this when import maps will be widely supported --> | |||
| <script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script> | |||
| <script type="importmap"> | |||
| { | |||
| "imports": { | |||
| "threepipe": "./../../dist/index.mjs" | |||
| } | |||
| } | |||
| </script> | |||
| <style id="example-style"> | |||
| html, body, #canvas-container, #mcanvas { | |||
| width: 100%; | |||
| height: 100%; | |||
| margin: 0; | |||
| overflow: hidden; | |||
| } | |||
| </style> | |||
| <script type="module" src="../examples-utils/simple-code-preview.mjs"></script> | |||
| <script id="example-script" type="module" src="./script.js" data-scripts="./script.ts;./script.js"></script> | |||
| </head> | |||
| <body> | |||
| <div id="canvas-container"> | |||
| <canvas id="mcanvas"></canvas> | |||
| </div> | |||
| </body> | |||
| @@ -0,0 +1,57 @@ | |||
| import { | |||
| _testFinish, | |||
| downloadBlob, | |||
| HalfFloatType, | |||
| NoColorSpace, | |||
| NormalBufferPlugin, | |||
| RenderTargetPreviewPlugin, | |||
| SRGBColorSpace, | |||
| ThreeViewer, | |||
| } from 'threepipe' | |||
| import {createSimpleButtons} from '../examples-utils/simple-bottom-buttons.js' | |||
| const viewer = new ThreeViewer({ | |||
| canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | |||
| }) | |||
| async function init() { | |||
| const normalPlugin = viewer.addPluginSync(new NormalBufferPlugin(HalfFloatType)) | |||
| await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr') | |||
| await viewer.load('https://threejs.org/examples/models/gltf/kira.glb', { | |||
| autoCenter: true, | |||
| autoScale: true, | |||
| }) | |||
| const normalTarget = normalPlugin.target | |||
| if (!normalTarget) { | |||
| throw new Error('normalPlugin.target returned undefined') | |||
| } | |||
| viewer.renderManager.screenPass.overrideReadBuffer = normalTarget | |||
| viewer.renderManager.screenPass.outputColorSpace = NoColorSpace // default is SRGBColorSpace | |||
| const targetPreview = await viewer.addPlugin(RenderTargetPreviewPlugin) | |||
| targetPreview.addTarget(()=>normalTarget, 'normal') | |||
| createSimpleButtons({ | |||
| ['Toggle Normal rendering']: () => { | |||
| const state = !!viewer.renderManager.screenPass.overrideReadBuffer | |||
| viewer.renderManager.screenPass.overrideReadBuffer = state ? null : normalTarget | |||
| viewer.renderManager.screenPass.outputColorSpace = state ? SRGBColorSpace : NoColorSpace | |||
| viewer.setDirty() | |||
| }, | |||
| ['Download snapshot']: async(btn: HTMLButtonElement) => { | |||
| btn.disabled = true | |||
| const blob = await viewer.getScreenshotBlob({mimeType: 'image/png'}) | |||
| if (blob) downloadBlob(blob, 'file.png') | |||
| else console.error('Unable to get screenshot') | |||
| btn.disabled = false | |||
| }, | |||
| }) | |||
| } | |||
| init().then(_testFinish) | |||
| @@ -1,4 +1,11 @@ | |||
| import {_testFinish, DepthBufferPlugin, HalfFloatType, RenderTargetPreviewPlugin, ThreeViewer} from 'threepipe' | |||
| import { | |||
| _testFinish, | |||
| DepthBufferPlugin, | |||
| HalfFloatType, | |||
| NormalBufferPlugin, | |||
| RenderTargetPreviewPlugin, | |||
| ThreeViewer, | |||
| } from 'threepipe' | |||
| const viewer = new ThreeViewer({ | |||
| canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | |||
| @@ -10,6 +17,8 @@ const viewer = new ThreeViewer({ | |||
| async function init() { | |||
| const depth = viewer.addPluginSync(new DepthBufferPlugin(HalfFloatType, true)) | |||
| const normal = viewer.addPluginSync(new NormalBufferPlugin(HalfFloatType)) | |||
| const targetPreview = viewer.addPluginSync(RenderTargetPreviewPlugin) | |||
| await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr') | |||
| @@ -21,7 +30,8 @@ async function init() { | |||
| viewer.renderManager.autoBuildPipeline = false | |||
| viewer.renderManager.pipeline = ['depth', 'render', 'screen'] | |||
| targetPreview.addTarget(()=>depth.getTarget(), 'depth', false, true) | |||
| targetPreview.addTarget(()=>depth.target, 'depth', false, true) | |||
| targetPreview.addTarget(()=>normal.target, 'normal', false, true) | |||
| targetPreview.addTarget(()=>viewer.renderManager.composerTarget, 'composer-1', false, false) | |||
| targetPreview.addTarget(()=>viewer.renderManager.composerTarget2, 'composer-2', false, false) | |||
| @@ -1,4 +1,5 @@ | |||
| export {DepthBufferPlugin} from './pipeline/DepthBufferPlugin' | |||
| export {NormalBufferPlugin} from './pipeline/NormalBufferPlugin' | |||
| export type {DepthBufferPluginEventTypes, DepthBufferPluginPass, DepthBufferPluginTarget} from './pipeline/DepthBufferPlugin' | |||
| export {PipelinePassPlugin} from './base/PipelinePassPlugin' | |||
| export {RenderTargetPreviewPlugin} from './ui/RenderTargetPreviewPlugin' | |||
| @@ -25,8 +25,8 @@ export class DepthBufferPlugin | |||
| readonly passId = 'depth' | |||
| public static readonly PluginType = 'DepthBufferPlugin' | |||
| private _depthTarget?: DepthBufferPluginTarget | |||
| private _depthTexture?: Texture | |||
| target?: DepthBufferPluginTarget | |||
| texture?: Texture | |||
| readonly material: MeshDepthMaterial = new MeshDepthMaterial({ | |||
| depthPacking: BasicDepthPacking, | |||
| blending: NoBlending, | |||
| @@ -34,7 +34,7 @@ export class DepthBufferPlugin | |||
| // private _gbufferPass?: IFilter<GBufferRenderPass<WebGLMultipleRenderTargets> | |||
| createPass(v: ThreeViewer) { | |||
| const target = v.renderManager.createTarget<DepthBufferPluginTarget>( | |||
| if (!this.target) this.target = v.renderManager.createTarget<DepthBufferPluginTarget>( | |||
| { | |||
| depthBuffer: true, | |||
| samples: v.renderManager.composerTarget.samples || 0, | |||
| @@ -44,14 +44,15 @@ export class DepthBufferPlugin | |||
| // generateMipmaps: false, | |||
| // encoding: LinearEncoding, | |||
| }) | |||
| target.texture.name = 'depthBuffer' | |||
| this._depthTexture = target.texture | |||
| this._depthTarget = target | |||
| this.texture = this.target.texture | |||
| this.texture.name = 'depthBuffer' | |||
| if (this.isPrimaryGBuffer) v.renderManager.gbufferTarget = target | |||
| if (this.isPrimaryGBuffer) v.renderManager.gbufferTarget = this.target | |||
| this.material.userData.isGBufferMaterial = true | |||
| const pass = new GBufferRenderPass('depth', target, this.material, new Color(0, 0, 0), 1) | |||
| const pass = new GBufferRenderPass('depth', this.target, this.material, new Color(0, 0, 0), 1) | |||
| const preprocessMaterial = pass.preprocessMaterial | |||
| pass.preprocessMaterial = (m) => preprocessMaterial(m, m.userData.renderToDepth) | |||
| pass.before = ['render'] | |||
| pass.after = [] | |||
| pass.required = ['render'] | |||
| @@ -66,19 +67,15 @@ export class DepthBufferPlugin | |||
| } | |||
| onRemove(viewer: ThreeViewer): void { | |||
| if (this._depthTarget) { | |||
| viewer.renderManager.disposeTarget(this._depthTarget) | |||
| this._depthTarget = undefined | |||
| if (this.target) { | |||
| viewer.renderManager.disposeTarget(this.target) | |||
| this.target = undefined | |||
| } | |||
| return super.onRemove(viewer) | |||
| } | |||
| getTarget() { | |||
| return this._depthTarget | |||
| } | |||
| updateShaderProperties(material: {defines: Record<string, string | number | undefined>; uniforms: {[p: string]: IUniform}}): this { | |||
| if (material.uniforms.tDepth) material.uniforms.tDepth.value = this._depthTexture ?? undefined | |||
| if (material.uniforms.tDepth) material.uniforms.tDepth.value = this.texture ?? undefined | |||
| else this._viewer?.console.warn('BaseRenderer: no uniform: tDepth') | |||
| return this | |||
| } | |||
| @@ -0,0 +1,82 @@ | |||
| import { | |||
| Color, | |||
| HalfFloatType, | |||
| IUniform, | |||
| LinearSRGBColorSpace, | |||
| MeshNormalMaterial, | |||
| NearestFilter, | |||
| NoBlending, | |||
| Texture, | |||
| TextureDataType, | |||
| WebGLRenderTarget, | |||
| } from 'three' | |||
| import {GBufferRenderPass} from '../../postprocessing' | |||
| import {ThreeViewer} from '../../viewer' | |||
| import {IShaderPropertiesUpdater} from '../../materials' | |||
| import {PipelinePassPlugin} from '../base/PipelinePassPlugin' | |||
| export type NormalBufferPluginEventTypes = '' | |||
| // type NormalBufferPluginTarget = WebGLMultipleRenderTargets | WebGLRenderTarget | |||
| export type NormalBufferPluginTarget = WebGLRenderTarget | |||
| export type NormalBufferPluginPass = GBufferRenderPass<'normal', NormalBufferPluginTarget> | |||
| export class NormalBufferPlugin | |||
| extends PipelinePassPlugin<NormalBufferPluginPass, 'normal', NormalBufferPluginEventTypes> | |||
| implements IShaderPropertiesUpdater { | |||
| readonly passId = 'normal' | |||
| public static readonly PluginType = 'NormalBufferPlugin' | |||
| target?: NormalBufferPluginTarget | |||
| texture?: Texture | |||
| readonly material: MeshNormalMaterial = new MeshNormalMaterial({ | |||
| blending: NoBlending, | |||
| }) | |||
| // private _gbufferPass?: IFilter<GBufferRenderPass<WebGLMultipleRenderTargets> | |||
| createPass(v: ThreeViewer) { | |||
| if (!this.target) this.target = v.renderManager.createTarget<NormalBufferPluginTarget>( | |||
| { | |||
| depthBuffer: true, | |||
| // samples: v.renderManager.composerTarget.samples || 0, | |||
| samples: 0, | |||
| type: this.bufferType, | |||
| magFilter: NearestFilter, | |||
| minFilter: NearestFilter, | |||
| generateMipmaps: false, | |||
| colorSpace: LinearSRGBColorSpace, | |||
| }) | |||
| this.texture = this.target.texture | |||
| this.texture.name = 'normalBuffer' | |||
| this.material.userData.isGBufferMaterial = true | |||
| const pass = new GBufferRenderPass('normal', this.target, this.material, new Color(0, 0, 0), 1) | |||
| const preprocessMaterial = pass.preprocessMaterial | |||
| pass.preprocessMaterial = (m) => preprocessMaterial(m, true) | |||
| pass.before = ['render'] | |||
| pass.after = [] | |||
| pass.required = ['render'] | |||
| return pass | |||
| } | |||
| constructor( | |||
| public readonly bufferType: TextureDataType = HalfFloatType, | |||
| ) { | |||
| super() | |||
| } | |||
| onRemove(viewer: ThreeViewer): void { | |||
| if (this.target) { | |||
| viewer.renderManager.disposeTarget(this.target) | |||
| this.target = undefined | |||
| } | |||
| return super.onRemove(viewer) | |||
| } | |||
| updateShaderProperties(material: {defines: Record<string, string | number | undefined>; uniforms: {[p: string]: IUniform}}): this { | |||
| if (material.uniforms.tNormal) material.uniforms.tNormal.value = this.texture ?? undefined | |||
| else this._viewer?.console.warn('BaseRenderer: no uniform: tNormal') | |||
| return this | |||
| } | |||
| } | |||
| @@ -18,6 +18,26 @@ export class GBufferRenderPass<TP extends IPassID, T extends WebGLMultipleRender | |||
| private _transparentMats = new Set<IMaterial>() | |||
| private _transmissiveMats = new Set<[IMaterial, number]>() | |||
| preprocessMaterial = (material: IMaterial, renderToGBuffer?: boolean) => { | |||
| renderToGBuffer = renderToGBuffer ?? material.userData.renderToGBuffer | |||
| if ( | |||
| material.transparent && renderToGBuffer || // transparent and render to gbuffer | |||
| !material.transparent && !material.transmission && renderToGBuffer === false // opaque and dont render to gbuffer | |||
| ) { | |||
| this._transparentMats.add(material) | |||
| material.transparent = !material.transparent | |||
| // material.needsUpdate = true | |||
| } | |||
| if ( | |||
| material.transmission && | |||
| Math.abs(material.transmission || 0) > 0 && renderToGBuffer // transmission and render to gbuffer | |||
| ) { | |||
| this._transmissiveMats.add([material, material.transmission]) | |||
| material.transmission = 0 | |||
| // material.needsUpdate = true | |||
| } | |||
| } | |||
| /** | |||
| * Renders to {@link target} | |||
| * @param renderer | |||
| @@ -33,30 +53,10 @@ export class GBufferRenderPass<TP extends IPassID, T extends WebGLMultipleRender | |||
| const activeCubeFace = renderer.getActiveCubeFace() | |||
| const activeMipLevel = renderer.getActiveMipmapLevel() | |||
| const preprocessMaterial = (material: IMaterial) => { | |||
| const renderToGBuffer = material.userData.renderToGBuffer === undefined ? material.userData.renderToDepth : material.userData.renderToGBuffer | |||
| if ( | |||
| material.transparent && material.userData.renderToDepth || // transparent and render to gbuffer | |||
| !material.transparent && !material.transmission && renderToGBuffer === false // opaque and dont render to gbuffer | |||
| ) { | |||
| this._transparentMats.add(material) | |||
| material.transparent = !material.transparent | |||
| // material.needsUpdate = true | |||
| } | |||
| if ( | |||
| material.transmission && | |||
| Math.abs(material.transmission || 0) > 0 && renderToGBuffer // transmission and render to gbuffer | |||
| ) { | |||
| this._transmissiveMats.add([material, material.transmission]) | |||
| material.transmission = 0 | |||
| // material.needsUpdate = true | |||
| } | |||
| } | |||
| this.scene.traverse(({material}) => { | |||
| if (!material) return | |||
| if (Array.isArray(material)) material.forEach(preprocessMaterial) | |||
| else preprocessMaterial(material) | |||
| if (Array.isArray(material)) material.forEach((m)=>this.preprocessMaterial(m)) | |||
| else this.preprocessMaterial(material) | |||
| }) | |||
| // todo; copy double sided, check with post processing | |||