diff --git a/src/core/meshes/mixins/MeshBaseMixin.ts b/src/core/meshes/mixins/MeshBaseMixin.ts index 4671bfd8c..a7f1f6720 100644 --- a/src/core/meshes/mixins/MeshBaseMixin.ts +++ b/src/core/meshes/mixins/MeshBaseMixin.ts @@ -42,7 +42,7 @@ export interface MeshBaseParams extends MeshBaseRenderParams { /** * Base options used to create this Mesh */ -export interface MeshBaseOptions { +export interface MeshBaseOptions extends RenderMaterialParams { /** The label of this Mesh, sent to various GPU objects for debugging purpose */ label?: MeshBaseParams['label'] /** Shaders to use by this Mesh {@link RenderMaterial} */ @@ -766,12 +766,30 @@ function MeshBaseMixin(Base: TBase): MixinConstr ...(renderPass.options.colorAttachments.length > 1 && { additionalTargets: renderPass.options.colorAttachments .filter((c, i) => i > 0) - .map((colorAttachment) => { + .map((colorAttachment, index) => { return { format: colorAttachment.targetFormat, + ...(this.options.additionalTargets.length && + this.options.additionalTargets[index] && + this.options.additionalTargets[index].blend && { + blend: this.options.additionalTargets[index].blend, + }), } }), }), + // TODO + ...(renderPass.options.colorAttachments.length && { + targets: renderPass.options.colorAttachments.map((colorAttachment, index) => { + return { + format: colorAttachment.targetFormat, + ...(this.options.targets?.length && + this.options.targets[index] && + this.options.targets[index].blend && { + blend: this.options.targets[index].blend, + }), + } + }), + }), }), // depth depth: renderPass.options.useDepth, @@ -780,6 +798,8 @@ function MeshBaseMixin(Base: TBase): MixinConstr }), } + console.log(this.options.label, renderingOptions) + this.material?.setRenderingOptions(renderingOptions) } diff --git a/src/core/meshes/mixins/ProjectedMeshBaseMixin.ts b/src/core/meshes/mixins/ProjectedMeshBaseMixin.ts index 8b7a8f210..de9da328e 100644 --- a/src/core/meshes/mixins/ProjectedMeshBaseMixin.ts +++ b/src/core/meshes/mixins/ProjectedMeshBaseMixin.ts @@ -12,10 +12,8 @@ import { GPUCurtains } from '../../../curtains/GPUCurtains' import { DOMElementBoundingRect, RectCoords } from '../../DOM/DOMElement' import { RenderMaterialParams, ShaderOptions } from '../../../types/Materials' import { ProjectedObject3D } from '../../objects3D/ProjectedObject3D' -import { DOMObject3D } from '../../../curtains/objects3D/DOMObject3D' import default_projected_vsWgsl from '../../shaders/chunks/default_projected_vs.wgsl' import default_normal_fsWgsl from '../../shaders/chunks/default_normal_fs.wgsl' -import { ShaderPassParams } from '../../renderPasses/ShaderPass' /** * Base parameters used to create a ProjectedMesh @@ -144,9 +142,9 @@ export declare class ProjectedMeshBaseClass extends MeshBaseClass { } /** - * Used to add the properties and methods defined in {@link ProjectedMeshBaseClass} to the {@link MeshBaseClass} and mix it with a given Base of type {@link ProjectedObject3D} or {@link DOMObject3D}. + * Used to add the properties and methods defined in {@link ProjectedMeshBaseClass} to the {@link MeshBaseClass} and mix it with a given Base of type {@link ProjectedObject3D} or {@link curtains/objects3D/DOMObject3D.DOMObject3D | DOMObject3D}. * @exports - * @param Base - the class to mix onto, should be of {@link ProjectedObject3D} or {@link DOMObject3D} type + * @param Base - the class to mix onto, should be of {@link ProjectedObject3D} or {@link curtains/objects3D/DOMObject3D.DOMObject3D | DOMObject3D} type * @returns - the mixed classes, creating a Projected Mesh. */ function ProjectedMeshBaseMixin>( diff --git a/src/core/pipelines/RenderPipelineEntry.ts b/src/core/pipelines/RenderPipelineEntry.ts index e5c6af75b..4e138a4e1 100644 --- a/src/core/pipelines/RenderPipelineEntry.ts +++ b/src/core/pipelines/RenderPipelineEntry.ts @@ -294,18 +294,47 @@ export class RenderPipelineEntry extends PipelineEntry { // we either disable blending if mesh if opaque // use a custom blending if set // or use this blend equation if mesh is transparent (see https://limnu.com/webgl-blending-youre-probably-wrong/) - const blend = - this.options.rendering.blend ?? - (this.options.rendering.transparent && { - color: { - srcFactor: 'src-alpha', - dstFactor: 'one-minus-src-alpha', - }, - alpha: { - srcFactor: 'one', - dstFactor: 'one-minus-src-alpha', + // const blend = + // this.options.rendering.blend ?? + // (this.options.rendering.transparent && { + // color: { + // srcFactor: 'src-alpha', + // dstFactor: 'one-minus-src-alpha', + // }, + // alpha: { + // srcFactor: 'one', + // dstFactor: 'one-minus-src-alpha', + // }, + // }) + + if (this.options.rendering.targets.length) { + // we will assume our renderer alphaMode is set to 'premultiplied' + // we either disable blending if mesh if opaque + // use a custom blending if set + // or use this blend equation if mesh is transparent (see https://limnu.com/webgl-blending-youre-probably-wrong/) + if (this.options.rendering.transparent) { + this.options.rendering.targets[0].blend = this.options.rendering.targets[0].blend + ? this.options.rendering.targets[0].blend + : { + color: { + srcFactor: 'src-alpha', + dstFactor: 'one-minus-src-alpha', + }, + alpha: { + srcFactor: 'one', + dstFactor: 'one-minus-src-alpha', + }, + } + } + } else { + this.options.rendering.targets = [ + { + format: this.renderer.options.preferredFormat, }, - }) + ] + } + + console.log(this.options.rendering.targets) this.descriptor = { label: this.options.label, @@ -332,15 +361,16 @@ export class RenderPipelineEntry extends PipelineEntry { fragment: { module: this.shaders.fragment.module, entryPoint: (this.options.shaders.fragment as ShaderOptions).entryPoint, - targets: [ - { - format: this.options.rendering.targetFormat ?? this.renderer.options.preferredFormat, - ...(blend && { - blend, - }), - }, - ...(this.options.rendering.additionalTargets ?? []), // merge with additional targets if any - ], + // targets: [ + // { + // format: this.options.rendering.targetFormat ?? this.renderer.options.preferredFormat, + // ...(blend && { + // blend, + // }), + // }, + // ...(this.options.rendering.additionalTargets ?? []), // merge with additional targets if any + // ], + targets: this.options.rendering.targets, }, }), primitive: { diff --git a/src/core/renderPasses/ShaderPass.ts b/src/core/renderPasses/ShaderPass.ts index 7322cd14d..aeeced987 100644 --- a/src/core/renderPasses/ShaderPass.ts +++ b/src/core/renderPasses/ShaderPass.ts @@ -69,6 +69,7 @@ export class ShaderPass extends FullscreenPlane { isRenderer(renderer, parameters.label ? parameters.label + ' ShaderPass' : 'ShaderPass') // force transparency to allow for correct blending between successive passes + // TODO should we disable depth instead? parameters.transparent = true parameters.label = parameters.label ?? 'ShaderPass ' + renderer.shaderPasses?.length diff --git a/src/types/Materials.ts b/src/types/Materials.ts index dbad339ea..e24edf499 100644 --- a/src/types/Materials.ts +++ b/src/types/Materials.ts @@ -133,15 +133,18 @@ export interface RenderMaterialBaseRenderingOptions { /** Cull mode to use with this {@link core/materials/RenderMaterial.RenderMaterial | RenderMaterial} */ cullMode: GPUCullMode /** Custom blending to use with this {@link core/materials/RenderMaterial.RenderMaterial | RenderMaterial}. Can override default transparent blending if set */ - blend?: GPUBlendState + //blend?: GPUBlendState /** Custom write mask value to use with this {@link core/materials/RenderMaterial.RenderMaterial | RenderMaterial}. */ - writeMask?: GPUColorWriteFlags + //writeMask?: GPUColorWriteFlags /** Optional texture format of the {@link core/pipelines/RenderPipelineEntry.RenderPipelineEntry | render pipeline} color target. Default to the renderer preferred format. */ - targetFormat: GPUTextureFormat + //targetFormat: GPUTextureFormat /** The {@link core/renderPasses/RenderPass.RenderPassParams#sampleCount | sampleCount} of the {@link core/renderPasses/RenderPass.RenderPass | RenderPass} onto which we'll be drawing. Set internally. */ sampleCount: GPUSize32 /** Define the additional targets properties in case this {@link core/materials/RenderMaterial.RenderMaterial | RenderMaterial} should be drawn to multiple targets. */ additionalTargets: GPUColorTargetState[] + + // TODO + targets: GPUColorTargetState[] } /** Rendering options to send to the {@link core/pipelines/RenderPipelineEntry.RenderPipelineEntry#pipeline | render pipeline} */ diff --git a/tests/index.html b/tests/index.html index 09a6a06aa..e123ac2ef 100644 --- a/tests/index.html +++ b/tests/index.html @@ -75,6 +75,9 @@

gpu-curtains tests

  • Orbit camera and selective passes
  • +
  • + Weighted blended OIT +
  • Plane transformations
  • diff --git a/tests/objects-removal/TestPingPong.js b/tests/objects-removal/TestPingPong.js index 526d3dc8b..e2150c9da 100644 --- a/tests/objects-removal/TestPingPong.js +++ b/tests/objects-removal/TestPingPong.js @@ -84,6 +84,11 @@ export class TestPingPong { }, }, targetFormat: 'rgba16float', // important, we'll be using floating point textures + targets: [ + { + format: 'rgba16float', // important, we'll be using floating point textures + }, + ], uniforms: { flowmap: { label: 'Flowmap', diff --git a/tests/order-independent-transparency/index.html b/tests/order-independent-transparency/index.html new file mode 100644 index 000000000..b3c36d806 --- /dev/null +++ b/tests/order-independent-transparency/index.html @@ -0,0 +1,25 @@ + + + + + + + + gpu-curtains | Orbit camera + depth textures basic test + + + + + + + + +
    + + + + diff --git a/tests/order-independent-transparency/main.js b/tests/order-independent-transparency/main.js new file mode 100644 index 000000000..e837498a8 --- /dev/null +++ b/tests/order-independent-transparency/main.js @@ -0,0 +1,362 @@ +// Weighted, blended order-independent transparency implementation +// from https://learnopengl.com/Guest-Articles/2020/OIT/Weighted-Blended +// and https://casual-effects.blogspot.com/2015/03/implemented-weighted-blended-order.html +window.addEventListener('load', async () => { + const path = location.hostname === 'localhost' ? '../../src/index.ts' : '../../dist/esm/index.mjs' + const { + GPUCameraRenderer, + GPUDeviceManager, + Object3D, + PlaneGeometry, + Mesh, + RenderTarget, + ShaderPass, + Vec3, + RenderTexture, + } = await import(/* @vite-ignore */ path) + + // here is an example of how we can use a simple GPUCameraRenderer instead of GPUCurtains + // this shows us how to use gpu-curtains as a basic genuine 3D engine, not related to DOM + + // first, we need a WebGPU device, that's what GPUDeviceManager is for + const gpuDeviceManager = new GPUDeviceManager({ + label: 'Custom device manager', + }) + + // we need to wait for the device to be created + await gpuDeviceManager.init() + + // then we can create a camera renderer + const gpuCameraRenderer = new GPUCameraRenderer({ + deviceManager: gpuDeviceManager, // the renderer is going to use our WebGPU device to create its context + container: document.querySelector('#canvas'), + pixelRatio: Math.min(1.5, window.devicePixelRatio), // limit pixel ratio for performance + }) + + // get the camera + const { camera } = gpuCameraRenderer + + const cameraPivot = new Object3D() + camera.position.z = 5 + camera.parent = cameraPivot + + camera.lookAt(new Vec3()) + + // render our scene manually + const animate = () => { + cameraPivot.rotation.y += 0.005 + gpuDeviceManager.render() + + requestAnimationFrame(animate) + } + + animate() + + const planeGeometry = new PlaneGeometry() + + const sampleCount = 4 + + // depth texture if needed + const OITDepthTexture = + sampleCount === 1 + ? new RenderTexture(gpuCameraRenderer, { + label: 'OIT depth texture', + name: 'oITDepthTexture', + usage: 'depth', + format: 'depth24plus', + sampleCount, + }) + : null + + const planesFs = /* wgsl */ ` + struct VSOutput { + @builtin(position) position: vec4f, + @location(0) uv: vec2f, + }; + + @fragment fn main(fsInput: VSOutput) -> @location(0) vec4f { + return vec4(shading.color, 1.0); + } + ` + + const OITOpaqueTarget = new RenderTarget(gpuCameraRenderer, { + label: 'Opaque MRT', + sampleCount, + //shouldUpdateView: false, // we don't want to render to the swap chain + ...(OITDepthTexture && { depthTexture: OITDepthTexture }), + }) + + const opaquePlane = new Mesh(gpuCameraRenderer, { + label: 'Opaque plane', + geometry: planeGeometry, + outputTarget: OITOpaqueTarget, + cullMode: 'none', + shaders: { + fragment: { + code: planesFs, + }, + }, + uniforms: { + shading: { + struct: { + color: { + type: 'vec3f', + value: new Vec3(1, 0, 0), + }, + }, + }, + }, + }) + + const OITTransparentTarget = new RenderTarget(gpuCameraRenderer, { + label: 'Transparent MRT', + sampleCount, + shouldUpdateView: false, // we don't want to render to the swap chain + colorAttachments: [ + { + loadOp: 'clear', + clearValue: [0, 0, 0, 0], + targetFormat: 'rgba16float', // accum + }, + { + loadOp: 'clear', + clearValue: [1, 0, 0, 0], + targetFormat: 'r8unorm', // revealage + }, + ], + ...(OITDepthTexture && { depthTexture: OITDepthTexture }), + depthLoadOp: 'load', // read from opaque depth! + }) + + const OITtargetFs = /* wgsl */ ` + struct VSOutput { + @builtin(position) position: vec4f, + @location(0) uv: vec2f, + @location(1) normal: vec3f, + }; + + struct OITTargetOutput { + // Textures: diffuse color, specular color, smoothness, emissive etc. could go here + @location(0) accum : vec4, + @location(1) reveal : f32, + }; + + @fragment fn main(fsInput: VSOutput) -> OITTargetOutput { + var output : OITTargetOutput; + + var color: vec4f = vec4(shading.color, shading.alpha); + + // insert your favorite weighting function here. the color-based factor + // avoids color pollution from the edges of wispy clouds. the z-based + // factor gives precedence to nearer surfaces + let weight: f32 = + clamp(pow(min(1.0, color.a * 10.0) + 0.01, 3.0) * 1e8 * + pow(1.0 - fsInput.position.z * 0.9, 3.0), 1e-2, 3e3); + + // blend func: GL_ONE, GL_ONE + // switch to pre-multiplied alpha and weight + output.accum = vec4(color.rgb * color.a, color.a) * weight; + + // blend func: GL_ZERO, GL_ONE_MINUS_SRC_ALPHA + output.reveal = color.a; + + return output; + } + ` + + for (let i = 0; i < 2; i++) { + const transparentPlane = new Mesh(gpuCameraRenderer, { + label: 'Transparent plane ' + i, + geometry: planeGeometry, + depthWriteEnabled: false, // read from opaque depth but not write to depth + outputTarget: OITTransparentTarget, + cullMode: 'none', + shaders: { + fragment: { + code: OITtargetFs, + }, + }, + //targetFormat: 'rgba16float', + blend: { + // accum + color: { + srcFactor: 'one', + dstFactor: 'one', + }, + alpha: { + srcFactor: 'one', + dstFactor: 'one', + }, + }, + additionalTargets: [ + // reveal + { + //format: 'r8unorm', // this would be patched anyway if not set here + blend: { + color: { + srcFactor: 'zero', + dstFactor: 'one-minus-src', + }, + alpha: { + srcFactor: 'one', + dstFactor: 'one', + }, + }, + }, + ], + targets: [ + { + //format: 'rgba16float', + blend: { + // accum + color: { + srcFactor: 'one', + dstFactor: 'one', + }, + alpha: { + srcFactor: 'one', + dstFactor: 'one', + }, + }, + }, + { + //format: 'r8unorm', // this would be patched anyway if not set here + blend: { + color: { + srcFactor: 'zero', + dstFactor: 'one-minus-src', + }, + alpha: { + srcFactor: 'one', + dstFactor: 'one', + }, + }, + }, + ], + uniforms: { + shading: { + struct: { + color: { + type: 'vec3f', + //value: new Vec3(Math.random() * 0.5 + 0.5, Math.random() * 0.5 + 0.5, Math.random() * 0.5 + 0.5), + value: i === 0 ? new Vec3(0, 1, 0) : new Vec3(0, 0, 1), + }, + alpha: { + type: 'f32', + value: 0.5, + }, + }, + }, + }, + }) + + transparentPlane.position.z = i + 1 + + transparentPlane.userData.time = 0 + + transparentPlane.onRender(() => { + transparentPlane.position.z = Math.cos(transparentPlane.userData.time * 0.01) * (i + 1) + transparentPlane.userData.time++ + }) + + console.log(transparentPlane) + } + + // opaque buffer + const OITOpaqueTexture = new RenderTexture(gpuCameraRenderer, { + label: 'OIT opaque texture', + name: 'oITOpaqueTexture', + format: OITOpaqueTarget.renderPass.options.colorAttachments[0].targetFormat, + fromTexture: OITOpaqueTarget.renderPass.viewTextures[0], + sampleCount, + }) + + // create 2 textures based on our OIT MRT output + const OITAccumTexture = new RenderTexture(gpuCameraRenderer, { + label: 'OIT accum texture', + name: 'oITAccumTexture', + format: OITTransparentTarget.renderPass.options.colorAttachments[0].targetFormat, + fromTexture: OITTransparentTarget.renderPass.viewTextures[0], + sampleCount, + }) + + const OITRevealTexture = new RenderTexture(gpuCameraRenderer, { + label: 'OIT reveal texture', + name: 'oITRevealTexture', + format: OITTransparentTarget.renderPass.options.colorAttachments[1].targetFormat, + fromTexture: OITTransparentTarget.renderPass.viewTextures[1], + sampleCount, + }) + + const compositingPassFs = /* wgsl */ ` + struct VSOutput { + @builtin(position) position: vec4f, + @location(0) uv: vec2f, + }; + + // epsilon number + const EPSILON: f32 = 0.00001; + + // calculate floating point numbers equality accurately + fn isApproximatelyEqual(a: f32, b: f32) -> bool { + return abs(a - b) <= select(abs(a), abs(b), abs(a) < abs(b)) * EPSILON; + } + + // get the max value between three values + fn max3(v: vec3f) -> f32 { + return max(max(v.x, v.y), v.z); + } + + fn isInf(value: f32) -> bool { + return abs(value) > 999999999999.99; + } + + @fragment fn main(fsInput: VSOutput) -> @location(0) vec4f { + let opaqueColor = textureLoad( + oITOpaqueTexture, + vec2(floor(fsInput.position.xy)), + 0 + ); + + // fragment revealage + let revealage = textureLoad( + oITRevealTexture, + vec2(floor(fsInput.position.xy)), + 0 + ).r; + + // save the blending and color texture fetch cost if there is not a transparent fragment + if (isApproximatelyEqual(revealage, 1.0)) { + return opaqueColor; + } + + // fragment color + var accumulation = textureLoad( + oITAccumTexture, + vec2(floor(fsInput.position.xy)), + 0 + ); + + // suppress overflow + if (isInf(max3(abs(accumulation.rgb)))) { + accumulation = vec4(accumulation.a); + } + + // prevent floating point precision bug + var averageColor = accumulation.rgb / max(accumulation.a, EPSILON); + + // alpha blending between opaque and transparent + return vec4(opaqueColor.rgb * revealage, opaqueColor.a) + vec4(averageColor, 1.0 - revealage); + } + ` + + const compositingPass = new ShaderPass(gpuCameraRenderer, { + label: 'Compositing pass', + renderTextures: [OITOpaqueTexture, OITAccumTexture, OITRevealTexture], + shaders: { + fragment: { + code: compositingPassFs, + }, + }, + }) +}) diff --git a/tests/stress-test/index.html b/tests/stress-test/index.html index 32d743c7d..aa1ec7218 100644 --- a/tests/stress-test/index.html +++ b/tests/stress-test/index.html @@ -21,6 +21,7 @@
    + diff --git a/tests/stress-test/main.js b/tests/stress-test/main.js index 1d70fea5b..a0f95cbbe 100644 --- a/tests/stress-test/main.js +++ b/tests/stress-test/main.js @@ -42,7 +42,13 @@ window.addEventListener('load', async () => { // not specifically designed to be responsive const aspectRatio = gpuCurtains.boundingRect.width / gpuCurtains.boundingRect.height - for (let i = 0; i < 3000; i++) { + console.time('creation time') + let createdMeshes = 0 + let nbMeshes = 3000 + + const meshes = [] + + const addMesh = (index) => { const mesh = new Mesh(gpuCurtains, { geometry: Math.random() > 0.5 ? cubeGeometry : sphereGeometry, //frustumCulled: false, // you can also gain a few fps without checking for frustum @@ -58,5 +64,42 @@ window.addEventListener('load', async () => { mesh.rotation.y += rotationSpeed mesh.rotation.z += rotationSpeed }) + + meshes.push(mesh) + } + + for (let i = 0; i < nbMeshes; i++) { + addMesh(i) + + meshes[i].onReady(() => { + createdMeshes++ + if (createdMeshes === nbMeshes) { + console.timeEnd('creation time') + } + }) } + + // GUI + const gui = new lil.GUI({ + title: 'Stress test', + }) + + gui + .add({ nbMeshes }, 'nbMeshes', 500, 5000, 1) + .name('Number of meshes') + .onFinishChange((value) => { + if (value < nbMeshes) { + for (let i = nbMeshes - 1; i >= value; i--) { + meshes[i].remove() + } + + meshes.splice(value, nbMeshes - value) + } else { + for (let i = nbMeshes; i < value; i++) { + addMesh(i) + } + } + + nbMeshes = value + }) })