diff --git a/common/changes/@zephyr3d/backend-webgl/develop_2024-03-25-11-16.json b/common/changes/@zephyr3d/backend-webgl/develop_2024-03-25-11-16.json new file mode 100644 index 00000000..a68bed13 --- /dev/null +++ b/common/changes/@zephyr3d/backend-webgl/develop_2024-03-25-11-16.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@zephyr3d/backend-webgl", + "comment": "", + "type": "none" + } + ], + "packageName": "@zephyr3d/backend-webgl" +} \ No newline at end of file diff --git a/common/changes/@zephyr3d/backend-webgl/develop_2024-04-19-17-44.json b/common/changes/@zephyr3d/backend-webgl/develop_2024-04-19-17-44.json new file mode 100644 index 00000000..0c736292 --- /dev/null +++ b/common/changes/@zephyr3d/backend-webgl/develop_2024-04-19-17-44.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@zephyr3d/backend-webgl", + "comment": "implement render bundle", + "type": "patch" + } + ], + "packageName": "@zephyr3d/backend-webgl" +} \ No newline at end of file diff --git a/common/changes/@zephyr3d/backend-webgpu/develop_2024-04-19-17-44.json b/common/changes/@zephyr3d/backend-webgpu/develop_2024-04-19-17-44.json new file mode 100644 index 00000000..b40cdbc5 --- /dev/null +++ b/common/changes/@zephyr3d/backend-webgpu/develop_2024-04-19-17-44.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@zephyr3d/backend-webgpu", + "comment": "Add render bundle support", + "type": "patch" + } + ], + "packageName": "@zephyr3d/backend-webgpu" +} \ No newline at end of file diff --git a/common/changes/@zephyr3d/base/develop_2024-04-19-17-44.json b/common/changes/@zephyr3d/base/develop_2024-04-19-17-44.json new file mode 100644 index 00000000..876cda30 --- /dev/null +++ b/common/changes/@zephyr3d/base/develop_2024-04-19-17-44.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@zephyr3d/base", + "comment": "", + "type": "none" + } + ], + "packageName": "@zephyr3d/base" +} \ No newline at end of file diff --git a/common/changes/@zephyr3d/device/develop_2024-03-25-11-16.json b/common/changes/@zephyr3d/device/develop_2024-03-25-11-16.json new file mode 100644 index 00000000..686dbac6 --- /dev/null +++ b/common/changes/@zephyr3d/device/develop_2024-03-25-11-16.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@zephyr3d/device", + "comment": "Implement uniform buffer with dynamic offset", + "type": "patch" + } + ], + "packageName": "@zephyr3d/device" +} \ No newline at end of file diff --git a/common/changes/@zephyr3d/device/develop_2024-04-19-17-44.json b/common/changes/@zephyr3d/device/develop_2024-04-19-17-44.json new file mode 100644 index 00000000..3a18e7e1 --- /dev/null +++ b/common/changes/@zephyr3d/device/develop_2024-04-19-17-44.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@zephyr3d/device", + "comment": "Add render bundle support", + "type": "patch" + } + ], + "packageName": "@zephyr3d/device" +} \ No newline at end of file diff --git a/common/changes/@zephyr3d/imgui/develop_2024-04-19-17-44.json b/common/changes/@zephyr3d/imgui/develop_2024-04-19-17-44.json new file mode 100644 index 00000000..54d6849d --- /dev/null +++ b/common/changes/@zephyr3d/imgui/develop_2024-04-19-17-44.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@zephyr3d/imgui", + "comment": "", + "type": "none" + } + ], + "packageName": "@zephyr3d/imgui" +} \ No newline at end of file diff --git a/common/changes/@zephyr3d/scene/develop_2024-03-25-11-16.json b/common/changes/@zephyr3d/scene/develop_2024-03-25-11-16.json new file mode 100644 index 00000000..47013eaf --- /dev/null +++ b/common/changes/@zephyr3d/scene/develop_2024-03-25-11-16.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@zephyr3d/scene", + "comment": "Improve rendering performance for geometry instancing", + "type": "patch" + } + ], + "packageName": "@zephyr3d/scene" +} \ No newline at end of file diff --git a/common/changes/@zephyr3d/scene/develop_2024-04-19-17-44.json b/common/changes/@zephyr3d/scene/develop_2024-04-19-17-44.json new file mode 100644 index 00000000..d86674de --- /dev/null +++ b/common/changes/@zephyr3d/scene/develop_2024-04-19-17-44.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@zephyr3d/scene", + "comment": "Add support for command buffer reuse optimization", + "type": "minor" + } + ], + "packageName": "@zephyr3d/scene" +} \ No newline at end of file diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 05d0ec23..7b49b7bf 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -72,7 +72,7 @@ importers: dependencies: '@types/colors': 1.2.1 '@types/diff': 5.0.3 - '@webgpu/types': 0.1.34 + '@webgpu/types': 0.1.40 '@zephyr3d/backend-webgl': link:../libs/backend-webgl '@zephyr3d/backend-webgpu': link:../libs/backend-webgpu '@zephyr3d/base': link:../libs/base @@ -134,7 +134,7 @@ importers: typescript: ^5.1.3 dependencies: '@types/node': 18.17.6 - '@webgpu/types': 0.1.34 + '@webgpu/types': 0.1.40 devDependencies: '@babel/core': 7.22.10 '@babel/preset-env': 7.22.10_@babel+core@7.22.10 @@ -179,7 +179,7 @@ importers: typescript: ^5.1.3 dependencies: '@types/node': 18.17.6 - '@webgpu/types': 0.1.34 + '@webgpu/types': 0.1.40 devDependencies: '@babel/core': 7.22.10 '@babel/preset-env': 7.22.10_@babel+core@7.22.10 @@ -262,7 +262,7 @@ importers: typescript: ^5.1.3 dependencies: '@types/node': 18.17.6 - '@webgpu/types': 0.1.34 + '@webgpu/types': 0.1.40 devDependencies: '@babel/core': 7.22.10 '@babel/preset-env': 7.22.10_@babel+core@7.22.10 @@ -309,7 +309,7 @@ importers: dependencies: '@types/emscripten': 1.39.7 '@types/node': 18.17.6 - '@webgpu/types': 0.1.34 + '@webgpu/types': 0.1.40 devDependencies: '@babel/core': 7.22.10 '@babel/preset-env': 7.22.10_@babel+core@7.22.10 @@ -353,7 +353,7 @@ importers: shx: ^0.3.4 typescript: ^5.1.3 dependencies: - '@webgpu/types': 0.1.34 + '@webgpu/types': 0.1.40 devDependencies: '@babel/core': 7.22.10 '@babel/preset-env': 7.22.10_@babel+core@7.22.10 @@ -424,7 +424,7 @@ importers: '@popperjs/core': 2.11.8 '@types/colors': 1.2.1 '@types/diff': 5.0.3 - '@webgpu/types': 0.1.34 + '@webgpu/types': 0.1.40 '@zephyr3d/backend-webgl': link:../libs/backend-webgl '@zephyr3d/backend-webgpu': link:../libs/backend-webgpu '@zephyr3d/base': link:../libs/base @@ -509,7 +509,7 @@ importers: dependencies: '@types/colors': 1.2.1 '@types/diff': 5.0.3 - '@webgpu/types': 0.1.34 + '@webgpu/types': 0.1.40 '@zephyr3d/backend-webgl': link:../libs/backend-webgl '@zephyr3d/backend-webgpu': link:../libs/backend-webgpu '@zephyr3d/base': link:../libs/base @@ -2612,8 +2612,8 @@ packages: parse5: 6.0.1 dev: true - /@webgpu/types/0.1.34: - resolution: {integrity: sha512-9mXtH+CC8q+Ku7Z+1XazNIte81FvfdXwR2lLRO7Ykzjd/hh1J1krJa0gtnkF1kvP11psUmKEPKo7iMTeEcUpNA==} + /@webgpu/types/0.1.40: + resolution: {integrity: sha512-/BBkHLS6/eQjyWhY2H7Dx5DHcVrS2ICj9owvSRdgtQT6KcafLZA86tPze0xAOsd4FbsYKCUBUQyNi87q7gV7kw==} dev: false /@zip.js/zip.js/2.7.35: diff --git a/examples/package.json b/examples/package.json index 6723d734..0d0776fb 100644 --- a/examples/package.json +++ b/examples/package.json @@ -54,7 +54,7 @@ "dependencies": { "@types/colors": "^1.2.1", "@types/diff": "^5.0.2", - "@webgpu/types": "^0.1.31", + "@webgpu/types": "^0.1.40", "colors": "^1.4.0", "diff": "^5.0.0", "@zephyr3d/base": "workspace:^0.1.3", diff --git a/examples/src/instancecube/main.ts b/examples/src/instancecube/main.ts index 153a3dc9..b127c9aa 100644 --- a/examples/src/instancecube/main.ts +++ b/examples/src/instancecube/main.ts @@ -1,7 +1,7 @@ import { Matrix4x4, Quaternion, Vector4 } from '@zephyr3d/base'; import { backendWebGL1, backendWebGL2 } from '@zephyr3d/backend-webgl'; import { backendWebGPU } from '@zephyr3d/backend-webgpu'; -import type { DeviceBackend } from '@zephyr3d/device'; +import type { DeviceBackend, RenderBundle } from '@zephyr3d/device'; import { DrawText } from '@zephyr3d/device'; (async function () { @@ -106,6 +106,7 @@ import { DrawText } from '@zephyr3d/device'; // create bind group const bindGroup = device.createBindGroup(program.bindGroupLayouts[0]); + let renderBundle: RenderBundle = null; // start render loop device.runLoop((device) => { const t = device.frameInfo.elapsedOverall * 0.002; @@ -117,11 +118,16 @@ import { DrawText } from '@zephyr3d/device'; ); device.clearFrameBuffer(new Vector4(0, 0, 0.5, 1), 1, 0); - device.setProgram(program); - device.setVertexLayout(vertexLayout); - device.setBindGroup(0, bindGroup); - device.drawInstanced('triangle-list', 0, 36, 16); - + if (!renderBundle) { + device.beginCapture(); + device.setProgram(program); + device.setVertexLayout(vertexLayout); + device.setBindGroup(0, bindGroup); + device.drawInstanced('triangle-list', 0, 36, 16); + renderBundle = device.endCapture(); + } else { + device.executeRenderBundle(renderBundle); + } DrawText.drawText(device, `Device: ${device.type}`, '#ffffff', 30, 30); DrawText.drawText(device, `FPS: ${device.frameInfo.FPS.toFixed(2)}`, '#ffff00', 30, 50); }); diff --git a/libs/backend-webgl/package.json b/libs/backend-webgl/package.json index 1d9b5eff..09d282af 100644 --- a/libs/backend-webgl/package.json +++ b/libs/backend-webgl/package.json @@ -65,7 +65,12 @@ "@zephyr3d/device": "workspace:^0.2.1" }, "dependencies": { +<<<<<<< HEAD "@types/node": "^18.13.0", "@webgpu/types": "^0.1.31" +======= + "@webgpu/types": "^0.1.40", + "@types/node": "^18.13.0" +>>>>>>> develop } } diff --git a/libs/backend-webgl/src/basetexture_webgl.ts b/libs/backend-webgl/src/basetexture_webgl.ts index a29eb4af..b653deb2 100644 --- a/libs/backend-webgl/src/basetexture_webgl.ts +++ b/libs/backend-webgl/src/basetexture_webgl.ts @@ -94,6 +94,7 @@ export abstract class WebGLBaseTexture extends WebGLGPUObject { destroy(): void { if (this._object) { this._device.context.deleteTexture(this._object); + this._device.invalidateBindingTextures(); this._object = null; this._device.updateVideoMemoryCost(-this._memCost); this._memCost = 0; @@ -191,6 +192,7 @@ export abstract class WebGLBaseTexture extends WebGLGPUObject { const obj = this._object; this._device.runNextFrame(() => { this._device.context.deleteTexture(obj); + this._device.invalidateBindingTextures(); }); this._object = null; } @@ -203,7 +205,8 @@ export abstract class WebGLBaseTexture extends WebGLGPUObject { if (!this._device.isContextLost()) { this._object = this._device.context.createTexture(); const gl = this._device.context; - gl.bindTexture(textureTargetMap[this._target], this._object); + this._device.bindTexture(textureTargetMap[this._target], 0, this); + //gl.bindTexture(textureTargetMap[this._target], this._object); const params = (this.getTextureCaps() as WebGLTextureCaps).getTextureFormatInfo(this._format); if (isWebGL2(gl) && !this.isTextureVideo()) { if (!this.isTexture3D() && !this.isTexture2DArray()) { diff --git a/libs/backend-webgl/src/bindgroup_webgl.ts b/libs/backend-webgl/src/bindgroup_webgl.ts index 3fead472..68517ec2 100644 --- a/libs/backend-webgl/src/bindgroup_webgl.ts +++ b/libs/backend-webgl/src/bindgroup_webgl.ts @@ -16,17 +16,30 @@ import type { WebGLGPUProgram } from './gpuprogram_webgl'; import type { WebGLTextureSampler } from './sampler_webgl'; import type { TypedArray } from '@zephyr3d/base'; import type { WebGLDevice } from './device_webgl'; -import type { WebGLGPUBuffer } from './buffer_webgl'; +import { WebGLGPUBuffer } from './buffer_webgl'; export class WebGLBindGroup extends WebGLGPUObject implements BindGroup { private _layout: BindGroupLayout; + private _dynamicOffsets: number[]; private _resources: Record; constructor(device: WebGLDevice, layout: BindGroupLayout) { super(device); this._device = device; this._layout = layout; + this._dynamicOffsets = null; this._resources = {}; this._object = {}; + for (const entry of this._layout.entries) { + if (entry.buffer && entry.buffer.hasDynamicOffset) { + if (!this._dynamicOffsets) { + this._dynamicOffsets = []; + } + this._dynamicOffsets[entry.buffer.dynamicOffsetIndex] = 0; + } + } + } + getGPUId(): string { + return String(this._uid); } getLayout(): BindGroupLayout { return this._layout; @@ -34,7 +47,10 @@ export class WebGLBindGroup extends WebGLGPUObject implements BindGroup getBuffer(name: string): GPUDataBuffer { return this._getBuffer(name, true); } - setBuffer(name: string, buffer: GPUDataBuffer) { + getDynamicOffsets(): number[] { + return this._dynamicOffsets; + } + setBuffer(name: string, buffer: GPUDataBuffer, offset?: number, bindOffset?: number, bindSize?: number) { const bindName = this._layout.nameMap?.[name] ?? name; for (const entry of this._layout.entries) { if (entry.name === bindName) { @@ -46,6 +62,9 @@ export class WebGLBindGroup extends WebGLGPUObject implements BindGroup } else if (buffer !== this._resources[entry.name]) { this._resources[entry.name] = buffer as WebGLGPUBuffer; } + if (entry.buffer.hasDynamicOffset) { + this._dynamicOffsets[entry.buffer.dynamicOffsetIndex] = offset ?? 0; + } } return; } @@ -122,20 +141,22 @@ export class WebGLBindGroup extends WebGLGPUObject implements BindGroup } apply(program: WebGLGPUProgram, offsets?: Iterable) { const webgl2 = this._device.isWebGL2; - let dynamicOffsetIndex = 0; + const dynamicOffsets = offsets ?? this.getDynamicOffsets(); for (let i = 0; i < this._layout.entries.length; i++) { const entry = this._layout.entries[i]; const res = this._resources[entry.name]; - if (res instanceof WebGLStructuredBuffer) { + if (res instanceof WebGLGPUBuffer) { if (webgl2) { if (entry.buffer.hasDynamicOffset) { - const offset = offsets?.[dynamicOffsetIndex] || 0; - dynamicOffsetIndex++; - program.setBlock((entry.type as PBStructTypeInfo).structName, res, offset); + program.setBlock( + (entry.type as PBStructTypeInfo).structName, + res, + dynamicOffsets[entry.buffer.dynamicOffsetIndex] + ); } else { program.setBlock((entry.type as PBStructTypeInfo).structName, res, 0); } - } else { + } else if (res instanceof WebGLStructuredBuffer) { program.setUniform(entry.name, res.getUniformData().uniforms); } } else if (Array.isArray(res)) { diff --git a/libs/backend-webgl/src/capabilities_webgl.ts b/libs/backend-webgl/src/capabilities_webgl.ts index 8e32ac68..f5e99653 100644 --- a/libs/backend-webgl/src/capabilities_webgl.ts +++ b/libs/backend-webgl/src/capabilities_webgl.ts @@ -242,9 +242,13 @@ export class WebGLShaderCaps implements ShaderCaps { supportHighPrecisionInt: boolean; maxUniformBufferSize: number; uniformBufferOffsetAlignment: number; + maxStorageBufferSize: number; + storageBufferOffsetAlignment: number; constructor(gl: WebGLContext) { this._extFragDepth = null; this._extStandardDerivatives = null; + this.maxStorageBufferSize = 0; + this.storageBufferOffsetAlignment = 0; if (isWebGL2(gl)) { this.supportFragmentDepth = true; this.supportStandardDerivatives = true; diff --git a/libs/backend-webgl/src/device_webgl.ts b/libs/backend-webgl/src/device_webgl.ts index fd1c2d03..845ad484 100644 --- a/libs/backend-webgl/src/device_webgl.ts +++ b/libs/backend-webgl/src/device_webgl.ts @@ -36,7 +36,8 @@ import type { PBStructTypeInfo, DeviceBackend, DeviceEventMap, - AbstractDevice + AbstractDevice, + RenderBundle } from '@zephyr3d/device'; import { hasAlphaChannel, @@ -72,10 +73,17 @@ import { GPUTimer } from './gpu_timer'; import { WebGLTextureCaps, WebGLFramebufferCaps, WebGLMiscCaps, WebGLShaderCaps } from './capabilities_webgl'; import { WebGLBindGroup } from './bindgroup_webgl'; import { WebGLGPUProgram } from './gpuprogram_webgl'; -import { primitiveTypeMap, typeMap } from './constants_webgl'; +import { + primitiveTypeMap, + textureMagFilterToWebGL, + textureMinFilterToWebGL, + textureWrappingMap, + typeMap +} from './constants_webgl'; import { SamplerCache } from './sampler_cache'; import { WebGLStructuredBuffer } from './structuredbuffer_webgl'; import type { WebGLTextureSampler } from './sampler_webgl'; +import type { WebGLBaseTexture } from './basetexture_webgl'; declare global { interface WebGLRenderingContext { @@ -90,6 +98,18 @@ declare global { type VAOObject = WebGLVertexArrayObject | WebGLVertexArrayObjectOES; +type WebGLRenderBundle = { + program: WebGLGPUProgram; + bindGroups: WebGLBindGroup[]; + bindGroupOffsets: Iterable[]; + vertexLayout: WebGLVertexLayout; + primitiveType: PrimitiveType; + renderStateSet: RenderStateSet; + first: number; + count: number; + numInstances: number; +}[]; + export interface VertexArrayObjectEXT { createVertexArray: () => VAOObject; bindVertexArray: (arrayObject: VAOObject) => void; @@ -119,6 +139,7 @@ const tempUint32Array = new Uint32Array(4); export class WebGLDevice extends BaseDevice { private _context: WebGLContext; + private _isWebGL2: boolean; private _msaaSampleCount: number; private _loseContextExtension: WEBGL_lose_context; private _contextLost: boolean; @@ -138,10 +159,16 @@ export class WebGLDevice extends BaseDevice { private _currentScissorRect: DeviceViewport; private _samplerCache: SamplerCache; private _textureSamplerMap: WeakMap; + private _captureRenderBundle: WebGLRenderBundle; + private _deviceUniformBuffers: WebGLBuffer[]; + private _deviceUniformBufferOffsets: number[]; + private _bindTextures: Record; + private _bindSamplers: WebGLSampler[]; constructor(backend: DeviceBackend, cvs: HTMLCanvasElement, options?: DeviceOptions) { super(cvs, backend); this._dpr = Math.max(1, Math.floor(options?.dpr ?? window.devicePixelRatio)); this._isRendering = false; + this._captureRenderBundle = null; this._msaaSampleCount = options?.msaa ? 4 : 1; let context: WebGLContext = null; context = this.canvas.getContext(backend === backend1 ? 'webgl' : 'webgl2', { @@ -153,6 +180,7 @@ export class WebGLDevice extends BaseDevice { if (!context) { throw new Error('Invalid argument or no webgl support'); } + this._isWebGL2 = isWebGL2(context); this._contextLost = false; this._reverseWindingOrder = false; this._deviceCaps = null; @@ -164,6 +192,15 @@ export class WebGLDevice extends BaseDevice { this._currentBindGroupOffsets = []; this._currentViewport = null; this._currentScissorRect = null; + this._deviceUniformBuffers = []; + this._deviceUniformBufferOffsets = []; + this._bindTextures = { + [WebGLEnum.TEXTURE_2D]: [], + [WebGLEnum.TEXTURE_CUBE_MAP]: [], + [WebGLEnum.TEXTURE_3D]: [], + [WebGLEnum.TEXTURE_2D_ARRAY]: [] + }; + this._bindSamplers = []; this._samplerCache = new SamplerCache(this); this._textureSamplerMap = new WeakMap(); this._loseContextExtension = this._context.getExtension('WEBGL_lose_context'); @@ -192,7 +229,7 @@ export class WebGLDevice extends BaseDevice { return this.getFramebuffer()?.getSampleCount() ?? this._msaaSampleCount; } get isWebGL2(): boolean { - return this._context && isWebGL2(this._context); + return this._isWebGL2; } get drawingBufferWidth() { return this.getDrawingBufferWidth(); @@ -236,6 +273,76 @@ export class WebGLDevice extends BaseDevice { getBackBufferHeight(): number { return this.canvas.height; } + invalidateBindingTextures() { + this._bindTextures = { + [WebGLEnum.TEXTURE_2D]: [], + [WebGLEnum.TEXTURE_CUBE_MAP]: [], + [WebGLEnum.TEXTURE_3D]: [], + [WebGLEnum.TEXTURE_2D_ARRAY]: [] + }; + } + bindTexture(target: number, layer: number, texture: WebGLBaseTexture, sampler?: WebGLTextureSampler) { + const tex = texture?.object ?? null; + const gl = this._context; + gl.activeTexture(WebGLEnum.TEXTURE0 + layer); + if (this._bindTextures[target][layer] !== tex) { + gl.bindTexture(target, tex); + this._bindTextures[target][layer] = tex; + } + if (this._isWebGL2) { + const samp = sampler?.object ?? null; + if (samp && this._bindSamplers[layer] !== samp) { + (gl as WebGL2RenderingContext).bindSampler(layer, samp); + this._bindSamplers[layer] = samp; + } + } else if (texture && sampler && this._textureSamplerMap.get(texture) !== sampler) { + const fallback = texture.isWebGL1Fallback; + this._textureSamplerMap.set(texture, sampler); + gl.texParameteri( + target, + WebGLEnum.TEXTURE_WRAP_S, + textureWrappingMap[false && fallback ? 'clamp' : sampler.addressModeU] + ); + gl.texParameteri( + target, + WebGLEnum.TEXTURE_WRAP_T, + textureWrappingMap[false && fallback ? 'clamp' : sampler.addressModeV] + ); + gl.texParameteri(target, WebGLEnum.TEXTURE_MAG_FILTER, textureMagFilterToWebGL(sampler.magFilter)); + gl.texParameteri( + target, + WebGLEnum.TEXTURE_MIN_FILTER, + textureMinFilterToWebGL(sampler.minFilter, fallback ? 'none' : sampler.mipFilter) + ); + if (this.getDeviceCaps().textureCaps.supportAnisotropicFiltering) { + gl.texParameterf(target, WebGLEnum.TEXTURE_MAX_ANISOTROPY, sampler.maxAnisotropy); + } + } + } + bindUniformBuffer(index: number, buffer: WebGLGPUBuffer, offset: number) { + if ( + this._deviceUniformBuffers[index] !== buffer.object || + this._deviceUniformBufferOffsets[index] !== offset + ) { + if (offset) { + (this.context as WebGL2RenderingContext).bindBufferRange( + WebGLEnum.UNIFORM_BUFFER, + index, + buffer.object, + offset, + buffer.byteLength - offset + ); + } else { + (this.context as WebGL2RenderingContext).bindBufferBase( + WebGLEnum.UNIFORM_BUFFER, + index, + buffer.object + ); + } + this._deviceUniformBuffers[index] = buffer.object; + this._deviceUniformBufferOffsets[index] = offset; + } + } async initContext() { this.initContextState(); this.on('resize', (evt) => { @@ -525,6 +632,22 @@ export class WebGLDevice extends BaseDevice { createBuffer(sizeInBytes: number, options: BufferCreationOptions): GPUDataBuffer { return new WebGLGPUBuffer(this, this.parseBufferOptions(options), sizeInBytes); } + copyBuffer( + sourceBuffer: GPUDataBuffer, + destBuffer: GPUDataBuffer, + srcOffset: number, + dstOffset: number, + bytes: number + ) { + if (!this.isWebGL2) { + console.error(`copyBuffer() is not supported for current device`); + return; + } + const gl = this._context as WebGL2RenderingContext; + gl.bindBuffer(gl.COPY_READ_BUFFER, sourceBuffer.object); + gl.bindBuffer(gl.COPY_WRITE_BUFFER, destBuffer.object); + gl.copyBufferSubData(gl.COPY_READ_BUFFER, gl.COPY_WRITE_BUFFER, srcOffset, dstOffset, bytes); + } createIndexBuffer(data: Uint16Array | Uint32Array, options?: BufferCreationOptions): IndexBuffer { return new WebGLIndexBuffer(this, data, this.parseBufferOptions(options, 'index')); } @@ -761,6 +884,35 @@ export class WebGLDevice extends BaseDevice { } } } + beginCapture(): void { + if (this._captureRenderBundle) { + throw new Error('Device.beginCapture() failed: device is already capturing draw commands'); + } + this._captureRenderBundle = []; + } + endCapture(): unknown { + if (!this._captureRenderBundle) { + throw new Error('Device.endCapture() failed: device is not capturing draw commands'); + } + const result = this._captureRenderBundle; + this._captureRenderBundle = null; + return result; + } + executeRenderBundle(renderBundle: RenderBundle) { + for (const drawcall of renderBundle as WebGLRenderBundle) { + this.setProgram(drawcall.program); + this.setVertexLayout(drawcall.vertexLayout); + this.setRenderStates(drawcall.renderStateSet); + for (let i = 0; i < 4; i++) { + this.setBindGroup(i, drawcall.bindGroups[i], drawcall.bindGroupOffsets[i]); + } + if (drawcall.numInstances === 0) { + this.draw(drawcall.primitiveType, drawcall.first, drawcall.count); + } else { + this.drawInstanced(drawcall.primitiveType, drawcall.first, drawcall.count, drawcall.numInstances); + } + } + } /** @internal */ protected onBeginFrame(): boolean { if (this._contextLost) { @@ -812,6 +964,30 @@ export class WebGLDevice extends BaseDevice { } (this._context._currentFramebuffer as WebGLFrameBuffer)?.tagDraw(); } + if (this._captureRenderBundle) { + const rs = this._currentStateSet?.clone() ?? this.createRenderStateSet(); + if (this._reverseWindingOrder) { + const rasterState = rs.rasterizerState; + if (!rasterState) { + rs.useRasterizerState().setCullMode('front'); + } else if (rasterState.cullMode === 'back') { + rasterState.cullMode = 'front'; + } else if (rasterState.cullMode === 'front') { + rasterState.cullMode = 'back'; + } + } + this._captureRenderBundle.push({ + bindGroups: [...this._currentBindGroups], + bindGroupOffsets: this._currentBindGroupOffsets.map((val) => (val ? [...val] : null)), + program: this._currentProgram, + vertexLayout: this._currentVertexData, + primitiveType: primitiveType, + renderStateSet: rs, + count, + first, + numInstances: 0 + }); + } } /** @internal */ protected _drawInstanced( @@ -854,6 +1030,19 @@ export class WebGLDevice extends BaseDevice { } (this._context._currentFramebuffer as WebGLFrameBuffer)?.tagDraw(); } + if (this._captureRenderBundle) { + this._captureRenderBundle.push({ + bindGroups: [...this._currentBindGroups], + bindGroupOffsets: this._currentBindGroupOffsets.map((val) => (val ? [...val] : null)), + program: this._currentProgram, + vertexLayout: this._currentVertexData, + primitiveType: primitiveType, + renderStateSet: this._currentStateSet?.clone() ?? null, + count, + first, + numInstances + }); + } } /** @internal */ protected _compute(): void { @@ -965,6 +1154,15 @@ export class WebGLDevice extends BaseDevice { this.enableGPUTimeRecording(true); this._context._currentFramebuffer = undefined; this._context._currentProgram = undefined; + this._deviceUniformBuffers = []; + this._deviceUniformBufferOffsets = []; + this._bindTextures = { + [WebGLEnum.TEXTURE_2D]: [], + [WebGLEnum.TEXTURE_CUBE_MAP]: [], + [WebGLEnum.TEXTURE_3D]: [], + [WebGLEnum.TEXTURE_2D_ARRAY]: [] + }; + this._bindSamplers = []; } /** @internal */ clearErrors() { diff --git a/libs/backend-webgl/src/framebuffer_webgl.ts b/libs/backend-webgl/src/framebuffer_webgl.ts index d7e82ccd..35f7bd30 100644 --- a/libs/backend-webgl/src/framebuffer_webgl.ts +++ b/libs/backend-webgl/src/framebuffer_webgl.ts @@ -6,6 +6,8 @@ import { WebGLEnum } from './webgl_enum'; import { cubeMapFaceMap } from './constants_webgl'; import type { WebGLTextureCaps } from './capabilities_webgl'; import type { WebGLDevice } from './device_webgl'; +import type { WebGLTexture2D } from './texture2d_webgl'; +import type { WebGLTextureCube } from './texturecube_webgl'; type FrameBufferTextureAttachment = { texture: BaseTexture; @@ -39,6 +41,7 @@ export class WebGLFrameBuffer private _height: number; private _isMRT: boolean; private _drawBuffers: number[]; + private _hash: string; private _depthAttachmentTarget: number; private _colorAttachmentsAA: WebGLRenderbuffer[]; private _depthAttachmentAA: WebGLRenderbuffer; @@ -121,6 +124,10 @@ export class WebGLFrameBuffer } else { this._depthAttachmentTarget = WebGLEnum.NONE; } + const colorAttachmentHash = + this._options.colorAttachments?.map((tex) => tex.texture.format).join(':') ?? ''; + const depthAttachmentHash = this._options.depthAttachment?.texture.format ?? ''; + this._hash = `${colorAttachmentHash}-${depthAttachmentHash}-${this._options.sampleCount ?? 1}`; this._init(); } tagDraw() { @@ -137,6 +144,9 @@ export class WebGLFrameBuffer const attachment = this._options.colorAttachments?.[0] ?? this._options.depthAttachment; return Math.max(attachment.texture.height >> attachment.level, 1); } + getHash(): string { + return this._hash; + } async restore() { if (!this._object && !this._device.isContextLost()) { if (this._options?.depthAttachment?.texture?.disposed) { @@ -175,6 +185,7 @@ export class WebGLFrameBuffer for (const rb of entry[1]) { if (rb) { this._device.context.deleteTexture(rb.texture); + this._device.invalidateBindingTextures(); } } } @@ -311,7 +322,8 @@ export class WebGLFrameBuffer 0 ); if (tex.isTexture2D()) { - this._device.context.bindTexture(WebGLEnum.TEXTURE_2D, tex.object); + this._device.bindTexture(WebGLEnum.TEXTURE_2D, 0, tex as WebGLTexture2D); + //this._device.context.bindTexture(WebGLEnum.TEXTURE_2D, tex.object); this._device.context.copyTexSubImage2D( WebGLEnum.TEXTURE_2D, attachment.level, @@ -323,7 +335,8 @@ export class WebGLFrameBuffer texture.height ); } else if (tex.isTextureCube()) { - this._device.context.bindTexture(WebGLEnum.TEXTURE_CUBE_MAP, tex.object); + this._device.bindTexture(WebGLEnum.TEXTURE_CUBE_MAP, 0, tex as WebGLTextureCube); + //this._device.context.bindTexture(WebGLEnum.TEXTURE_CUBE_MAP, tex.object); this._device.context.copyTexSubImage2D( cubeMapFaceMap[attachment.face ?? CubeFace.PX], attachment.level, @@ -449,6 +462,7 @@ export class WebGLFrameBuffer this.device.getDeviceCaps().textureCaps as WebGLTextureCaps ).getTextureFormatInfo(info.texture.format); intermediateTexture = this._device.context.createTexture(); + this._device.context.activeTexture(WebGLEnum.TEXTURE0); this._device.context.bindTexture(WebGLEnum.TEXTURE_2D, intermediateTexture); this._device.context.texImage2D( WebGLEnum.TEXTURE_2D, @@ -462,6 +476,7 @@ export class WebGLFrameBuffer null ); intermediateAttachments[info.level] = { texture: intermediateTexture, width, height }; + this._device.bindTexture(WebGLEnum.TEXTURE_2D, 0, null); } else { intermediateTexture = intermediateAttachments[info.level].texture; } diff --git a/libs/backend-webgl/src/gpuprogram_webgl.ts b/libs/backend-webgl/src/gpuprogram_webgl.ts index dd618292..4556b4af 100644 --- a/libs/backend-webgl/src/gpuprogram_webgl.ts +++ b/libs/backend-webgl/src/gpuprogram_webgl.ts @@ -11,12 +11,10 @@ import type { ShaderKind } from '@zephyr3d/device'; import { semanticList } from '@zephyr3d/device'; -import { textureMagFilterToWebGL, textureMinFilterToWebGL, textureWrappingMap } from './constants_webgl'; import type { WebGLTextureSampler } from './sampler_webgl'; import type { WebGLBaseTexture } from './basetexture_webgl'; import type { WebGLGPUBuffer } from './buffer_webgl'; import type { WebGLDevice } from './device_webgl'; -import type { WebGLStructuredBuffer } from './structuredbuffer_webgl'; type TypedArray = Int8Array | Uint8Array | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array; type TypedArrayConstructor = { @@ -147,9 +145,11 @@ export class WebGLGPUProgram extends WebGLGPUObject implements GPU } } } - setBlock(name: string, value: WebGLStructuredBuffer, offset: number) { + setBlock(name: string, value: WebGLGPUBuffer, offset: number) { const info = this._blockInfo[name]; if (info) { + this._device.bindUniformBuffer(info.index, value, offset); + /* if (offset) { (this._device.context as WebGL2RenderingContext).bindBufferRange( WebGLEnum.UNIFORM_BUFFER, @@ -165,6 +165,7 @@ export class WebGLGPUProgram extends WebGLGPUObject implements GPU value.object ); } + */ } else { console.error(`Block not found: ${name}`); } @@ -195,8 +196,10 @@ export class WebGLGPUProgram extends WebGLGPUObject implements GPU if (!this.checkLoad()) { return false; } + /* this._device.context._currentProgram = this; this._device.context.useProgram(this._object); + */ } return true; } @@ -266,13 +269,19 @@ export class WebGLGPUProgram extends WebGLGPUObject implements GPU this._object = null; return false; } + this._device.context._currentProgram = this; + this._device.context.useProgram(this._object); this._uniformSetters = this.createUniformSetters(); + } else { + this._device.context._currentProgram = this; + this._device.context.useProgram(this._object); } return true; } private createUniformSetter(info: ProgramUniformInfo) { const loc = info.location; const isArray = info.isArray; + const gl = this._device.context; switch (info.type) { case WebGLEnum.FLOAT: return this.getUniformSetterfv(loc); @@ -331,6 +340,7 @@ export class WebGLGPUProgram extends WebGLGPUObject implements GPU const unit = this._unitCounter; this._unitCounter += info.size; if (!isArray) { + gl.uniform1i(loc, unit); return this.getSamplerSetter(loc, WebGLEnum.TEXTURE_2D, unit); } } @@ -341,6 +351,7 @@ export class WebGLGPUProgram extends WebGLGPUObject implements GPU const unit = this._unitCounter; this._unitCounter += info.size; if (!isArray) { + gl.uniform1i(loc, unit); return this.getSamplerSetter(loc, WebGLEnum.TEXTURE_2D_ARRAY, unit); } } @@ -351,6 +362,7 @@ export class WebGLGPUProgram extends WebGLGPUObject implements GPU const unit = this._unitCounter; this._unitCounter += info.size; if (!isArray) { + gl.uniform1i(loc, unit); return this.getSamplerSetter(loc, WebGLEnum.TEXTURE_CUBE_MAP, unit); } } @@ -360,6 +372,7 @@ export class WebGLGPUProgram extends WebGLGPUObject implements GPU const unit = this._unitCounter; this._unitCounter += info.size; if (!isArray) { + gl.uniform1i(loc, unit); return this.getSamplerSetter(loc, WebGLEnum.TEXTURE_3D, unit); } } @@ -457,11 +470,6 @@ export class WebGLGPUProgram extends WebGLGPUObject implements GPU } return uniformSetters; } - private getUniformSetterf(location: WebGLUniformLocation) { - return (value: number) => { - this._device.context.uniform1f(location, value); - }; - } private getUniformSetterfv(location: WebGLUniformLocation) { return (value: any) => { this._device.context.uniform1fv(location, value); @@ -482,11 +490,6 @@ export class WebGLGPUProgram extends WebGLGPUObject implements GPU this._device.context.uniform4fv(location, value); }; } - private getUniformSetteri(location: WebGLUniformLocation) { - return (value: number) => { - this._device.context.uniform1i(location, value); - }; - } private getUniformSetteriv(location: WebGLUniformLocation) { return (value: any) => { this._device.context.uniform1iv(location, value); @@ -507,11 +510,6 @@ export class WebGLGPUProgram extends WebGLGPUObject implements GPU this._device.context.uniform4iv(location, value); }; } - private getUniformSetterui(location: WebGLUniformLocation) { - return (value: number) => { - (this._device.context as WebGL2RenderingContext).uniform1ui(location, value); - }; - } private getUniformSetteruiv(location: WebGLUniformLocation) { return (value: any) => { (this._device.context as WebGL2RenderingContext).uniform1uiv(location, value); @@ -578,12 +576,15 @@ export class WebGLGPUProgram extends WebGLGPUObject implements GPU }; } private getSamplerSetter(location: WebGLUniformLocation, target: number, unit: number) { + return (texture: [WebGLBaseTexture, WebGLTextureSampler]) => + this._device.bindTexture(target, unit, texture[0], texture[1]); + /* const gl = this._device.context; return isWebGL2(gl) ? (texture: [WebGLBaseTexture, WebGLTextureSampler]) => { const tex = texture?.[0].object ?? null; const sampler = texture?.[1].object ?? null; - gl.uniform1i(location, unit); + //gl.uniform1i(location, unit); gl.activeTexture(this._device.context.TEXTURE0 + unit); gl.bindTexture(target, tex); gl.bindSampler(unit, sampler); @@ -591,9 +592,9 @@ export class WebGLGPUProgram extends WebGLGPUObject implements GPU : (texture: [WebGLBaseTexture, WebGLTextureSampler]) => { const tex = texture?.[0] ?? null; const sampler = texture?.[1] ?? null; - gl.uniform1i(location, unit); + //gl.uniform1i(location, unit); gl.activeTexture(this._device.context.TEXTURE0 + unit); - gl.bindTexture(target, texture?.[0]?.object || null); + gl.bindTexture(target, tex?.object ?? null); if (tex && sampler && this._device.getCurrentSamplerForTexture(tex) !== sampler) { const fallback = tex.isWebGL1Fallback; this._device.setCurrentSamplerForTexture(tex, sampler); @@ -622,6 +623,7 @@ export class WebGLGPUProgram extends WebGLGPUObject implements GPU } } }; + */ } private getTypedArrayInfo(type: number): { ctor: TypedArrayConstructor; diff --git a/libs/backend-webgl/src/renderstate_webgl.ts b/libs/backend-webgl/src/renderstate_webgl.ts index d3bcda91..4db80fa5 100644 --- a/libs/backend-webgl/src/renderstate_webgl.ts +++ b/libs/backend-webgl/src/renderstate_webgl.ts @@ -434,6 +434,15 @@ export class WebGLRenderStateSet implements RenderStateSet { this.depthState = null; this.stencilState = null; } + clone(): RenderStateSet { + const newStateSet = new WebGLRenderStateSet(this._gl); + newStateSet.colorState = (this.colorState?.clone() as WebGLColorState) ?? null; + newStateSet.blendingState = (this.blendingState?.clone() as WebGLBlendingState) ?? null; + newStateSet.rasterizerState = (this.rasterizerState?.clone() as WebGLRasterizerState) ?? null; + newStateSet.depthState = (this.depthState?.clone() as WebGLDepthState) ?? null; + newStateSet.stencilState = (this.stencilState?.clone() as WebGLStencilState) ?? null; + return newStateSet; + } copyFrom(stateSet: RenderStateSet): void { this.colorState = stateSet.colorState as WebGLColorState; this.blendingState = stateSet.blendingState as WebGLBlendingState; diff --git a/libs/backend-webgl/src/sampler_webgl.ts b/libs/backend-webgl/src/sampler_webgl.ts index 89532ff6..00664ffa 100644 --- a/libs/backend-webgl/src/sampler_webgl.ts +++ b/libs/backend-webgl/src/sampler_webgl.ts @@ -81,7 +81,8 @@ export class WebGLTextureSampler if (texture?.object && !this._device.isWebGL2 && !this._device.isContextLost()) { const gl = this._device.context; const target = textureTargetMap[texture.target]; - gl.bindTexture(target, texture.object); + this._device.bindTexture(target, 0, texture); + //gl.bindTexture(target, texture.object); gl.texParameteri(target, WebGLEnum.TEXTURE_WRAP_S, textureWrappingMap[this._options.addressU]); gl.texParameteri(target, WebGLEnum.TEXTURE_WRAP_T, textureWrappingMap[this._options.addressV]); gl.texParameteri( diff --git a/libs/backend-webgl/src/texture2d_webgl.ts b/libs/backend-webgl/src/texture2d_webgl.ts index 188c333e..a048737b 100644 --- a/libs/backend-webgl/src/texture2d_webgl.ts +++ b/libs/backend-webgl/src/texture2d_webgl.ts @@ -30,7 +30,8 @@ export class WebGLTexture2D extends WebGLBaseTexture implements Texture2D 1) { const target = textureTargetMap[this._target]; - this._device.context.bindTexture(target, this._object); + this._device.bindTexture(target, 0, this); + //this._device.context.bindTexture(target, this._object); this._device.context.generateMipmap(target); } } @@ -216,7 +219,8 @@ export class WebGLTexture2D extends WebGLBaseTexture implements Texture2D 1) { const target = textureTargetMap[this._target]; - this._device.context.bindTexture(target, this._object); + this._device.bindTexture(target, 0, this); + //this._device.context.bindTexture(target, this._object); this._device.context.generateMipmap(target); } } diff --git a/libs/backend-webgl/src/texture3d_webgl.ts b/libs/backend-webgl/src/texture3d_webgl.ts index e30de455..fe7b3bb9 100644 --- a/libs/backend-webgl/src/texture3d_webgl.ts +++ b/libs/backend-webgl/src/texture3d_webgl.ts @@ -39,7 +39,8 @@ export class WebGLTexture3D extends WebGLBaseTexture implements Texture3D 1) { const target = textureTargetMap[this._target]; - this._device.context.bindTexture(target, this._object); + this._device.bindTexture(target, 0, this); + //this._device.context.bindTexture(target, this._object); this._device.context.generateMipmap(target); } } @@ -155,7 +157,8 @@ export class WebGLTexture3D extends WebGLBaseTexture implements Texture3D 1) { const target = textureTargetMap[this._target]; - this._device.context.bindTexture(target, this._object); + this._device.bindTexture(target, 0, this); + //this._device.context.bindTexture(target, this._object); this._device.context.generateMipmap(target); } } @@ -206,7 +209,8 @@ export class WebGLTextureCube extends WebGLBaseTexture implements TextureCube>>>>>> develop } } diff --git a/libs/backend-webgpu/src/basetexture_webgpu.ts b/libs/backend-webgpu/src/basetexture_webgpu.ts index c021c4de..915e1c14 100644 --- a/libs/backend-webgpu/src/basetexture_webgpu.ts +++ b/libs/backend-webgpu/src/basetexture_webgpu.ts @@ -131,10 +131,6 @@ export abstract class WebGPUBaseTexture< return true; } /** @internal */ - getPendingUploads(): (UploadTexture | UploadImage)[] { - return this._pendingUploads; - } - /** @internal */ clearPendingUploads() { if (this._pendingUploads.length > 0) { this._pendingUploads = []; @@ -248,6 +244,7 @@ export abstract class WebGPUBaseTexture< } generateMipmaps() { this._mipmapDirty = true; + this._device.textureUpload(this as WebGPUBaseTexture); } beginSyncChanges(encoder: GPUCommandEncoder) { if (!this.isTextureVideo() && this._pendingUploads.length > 0 && this._object) { @@ -332,6 +329,8 @@ export abstract class WebGPUBaseTexture< abstract createView(level?: number, face?: number, mipCount?: number): GPUTextureView; /** @internal */ private sync() { + this._device.flush(); + /* if (this._pendingUploads) { if (this._device.isTextureUploading(this as WebGPUBaseTexture)) { this._device.currentPass.end(); @@ -340,6 +339,7 @@ export abstract class WebGPUBaseTexture< this.endSyncChanges(); } } + */ } /** @internal */ protected _calcMipLevelCount(format: TextureFormat, width: number, height: number, depth: number): number { @@ -513,6 +513,10 @@ export abstract class WebGPUBaseTexture< offsetZ: number, miplevel: number ) { + if (this.isTextureVideo()) { + console.error('BaseTexture.uploadRaw(): Cannot upload to video texture'); + return; + } const data = new Uint8Array(pixels.buffer, pixels.byteOffset, pixels.byteLength); const info = (this.getTextureCaps() as WebGPUTextureCaps).getTextureFormatInfo(this._format); const blockWidth = info.blockWidth || 1; @@ -576,6 +580,7 @@ export abstract class WebGPUBaseTexture< bufferStride: bufferStride, mipLevel: miplevel }); + this._device.textureUpload(this as WebGPUBaseTexture); } } /** @internal */ @@ -590,6 +595,10 @@ export abstract class WebGPUBaseTexture< miplevel: number, layer: number ) { + if (this.isTextureVideo()) { + console.error('BaseTexture.uploadImageData(): Cannot upload to video texture'); + return; + } if ( false && !this._device.isTextureUploading(this as any) && @@ -612,24 +621,6 @@ export abstract class WebGPUBaseTexture< depthOrArrayLayers: 1 }); } else { - /* - // can not use getImageData() because it is not accurate - const tmpCanvas = document.createElement('canvas'); - let gl = tmpCanvas.getContext("webgl2"); - gl.activeTexture(gl.TEXTURE0); - let texture = gl.createTexture(); - gl.bindTexture(gl.TEXTURE_2D, texture); - const framebuffer = gl.createFramebuffer(); - gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); - gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, data); - gl.drawBuffers([gl.COLOR_ATTACHMENT0]); - let pixels = new Uint8Array(width * height * 4); - gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels); - this.uploadRaw(pixels, width, height, 1, offsetX, offsetY, faceIndex, miplevel); - tmpCanvas.width = 0; - tmpCanvas.height = 0; - */ this._pendingUploads.push({ image: data, offsetX: destX, @@ -643,6 +634,7 @@ export abstract class WebGPUBaseTexture< depth: 1, mipLevel: miplevel ?? 0 }); + this._device.textureUpload(this as WebGPUBaseTexture); } } /** @internal */ diff --git a/libs/backend-webgpu/src/bindgroup_cache.ts b/libs/backend-webgpu/src/bindgroup_cache.ts index 3e2fdc9a..9f771e88 100644 --- a/libs/backend-webgpu/src/bindgroup_cache.ts +++ b/libs/backend-webgpu/src/bindgroup_cache.ts @@ -5,12 +5,12 @@ import type { WebGPUDevice } from './device'; export class BindGroupCache { private _device: WebGPUDevice; - private _bindGroupLayoutCache: Record; + private _bindGroupLayoutCache: Record; constructor(device: WebGPUDevice) { this._device = device; this._bindGroupLayoutCache = {}; } - fetchBindGroupLayout(desc: BindGroupLayout): GPUBindGroupLayout { + fetchBindGroupLayout(desc: BindGroupLayout): [GPUBindGroupLayoutDescriptor, GPUBindGroupLayout] { const hash = desc ? this.getLayoutHash(desc) : ''; let bgl = this._bindGroupLayoutCache[hash]; if (!bgl) { @@ -44,7 +44,7 @@ export class BindGroupCache { } return hash; } - private createBindGroupLayout(desc: BindGroupLayout): GPUBindGroupLayout { + private createBindGroupLayout(desc: BindGroupLayout): [GPUBindGroupLayoutDescriptor, GPUBindGroupLayout] { const layoutDescriptor: GPUBindGroupLayoutDescriptor = { entries: desc?.entries.map((entry) => { @@ -101,6 +101,6 @@ export class BindGroupCache { if (desc?.label) { layoutDescriptor.label = desc.label; } - return this._device.device.createBindGroupLayout(layoutDescriptor); + return [layoutDescriptor, this._device.device.createBindGroupLayout(layoutDescriptor)]; } } diff --git a/libs/backend-webgpu/src/bindgroup_webgpu.ts b/libs/backend-webgpu/src/bindgroup_webgpu.ts index b29648f3..0d580c34 100644 --- a/libs/backend-webgpu/src/bindgroup_webgpu.ts +++ b/libs/backend-webgpu/src/bindgroup_webgpu.ts @@ -22,22 +22,42 @@ import type { WebGPUBuffer } from './buffer_webgpu'; export class WebGPUBindGroup extends WebGPUObject implements BindGroup { private _layout: BindGroupLayout; + private _layoutDesc: GPUBindGroupLayoutDescriptor; + private _entries: GPUBindGroupEntry[]; private _bindGroup: GPUBindGroup; private _buffers: WebGPUBuffer[]; private _textures: WebGPUBaseTexture[]; + private _gpuId: number; private _videoTextures: WebGPUTextureVideo[]; + private _dynamicOffsets: number[]; private _resources: { - [name: string]: WebGPUBuffer | WebGPUTextureVideo | [WebGPUBaseTexture, GPUTextureView] | GPUSampler; + [name: string]: + | [WebGPUBuffer, number, number] + | WebGPUTextureVideo + | [WebGPUBaseTexture, GPUTextureView] + | GPUSampler; }; constructor(device: WebGPUDevice, layout: BindGroupLayout) { super(device); this._device = device; this._layout = layout; + this._layoutDesc = null; + this._entries = null; this._bindGroup = null; + this._dynamicOffsets = null; + this._gpuId = 0; this._resources = {}; this._buffers = []; this._textures = []; this._videoTextures = null; + for (const entry of this._layout.entries) { + if (entry.buffer && entry.buffer.hasDynamicOffset) { + if (!this._dynamicOffsets) { + this._dynamicOffsets = []; + } + this._dynamicOffsets[entry.buffer.dynamicOffsetIndex] = 0; + } + } } get bindGroup() { if (!this._bindGroup) { @@ -45,34 +65,62 @@ export class WebGPUBindGroup extends WebGPUObject implements BindGroup } return this._bindGroup; } + get layoutDescriptor(): GPUBindGroupLayoutDescriptor { + if (!this._bindGroup) { + this._bindGroup = this._create(); + } + return this._layoutDesc; + } + get entries(): GPUBindGroupEntry[] { + if (!this._bindGroup) { + this._bindGroup = this._create(); + } + return this._entries; + } + getGPUId(): string { + return `${this._uid}:${this._gpuId}`; + } get bufferList(): WebGPUBuffer[] { return this._buffers; } get textureList(): WebGPUBaseTexture[] { return this._textures; } + invalidate() { + this._bindGroup = null; + this._gpuId++; + } getLayout(): BindGroupLayout { return this._layout; } + getDynamicOffsets(): number[] { + return this._dynamicOffsets; + } getBuffer(name: string): GPUDataBuffer { return this._getBuffer(name, GPUResourceUsageFlags.BF_UNIFORM | GPUResourceUsageFlags.BF_STORAGE, true); } - setBuffer(name: string, buffer: GPUDataBuffer) { + setBuffer(name: string, buffer: GPUDataBuffer, offset?: number, bindOffset?: number, bindSize?: number) { const bindName = this._layout.nameMap?.[name] ?? name; for (const entry of this._layout.entries) { if (entry.name === bindName) { if (!entry.buffer) { console.log(`setBuffer() failed: resource '${name}' is not buffer`); } else { + bindOffset = bindOffset ?? 0; + bindSize = bindSize ?? (buffer ? Math.max(0, buffer.byteLength - bindOffset) : 0); + const info = this._resources[entry.name] as [WebGPUBuffer, number, number]; const bufferUsage = entry.buffer.type === 'uniform' ? GPUResourceUsageFlags.BF_UNIFORM : GPUResourceUsageFlags.BF_STORAGE; - if (buffer && !(buffer.usage & bufferUsage)) { + if (!buffer || !(buffer.usage & bufferUsage)) { console.log(`setBuffer() failed: buffer resource '${name}' must be type '${entry.buffer.type}'`); - } else if (buffer !== this._resources[entry.name]) { - this._resources[entry.name] = buffer as WebGPUBuffer; - this._bindGroup = null; + } else if (buffer !== info?.[0] || bindOffset !== info?.[1] || bindSize !== info?.[2]) { + this._resources[entry.name] = [buffer as WebGPUBuffer, bindOffset, bindSize]; + this.invalidate(); + } + if (entry.buffer.hasDynamicOffset) { + this._dynamicOffsets[entry.buffer.dynamicOffsetIndex] = offset ?? 0; } } return; @@ -154,7 +202,7 @@ export class WebGPUBindGroup extends WebGPUObject implements BindGroup const view = (value as WebGPUBaseTexture).getView(level, face, mipCount); if (!t || t[1] !== view) { this._resources[name] = [value as WebGPUBaseTexture, view]; - this._bindGroup = null; + this.invalidate(); } if (entry.texture?.autoBindSampler) { const samplerEntry = this._findSamplerLayout(entry.texture.autoBindSampler); @@ -168,7 +216,7 @@ export class WebGPUBindGroup extends WebGPUObject implements BindGroup ) as WebGPUTextureSampler; if (s.object !== this._resources[entry.texture.autoBindSampler]) { this._resources[entry.texture.autoBindSampler] = s.object; - this._bindGroup = null; + this.invalidate(); } } if (entry.texture?.autoBindSamplerComparison) { @@ -183,7 +231,7 @@ export class WebGPUBindGroup extends WebGPUObject implements BindGroup ) as WebGPUTextureSampler; if (s.object !== this._resources[entry.texture.autoBindSamplerComparison]) { this._resources[entry.texture.autoBindSamplerComparison] = s.object; - this._bindGroup = null; + this.invalidate(); } } } else { @@ -205,8 +253,14 @@ export class WebGPUBindGroup extends WebGPUObject implements BindGroup ); } if (!t || t !== value) { + if (t) { + (t as WebGPUTextureVideo).removeBindGroupReference(this); + } + if (value) { + (value as WebGPUTextureVideo).addBindGroupReference(this); + } this._resources[name] = value as WebGPUTextureVideo; - this._bindGroup = null; + this.invalidate(); this._videoTextures = []; for (const entry of this._layout.entries) { if (entry.externalTexture) { @@ -229,7 +283,7 @@ export class WebGPUBindGroup extends WebGPUObject implements BindGroup } if (!t || t[0] !== value) { this._resources[name] = [value as WebGPUBaseTexture, view]; - this._bindGroup = null; + this.invalidate(); } } const autoBindSampler = entry.texture?.autoBindSampler || entry.externalTexture?.autoBindSampler; @@ -245,7 +299,7 @@ export class WebGPUBindGroup extends WebGPUObject implements BindGroup ) as WebGPUTextureSampler; if (s.object !== this._resources[autoBindSampler]) { this._resources[autoBindSampler] = s.object; - this._bindGroup = null; + this.invalidate(); } } const autoBindSamplerComparison = entry.texture?.autoBindSamplerComparison; @@ -261,7 +315,7 @@ export class WebGPUBindGroup extends WebGPUObject implements BindGroup ) as WebGPUTextureSampler; if (s.object !== this._resources[autoBindSamplerComparison]) { this._resources[autoBindSamplerComparison] = s.object; - this._bindGroup = null; + this.invalidate(); } } } else { @@ -278,12 +332,12 @@ export class WebGPUBindGroup extends WebGPUObject implements BindGroup console.log(`WebGPUBindGroup.setSampler() failed: no sampler uniform named '${name}'`); } else { this._resources[name] = sampler; - this._bindGroup = null; + this.invalidate(); } } } destroy() { - this._bindGroup = null; + this.invalidate(); this._resources = {}; this._buffers = []; this._textures = []; @@ -291,7 +345,7 @@ export class WebGPUBindGroup extends WebGPUObject implements BindGroup this._object = null; } async restore() { - this._bindGroup = null; + this.invalidate(); this._object = {}; } isBindGroup(): this is BindGroup { @@ -301,7 +355,7 @@ export class WebGPUBindGroup extends WebGPUObject implements BindGroup updateVideoTextures() { this._videoTextures?.forEach((t) => { if (t.updateVideoFrame()) { - this._bindGroup = null; + this.invalidate(); } }); } @@ -325,6 +379,11 @@ export class WebGPUBindGroup extends WebGPUObject implements BindGroup } /** @internal */ private _getBuffer(name: string, usage: number, nocreate = false): GPUDataBuffer { + const info = this._getBufferInfo(name, usage, nocreate); + return info?.[0] ?? null; + } + /** @internal */ + private _getBufferInfo(name: string, usage: number, nocreate = false): [WebGPUBuffer, number, number] { const bindName = this._layout.nameMap?.[name] ?? name; for (const entry of this._layout.entries) { if (entry.buffer && entry.name === bindName) { @@ -335,20 +394,21 @@ export class WebGPUBindGroup extends WebGPUObject implements BindGroup if (!(usage & bufferUsage)) { return null; } - let buffer = this._resources[entry.name]; - if (!buffer && !nocreate) { + let buffer = this._resources[entry.name] as [WebGPUBuffer, number, number]; + if ((!buffer || !buffer[0]) && !nocreate) { const options: BufferCreationOptions = { usage: bufferUsage === GPUResourceUsageFlags.BF_UNIFORM ? 'uniform' : null, storage: bufferUsage === GPUResourceUsageFlags.BF_STORAGE, dynamic: true }; - buffer = this._device.createStructuredBuffer( + const gpuBuffer = this._device.createStructuredBuffer( entry.type as PBStructTypeInfo, options ) as WebGPUStructuredBuffer; + buffer = [gpuBuffer, 0, gpuBuffer.byteLength]; this._resources[entry.name] = buffer; } - return buffer as GPUDataBuffer; + return buffer; } } return null; @@ -356,6 +416,8 @@ export class WebGPUBindGroup extends WebGPUObject implements BindGroup /** @internal */ private _create(): GPUBindGroup { let bindGroup = null; + this._layoutDesc = null; + this._entries = null; this._textures = []; this._buffers = []; const entries = [] as GPUBindGroupEntry[]; @@ -363,27 +425,27 @@ export class WebGPUBindGroup extends WebGPUObject implements BindGroup for (const entry of this._layout.entries) { const ge = { binding: entry.binding } as GPUBindGroupEntry; if (entry.buffer) { - const buffer = this._getBuffer( + const buffer = this._getBufferInfo( entry.name, entry.buffer.type === 'uniform' ? GPUResourceUsageFlags.BF_UNIFORM : GPUResourceUsageFlags.BF_STORAGE, true - ) as WebGPUBuffer; + ); if (!buffer) { throw new Error( `Uniform buffer '${entry.name}' not exists, maybe you forgot settings some uniform values` ); } - if (this._buffers.indexOf(buffer) < 0) { - this._buffers.push(buffer); + if (this._buffers.indexOf(buffer[0]) < 0) { + this._buffers.push(buffer[0]); } ge.resource = { - buffer: buffer.object, - offset: 0, - size: buffer.byteLength + buffer: buffer[0].object, + offset: buffer[1], + size: buffer[2] }; - resourceOk = resourceOk && !!buffer.object; + resourceOk = resourceOk && !!buffer[0].object; } else if (entry.texture || entry.storageTexture) { const t = this._resources[entry.name] as [WebGPUBaseTexture, GPUTextureView]; if (!t) { @@ -410,9 +472,9 @@ export class WebGPUBindGroup extends WebGPUObject implements BindGroup if (!resourceOk) { return null; } - const layout = this._device.fetchBindGroupLayout(this._layout); + const [desc, layout] = this._device.fetchBindGroupLayout(this._layout); const descriptor: GPUBindGroupDescriptor = { - layout, + layout: layout, entries }; if (layout.label) { @@ -422,6 +484,8 @@ export class WebGPUBindGroup extends WebGPUObject implements BindGroup if (!bindGroup) { console.log('Create bindgroup failed'); } + this._layoutDesc = desc; + this._entries = entries; return bindGroup; } } diff --git a/libs/backend-webgpu/src/buffer_webgpu.ts b/libs/backend-webgpu/src/buffer_webgpu.ts index 896c8933..0d7ca5d9 100644 --- a/libs/backend-webgpu/src/buffer_webgpu.ts +++ b/libs/backend-webgpu/src/buffer_webgpu.ts @@ -39,15 +39,21 @@ export class WebGPUBuffer extends WebGPUObject implements GPUDataBuff get gpuUsage(): number { return this._gpuUsage; } - getPendingUploads(): UploadBuffer[] { - return this._pendingUploads; - } - clearPendingUploads() { - if (this._pendingUploads.length > 0) { - this._pendingUploads = []; - this.beginSyncChanges(null); - this.endSyncChanges(); + private searchInsertPosition(dstByteOffset: number): number { + let left = 0; + let right = this._pendingUploads.length - 1; + let insertIndex = this._pendingUploads.length; + while (left <= right) { + const mid = Math.floor((left + right) / 2); + const upload = this._pendingUploads[mid]; + if (upload.uploadOffset < dstByteOffset) { + left = mid + 1; + } else { + insertIndex = mid; + right = mid - 1; + } } + return insertIndex; } bufferSubData(dstByteOffset: number, data: TypedArray, srcOffset?: number, srcLength?: number): void { srcOffset = Number(srcOffset) || 0; @@ -59,56 +65,50 @@ export class WebGPUBuffer extends WebGPUObject implements GPUDataBuff if (dstByteOffset + srcLength * data.BYTES_PER_ELEMENT > this.byteLength) { throw new Error('bufferSubData() failed: dest buffer is too small'); } - let uploadSize = srcLength * data.BYTES_PER_ELEMENT; + const uploadSize = srcLength * data.BYTES_PER_ELEMENT; if ((dstByteOffset & 3) !== 0 || (uploadSize & 3) !== 0) { throw new Error( 'bufferSubData() failed: destination byte offset or upload size must be 4 bytes aligned' ); } const uploadOffset = data.byteOffset + srcOffset * data.BYTES_PER_ELEMENT; - const writeOffset = dstByteOffset; - const writeSize = uploadSize; + const insertIndex = this.searchInsertPosition(dstByteOffset); + if (insertIndex < this._pendingUploads.length) { + const upload = this._pendingUploads[insertIndex]; + if ( + upload.uploadOffset < dstByteOffset + uploadSize && + upload.uploadOffset + upload.uploadSize > dstByteOffset + ) { + // Flush if overlapped + this._device.bufferUpload(this); + } + } + let commit = false; if (this._pendingUploads.length === 0) { - this.pushUpload(this._pendingUploads, data.buffer, uploadOffset, dstByteOffset, uploadSize); + this.pushUpload(dstByteOffset, uploadSize, 0); + commit = true; } else { - let newPendings: UploadBuffer[] = []; - let added = false; - for (let i = 0; i < this._pendingUploads.length; i++) { - const upload = this._pendingUploads[i]; - if (upload.uploadOffset + upload.uploadSize < dstByteOffset) { - // current upload in front of new upload - newPendings.push(upload); - } else if (upload.uploadOffset > dstByteOffset + uploadSize) { - // current upload behind of new upload - if (!added) { - added = true; - this.pushUpload(newPendings, null, 0, dstByteOffset, uploadSize); - } - newPendings.push(upload); + let start = dstByteOffset; + let end = dstByteOffset + uploadSize; + while (insertIndex < this._pendingUploads.length) { + const upload = this._pendingUploads[insertIndex]; + const uploadStart = upload.uploadOffset; + const uploadEnd = uploadStart + upload.uploadSize; + if (uploadStart < end && uploadEnd > start) { + start = Math.min(start, uploadStart); + end = Math.max(end, uploadEnd); + this._pendingUploads.splice(insertIndex, 1); } else { - const start = Math.min(dstByteOffset, upload.uploadOffset); - const end = Math.max(dstByteOffset + uploadSize, upload.uploadOffset + upload.uploadSize); - if ( - end - start < uploadSize + upload.uploadSize && - this._device.currentPass?.isBufferUploading(this) - ) { - // data overlaps and previous data is in use, refresh data by restarting current render pass or compute pass - this._device.currentPass.end(); - // now, the pending uploads should be cleared - newPendings = []; - break; - } - dstByteOffset = start; - uploadSize = end - start; + break; } } - if (!added) { - this.pushUpload(newPendings, null, 0, dstByteOffset, uploadSize); - } - this._pendingUploads = newPendings; - new Uint8Array(this._pendingUploads[0].mappedBuffer.mappedRange, writeOffset, writeSize).set( - new Uint8Array(data.buffer, uploadOffset, writeSize) - ); + this.pushUpload(start, end - start, insertIndex); + } + new Uint8Array(this._pendingUploads[0].mappedBuffer.mappedRange, dstByteOffset, uploadSize).set( + new Uint8Array(data.buffer, uploadOffset, uploadSize) + ); + if (commit) { + this._device.bufferUpload(this); } } async getBufferSubData( @@ -195,11 +195,11 @@ export class WebGPUBuffer extends WebGPUObject implements GPUDataBuff label += '[index]'; } if (this._usage & GPUResourceUsageFlags.BF_UNIFORM) { - this._gpuUsage |= GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST; + this._gpuUsage |= GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC; label += '[uniform]'; } if (this._usage & GPUResourceUsageFlags.BF_STORAGE) { - this._gpuUsage |= GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST; + this._gpuUsage |= GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC; label += '[storage]'; } if (this._usage & GPUResourceUsageFlags.BF_READ) { @@ -235,28 +235,12 @@ export class WebGPUBuffer extends WebGPUObject implements GPUDataBuff } private sync() { if (this._pendingUploads) { - if (this._device.isBufferUploading(this)) { - this._device.currentPass.end(); - } else { - this.beginSyncChanges(null); - this.endSyncChanges(); - } + this._device.flushUploads(); } } - private pushUpload( - pending: UploadBuffer[], - data: ArrayBuffer, - srcByteOffset: number, - dstByteOffset: number, - byteSize: number - ) { + private pushUpload(dstByteOffset: number, byteSize: number, insertIndex: number) { const bufferMapped = this._ringBuffer.fetchBufferMapped(byteSize, true); - if (data) { - new Uint8Array(bufferMapped.mappedRange, dstByteOffset, byteSize).set( - new Uint8Array(data, srcByteOffset, byteSize) - ); - } - pending.push({ + this._pendingUploads.splice(insertIndex, 0, { mappedBuffer: { buffer: bufferMapped.buffer, size: bufferMapped.size, diff --git a/libs/backend-webgpu/src/capabilities_webgpu.ts b/libs/backend-webgpu/src/capabilities_webgpu.ts index 1c018944..a2578cbc 100644 --- a/libs/backend-webgpu/src/capabilities_webgpu.ts +++ b/libs/backend-webgpu/src/capabilities_webgpu.ts @@ -71,6 +71,8 @@ export class WebGPUShaderCaps implements ShaderCaps { supportHighPrecisionInt: boolean; maxUniformBufferSize: number; uniformBufferOffsetAlignment: number; + maxStorageBufferSize: number; + storageBufferOffsetAlignment: number; constructor(device: WebGPUDevice) { this.supportFragmentDepth = true; this.supportStandardDerivatives = true; @@ -78,6 +80,8 @@ export class WebGPUShaderCaps implements ShaderCaps { this.supportHighPrecisionFloat = true; this.maxUniformBufferSize = device.device.limits.maxUniformBufferBindingSize || 65536; this.uniformBufferOffsetAlignment = device.device.limits.minUniformBufferOffsetAlignment || 256; + this.maxStorageBufferSize = device.device.limits.maxStorageBufferBindingSize || 128 * 1024 * 1024; + this.storageBufferOffsetAlignment = device.device.limits.minStorageBufferOffsetAlignment || 256; } } export class WebGPUTextureCaps implements TextureCaps { diff --git a/libs/backend-webgpu/src/commandqueue.ts b/libs/backend-webgpu/src/commandqueue.ts index f2e5e684..08d31bc4 100644 --- a/libs/backend-webgpu/src/commandqueue.ts +++ b/libs/backend-webgpu/src/commandqueue.ts @@ -11,27 +11,65 @@ import type { WebGPUFrameBuffer } from './framebuffer_webgpu'; import type { FrameBufferInfo } from './pipeline_cache'; import type { WebGPUBuffer } from './buffer_webgpu'; import type { WebGPUBaseTexture } from './basetexture_webgpu'; +import { WebGPUMipmapGenerator } from './utils_webgpu'; export class CommandQueueImmediate { protected _renderPass: WebGPURenderPass; protected _computePass: WebGPUComputePass; + private _bufferUploads: Map; + private _textureUploads: Map; + private _device: WebGPUDevice; + private _drawcallCounter: number; constructor(device: WebGPUDevice) { + this._device = device; + this._bufferUploads = new Map(); + this._textureUploads = new Map(); this._renderPass = new WebGPURenderPass(device); this._computePass = new WebGPUComputePass(device); + this._drawcallCounter = 0; + } + isBufferUploading(buffer: WebGPUBuffer): boolean { + return !!this._bufferUploads.has(buffer); + } + isTextureUploading(tex: WebGPUBaseTexture): boolean { + return !!this._textureUploads.has(tex); + } + flushUploads() { + if (this._bufferUploads.size > 0 || this._textureUploads.size > 0) { + this._drawcallCounter = 0; + const bufferUploads = this._bufferUploads; + this._bufferUploads = new Map(); + const textureUploads = this._textureUploads; + this._textureUploads = new Map(); + const uploadCommandEncoder = this._device.device.createCommandEncoder(); + bufferUploads.forEach((_, buffer) => buffer.beginSyncChanges(uploadCommandEncoder)); + textureUploads.forEach((_, tex) => { + tex.beginSyncChanges(uploadCommandEncoder); + if (tex.isMipmapDirty()) { + WebGPUMipmapGenerator.generateMipmap(this._device, tex, uploadCommandEncoder); + } + }); + this._device.device.queue.submit([uploadCommandEncoder.finish()]); + bufferUploads.forEach((_, buffer) => buffer.endSyncChanges()); + textureUploads.forEach((_, tex) => tex.endSyncChanges()); + this._renderPass.end(); + this._computePass.end(); + } } get currentPass(): WebGPURenderPass | WebGPUComputePass { return this._renderPass.active ? this._renderPass : this._computePass.active ? this._computePass : null; } beginFrame(): void {} endFrame(): void { - this._renderPass.end(); - this._computePass.end(); + this.flush(); } flush() { + this.flushUploads(); this._renderPass.end(); this._computePass.end(); } setFramebuffer(fb: WebGPUFrameBuffer): void { + this.flushUploads(); this._renderPass.setFramebuffer(fb); } getFramebuffer(): WebGPUFrameBuffer { @@ -40,6 +78,40 @@ export class CommandQueueImmediate { getFramebufferInfo(): FrameBufferInfo { return this._renderPass.getFrameBufferInfo(); } + executeRenderBundle(renderBundle: GPURenderBundle) { + this._computePass.end(); + this._renderPass.executeRenderBundle(renderBundle); + } + bufferUpload(buffer: WebGPUBuffer) { + if (this._bufferUploads.has(buffer)) { + if (this._drawcallCounter > this._bufferUploads.get(buffer)) { + this.flushUploads(); + } + } else { + this._bufferUploads.set(buffer, this._drawcallCounter); + } + } + textureUpload(tex: WebGPUBaseTexture) { + if (this._textureUploads.has(tex)) { + if (this._drawcallCounter > this._textureUploads.get(tex)) { + this.flushUploads(); + } + } else { + this._textureUploads.set(tex, this._drawcallCounter); + } + } + copyBuffer( + srcBuffer: WebGPUBuffer, + dstBuffer: WebGPUBuffer, + srcOffset: number, + dstOffset: number, + bytes: number + ) { + this.flushUploads(); + const copyCommandEncoder = this._device.device.createCommandEncoder(); + copyCommandEncoder.copyBufferToBuffer(srcBuffer.object, srcOffset, dstBuffer.object, dstOffset, bytes); + this._device.device.queue.submit([copyCommandEncoder.finish()]); + } compute( program: WebGPUProgram, bindGroups: WebGPUBindGroup[], @@ -48,6 +120,7 @@ export class CommandQueueImmediate { workgroupCountY: number, workgroupCountZ: number ) { + this._drawcallCounter++; this._renderPass.end(); this._computePass.compute( program, @@ -69,6 +142,7 @@ export class CommandQueueImmediate { count: number, numInstances: number ): void { + this._drawcallCounter++; this._computePass.end(); this._renderPass.draw( program, @@ -82,6 +156,33 @@ export class CommandQueueImmediate { numInstances ); } + capture( + renderBundleEncoder: GPURenderBundleEncoder, + program: WebGPUProgram, + vertexData: WebGPUVertexLayout, + stateSet: WebGPURenderStateSet, + bindGroups: WebGPUBindGroup[], + bindGroupOffsets: Iterable[], + primitiveType: PrimitiveType, + first: number, + count: number, + numInstances: number + ): void { + this._drawcallCounter++; + this._computePass.end(); + this._renderPass.capture( + renderBundleEncoder, + program, + vertexData, + stateSet, + bindGroups, + bindGroupOffsets, + primitiveType, + first, + count, + numInstances + ); + } setViewport(vp?: number[] | DeviceViewport) { this._renderPass.setViewport(vp); } @@ -97,10 +198,4 @@ export class CommandQueueImmediate { clear(color: Vector4, depth: number, stencil: number): void { this._renderPass.clear(color, depth, stencil); } - isBufferUploading(buffer: WebGPUBuffer): boolean { - return this._renderPass.isBufferUploading(buffer) || this._computePass.isBufferUploading(buffer); - } - isTextureUploading(tex: WebGPUBaseTexture): boolean { - return this._renderPass.isTextureUploading(tex) || this._computePass.isTextureUploading(tex); - } } diff --git a/libs/backend-webgpu/src/computepass_webgpu.ts b/libs/backend-webgpu/src/computepass_webgpu.ts index d7ae395c..dbe82985 100644 --- a/libs/backend-webgpu/src/computepass_webgpu.ts +++ b/libs/backend-webgpu/src/computepass_webgpu.ts @@ -1,39 +1,22 @@ import type { WebGPUProgram } from './gpuprogram_webgpu'; import type { WebGPUBindGroup } from './bindgroup_webgpu'; -import { WebGPUMipmapGenerator } from './utils_webgpu'; -import type { WebGPUBaseTexture } from './basetexture_webgpu'; -import type { WebGPUBuffer } from './buffer_webgpu'; import type { WebGPUDevice } from './device'; import type { WebGPUFrameBuffer } from './framebuffer_webgpu'; -const VALIDATION_NEED_NEW_PASS = 1 << 0; -const VALIDATION_NEED_GENERATE_MIPMAP = 1 << 1; -const VALIDATION_FAILED = 1 << 2; +const VALIDATION_FAILED = 1 << 0; export class WebGPUComputePass { private _device: WebGPUDevice; - private _bufferUploads: Set; - private _textureUploads: Set; - private _uploadCommandEncoder: GPUCommandEncoder; private _computeCommandEncoder: GPUCommandEncoder; private _computePassEncoder: GPUComputePassEncoder; constructor(device: WebGPUDevice, frameBuffer?: WebGPUFrameBuffer) { this._device = device; - this._bufferUploads = new Set(); - this._textureUploads = new Set(); - this._uploadCommandEncoder = this._device.device.createCommandEncoder(); this._computeCommandEncoder = this._device.device.createCommandEncoder(); this._computePassEncoder = null; } get active(): boolean { return !!this._computePassEncoder; } - isBufferUploading(buffer: WebGPUBuffer): boolean { - return !!this._bufferUploads.has(buffer); - } - isTextureUploading(tex: WebGPUBaseTexture): boolean { - return !!this._textureUploads.has(tex); - } compute( program: WebGPUProgram, bindGroups: WebGPUBindGroup[], @@ -46,14 +29,6 @@ export class WebGPUComputePass { if (validation & VALIDATION_FAILED) { return; } - if (validation & VALIDATION_NEED_NEW_PASS || validation & VALIDATION_NEED_GENERATE_MIPMAP) { - if (this._computePassEncoder) { - this.end(); - } - } - if (validation & VALIDATION_NEED_GENERATE_MIPMAP) { - WebGPUMipmapGenerator.generateMipmapsForBindGroups(this._device, bindGroups); - } if (!this._computePassEncoder) { this.begin(); } @@ -77,7 +52,12 @@ export class WebGPUComputePass { if (!bindGroup) { return false; } - computePassEncoder.setBindGroup(i, bindGroup, bindGroupOffsets?.[i] || undefined); + const bindGroupOffset = bindGroupOffsets?.[i]; + if (bindGroupOffset) { + computePassEncoder.setBindGroup(i, bindGroup, bindGroupOffset); + } else { + computePassEncoder.setBindGroup(i, bindGroup); + } } else { computePassEncoder.setBindGroup(i, this._device.emptyBindGroup); } @@ -90,7 +70,6 @@ export class WebGPUComputePass { console.error('WebGPUComputePass.begin() failed: WebGPUComputePass.begin() has already been called'); return; } - this._uploadCommandEncoder = this._device.device.createCommandEncoder(); this._computeCommandEncoder = this._device.device.createCommandEncoder(); this._computePassEncoder = this._computeCommandEncoder.beginComputePass(); } @@ -98,17 +77,7 @@ export class WebGPUComputePass { if (this.active) { this._computePassEncoder.end(); this._computePassEncoder = null; - this._bufferUploads.forEach((buffer) => buffer.beginSyncChanges(this._uploadCommandEncoder)); - this._textureUploads.forEach((tex) => tex.beginSyncChanges(this._uploadCommandEncoder)); - this._device.device.queue.submit([ - this._uploadCommandEncoder.finish(), - this._computeCommandEncoder.finish() - ]); - this._bufferUploads.forEach((buffer) => buffer.endSyncChanges()); - this._textureUploads.forEach((tex) => tex.endSyncChanges()); - this._bufferUploads.clear(); - this._textureUploads.clear(); - this._uploadCommandEncoder = null; + this._device.device.queue.submit([this._computeCommandEncoder.finish()]); this._computeCommandEncoder = null; } } @@ -123,20 +92,11 @@ export class WebGPUComputePass { if (ubo.disposed) { validation |= VALIDATION_FAILED; } - if (ubo.getPendingUploads().length > 0) { - this._bufferUploads.add(ubo); - } } for (const tex of bindGroup.textureList) { if (tex.disposed) { validation |= VALIDATION_FAILED; } - if (tex.isMipmapDirty()) { - validation |= VALIDATION_NEED_GENERATE_MIPMAP; - } - if (tex.getPendingUploads().length > 0) { - this._textureUploads.add(tex); - } } } } else { diff --git a/libs/backend-webgpu/src/device.ts b/libs/backend-webgpu/src/device.ts index f3edf7c5..ca6f016d 100644 --- a/libs/backend-webgpu/src/device.ts +++ b/libs/backend-webgpu/src/device.ts @@ -31,7 +31,8 @@ import type { TextureFormat, DeviceBackend, DeviceViewport, - BaseTexture + BaseTexture, + RenderBundle } from '@zephyr3d/device'; import { getTextureFormatBlockSize, DeviceResizeEvent, BaseDevice } from '@zephyr3d/device'; import type { WebGPUTextureSampler } from './sampler_webgpu'; @@ -93,6 +94,7 @@ export class WebGPUDevice extends BaseDevice { private _defaultRenderPassDesc: GPURenderPassDescriptor; private _sampleCount: number; private _emptyBindGroup: GPUBindGroup; + private _captureRenderBundle: GPURenderBundleEncoder; constructor(backend: DeviceBackend, cvs: HTMLCanvasElement, options?: DeviceOptions) { super(cvs, backend); this._dpr = Math.max(1, Math.floor(options?.dpr ?? window.devicePixelRatio)); @@ -118,6 +120,7 @@ export class WebGPUDevice extends BaseDevice { this._gpuObjectHasher = new WeakMap(); this._gpuObjectHashCounter = 1; this._emptyBindGroup = null; + this._captureRenderBundle = null; this._samplerCache = new SamplerCache(this); } get context() { @@ -132,6 +135,9 @@ export class WebGPUDevice extends BaseDevice { get adapter(): GPUAdapter { return this._adapter; } + get commandQueue(): CommandQueueImmediate { + return this._commandQueue; + } get drawingBufferWidth() { return this.getDrawingBufferWidth(); } @@ -207,7 +213,8 @@ export class WebGPUDevice extends BaseDevice { console.warn('using a fallback adapter'); } this._device = await this._adapter.requestDevice({ - requiredFeatures: [...this._adapter.features] as GPUFeatureName[] + requiredFeatures: [...this._adapter.features] as GPUFeatureName[], + requiredLimits: { ...this._adapter.limits } as any }); console.log('WebGPU device features:'); for (const feature of this._device.features) { @@ -477,6 +484,21 @@ export class WebGPUDevice extends BaseDevice { createBuffer(sizeInBytes: number, options: BufferCreationOptions): GPUDataBuffer { return new WebGPUBuffer(this, this.parseBufferOptions(options), sizeInBytes); } + copyBuffer( + sourceBuffer: GPUDataBuffer, + destBuffer: GPUDataBuffer, + srcOffset: number, + dstOffset: number, + bytes: number + ) { + this._commandQueue.copyBuffer( + sourceBuffer as WebGPUBuffer, + destBuffer as WebGPUBuffer, + srcOffset, + dstOffset, + bytes + ); + } createIndexBuffer(data: Uint16Array | Uint32Array, options?: BufferCreationOptions): IndexBuffer { return new WebGPUIndexBuffer(this, data, this.parseBufferOptions(options, 'index')); } @@ -499,7 +521,7 @@ export class WebGPUDevice extends BaseDevice { } setBindGroup(index: number, bindGroup: BindGroup, dynamicOffsets?: Iterable) { this._currentBindGroups[index] = bindGroup as WebGPUBindGroup; - this._currentBindGroupOffsets[index] = dynamicOffsets || null; + this._currentBindGroupOffsets[index] = dynamicOffsets ?? bindGroup?.getDynamicOffsets() ?? null; } getBindGroup(index: number): [BindGroup, Iterable] { return [this._currentBindGroups[index], this._currentBindGroupOffsets[index]]; @@ -641,7 +663,7 @@ export class WebGPUDevice extends BaseDevice { return this._samplerCache.fetchSampler(options); } /** @internal */ - fetchBindGroupLayout(desc: BindGroupLayout): GPUBindGroupLayout { + fetchBindGroupLayout(desc: BindGroupLayout): [GPUBindGroupLayoutDescriptor, GPUBindGroupLayout] { return this._bindGroupCache.fetchBindGroupLayout(desc); } flush(): void { @@ -714,6 +736,38 @@ export class WebGPUDevice extends BaseDevice { restoreContext(): void { // not implemented } + beginCapture(): void { + if (this._captureRenderBundle) { + throw new Error('Device.beginCapture() failed: device is already capturing draw commands'); + } + const frameBuffer = this.getFramebufferInfo(); + const desc: GPURenderBundleEncoderDescriptor = { + colorFormats: frameBuffer.colorFormats, + depthStencilFormat: frameBuffer.depthFormat, + sampleCount: frameBuffer.sampleCount + }; + this._captureRenderBundle = this._device.createRenderBundleEncoder(desc); + } + endCapture(): RenderBundle { + if (!this._captureRenderBundle) { + throw new Error('Device.endCapture() failed: device is not capturing draw commands'); + } + const renderBundle = this._captureRenderBundle.finish(); + this._captureRenderBundle = null; + return renderBundle; + } + executeRenderBundle(renderBundle: RenderBundle) { + this._commandQueue.executeRenderBundle(renderBundle as GPURenderBundle); + } + bufferUpload(buffer: WebGPUBuffer) { + this._commandQueue.bufferUpload(buffer); + } + textureUpload(tex: WebGPUBaseTexture) { + this._commandQueue.textureUpload(tex); + } + flushUploads() { + this._commandQueue.flushUploads(); + } /** @internal */ protected onBeginFrame(): boolean { if (this._canRender) { @@ -740,6 +794,20 @@ export class WebGPUDevice extends BaseDevice { count, 1 ); + if (this._captureRenderBundle) { + this._commandQueue.capture( + this._captureRenderBundle, + this._currentProgram, + this._currentVertexData, + this._currentStateSet, + this._currentBindGroups, + this._currentBindGroupOffsets, + primitiveType, + first, + count, + 1 + ); + } } /** @internal */ protected _drawInstanced( @@ -759,6 +827,20 @@ export class WebGPUDevice extends BaseDevice { count, numInstances ); + if (this._captureRenderBundle) { + this._commandQueue.capture( + this._captureRenderBundle, + this._currentProgram, + this._currentVertexData, + this._currentStateSet, + this._currentBindGroups, + this._currentBindGroupOffsets, + primitiveType, + first, + count, + numInstances + ); + } } /** @internal */ protected _compute(workgroupCountX, workgroupCountY, workgroupCountZ): void { diff --git a/libs/backend-webgpu/src/framebuffer_webgpu.ts b/libs/backend-webgpu/src/framebuffer_webgpu.ts index b51258e7..b496f277 100644 --- a/libs/backend-webgpu/src/framebuffer_webgpu.ts +++ b/libs/backend-webgpu/src/framebuffer_webgpu.ts @@ -24,6 +24,7 @@ export class WebGPUFrameBuffer extends WebGPUObject implements FrameBuf private _width: number; private _height: number; private _bindFlag: number; + private _hash: string; private _msaaColorTextures: GPUTexture[]; private _msaaDepthTexture: GPUTexture; constructor( @@ -83,6 +84,10 @@ export class WebGPUFrameBuffer extends WebGPUObject implements FrameBuf this._bindFlag = 0; this._msaaColorTextures = null; this._msaaDepthTexture = null; + const colorAttachmentHash = + this._options.colorAttachments?.map((tex) => tex.texture.format).join(':') ?? ''; + const depthAttachmentHash = this._options.depthAttachment?.texture.format ?? ''; + this._hash = `${colorAttachmentHash}-${depthAttachmentHash}-${this._options.sampleCount ?? 1}`; this._init(); } getOptions(): Readonly { @@ -91,6 +96,9 @@ export class WebGPUFrameBuffer extends WebGPUObject implements FrameBuf get bindFlag(): number { return this._bindFlag; } + getHash(): string { + return this._hash; + } getWidth(): number { const attachment = this._options.colorAttachments?.[0] ?? this._options.depthAttachment; return attachment ? Math.max(attachment.texture.width >> attachment.level, 1) : 0; diff --git a/libs/backend-webgpu/src/gpuprogram_webgpu.ts b/libs/backend-webgpu/src/gpuprogram_webgpu.ts index 397b128a..bb702d0d 100644 --- a/libs/backend-webgpu/src/gpuprogram_webgpu.ts +++ b/libs/backend-webgpu/src/gpuprogram_webgpu.ts @@ -141,7 +141,7 @@ export class WebGPUProgram extends WebGPUObject implements GPUProgram { private createPipelineLayout(bindGroupLayouts: BindGroupLayout[]): GPUPipelineLayout { const layouts: GPUBindGroupLayout[] = []; bindGroupLayouts.forEach((val) => { - layouts.push(this._device.fetchBindGroupLayout(val)); + layouts.push(this._device.fetchBindGroupLayout(val)[1]); }); return this._device.device.createPipelineLayout({ bindGroupLayouts: layouts diff --git a/libs/backend-webgpu/src/pipeline_cache.ts b/libs/backend-webgpu/src/pipeline_cache.ts index 2b1423af..6f26e234 100644 --- a/libs/backend-webgpu/src/pipeline_cache.ts +++ b/libs/backend-webgpu/src/pipeline_cache.ts @@ -13,6 +13,7 @@ import type { WebGPUVertexLayout } from './vertexlayout_webgpu'; import type { WebGPUProgram } from './gpuprogram_webgpu'; import type { WebGPUDevice } from './device'; import type { WebGPURenderStateSet } from './renderstates_webgpu'; +import type { WebGPUFrameBuffer } from './framebuffer_webgpu'; const typeU16 = PBPrimitiveTypeInfo.getCachedTypeInfo(PBPrimitiveType.U16); const stencilFormats = ['stencil8', 'depth24plus-stencil8', 'depth24unorm-stencil8', 'depth32float-stencil8']; @@ -25,6 +26,7 @@ const depthFormats = [ 'depth32float-stencil8' ]; export type FrameBufferInfo = { + frameBuffer: WebGPUFrameBuffer; colorFormats: GPUTextureFormat[]; depthFormat: GPUTextureFormat; sampleCount: number; diff --git a/libs/backend-webgpu/src/renderpass_webgpu.ts b/libs/backend-webgpu/src/renderpass_webgpu.ts index 30c45d30..e1c02cdd 100644 --- a/libs/backend-webgpu/src/renderpass_webgpu.ts +++ b/libs/backend-webgpu/src/renderpass_webgpu.ts @@ -10,30 +10,22 @@ import { import type { WebGPUProgram } from './gpuprogram_webgpu'; import type { WebGPURenderStateSet } from './renderstates_webgpu'; import type { WebGPUBindGroup } from './bindgroup_webgpu'; -import { WebGPUMipmapGenerator, WebGPUClearQuad } from './utils_webgpu'; import type { WebGPUBaseTexture } from './basetexture_webgpu'; -import type { WebGPUBuffer } from './buffer_webgpu'; import type { WebGPUDevice } from './device'; import type { WebGPUFrameBuffer } from './framebuffer_webgpu'; import type { WebGPUVertexLayout } from './vertexlayout_webgpu'; import type { WebGPUIndexBuffer } from './indexbuffer_webgpu'; import type { FrameBufferInfo } from './pipeline_cache'; -import type { WebGPUStructuredBuffer } from './structuredbuffer_webgpu'; import { textureFormatInvMap } from './constants_webgpu'; +import { WebGPUClearQuad } from './utils_webgpu'; const VALIDATION_NEED_NEW_PASS = 1 << 0; -const VALIDATION_NEED_GENERATE_MIPMAP = 1 << 1; -const VALIDATION_FAILED = 1 << 2; +const VALIDATION_FAILED = 1 << 1; const typeU16 = PBPrimitiveTypeInfo.getCachedTypeInfo(PBPrimitiveType.U16); export class WebGPURenderPass { private _device: WebGPUDevice; - private _frameBuffer: WebGPUFrameBuffer; - private _bufferUploads: Set; - private _textureUploads: Set; - private _bufferUploadsNext: Set; - private _textureUploadsNext: Set; private _renderCommandEncoder: GPUCommandEncoder; private _renderPassEncoder: GPURenderPassEncoder; private _fbBindFlag: number; @@ -42,37 +34,53 @@ export class WebGPURenderPass { private _frameBufferInfo: FrameBufferInfo; constructor(device: WebGPUDevice) { this._device = device; - this._bufferUploads = new Set(); - this._textureUploads = new Set(); - this._bufferUploadsNext = new Set(); - this._textureUploadsNext = new Set(); this._renderCommandEncoder = this._device.device.createCommandEncoder(); this._renderPassEncoder = null; - this._frameBuffer = null; this._fbBindFlag = null; this._currentViewport = null; this._currentScissor = null; - this._frameBufferInfo = null; + this._frameBufferInfo = this.createFrameBufferInfo(null); } get active(): boolean { return !!this._renderPassEncoder; } - isBufferUploading(buffer: WebGPUBuffer): boolean { - return !!this._bufferUploads.has(buffer); - } - isTextureUploading(tex: WebGPUBaseTexture): boolean { - return !!this._textureUploads.has(tex); + private createFrameBufferInfo(fb: WebGPUFrameBuffer): FrameBufferInfo { + const info: FrameBufferInfo = !fb + ? { + frameBuffer: null, + colorFormats: [this._device.backbufferFormat], + depthFormat: this._device.backbufferDepthFormat, + sampleCount: this._device.sampleCount, + hash: null, + clearHash: 'f' + } + : { + frameBuffer: fb, + colorFormats: fb.getColorAttachments().map((val) => (val as WebGPUBaseTexture).gpuFormat), + depthFormat: (fb.getDepthAttachment() as WebGPUBaseTexture)?.gpuFormat, + sampleCount: fb.getOptions().sampleCount, + hash: null, + clearHash: fb + .getColorAttachments() + .map((val) => { + const fmt = textureFormatInvMap[(val as WebGPUBaseTexture).gpuFormat]; + return isIntegerTextureFormat(fmt) ? (isSignedTextureFormat(fmt) ? 'i' : 'u') : 'f'; + }) + .join('') + }; + info.hash = `${info.colorFormats.join('-')}:${info.depthFormat}:${info.sampleCount}`; + return info; } setFramebuffer(fb: WebGPUFrameBuffer): void { - if (this._frameBuffer !== fb) { + if (this._frameBufferInfo.frameBuffer !== fb) { this.end(); - this._frameBuffer = fb; + this._frameBufferInfo = this.createFrameBufferInfo(fb); this.setViewport(null); this.setScissor(null); } } getFramebuffer(): WebGPUFrameBuffer { - return this._frameBuffer; + return this._frameBufferInfo.frameBuffer; } setViewport(vp?: number[] | DeviceViewport) { if (!vp || (!Array.isArray(vp) && vp.default)) { @@ -164,6 +172,12 @@ export class WebGPURenderPass { getScissor(): DeviceViewport { return Object.assign({}, this._currentScissor); } + executeRenderBundle(renderBundle: GPURenderBundle) { + if (!this.active) { + this.begin(); + } + this._renderPassEncoder.executeBundles([renderBundle]); + } draw( program: WebGPUProgram, vertexData: WebGPUVertexLayout, @@ -175,16 +189,13 @@ export class WebGPURenderPass { count: number, numInstances: number ): void { - const validation = this.validateDraw(program, vertexData, bindGroups); + const validation = this.validateDraw(program, bindGroups); if (validation & VALIDATION_FAILED) { return; } - if (validation & VALIDATION_NEED_NEW_PASS || validation & VALIDATION_NEED_GENERATE_MIPMAP) { + if (validation & VALIDATION_NEED_NEW_PASS) { this.end(); } - if (validation & VALIDATION_NEED_GENERATE_MIPMAP) { - WebGPUMipmapGenerator.generateMipmapsForBindGroups(this._device, bindGroups); - } if (!this.active) { this.begin(); } @@ -226,15 +237,8 @@ export class WebGPURenderPass { return; } this._renderCommandEncoder = this._device.device.createCommandEncoder(); - if (!this._frameBuffer) { - const fmt = textureFormatInvMap[this._device.backbufferFormat]; - this._frameBufferInfo = { - colorFormats: [this._device.backbufferFormat], - depthFormat: this._device.backbufferDepthFormat, - sampleCount: this._device.sampleCount, - hash: `${this._device.backbufferFormat}:${this._device.backbufferDepthFormat}:${this._device.sampleCount}`, - clearHash: isIntegerTextureFormat(fmt) ? (isSignedTextureFormat(fmt) ? 'i' : 'u') : 'f' - }; + const frameBuffer = this._frameBufferInfo.frameBuffer; + if (!frameBuffer) { const mainPassDesc = this._device.defaultRenderPassDesc; const colorAttachmentDesc = this._device.defaultRenderPassDesc.colorAttachments[0]; if (this._frameBufferInfo.sampleCount > 1) { @@ -251,12 +255,11 @@ export class WebGPURenderPass { depthAttachmentDesc.stencilClearValue = stencil; this._renderPassEncoder = this._renderCommandEncoder.beginRenderPass(mainPassDesc); } else { - const colorAttachmentTextures = this._frameBuffer.getColorAttachments() as WebGPUBaseTexture[]; - const depthAttachmentTexture = this._frameBuffer.getDepthAttachment() as WebGPUBaseTexture; + const depthAttachmentTexture = frameBuffer.getDepthAttachment() as WebGPUBaseTexture; let depthTextureView: GPUTextureView; if (depthAttachmentTexture) { depthAttachmentTexture._markAsCurrentFB(true); - const attachment = this._frameBuffer.getOptions().depthAttachment; + const attachment = frameBuffer.getOptions().depthAttachment; const layer = depthAttachmentTexture.isTexture2DArray() || depthAttachmentTexture.isTexture3D() ? attachment.layer @@ -265,27 +268,12 @@ export class WebGPURenderPass { : 0; depthTextureView = depthAttachmentTexture.getView(0, layer ?? 0, 1); } - this._frameBufferInfo = { - colorFormats: colorAttachmentTextures.map((val) => val.gpuFormat), - depthFormat: depthAttachmentTexture?.gpuFormat, - sampleCount: this._frameBuffer.getOptions().sampleCount, - hash: null, - clearHash: colorAttachmentTextures - .map((val) => { - const fmt = textureFormatInvMap[val.gpuFormat]; - return isIntegerTextureFormat(fmt) ? (isSignedTextureFormat(fmt) ? 'i' : 'u') : 'f'; - }) - .join('') - }; - this._frameBufferInfo.hash = `${this._frameBufferInfo.colorFormats.join('-')}:${ - this._frameBufferInfo.depthFormat - }:${this._frameBufferInfo.sampleCount}`; - this._fbBindFlag = this._frameBuffer.bindFlag; + this._fbBindFlag = frameBuffer.bindFlag; const passDesc: GPURenderPassDescriptor = { label: `customRenderPass:${this._frameBufferInfo.hash}`, colorAttachments: - this._frameBuffer.getOptions().colorAttachments?.map((attachment, index) => { + frameBuffer.getOptions().colorAttachments?.map((attachment, index) => { const tex = attachment.texture as WebGPUBaseTexture; if (tex) { tex._markAsCurrentFB(true); @@ -295,7 +283,7 @@ export class WebGPURenderPass { : tex.isTextureCube() ? attachment.face : 0; - if (this._frameBuffer.getOptions().sampleCount === 1) { + if (frameBuffer.getOptions().sampleCount === 1) { return { view: tex.getView(attachment.level ?? 0, layer ?? 0, 1), loadOp: color ? 'clear' : 'load', @@ -303,7 +291,7 @@ export class WebGPURenderPass { storeOp: 'store' } as GPURenderPassColorAttachment; } else { - const msaaTexture = this._frameBuffer.getMSAAColorAttacments()[index]; + const msaaTexture = frameBuffer.getMSAAColorAttacments()[index]; const msaaView = this._device.gpuCreateTextureView(msaaTexture, { dimension: '2d', baseMipLevel: attachment.level ?? 0, @@ -324,7 +312,7 @@ export class WebGPURenderPass { } }) ?? [], depthStencilAttachment: depthAttachmentTexture - ? this._frameBuffer.getOptions().sampleCount === 1 + ? frameBuffer.getOptions().sampleCount === 1 ? { view: depthTextureView, depthLoadOp: typeof depth === 'number' ? 'clear' : 'load', @@ -339,7 +327,7 @@ export class WebGPURenderPass { stencilStoreOp: hasStencilChannel(depthAttachmentTexture.format) ? 'store' : undefined } : { - view: this._frameBuffer.getMSAADepthAttachment().createView(), + view: frameBuffer.getMSAADepthAttachment().createView(), depthLoadOp: typeof depth === 'number' ? 'clear' : 'load', depthClearValue: depth, depthStoreOp: 'store', @@ -359,14 +347,6 @@ export class WebGPURenderPass { this.setScissor(this._currentScissor); } end() { - const commands: GPUCommandBuffer[] = []; - // upload the resources needed for this rendering pass - if (this._bufferUploads.size > 0 || this._textureUploads.size > 0) { - const uploadCommandEncoder = this._device.device.createCommandEncoder(); - this._bufferUploads.forEach((buffer) => buffer.beginSyncChanges(uploadCommandEncoder)); - this._textureUploads.forEach((tex) => tex.beginSyncChanges(uploadCommandEncoder)); - commands.push(uploadCommandEncoder.finish()); - } // finish current render pass command if (this._renderPassEncoder) { this._renderPassEncoder.end(); @@ -374,26 +354,12 @@ export class WebGPURenderPass { } // render commands if (this._renderCommandEncoder) { - commands.push(this._renderCommandEncoder.finish()); + this._device.device.queue.submit([this._renderCommandEncoder.finish()]); this._renderCommandEncoder = null; } - // submit to GPU - if (commands.length > 0) { - this._device.device.queue.submit(commands); - } - // free up resource upload buffers - this._bufferUploads.forEach((buffer) => buffer.endSyncChanges()); - this._textureUploads.forEach((tex) => tex.endSyncChanges()); - this._bufferUploads.clear(); - this._textureUploads.clear(); - - // next pass uploading becomes current pass uploading - [this._bufferUploads, this._bufferUploadsNext] = [this._bufferUploadsNext, this._bufferUploads]; - [this._textureUploads, this._textureUploadsNext] = [this._textureUploadsNext, this._textureUploads]; - // unmark render target flags and generate render target mipmaps if needed - if (this._frameBuffer) { - const options = this._frameBuffer.getOptions(); + if (this._frameBufferInfo.frameBuffer) { + const options = this._frameBufferInfo.frameBuffer.getOptions(); if (options.colorAttachments) { for (const attachment of options.colorAttachments) { (attachment.texture as WebGPUBaseTexture)._markAsCurrentFB(false); @@ -405,6 +371,32 @@ export class WebGPURenderPass { (options.depthAttachment?.texture as WebGPUBaseTexture)?._markAsCurrentFB(false); } } + capture( + renderBundleEncoder: GPURenderBundleEncoder, + program: WebGPUProgram, + vertexData: WebGPUVertexLayout, + stateSet: WebGPURenderStateSet, + bindGroups: WebGPUBindGroup[], + bindGroupOffsets: Iterable[], + primitiveType: PrimitiveType, + first: number, + count: number, + numInstances: number + ): void { + this.drawInternal( + this._renderPassEncoder, + program, + vertexData, + stateSet, + bindGroups, + bindGroupOffsets, + primitiveType, + first, + count, + numInstances, + renderBundleEncoder + ); + } private drawInternal( renderPassEncoder: GPURenderPassEncoder, program: WebGPUProgram, @@ -415,9 +407,18 @@ export class WebGPURenderPass { primitiveType: PrimitiveType, first: number, count: number, - numInstances: number + numInstances: number, + renderBundleEncoder?: GPURenderBundleEncoder ): void { - if (this.setBindGroupsForRender(renderPassEncoder, program, vertexData, bindGroups, bindGroupOffsets)) { + if ( + this.setBindGroupsForRender( + renderPassEncoder, + program, + bindGroups, + bindGroupOffsets, + renderBundleEncoder + ) + ) { const pipeline = this._device.pipelineCache.fetchRenderPipeline( program, vertexData, @@ -427,6 +428,7 @@ export class WebGPURenderPass { ); if (pipeline) { renderPassEncoder.setPipeline(pipeline); + renderBundleEncoder?.setPipeline(pipeline); const stencilState = stateSet?.stencilState; if (stencilState) { renderPassEncoder.setStencilReference(stencilState.ref); @@ -435,6 +437,7 @@ export class WebGPURenderPass { const vertexBuffers = vertexData.getLayouts(program.vertexAttributes)?.buffers; vertexBuffers?.forEach((val, index) => { renderPassEncoder.setVertexBuffer(index, val.buffer.object as GPUBuffer, val.drawOffset); + renderBundleEncoder?.setVertexBuffer(index, val.buffer.object as GPUBuffer, val.drawOffset); }); const indexBuffer = vertexData.getIndexBuffer() as WebGPUIndexBuffer; if (indexBuffer) { @@ -442,24 +445,25 @@ export class WebGPURenderPass { indexBuffer.object, indexBuffer.indexType === typeU16 ? 'uint16' : 'uint32' ); + renderBundleEncoder?.setIndexBuffer( + indexBuffer.object, + indexBuffer.indexType === typeU16 ? 'uint16' : 'uint32' + ); renderPassEncoder.drawIndexed(count, numInstances, first); + renderBundleEncoder?.drawIndexed(count, numInstances, first); } else { renderPassEncoder.draw(count, numInstances, first); + renderBundleEncoder?.draw(count, numInstances, first); } } else { renderPassEncoder.draw(count, numInstances, first); + renderBundleEncoder?.draw(count, numInstances, first); } } } } - private validateDraw( - program: WebGPUProgram, - vertexData: WebGPUVertexLayout, - bindGroups: WebGPUBindGroup[] - ): number { + private validateDraw(program: WebGPUProgram, bindGroups: WebGPUBindGroup[]): number { let validation = 0; - const bufferUploads: WebGPUBuffer[] = []; - const textureUploads: WebGPUBaseTexture[] = []; if (bindGroups) { for (let i = 0; i < program.bindGroupLayouts.length; i++) { const bindGroup = bindGroups[i]; @@ -469,9 +473,6 @@ export class WebGPURenderPass { if (ubo.disposed) { validation |= VALIDATION_FAILED; } - if (ubo.getPendingUploads().length > 0) { - bufferUploads.push(ubo); - } } for (const tex of bindGroup.textureList) { if (tex.disposed) { @@ -481,16 +482,6 @@ export class WebGPURenderPass { console.error('bind resource texture can not be current render target'); validation |= VALIDATION_FAILED; } - if (tex.isMipmapDirty()) { - validation |= VALIDATION_NEED_GENERATE_MIPMAP; - } - if (tex.getPendingUploads().length > 0) { - if (tex.isMipmapDirty()) { - this._textureUploads.add(tex); - } else { - textureUploads.push(tex); - } - } } } } else { @@ -499,54 +490,39 @@ export class WebGPURenderPass { } } } - const vertexBuffers = vertexData?.getLayouts(program.vertexAttributes)?.buffers; - if (vertexBuffers) { - for (const buffer of vertexBuffers) { - if ((buffer.buffer as WebGPUStructuredBuffer).getPendingUploads().length > 0) { - bufferUploads.push(buffer.buffer as WebGPUStructuredBuffer); - } - } - } - const indexBuffer = vertexData?.getIndexBuffer() as unknown as WebGPUBuffer; - if (indexBuffer?.getPendingUploads().length > 0) { - bufferUploads.push(indexBuffer); - } - if (this._frameBuffer && this._frameBuffer.bindFlag !== this._fbBindFlag) { + if ( + this._frameBufferInfo.frameBuffer && + this._frameBufferInfo.frameBuffer.bindFlag !== this._fbBindFlag + ) { validation |= VALIDATION_NEED_NEW_PASS; } - const needNewPass = validation & VALIDATION_NEED_NEW_PASS || validation & VALIDATION_NEED_GENERATE_MIPMAP; - if (bufferUploads.length > 0) { - const bu = needNewPass ? this._bufferUploadsNext : this._bufferUploads; - for (const buffer of bufferUploads) { - bu.add(buffer); - } - } - if (textureUploads.length > 0) { - const tu = needNewPass ? this._textureUploadsNext : this._textureUploads; - for (const tex of textureUploads) { - tu.add(tex); - } - } return validation; } private setBindGroupsForRender( renderPassEncoder: GPURenderPassEncoder, program: WebGPUProgram, - vertexData: WebGPUVertexLayout, bindGroups: WebGPUBindGroup[], - bindGroupOffsets: Iterable[] + bindGroupOffsets: Iterable[], + renderBundleEncoder?: GPURenderBundleEncoder ): boolean { if (bindGroups) { for (let i = 0; i < 4; i++) { if (i < program.bindGroupLayouts.length) { - bindGroups[i].updateVideoTextures(); const bindGroup = bindGroups[i].bindGroup; if (!bindGroup) { return false; } - renderPassEncoder.setBindGroup(i, bindGroup, bindGroupOffsets?.[i] || undefined); + const bindGroupOffset = bindGroups[i].getDynamicOffsets() ?? bindGroupOffsets?.[i]; + if (bindGroupOffset) { + renderPassEncoder.setBindGroup(i, bindGroup, bindGroupOffset); + renderBundleEncoder?.setBindGroup(i, bindGroup, bindGroupOffset); + } else { + renderPassEncoder.setBindGroup(i, bindGroup); + renderBundleEncoder?.setBindGroup(i, bindGroup); + } } else { renderPassEncoder.setBindGroup(i, this._device.emptyBindGroup); + renderBundleEncoder?.setBindGroup(i, this._device.emptyBindGroup); } } } diff --git a/libs/backend-webgpu/src/renderstates_webgpu.ts b/libs/backend-webgpu/src/renderstates_webgpu.ts index b304fc89..44fe2d7e 100644 --- a/libs/backend-webgpu/src/renderstates_webgpu.ts +++ b/libs/backend-webgpu/src/renderstates_webgpu.ts @@ -576,6 +576,15 @@ export class WebGPURenderStateSet implements RenderStateSet { this.depthState = null; this.stencilState = null; } + clone(): RenderStateSet { + const newStateSet = new WebGPURenderStateSet(this._device); + newStateSet.colorState = (this.colorState?.clone() as WebGPUColorState) ?? null; + newStateSet.blendingState = (this.blendingState?.clone() as WebGPUBlendingState) ?? null; + newStateSet.rasterizerState = (this.rasterizerState?.clone() as WebGPURasterizerState) ?? null; + newStateSet.depthState = (this.depthState?.clone() as WebGPUDepthState) ?? null; + newStateSet.stencilState = (this.stencilState?.clone() as WebGPUStencilState) ?? null; + return newStateSet; + } copyFrom(stateSet: RenderStateSet): void { this.colorState = stateSet.colorState as WebGPUColorState; this.blendingState = stateSet.blendingState as WebGPUBlendingState; diff --git a/libs/backend-webgpu/src/texturevideo_webgpu.ts b/libs/backend-webgpu/src/texturevideo_webgpu.ts index 99be8301..b8d46b52 100644 --- a/libs/backend-webgpu/src/texturevideo_webgpu.ts +++ b/libs/backend-webgpu/src/texturevideo_webgpu.ts @@ -2,22 +2,34 @@ import type { TypedArray } from '@zephyr3d/base'; import type { GPUDataBuffer, TextureVideo } from '@zephyr3d/device'; import { WebGPUBaseTexture } from './basetexture_webgpu'; import type { WebGPUDevice } from './device'; +import type { WebGPUBindGroup } from './bindgroup_webgpu'; export class WebGPUTextureVideo extends WebGPUBaseTexture implements TextureVideo { private _source: HTMLVideoElement; + private _refBindGroups: WebGPUBindGroup[]; constructor(device: WebGPUDevice, element: HTMLVideoElement) { super(device, '2d'); this._source = element; this._width = 0; this._height = 0; + this._refBindGroups = []; this.loadFromElement(); } isTextureVideo(): this is TextureVideo { return true; } + addBindGroupReference(bindGroup: WebGPUBindGroup) { + this._refBindGroups.push(bindGroup); + } + removeBindGroupReference(bindGroup: WebGPUBindGroup) { + const index = this._refBindGroups.indexOf(bindGroup); + if (index >= 0) { + this._refBindGroups.splice(index, 1); + } + } get width(): number { return this._width; } @@ -83,6 +95,20 @@ export class WebGPUTextureVideo if (!this._device.isContextLost()) { if (element.readyState > 2) { this._object = this._device.gpuImportExternalTexture(element); + const that = this; + this._device.runNextFrame(function updateVideoFrame() { + if (!that.disposed) { + if (that._source.readyState > 2) { + const videoFrame = new (window as any).VideoFrame(that._source); + videoFrame.close(); + that._object = that._device.gpuImportExternalTexture(that._source); + for (const bindGroup of that._refBindGroups) { + bindGroup.invalidate(); + } + } + that._device.runNextFrame(updateVideoFrame); + } + }); } } return !!this._object; diff --git a/libs/backend-webgpu/src/utils_webgpu.ts b/libs/backend-webgpu/src/utils_webgpu.ts index fa06f139..b21775b9 100644 --- a/libs/backend-webgpu/src/utils_webgpu.ts +++ b/libs/backend-webgpu/src/utils_webgpu.ts @@ -36,17 +36,19 @@ export class WebGPUClearQuad { .useStencilState() .enable(bClearStencil) .setReference(bClearStencil ? clearStencil : 0); - renderPass.draw( - program.program, - null, - this._clearStateSet, - [program.bindGroup], - null, - 'triangle-strip', - 0, - 4, - 1 - ); + renderPass + .getDevice() + .commandQueue.draw( + program.program, + null, + this._clearStateSet, + [program.bindGroup], + null, + 'triangle-strip', + 0, + 4, + 1 + ); } private static getClearProgram( device: WebGPUDevice, @@ -121,69 +123,24 @@ export class WebGPUMipmapGenerator { static _mipmapGenerationProgram: WebGPUProgram = null; static _mipmapGenerationBindGroup: WeakMap = new WeakMap(); static _mipmapGenerationStateSet: WebGPURenderStateSet = null; - static generateMipmap(device: WebGPUDevice, tex: WebGPUBaseTexture) { + static generateMipmap(device: WebGPUDevice, tex: WebGPUBaseTexture, cmdEncoder?: GPUCommandEncoder) { + if (!tex.isRenderable()) { + return; + } if (!this._mipmapGenerationProgram) { this.initMipmapGeneration(device); } - const cmdEncoder = device.device.createCommandEncoder(); + const encoder = cmdEncoder ?? device.device.createCommandEncoder(); const miplevels = tex.mipLevelCount; const numLayers = tex.isTextureCube() ? 6 : tex.isTexture2DArray() ? tex.depth : 1; - let tmpTex = tex.object; - if (!tex.isRenderable()) { - tmpTex = device.gpuCreateTexture({ - size: { - width: tex.width, - height: tex.height, - depthOrArrayLayers: numLayers - }, - format: tex.gpuFormat, - mipLevelCount: tex.mipLevelCount, - sampleCount: 1, - dimension: '2d', - usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC - }); - } tex.setMipmapDirty(false); for (let face = 0; face < numLayers; face++) { for (let level = 1; level < miplevels; level++) { - this.generateMiplevel( - device, - cmdEncoder, - tex, - tmpTex, - tex.gpuFormat, - tmpTex === tex.object ? level : level - 1, - level, - face - ); - } - } - if (tmpTex !== tex.object) { - let width = tex.width; - let height = tex.height; - for (let level = 1; level < miplevels; level++) { - cmdEncoder.copyTextureToTexture( - { - texture: tmpTex, - mipLevel: level - 1 - }, - { - texture: tex.object, - mipLevel: level - }, - { - width: width, - height: height, - depthOrArrayLayers: numLayers - } - ); - width = Math.ceil(width / 2); - height = Math.ceil(height / 2); + this.generateMiplevel(device, encoder, tex, tex.object, tex.gpuFormat, level, level, face); } } - device.device.queue.submit([cmdEncoder.finish()]); - if (tmpTex !== tex.object) { - tmpTex.destroy(); + if (!cmdEncoder) { + device.device.queue.submit([encoder.finish()]); } } static generateMipmapsForBindGroups(device: WebGPUDevice, bindGroups: WebGPUBindGroup[]) { diff --git a/libs/base/src/utils.ts b/libs/base/src/utils.ts index bc65b2d6..c41982ee 100644 --- a/libs/base/src/utils.ts +++ b/libs/base/src/utils.ts @@ -368,21 +368,46 @@ export function parseColor(input: string): ColorRGBA { v.a = Math.min(1, v.a); return v; } +/** + * Extract mixin return type + * @public + */ +export type ExtractMixinReturnType = M extends (target: infer A) => infer R ? R : never; + +/** + * Extract mixin type + * @public + */ +export type ExtractMixinType = M extends [infer First] + ? ExtractMixinReturnType + : M extends [infer First, ...infer Rest] + ? ExtractMixinReturnType & ExtractMixinType<[...Rest]> + : never; + /** * Applies mixins to a constructor function. * - * This function takes a constructor function of a class (derivedCtor) and an array of constructor functions - * from which to inherit or "mix in" properties and methods. It effectively adds the properties and methods - * from the base constructors (baseCtors) to the prototype of the derived constructor, allowing the derived - * class to inherit features from multiple sources. + * @param target - The constructor function of the class that will receive the mixins. + * @param mixins - mixins + * @returns Mixed class * - * @param derivedCtor - The constructor function of the class that will receive the mixins. - * @param baseCtors - An array of constructor functions that will be mixed into the derivedCtor. + * @public */ -export function applyMixins(derivedCtor: any, baseCtors: any[]) { - baseCtors.forEach((baseCtor) => { - Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => { - derivedCtor.prototype[name] = baseCtor.prototype[name]; - }); - }); +export function applyMixins any)[], T>( + target: T, + ...mixins: M +): T & ExtractMixinType { + let r: any = target; + for (const m of mixins) { + r = m(r); + } + return r; } + +/** + * Convert union type to intersection + * @public + */ +export type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void + ? I + : never; diff --git a/libs/device/package.json b/libs/device/package.json index 95b2356b..921580d9 100644 --- a/libs/device/package.json +++ b/libs/device/package.json @@ -63,7 +63,12 @@ "@zephyr3d/base": "workspace:^0.1.3" }, "dependencies": { +<<<<<<< HEAD "@types/node": "^18.13.0", "@webgpu/types": "^0.1.31" +======= + "@webgpu/types": "^0.1.40", + "@types/node": "^18.13.0" +>>>>>>> develop } } diff --git a/libs/device/src/base_types.ts b/libs/device/src/base_types.ts index bb03e10a..84b04984 100644 --- a/libs/device/src/base_types.ts +++ b/libs/device/src/base_types.ts @@ -12,6 +12,7 @@ import type { GPUObject, GPUProgram, IndexBuffer, + RenderBundle, SamplerOptions, StructuredBuffer, Texture2D, @@ -1432,6 +1433,8 @@ export interface FrameInfo { computeCalls: number; /** @internal */ nextFrameCall: (() => void)[]; + /** @internal */ + nextFrameCallNext: (() => void)[]; } /** @@ -1525,6 +1528,10 @@ export interface ShaderCaps { maxUniformBufferSize: number; /** The uniform buffer offset alignment */ uniformBufferOffsetAlignment: number; + /** The maximum number of bytes of storage buffer */ + maxStorageBufferSize: number; + /** The storage buffer offset alignment */ + storageBufferOffsetAlignment: number; } /** @@ -1942,6 +1949,21 @@ export interface AbstractDevice extends IEventTarget { * @param options - The creation options */ createBuffer(sizeInBytes: number, options: BufferCreationOptions): GPUDataBuffer; + /** + * Copies a buffer to another buffer + * @param sourceBuffer - Source buffer + * @param destBuffer - destination buffer + * @param srcOffset - Source offset in bytes + * @param dstOffset - Destination offset in bytes + * @param bytes - How many bytes to be copy + */ + copyBuffer( + sourceBuffer: GPUDataBuffer, + destBuffer: GPUDataBuffer, + srcOffset: number, + dstOffset: number, + bytes: number + ); /** * Creates an index buffer * @param data - Data of the index buffer @@ -2067,6 +2089,20 @@ export interface AbstractDevice extends IEventTarget { * @param buffer - The output buffer */ readPixelsToBuffer(index: number, x: number, y: number, w: number, h: number, buffer: GPUDataBuffer): void; + /** + * Begin capture draw commands + */ + beginCapture(): void; + /** + * Executes render bundle + * @param renderBundle - RenderBundle to be execute + */ + executeRenderBundle(renderBundle: RenderBundle); + /** + * End capture draw commands + * @returns A RenderBundle that holds the captured draw commands + */ + endCapture(): RenderBundle; /** Get the video memory usage in bytes */ videoMemoryUsage: number; /** Get the current frame information */ @@ -2162,12 +2198,6 @@ export interface AbstractDevice extends IEventTarget { * @param f - The function to be scheduled */ runNextFrame(f: () => void): void; - /** - * Canels a pre scheduled function - * - * @param f - The function to be cancled - */ - cancelNextFrameCall(f: () => void): void; /** Exits from current rendering loop */ exitLoop(): void; /** diff --git a/libs/device/src/builder/ast.ts b/libs/device/src/builder/ast.ts index d66cc87b..1a7c0960 100644 --- a/libs/device/src/builder/ast.ts +++ b/libs/device/src/builder/ast.ts @@ -636,7 +636,10 @@ export class ASTPrimitive extends ASTExpression { isWritable(): boolean { const type = this.getType(); return ( - this.writable || type.isAtomicI32() || type.isAtomicU32() || (type.isStructType() && type.isWritable()) + this.writable || + type.isAtomicI32() || + type.isAtomicU32() || + (type.isStructType() && type.haveAtomicMembers()) ); } getAddressSpace(): PBAddressSpace { @@ -2068,14 +2071,11 @@ export class ASTDeclareVar extends ShaderAST { // prefix = `@location(${this.value.value.$location}) var `; throw new Error(`Internal error`); case DeclareType.DECLARE_TYPE_UNIFORM: - if (this.group === undefined) { - debugger; - } prefix = `@group(${this.group}) @binding(${this.binding}) var${isBlock ? '' : ''} `; break; case DeclareType.DECLARE_TYPE_STORAGE: prefix = `@group(${this.group}) @binding(${this.binding}) var `; break; case DeclareType.DECLARE_TYPE_WORKGROUP: diff --git a/libs/device/src/builder/base.ts b/libs/device/src/builder/base.ts index 3a66c5bc..067e3df5 100644 --- a/libs/device/src/builder/base.ts +++ b/libs/device/src/builder/base.ts @@ -58,7 +58,7 @@ export type ShaderTypeFunc = { }; /** @internal */ -export function makeConstructor(typeFunc: ShaderTypeFunc, elementType: PBTypeInfo): ShaderTypeFunc { +export function makeConstructor(typeFunc: ShaderTypeFunc, elementType: PBTypeInfo): ShaderTypeFunc { const wrappedTypeFunc = new Proxy(typeFunc, { get: function (target, prop) { if (typeof prop === 'symbol' || prop in target) { @@ -71,7 +71,13 @@ export function makeConstructor(typeFunc: ShaderTypeFunc, elementType: PBTypeInf } let ctor = entries[prop]; if (!ctor) { - if (elementType.isPrimitiveType() || elementType.isStructType() || elementType.isArrayType()) { + if ( + elementType.isPrimitiveType() || + elementType.isStructType() || + elementType.isArrayType() || + elementType.isAtomicI32() || + elementType.isAtomicU32() + ) { if (prop === 'ptr') { const pointerType = new PBPointerTypeInfo(elementType, PBAddressSpace.FUNCTION); ctor = function pointerCtor(this: ProgramBuilder, ...args: any[]) { @@ -175,6 +181,10 @@ export class PBShaderExp extends Proxiable { $declareType: DeclareType; /** @internal */ $isBuffer: boolean; + /** @internal */ + $readonly: boolean; + /** @internal */ + $bindingSize: number; [name: string]: any; /** @internal */ constructor(str: string, typeInfo: PBTypeInfo) { @@ -197,6 +207,8 @@ export class PBShaderExp extends Proxiable { this.$_group = null; this.$declareType = DeclareType.DECLARE_TYPE_NONE; this.$isBuffer = false; + this.$bindingSize = 0; + this.$readonly = false; if (typeInfo.isTextureType()) { if (typeInfo.isDepthTexture()) { this.$sampleType = 'depth'; @@ -237,7 +249,7 @@ export class PBShaderExp extends Proxiable { * @param group - The bind group index * @returns self */ - uniformBuffer(group: number): PBShaderExp { + uniformBuffer(group: number, bindingSize = 0): PBShaderExp { if ( !this.$typeinfo.isPrimitiveType() && !this.$typeinfo.isArrayType() && @@ -251,6 +263,7 @@ export class PBShaderExp extends Proxiable { this.$declareType = DeclareType.DECLARE_TYPE_UNIFORM; this.$group = group; this.$isBuffer = true; + this.$bindingSize = bindingSize; return this; } /** @@ -277,6 +290,17 @@ export class PBShaderExp extends Proxiable { this.$declareType = DeclareType.DECLARE_TYPE_STORAGE; this.$group = group; this.$isBuffer = false; + this.$readonly = false; + return this; + } + /** + * Point out that the variable is read-only and should be in storage address space + * @param group - The bind group index + * @returns self + */ + storageReadonly(group: number): PBShaderExp { + this.storage(group); + this.$readonly = true; return this; } /** @@ -284,11 +308,13 @@ export class PBShaderExp extends Proxiable { * @param group - The bind group index * @returns self */ - storageBuffer(group: number): PBShaderExp { + storageBuffer(group: number, bindingSize = 0): PBShaderExp { if ( !this.$typeinfo.isPrimitiveType() && !this.$typeinfo.isArrayType() && - !this.$typeinfo.isStructType() + !this.$typeinfo.isStructType() && + !this.$typeinfo.isAtomicI32() && + !this.$typeinfo.isAtomicU32() ) { throw new PBASTError( this.$ast, @@ -298,6 +324,18 @@ export class PBShaderExp extends Proxiable { this.$declareType = DeclareType.DECLARE_TYPE_STORAGE; this.$group = group; this.$isBuffer = true; + this.$bindingSize = bindingSize; + this.$readonly = false; + return this; + } + /** + * Point out that the variable is read-only and should be a storage buffer + * @param group - The bind group index + * @returns self + */ + storageBufferReadonly(group: number, bindingSize = 0): PBShaderExp { + this.storageBuffer(group, bindingSize); + this.$readonly = true; return this; } inout(): PBShaderExp { diff --git a/libs/device/src/builder/builtinfunc.ts b/libs/device/src/builder/builtinfunc.ts index 44b07b36..493f675b 100644 --- a/libs/device/src/builder/builtinfunc.ts +++ b/libs/device/src/builder/builtinfunc.ts @@ -842,10 +842,114 @@ const builtinFunctionsAll = { ...genType('mix', MASK_WEBGL2, 2, [2, 2, 3]) ] }, - floatBitsToInt: { overloads: genType('floatBitsToInt', MASK_WEBGL2, 1, [0]) }, - floatBitsToUint: { overloads: genType('floatBitsToUint', MASK_WEBGL2, 2, [0]) }, - intBitsToFloat: { overloads: genType('intBitsToFloat', MASK_WEBGL2, 0, [1]) }, - uintBitsToFloat: { overloads: genType('uintBitsToFloat', MASK_WEBGL2, 0, [2]) }, + floatBitsToInt: { + overloads: genType('floatBitsToInt', MASK_WEBGL2, 1, [0]), + normalizeFunc(pb: ProgramBuilder, name: string, ...args: ExpValueType[]) { + if (args.length !== 1) { + throw new PBParamLengthError('floatBitsToInt'); + } + if (!(args[0] instanceof PBShaderExp)) { + if (typeof args[0] !== 'number') { + throw new PBParamValueError('floatBitsToInt', 'x'); + } + } else { + const type = args[0].$ast.getType(); + if (type.typeId !== typeinfo.typeF32.typeId) { + throw new PBParamTypeError('floatBitsToInt', 'x'); + } + } + if (pb.getDevice().type === 'webgpu') { + return pb.$callFunctionNoCheck( + 'bitcast', + [args[0] instanceof PBShaderExp ? args[0].$ast : new ASTScalar(args[0], typeinfo.typeF32)], + typeinfo.typeI32 + ); + } else { + return callBuiltin(pb, name, ...args); + } + } + }, + floatBitsToUint: { + overloads: genType('floatBitsToUint', MASK_WEBGL2, 2, [0]), + normalizeFunc(pb: ProgramBuilder, name: string, ...args: ExpValueType[]) { + if (args.length !== 1) { + throw new PBParamLengthError('floatBitsToUint'); + } + if (!(args[0] instanceof PBShaderExp)) { + if (typeof args[0] !== 'number') { + throw new PBParamValueError('floatBitsToUint', 'x'); + } + } else { + const type = args[0].$ast.getType(); + if (type.typeId !== typeinfo.typeF32.typeId) { + throw new PBParamTypeError('floatBitsToUint', 'x'); + } + } + if (pb.getDevice().type === 'webgpu') { + return pb.$callFunctionNoCheck( + 'bitcast', + [args[0] instanceof PBShaderExp ? args[0].$ast : new ASTScalar(args[0], typeinfo.typeF32)], + typeinfo.typeU32 + ); + } else { + return callBuiltin(pb, name, ...args); + } + } + }, + intBitsToFloat: { + overloads: genType('intBitsToFloat', MASK_WEBGL2, 0, [1]), + normalizeFunc(pb: ProgramBuilder, name: string, ...args: ExpValueType[]) { + if (args.length !== 1) { + throw new PBParamLengthError('intBitsToFloat'); + } + if (!(args[0] instanceof PBShaderExp)) { + if (typeof args[0] !== 'number') { + throw new PBParamValueError('intBitsToFloat', 'x'); + } + } else { + const type = args[0].$ast.getType(); + if (type.typeId !== typeinfo.typeI32.typeId) { + throw new PBParamTypeError('intBitsToFloat', 'x'); + } + } + if (pb.getDevice().type === 'webgpu') { + return pb.$callFunctionNoCheck( + 'bitcast', + [args[0] instanceof PBShaderExp ? args[0].$ast : new ASTScalar(args[0], typeinfo.typeI32)], + typeinfo.typeF32 + ); + } else { + return callBuiltin(pb, name, ...args); + } + } + }, + uintBitsToFloat: { + overloads: genType('uintBitsToFloat', MASK_WEBGL2, 0, [2]), + normalizeFunc(pb: ProgramBuilder, name: string, ...args: ExpValueType[]) { + if (args.length !== 1) { + throw new PBParamLengthError('uintBitsToFloat'); + } + if (!(args[0] instanceof PBShaderExp)) { + if (typeof args[0] !== 'number') { + throw new PBParamValueError('uintBitsToFloat', 'x'); + } + } else { + const type = args[0].$ast.getType(); + if (type.typeId !== typeinfo.typeU32.typeId) { + throw new PBParamTypeError('uintBitsToFloat', 'x'); + } + } + if (pb.getDevice().type === 'webgpu') { + return pb.$callFunctionNoCheck( + 'bitcast', + [args[0] instanceof PBShaderExp ? args[0].$ast : new ASTScalar(args[0], typeinfo.typeU32)], + typeinfo.typeF32 + ); + } else { + return callBuiltin(pb, name, ...args); + } + } + }, pack4x8snorm: { overloads: genType('pack4x8snorm', MASK_WEBGPU, typeinfo.typeU32, [typeinfo.typeF32Vec4]) }, unpack4x8snorm: { overloads: genType('unpack4x8snorm', MASK_WEBGPU, typeinfo.typeF32Vec4, [typeinfo.typeU32]) @@ -1380,6 +1484,74 @@ const builtinFunctionsAll = { typeinfo.typeI32, typeinfo.typeI32 ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeF32Vec4, [ + typeinfo.typeTexStorage1D_bgra8unorm, + typeinfo.typeI32 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeF32Vec4, [ + typeinfo.typeTexStorage1D_r32float, + typeinfo.typeI32 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeI32Vec4, [ + typeinfo.typeTexStorage1D_r32sint, + typeinfo.typeI32 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeU32Vec4, [ + typeinfo.typeTexStorage1D_r32uint, + typeinfo.typeI32 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeF32Vec4, [ + typeinfo.typeTexStorage1D_rg32float, + typeinfo.typeI32 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeI32Vec4, [ + typeinfo.typeTexStorage1D_rg32sint, + typeinfo.typeI32 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeU32Vec4, [ + typeinfo.typeTexStorage1D_rg32uint, + typeinfo.typeI32 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeF32Vec4, [ + typeinfo.typeTexStorage1D_rgba16float, + typeinfo.typeI32 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeI32Vec4, [ + typeinfo.typeTexStorage1D_rgba16sint, + typeinfo.typeI32 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeU32Vec4, [ + typeinfo.typeTexStorage1D_rgba16uint, + typeinfo.typeI32 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeF32Vec4, [ + typeinfo.typeTexStorage1D_rgba32float, + typeinfo.typeI32 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeI32Vec4, [ + typeinfo.typeTexStorage1D_rgba32sint, + typeinfo.typeI32 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeU32Vec4, [ + typeinfo.typeTexStorage1D_rgba32uint, + typeinfo.typeI32 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeI32Vec4, [ + typeinfo.typeTexStorage1D_rgba8sint, + typeinfo.typeI32 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeU32Vec4, [ + typeinfo.typeTexStorage1D_rgba8uint, + typeinfo.typeI32 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeF32Vec4, [ + typeinfo.typeTexStorage1D_rgba8snorm, + typeinfo.typeI32 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeF32Vec4, [ + typeinfo.typeTexStorage1D_rgba8unorm, + typeinfo.typeI32 + ]), ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeF32Vec4, [ typeinfo.typeTex2D, typeinfo.typeI32Vec2, @@ -1395,6 +1567,74 @@ const builtinFunctionsAll = { typeinfo.typeI32Vec2, typeinfo.typeI32 ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeF32Vec4, [ + typeinfo.typeTexStorage2D_bgra8unorm, + typeinfo.typeI32Vec2 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeF32Vec4, [ + typeinfo.typeTexStorage2D_r32float, + typeinfo.typeI32Vec2 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeI32Vec4, [ + typeinfo.typeTexStorage2D_r32sint, + typeinfo.typeI32Vec2 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeU32Vec4, [ + typeinfo.typeTexStorage2D_r32uint, + typeinfo.typeI32Vec2 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeF32Vec4, [ + typeinfo.typeTexStorage2D_rg32float, + typeinfo.typeI32Vec2 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeI32Vec4, [ + typeinfo.typeTexStorage2D_rg32sint, + typeinfo.typeI32Vec2 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeU32Vec4, [ + typeinfo.typeTexStorage2D_rg32uint, + typeinfo.typeI32Vec2 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeF32Vec4, [ + typeinfo.typeTexStorage2D_rgba16float, + typeinfo.typeI32Vec2 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeI32Vec4, [ + typeinfo.typeTexStorage2D_rgba16sint, + typeinfo.typeI32Vec2 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeU32Vec4, [ + typeinfo.typeTexStorage2D_rgba16uint, + typeinfo.typeI32Vec2 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeF32Vec4, [ + typeinfo.typeTexStorage2D_rgba32float, + typeinfo.typeI32Vec2 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeI32Vec4, [ + typeinfo.typeTexStorage2D_rgba32sint, + typeinfo.typeI32Vec2 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeU32Vec4, [ + typeinfo.typeTexStorage2D_rgba32uint, + typeinfo.typeI32Vec2 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeI32Vec4, [ + typeinfo.typeTexStorage2D_rgba8sint, + typeinfo.typeI32Vec2 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeU32Vec4, [ + typeinfo.typeTexStorage2D_rgba8uint, + typeinfo.typeI32Vec2 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeF32Vec4, [ + typeinfo.typeTexStorage2D_rgba8snorm, + typeinfo.typeI32Vec2 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeF32Vec4, [ + typeinfo.typeTexStorage2D_rgba8unorm, + typeinfo.typeI32Vec2 + ]), ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeF32Vec4, [ typeinfo.typeTex3D, typeinfo.typeI32Vec3, @@ -1410,6 +1650,74 @@ const builtinFunctionsAll = { typeinfo.typeI32Vec3, typeinfo.typeI32 ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeF32Vec4, [ + typeinfo.typeTexStorage3D_bgra8unorm, + typeinfo.typeI32Vec3 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeF32Vec4, [ + typeinfo.typeTexStorage3D_r32float, + typeinfo.typeI32Vec3 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeI32Vec4, [ + typeinfo.typeTexStorage3D_r32sint, + typeinfo.typeI32Vec3 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeU32Vec4, [ + typeinfo.typeTexStorage3D_r32uint, + typeinfo.typeI32Vec3 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeF32Vec4, [ + typeinfo.typeTexStorage3D_rg32float, + typeinfo.typeI32Vec3 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeI32Vec4, [ + typeinfo.typeTexStorage3D_rg32sint, + typeinfo.typeI32Vec3 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeU32Vec4, [ + typeinfo.typeTexStorage3D_rg32uint, + typeinfo.typeI32Vec3 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeF32Vec4, [ + typeinfo.typeTexStorage3D_rgba16float, + typeinfo.typeI32Vec3 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeI32Vec4, [ + typeinfo.typeTexStorage3D_rgba16sint, + typeinfo.typeI32Vec3 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeU32Vec4, [ + typeinfo.typeTexStorage3D_rgba16uint, + typeinfo.typeI32Vec3 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeF32Vec4, [ + typeinfo.typeTexStorage3D_rgba32float, + typeinfo.typeI32Vec3 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeI32Vec4, [ + typeinfo.typeTexStorage3D_rgba32sint, + typeinfo.typeI32Vec3 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeU32Vec4, [ + typeinfo.typeTexStorage3D_rgba32uint, + typeinfo.typeI32Vec3 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeI32Vec4, [ + typeinfo.typeTexStorage3D_rgba8sint, + typeinfo.typeI32Vec3 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeU32Vec4, [ + typeinfo.typeTexStorage3D_rgba8uint, + typeinfo.typeI32Vec3 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeF32Vec4, [ + typeinfo.typeTexStorage3D_rgba8snorm, + typeinfo.typeI32Vec3 + ]), + ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeF32Vec4, [ + typeinfo.typeTexStorage3D_rgba8unorm, + typeinfo.typeI32Vec3 + ]), ...genType('textureLoad', MASK_WEBGPU, typeinfo.typeF32Vec4, [ typeinfo.typeTexMultisampled2D, typeinfo.typeI32Vec2, @@ -1524,8 +1832,13 @@ const builtinFunctionsAll = { } args[1] = pb.ivec2(args[1], 0); } - } else if (pb.getDevice().type === 'webgpu' && texType.isExternalTexture()) { - args = args.slice(0, 2); + } else if (pb.getDevice().type === 'webgpu') { + if (texType.isExternalTexture()) { + args = args.slice(0, 2); + } + if (texType.isStorageTexture()) { + texType.readable = true; + } } return callBuiltin(pb, name, ...args); } @@ -1740,6 +2053,11 @@ const builtinFunctionsAll = { typeinfo.typeU32Vec2, typeinfo.typeU32Vec4 ]), + ...genType('textureStore', MASK_WEBGPU, typeinfo.typeVoid, [ + typeinfo.typeTexStorage2D_r32uint, + typeinfo.typeI32Vec2, + typeinfo.typeU32Vec4 + ]), ...genType('textureStore', MASK_WEBGPU, typeinfo.typeVoid, [ typeinfo.typeTexStorage2D_r32sint, typeinfo.typeU32Vec2, @@ -1830,7 +2148,19 @@ const builtinFunctionsAll = { typeinfo.typeU32Vec3, typeinfo.typeF32Vec4 ]) - ] + ], + normalizeFunc(pb: ProgramBuilder, name: string, ...args: ExpValueType[]) { + if (pb.getDevice().type === 'webgpu') { + const tex = args[0]; + if (tex instanceof PBShaderExp) { + const texType = tex.$ast.getType(); + if (texType?.isTextureType() && texType.isStorageTexture()) { + texType.writable = true; + } + } + } + return callBuiltin(pb, name, ...args); + } }, // textureArrayStore(tex: PBShaderExp, coords: PBShaderExp, arrayIndex: number|PBShaderExp, value: PBShaderExp); textureArrayStore: { @@ -2112,6 +2442,9 @@ const builtinFunctionsAll = { throw new PBParamTypeError('textureSample', 'texture'); } if (pb.getDevice().type === 'webgpu') { + if (texType.isStorageTexture()) { + throw new PBParamTypeError('textureSample', 'texture'); + } const sampler = pb.getDefaultSampler(tex, false); const coords = args[1]; const ret = callBuiltin(pb, name, tex, sampler, coords); @@ -2918,7 +3251,8 @@ for (const name of [ 'atomicMin', 'atomicAnd', 'atomicOr', - 'atomicXor' + 'atomicXor', + 'atomicExchange' ]) { builtinFunctionsAll[name] = { overloades: [], diff --git a/libs/device/src/builder/constructors.ts b/libs/device/src/builder/constructors.ts index 8c2f985d..1498730d 100644 --- a/libs/device/src/builder/constructors.ts +++ b/libs/device/src/builder/constructors.ts @@ -154,45 +154,51 @@ export function setConstructors(cls: typeof ProgramBuilder) { Object.keys(texStorageCtors).forEach((k) => { cls.prototype[k] = makeStorageTextureCtor(texStorageCtors[k]); }); - cls.prototype['atomic_int'] = function (this: ProgramBuilder, ...args: any[]): PBShaderExp { - if (args.length > 1) { - throw new errors.PBParamLengthError('atomic_int'); - } - if (args.length === 1) { - if (typeof args[0] !== 'string') { - throw new errors.PBParamTypeError('atomic_int', 'name'); + cls.prototype['atomic_int'] = makeConstructor( + function (this: ProgramBuilder, ...args: any[]): PBShaderExp { + if (args.length > 1) { + throw new errors.PBParamLengthError('atomic_int'); } - return new PBShaderExp(args[0], typeinfo.typeAtomicI32); - } else { - const exp = new PBShaderExp('', typeinfo.typeAtomicI32); - exp.$ast = new AST.ASTShaderExpConstructor(exp.$typeinfo, []); - return exp; - } - }; - cls.prototype['atomic_uint'] = function (this: ProgramBuilder, ...args: any[]): PBShaderExp { - if (args.length > 1) { - throw new errors.PBParamLengthError('atomic_uint'); - } - if (args.length === 1 && typeof args[0] === 'string') { - return new PBShaderExp(args[0], typeinfo.typeAtomicU32); - } else if (args.length === 0) { - const exp = new PBShaderExp('', typeinfo.typeAtomicU32); - exp.$ast = new AST.ASTShaderExpConstructor(exp.$typeinfo, []); - return exp; - } - const arg = args[0]; - if ( - (typeof arg === 'number' && Number.isInteger(arg)) || - (arg instanceof PBShaderExp && arg.$ast.getType().typeId === typeinfo.typeU32.typeId) - ) { - const exp = new PBShaderExp('', typeinfo.typeAtomicU32); - exp.$ast = new AST.ASTShaderExpConstructor(exp.$typeinfo, [ - arg instanceof PBShaderExp ? arg.$ast : arg - ]); - return exp; - } - return null; - }; + if (args.length === 1) { + if (typeof args[0] !== 'string') { + throw new errors.PBParamTypeError('atomic_int', 'name'); + } + return new PBShaderExp(args[0], typeinfo.typeAtomicI32); + } else { + const exp = new PBShaderExp('', typeinfo.typeAtomicI32); + exp.$ast = new AST.ASTShaderExpConstructor(exp.$typeinfo, []); + return exp; + } + } as ShaderTypeFunc, + typeinfo.typeAtomicI32 + ); + cls.prototype['atomic_uint'] = makeConstructor( + function (this: ProgramBuilder, ...args: any[]): PBShaderExp { + if (args.length > 1) { + throw new errors.PBParamLengthError('atomic_uint'); + } + if (args.length === 1 && typeof args[0] === 'string') { + return new PBShaderExp(args[0], typeinfo.typeAtomicU32); + } else if (args.length === 0) { + const exp = new PBShaderExp('', typeinfo.typeAtomicU32); + exp.$ast = new AST.ASTShaderExpConstructor(exp.$typeinfo, []); + return exp; + } + const arg = args[0]; + if ( + (typeof arg === 'number' && Number.isInteger(arg)) || + (arg instanceof PBShaderExp && arg.$ast.getType().typeId === typeinfo.typeU32.typeId) + ) { + const exp = new PBShaderExp('', typeinfo.typeAtomicU32); + exp.$ast = new AST.ASTShaderExpConstructor(exp.$typeinfo, [ + arg instanceof PBShaderExp ? arg.$ast : arg + ]); + return exp; + } + return null; + } as ShaderTypeFunc, + typeinfo.typeAtomicU32 + ); } /* diff --git a/libs/device/src/builder/programbuilder.ts b/libs/device/src/builder/programbuilder.ts index a7f9f9d5..556c8b12 100644 --- a/libs/device/src/builder/programbuilder.ts +++ b/libs/device/src/builder/programbuilder.ts @@ -57,7 +57,7 @@ interface UniformInfo { mask: number; block?: { name: string; - dynamicOffset: boolean; + bindingSize: number; exp: PBShaderExp; }; texture?: { @@ -216,6 +216,26 @@ export interface ProgramBuilder { * @returns the 'referenceOf' expression */ referenceOf(ptr: PBShaderExp): PBShaderExp; + /** Atomic int type variable constructors */ + atomic_int: { + (): PBShaderExp; + (rhs: number): PBShaderExp; + (rhs: boolean): PBShaderExp; + (rhs: PBShaderExp): PBShaderExp; + (name: string): PBShaderExp; + ptr: ShaderTypeFunc; + [dim: number]: ShaderTypeFunc; + }; + /** Atomic uint type variable constructors */ + atomic_uint: { + (): PBShaderExp; + (rhs: number): PBShaderExp; + (rhs: boolean): PBShaderExp; + (rhs: PBShaderExp): PBShaderExp; + (name: string): PBShaderExp; + ptr: ShaderTypeFunc; + [dim: number]: ShaderTypeFunc; + }; /** float type variable constructors */ float: { (): PBShaderExp; @@ -790,13 +810,13 @@ export interface ProgramBuilder { arrayLength(x: PBShaderExp): PBShaderExp; /** Same as the select builtin function in WGSL, only valid for WebGPU device */ select(x: number | PBShaderExp, y: number | PBShaderExp, cond: boolean | PBShaderExp): PBShaderExp; - /** Same as floatBitsToInt builtin function in GLSL, only valid for WebGL2 device */ + /** Same as floatBitsToInt builtin function in GLSL, only valid for WebGL2 device and WebGPU device */ floatBitsToInt(x: number | PBShaderExp): PBShaderExp; - /** Same as floatBitsToUint builtin function in GLSL, only valid for WebGL2 device */ + /** Same as floatBitsToUint builtin function in GLSL, only valid for WebGL2 device and WebGPU device */ floatBitsToUint(x: number | PBShaderExp): PBShaderExp; - /** Same as intBitsToFloat builtin function in GLSL, only valid for WebGL2 device */ + /** Same as intBitsToFloat builtin function in GLSL, only valid for WebGL2 device and WebGPU device */ intBitsToFloat(x: number | PBShaderExp): PBShaderExp; - /** Same as uintBitsToFloat builtin function in GLSL, only valid for WebGL2 device */ + /** Same as uintBitsToFloat builtin function in GLSL, only valid for WebGL2 device and WebGPU device */ uintBitsToFloat(x: number | PBShaderExp): PBShaderExp; /** Same as pack4x8snorm builtin function in WGSL, only valid for WebGPU device */ pack4x8snorm(x: PBShaderExp): PBShaderExp; @@ -981,6 +1001,8 @@ export interface ProgramBuilder { atomicOr(ptr: PBShaderExp, value: number | PBShaderExp): PBShaderExp; /** atomicXor, only valid for WebGPU device */ atomicXor(ptr: PBShaderExp, value: number | PBShaderExp): PBShaderExp; + /** atomicExchange, only valid for WebGPU device */ + atomicExchange(ptr: PBShaderExp, value: number | PBShaderExp): PBShaderExp; } /** @@ -1792,21 +1814,23 @@ export class ProgramBuilder { variable.$location = location; variable.$declareType = AST.DeclareType.DECLARE_TYPE_OUT; this._outputs[location] = [name, new AST.ASTDeclareVar(new AST.ASTPrimitive(variable))]; - Object.defineProperty(this._outputScope, name, { - get: function (this: PBOutputScope) { - return variable; - }, - set: function (this: PBOutputScope, v) { - getCurrentProgramBuilder() - .getCurrentScope() - .$ast.statements.push( - new AST.ASTAssignment( - new AST.ASTLValueScalar(variable.$ast), - v instanceof PBShaderExp ? v.$ast : v - ) - ); - } - }); + for (const prop of [name, String(location)]) { + Object.defineProperty(this._outputScope, prop, { + get: function (this: PBOutputScope) { + return variable; + }, + set: function (this: PBOutputScope, v) { + getCurrentProgramBuilder() + .getCurrentScope() + .$ast.statements.push( + new AST.ASTAssignment( + new AST.ASTLValueScalar(variable.$ast), + v instanceof PBShaderExp ? v.$ast : v + ) + ); + } + }); + } } /** @internal */ getDefaultSampler(t: PBShaderExp, comparison: boolean): PBShaderExp { @@ -2132,6 +2156,8 @@ export class ProgramBuilder { const exp = new PBShaderExp(u.block.exp.$str, u.block.exp.$ast.getType()); exp.$declareType = u.block.exp.$declareType; exp.$isBuffer = u.block.exp.$isBuffer; + exp.$bindingSize = u.block.exp.$bindingSize; + exp.$readonly = u.block.exp.$readonly; uniformList[u.group].push({ member: exp, uniform: i }); } } @@ -2163,11 +2189,13 @@ export class ProgramBuilder { false, ...allLists[p].map((val) => val.member) ); + const readonly = i > 0 ? allLists[p].findIndex((val) => !val.member.$readonly) < 0 : true; const exp = t(); if (i === 0) { - exp.uniformBuffer(Number(k)); + exp.uniformBuffer(Number(k), p > 0 ? allLists[p][0].member.$bindingSize : 0); } else { - exp.storageBuffer(Number(k)); + exp.storageBuffer(Number(k), p > 0 ? allLists[p][0].member.$bindingSize : 0); + exp.$readonly = readonly; } globalScope[uname] = exp; const index = this._uniforms.findIndex((val) => val.block?.name === uname); @@ -2235,6 +2263,8 @@ export class ProgramBuilder { const exp = new PBShaderExp(u.block.exp.$str, u.block.exp.$ast.getType()); exp.$declareType = u.block.exp.$declareType; exp.$isBuffer = u.block.exp.$isBuffer; + exp.$bindingSize = u.block.exp.$bindingSize; + exp.$readonly = u.block.exp.$readonly; sharedUniformList[u.group].push({ member: exp, uniform: i }); //sharedUniformList[u.group].uniforms.push(i); } else if (v) { @@ -2244,6 +2274,8 @@ export class ProgramBuilder { const exp = new PBShaderExp(u.block.exp.$str, u.block.exp.$ast.getType()); exp.$declareType = u.block.exp.$declareType; exp.$isBuffer = u.block.exp.$isBuffer; + exp.$bindingSize = u.block.exp.$bindingSize; + exp.$readonly = u.block.exp.$readonly; vertexUniformList[u.group].push({ member: exp, uniform: i }); //vertexUniformList[u.group].uniforms.push(i); } else if (f) { @@ -2253,6 +2285,8 @@ export class ProgramBuilder { const exp = new PBShaderExp(u.block.exp.$str, u.block.exp.$ast.getType()); exp.$declareType = u.block.exp.$declareType; exp.$isBuffer = u.block.exp.$isBuffer; + exp.$bindingSize = u.block.exp.$bindingSize; + exp.$readonly = u.block.exp.$readonly; fragUniformList[u.group].push({ member: exp, uniform: i }); //members.push(exp); //fragUniformList[u.group].uniforms.push(i); } @@ -2295,21 +2329,27 @@ export class ProgramBuilder { false, ...allLists[p].map((val) => val.member) ); + const readonly = j > 0 ? allLists[p].findIndex((val) => !val.member.$readonly) < 0 : true; if (maskList[i] & ShaderType.Vertex) { const exp = t(); + if (j > 0 && !readonly) { + throw new Error(`Storage buffer in vertex shader must be read-only`); + } if (j === 0) { - exp.uniformBuffer(Number(k)); + exp.uniformBuffer(Number(k), p > 0 ? allLists[p][0].member.$bindingSize : 0); } else { - exp.storageBuffer(Number(k)); + exp.storageBuffer(Number(k), p > 0 ? allLists[p][0].member.$bindingSize : 0); + exp.$readonly = readonly; } globalScopeVertex[uname] = exp; } if (maskList[i] & ShaderType.Fragment) { const exp = t(); if (j === 0) { - exp.uniformBuffer(Number(k)); + exp.uniformBuffer(Number(k), p > 0 ? allLists[p][0].member.$bindingSize : 0); } else { - exp.storageBuffer(Number(k)); + exp.storageBuffer(Number(k), p > 0 ? allLists[p][0].member.$bindingSize : 0); + exp.$readonly = readonly; } globalScopeFragmet[uname] = exp; } @@ -2381,6 +2421,7 @@ export class ProgramBuilder { /** @internal */ private createBindGroupLayouts(label: string): BindGroupLayout[] { const layouts: BindGroupLayout[] = []; + const dynamicOffsetIndex = [0, 0, 0, 0]; for (const uniformInfo of this._uniforms) { let layout = layouts[uniformInfo.group]; if (!layout) { @@ -2405,13 +2446,11 @@ export class ProgramBuilder { ); const isStorage = uniformInfo.block.exp.$declareType === AST.DeclareType.DECLARE_TYPE_STORAGE; entry.buffer = { - type: isStorage - ? (uniformInfo.block.exp.$ast as AST.ASTPrimitive).isWritable() - ? 'storage' - : 'read-only-storage' - : 'uniform', - hasDynamicOffset: uniformInfo.block.dynamicOffset, - uniformLayout: entry.type.toBufferLayout(0, (entry.type as PBStructTypeInfo).layout) + type: isStorage ? (uniformInfo.block.exp.$readonly ? 'read-only-storage' : 'storage') : 'uniform', + minBindingSize: uniformInfo.block.bindingSize, + hasDynamicOffset: !!uniformInfo.block.bindingSize, + uniformLayout: entry.type.toBufferLayout(0, (entry.type as PBStructTypeInfo).layout), + dynamicOffsetIndex: !!uniformInfo.block.bindingSize ? dynamicOffsetIndex[uniformInfo.group]++ : -1 }; entry.name = uniformInfo.block.name; } else if (uniformInfo.texture) { @@ -2762,7 +2801,7 @@ export class PBScope extends Proxiable { } else { uniformInfo.block = { name: name, - dynamicOffset: false, + bindingSize: variable.$bindingSize, exp: variable }; // throw new Error(`unsupported uniform type: ${name}`); @@ -3016,7 +3055,8 @@ export class PBLocalScope extends PBScope { value instanceof PBShaderExp && (value.isConstructor() || (value.$typeinfo.isTextureType() && value.$ast instanceof AST.ASTPrimitive && !value.$ast.name)) && - value.$declareType === AST.DeclareType.DECLARE_TYPE_UNIFORM + (value.$declareType === AST.DeclareType.DECLARE_TYPE_UNIFORM || + value.$declareType === AST.DeclareType.DECLARE_TYPE_STORAGE) ) { // We are setting uniform a uniform, should invoke in the global scope this.$g[prop] = value; @@ -3032,6 +3072,8 @@ export class PBLocalScope extends PBScope { if (value instanceof PBShaderExp && !this.$_scope.$parent) { exp.$declareType = value.$declareType; exp.$isBuffer = value.$isBuffer; + exp.$bindingSize = value.$bindingSize; + exp.$readonly = value.$readonly; exp.$group = value.$group; exp.$attrib = value.$attrib; exp.$sampleType = value.$sampleType; diff --git a/libs/device/src/builder/types.ts b/libs/device/src/builder/types.ts index 3a262751..9578d13f 100644 --- a/libs/device/src/builder/types.ts +++ b/libs/device/src/builder/types.ts @@ -489,7 +489,13 @@ export interface StructTypeDetail { */ export interface ArrayTypeDetail { /** Type of array elements */ - elementType: PBPrimitiveTypeInfo | PBArrayTypeInfo | PBStructTypeInfo | PBAnyTypeInfo; + elementType: + | PBPrimitiveTypeInfo + | PBArrayTypeInfo + | PBStructTypeInfo + | PBAnyTypeInfo + | PBAtomicI32TypeInfo + | PBAtomicU32TypeInfo; /** Array dimension */ dimension: number; } @@ -592,6 +598,10 @@ export abstract class PBTypeInfo { get structMembers() { return this.detail.structMembers; } + /** Whether this struct has atomic members */ + haveAtomicMembers(): boolean { + for (const member of this.structMembers) { + if (member.type.isStructType() && member.type.haveAtomicMembers()) { + return true; + } else if (member.type.isArrayType() && member.type.haveAtomicMembers()) { + return true; + } else { + return member.type.isAtomicI32() || member.type.isAtomicU32(); + } + } + } /** * Creates a new struct type by extending this type * @param name - Name of the new struct type @@ -1065,18 +1087,6 @@ export class PBStructTypeInfo extends PBTypeInfo { } } /** @internal */ - isWritable(): boolean { - for (const member of this.structMembers) { - if (member.type.isAtomicI32() || member.type.isAtomicU32()) { - return true; - } - if (member.type.isStructType() && member.type.isWritable()) { - return true; - } - } - return false; - } - /** @internal */ getLayoutAlignment(layout: PBStructLayout): number { if (layout === 'packed') { return 1; @@ -1197,7 +1207,13 @@ export class PBStructTypeInfo extends PBTypeInfo { */ export class PBArrayTypeInfo extends PBTypeInfo { constructor( - elementType: PBPrimitiveTypeInfo | PBArrayTypeInfo | PBStructTypeInfo | PBAnyTypeInfo, + elementType: + | PBPrimitiveTypeInfo + | PBArrayTypeInfo + | PBStructTypeInfo + | PBAnyTypeInfo + | PBAtomicI32TypeInfo + | PBAtomicU32TypeInfo, dimension?: number ) { super(PBTypeClass.ARRAY, { @@ -1206,13 +1222,27 @@ export class PBArrayTypeInfo extends PBTypeInfo { }); } /** Get the element type */ - get elementType(): PBPrimitiveTypeInfo | PBArrayTypeInfo | PBStructTypeInfo | PBAnyTypeInfo { + get elementType(): + | PBPrimitiveTypeInfo + | PBArrayTypeInfo + | PBStructTypeInfo + | PBAnyTypeInfo + | PBAtomicI32TypeInfo + | PBAtomicU32TypeInfo { return this.detail.elementType; } /** Get dimension of the array type */ get dimension(): number { return this.detail.dimension; } + /** Wether array have atomic members */ + haveAtomicMembers(): boolean { + if (this.elementType.isStructType() || this.elementType.isArrayType()) { + return this.elementType.haveAtomicMembers(); + } else { + return this.elementType.isAtomicI32() || this.elementType.isAtomicU32(); + } + } /** {@inheritDoc PBTypeInfo.isArrayType} */ isArrayType(): this is PBArrayTypeInfo { return true; @@ -1324,6 +1354,10 @@ export class PBPointerTypeInfo extends PBTypeInfo { this.id = null; } } + /** {@inheritDoc PBTypeInfo.haveAtomicMembers} */ + haveAtomicMembers(): boolean { + return this.pointerType.haveAtomicMembers(); + } /** {@inheritDoc PBTypeInfo.isPointerType} */ isPointerType(): this is PBPointerTypeInfo { return true; @@ -1367,6 +1401,10 @@ export class PBAtomicI32TypeInfo extends PBTypeInfo { constructor() { super(PBTypeClass.ATOMIC_I32, null); } + /** {@inheritDoc PBTypeInfo.isPointerType} */ + haveAtomicMembers(): boolean { + return true; + } /** @internal */ isAtomicI32(): this is PBAtomicI32TypeInfo { return true; @@ -1418,6 +1456,10 @@ export class PBAtomicU32TypeInfo extends PBTypeInfo { constructor() { super(PBTypeClass.ATOMIC_U32, null); } + /** {@inheritDoc PBTypeInfo.isPointerType} */ + haveAtomicMembers(): boolean { + return true; + } /** @internal */ isAtomicU32(): this is PBAtomicU32TypeInfo { return true; @@ -1537,10 +1579,16 @@ export class PBTextureTypeInfo extends PBTypeInfo { get readable(): boolean { return this.detail.readable; } + set readable(val: boolean) { + this.detail.readable = !!val; + } /** Returns true if this is a writable storage texture type */ get writable(): boolean { return this.detail.writable; } + set writable(val: boolean) { + this.detail.writable = !!val; + } /** @internal */ isStorable(): boolean { return true; @@ -1600,7 +1648,7 @@ export class PBTextureTypeInfo extends PBTypeInfo { if (this.isStorageTexture()) { const storageTexelFormat = storageTexelFormatMap[this.storageTexelFormat]; // storage textures currently only support 'write' access control - const accessMode = 'write'; //this.readable ? (this.writable ? 'read_write' : 'read') : 'write'; + const accessMode = this.writable ? (this.readable ? 'read_write' : 'write') : 'read'; // this.readable ? (this.writable ? 'read_write' : 'read') : 'write'; typename = `${typename}<${storageTexelFormat}, ${accessMode}>`; } return varName ? `${varName}: ${typename}` : typename; diff --git a/libs/device/src/device.ts b/libs/device/src/device.ts index 3eea9c53..9fc9a1e7 100644 --- a/libs/device/src/device.ts +++ b/libs/device/src/device.ts @@ -28,7 +28,8 @@ import type { VertexSemantic, VertexAttribFormat, VertexLayoutOptions, - BaseTexture + BaseTexture, + RenderBundle } from './gpuobject'; import { GPUResourceUsageFlags, @@ -128,7 +129,8 @@ export abstract class BaseDevice { FPS: 0, drawCalls: 0, computeCalls: 0, - nextFrameCall: [] + nextFrameCall: [], + nextFrameCallNext: [] }; this._programBuilder = new ProgramBuilder(this); this._cpuTimer = new CPUTimer(); @@ -213,6 +215,13 @@ export abstract class BaseDevice { abstract createGPUProgram(params: GPUProgramConstructParams): GPUProgram; abstract createBindGroup(layout: BindGroupLayout): BindGroup; abstract createBuffer(sizeInBytes: number, options: BufferCreationOptions): GPUDataBuffer; + abstract copyBuffer( + sourceBuffer: GPUDataBuffer, + destBuffer: GPUDataBuffer, + srcOffset: number, + dstOffset: number, + bytes: number + ); abstract createIndexBuffer(data: Uint16Array | Uint32Array, options?: BufferCreationOptions): IndexBuffer; abstract createStructuredBuffer( structureType: PBStructTypeInfo, @@ -258,6 +267,9 @@ export abstract class BaseDevice { h: number, buffer: GPUDataBuffer ): void; + abstract beginCapture(): void; + abstract endCapture(): RenderBundle; + abstract executeRenderBundle(renderBundle: RenderBundle); abstract looseContext(): void; abstract restoreContext(): void; protected abstract _draw(primitiveType: PrimitiveType, first: number, count: number): void; @@ -439,12 +451,6 @@ export abstract class BaseDevice { this._frameInfo.nextFrameCall.push(f); } } - cancelNextFrameCall(f: () => void) { - const index = this._frameInfo.nextFrameCall.indexOf(f); - if (index >= 0) { - this._frameInfo.nextFrameCall.splice(index, 1); - } - } exitLoop() { if (this._runningLoop) { cancelAnimationFrame(this._runningLoop); @@ -620,10 +626,13 @@ export abstract class BaseDevice { this._frameInfo.elapsedTimeCPU = cpuTime; } } - for (const f of this._frameInfo.nextFrameCall) { + const tmp = this._frameInfo.nextFrameCall; + this._frameInfo.nextFrameCall = this._frameInfo.nextFrameCallNext; + this._frameInfo.nextFrameCallNext = tmp; + for (const f of this._frameInfo.nextFrameCallNext) { f(); } - this._frameInfo.nextFrameCall.length = 0; + this._frameInfo.nextFrameCallNext.length = 0; } private getGPUObjectList(obj: GPUObject): GPUObject[] { let list: GPUObject[] = null; diff --git a/libs/device/src/gpuobject.ts b/libs/device/src/gpuobject.ts index 71630a33..fd5824d0 100644 --- a/libs/device/src/gpuobject.ts +++ b/libs/device/src/gpuobject.ts @@ -1036,6 +1036,8 @@ export interface BufferBindingLayout { uniformLayout: UniformBufferLayout; /** minimum binding size of the buffer */ minBindingSize?: number; + /** dynamic offset index */ + dynamicOffsetIndex: number; } /** @@ -1396,6 +1398,7 @@ export interface FrameBuffer extends GPUObject { getWidth(): number; getHeight(): number; getSampleCount(): number; + getHash(): string; setColorAttachmentCubeFace(index: number, face: CubeFace): void; setColorAttachmentMipLevel(index: number, level: number): void; setColorAttachmentLayer(index: number, layer: number): void; @@ -1434,9 +1437,17 @@ export type StructuredValue = number | TypedArray | VectorBase | { [name: string */ export interface BindGroup extends GPUObject { getLayout(): BindGroupLayout; + getDynamicOffsets(): number[]; + getGPUId(): string; getBuffer(name: string): GPUDataBuffer; getTexture(name: string): BaseTexture; - setBuffer(name: string, buffer: GPUDataBuffer): void; + setBuffer( + name: string, + buffer: GPUDataBuffer, + offset?: number, + bindOffset?: number, + bindSize?: number + ): void; setValue(name: string, value: StructuredValue); setRawData(name: string, byteOffset: number, data: TypedArray, srcPos?: number, srcLength?: number); setTexture(name: string, texture: BaseTexture, sampler?: TextureSampler); @@ -1451,6 +1462,12 @@ export interface BindGroup extends GPUObject { setSampler(name: string, sampler: TextureSampler); } +/** + * Render bundle + * @public + */ +export type RenderBundle = unknown; + /** * Creates the default name for the type of given gpu object * @param obj - The gpu object diff --git a/libs/device/src/render_states.ts b/libs/device/src/render_states.ts index 14872c9a..f22e9799 100644 --- a/libs/device/src/render_states.ts +++ b/libs/device/src/render_states.ts @@ -282,7 +282,9 @@ export interface StencilState { * @public */ export interface RenderStateSet { - /** Creates a new RenderStateSet by copying this one */ + /** Creates a new RenderStateSet object by deep copy from this object */ + clone(): RenderStateSet; + /** Shallow copy existing RenderStateSet object to this */ copyFrom(stateSet: RenderStateSet): void; /** Fragment output related render statements or null if the default values should be used */ readonly colorState: ColorState; diff --git a/libs/imgui/package.json b/libs/imgui/package.json index 72976f1f..80a91c5e 100644 --- a/libs/imgui/package.json +++ b/libs/imgui/package.json @@ -63,7 +63,11 @@ "@zephyr3d/device": "workspace:^0.2.1" }, "dependencies": { +<<<<<<< HEAD "@types/emscripten": "^1.39.6", +======= + "@webgpu/types": "^0.1.40", +>>>>>>> develop "@types/node": "^18.13.0", "@webgpu/types": "^0.1.31" } diff --git a/libs/scene/package.json b/libs/scene/package.json index 23e9b1a5..8d8d95d6 100644 --- a/libs/scene/package.json +++ b/libs/scene/package.json @@ -65,6 +65,6 @@ "@zephyr3d/device": "workspace:^0.2.1" }, "dependencies": { - "@webgpu/types": "^0.1.31" + "@webgpu/types": "^0.1.40" } } diff --git a/libs/scene/src/asset/loaders/gltf/gltf_loader.ts b/libs/scene/src/asset/loaders/gltf/gltf_loader.ts index d73938a9..5306e4d0 100644 --- a/libs/scene/src/asset/loaders/gltf/gltf_loader.ts +++ b/libs/scene/src/asset/loaders/gltf/gltf_loader.ts @@ -16,7 +16,7 @@ import type { import { SharedModel, AssetSkeleton, AssetScene } from '../../model'; import { BoundingBox } from '../../../utility/bounding_volume'; import { Primitive } from '../../../render/primitive'; -import type { Material as M } from '../../../material'; +import type { MeshMaterial as M } from '../../../material/meshmaterial'; import { UnlitMaterial } from '../../../material'; import { ComponentType, GLTFAccessor } from './helpers'; import { AbstractModelLoader } from '../loader'; @@ -92,7 +92,6 @@ export class GLTFLoader extends AbstractModelLoader { return null; } async loadJson(url: string, gltf: GLTFContent): Promise { - console.log(`GLTF extensions used: ${gltf.extensionsUsed || []}`); gltf._accessors = []; gltf._bufferCache = {}; gltf._textureCache = {}; @@ -512,8 +511,7 @@ export class GLTFLoader extends AbstractModelLoader { unlitMaterial.alphaCutoff = assetMaterial.common.alphaCutoff; } if (assetMaterial.common.doubleSided) { - const rasterizerState = unlitMaterial.stateSet.useRasterizerState(); - rasterizerState.setCullMode('none'); + unlitMaterial.cullMode = 'none'; } return unlitMaterial; } else if (assetMaterial.type === 'pbrSpecularGlossiness') { @@ -570,8 +568,7 @@ export class GLTFLoader extends AbstractModelLoader { pbrMaterial.alphaCutoff = assetPBRMaterial.common.alphaCutoff; } if (assetPBRMaterial.common.doubleSided) { - const rasterizerState = pbrMaterial.stateSet.useRasterizerState(); - rasterizerState.setCullMode('none'); + pbrMaterial.cullMode = 'none'; } pbrMaterial.vertexNormal = !!assetMaterial.common.vertexNormal; return pbrMaterial; @@ -679,8 +676,7 @@ export class GLTFLoader extends AbstractModelLoader { pbrMaterial.alphaCutoff = assetPBRMaterial.common.alphaCutoff; } if (assetPBRMaterial.common.doubleSided) { - const rasterizerState = pbrMaterial.stateSet.useRasterizerState(); - rasterizerState.setCullMode('none'); + pbrMaterial.cullMode = 'none'; } pbrMaterial.vertexNormal = !!assetMaterial.common.vertexNormal; return pbrMaterial; diff --git a/libs/scene/src/camera/camera.ts b/libs/scene/src/camera/camera.ts index 4d894e3d..7b23548e 100644 --- a/libs/scene/src/camera/camera.ts +++ b/libs/scene/src/camera/camera.ts @@ -8,6 +8,7 @@ import type { Compositor } from '../posteffect'; import type { Scene } from '../scene/scene'; import type { BaseCameraController } from './base'; import type { RenderLogger } from '../logger/logger'; +import type { OIT } from '../render/oit'; /** * The camera node class @@ -46,6 +47,12 @@ export class Camera extends SceneNode { protected _clearColor: Vector4; /** @internal */ protected _clipMask: number; + /** @internal */ + protected _oit: OIT; + /** @internal */ + protected _depthPrePass: boolean; + /** @internal */ + protected _commandBufferReuse: boolean; /** * Creates a new camera node * @param scene - The scene that the camera belongs to @@ -68,6 +75,9 @@ export class Camera extends SceneNode { this._sampleCount = 1; this._frustum = null; this._frustumV = null; + this._oit = null; + this._depthPrePass = false; + this._commandBufferReuse = true; } /** Clip plane in camera space */ get clipPlane(): Plane { @@ -77,6 +87,20 @@ export class Camera extends SceneNode { this._clipPlane = plane; this._invalidate(false); } + /** Whether to perform a depth pass */ + get depthPrePass(): boolean { + return this._depthPrePass; + } + set depthPrePass(val: boolean) { + this._depthPrePass = !!val; + } + /** Whether to allow command buffer reuse optimization */ + get commandBufferReuse(): boolean { + return this._commandBufferReuse; + } + set commandBufferReuse(val: boolean) { + this._commandBufferReuse = !!val; + } /** * Sample count for MSAA * @@ -93,6 +117,13 @@ export class Camera extends SceneNode { this._sampleCount = val; } } + /** OIT */ + get oit(): OIT { + return this._oit; + } + set oit(val: OIT) { + this._oit = val; + } /** Clip plane mask */ get clipMask(): number { return this._clipMask; diff --git a/libs/scene/src/material/grassmaterial.ts b/libs/scene/src/material/grassmaterial.ts index b2e89279..6e3a010f 100644 --- a/libs/scene/src/material/grassmaterial.ts +++ b/libs/scene/src/material/grassmaterial.ts @@ -1,7 +1,7 @@ import { Vector2, Vector4 } from '@zephyr3d/base'; import { MeshMaterial, applyMaterialMixins } from './meshmaterial'; import { mixinPBRMetallicRoughness } from './mixins/lightmodel/pbrmetallicroughness'; -import type { BindGroup, PBFunctionScope, Texture2D } from '@zephyr3d/device'; +import type { BindGroup, PBFunctionScope, RenderStateSet, Texture2D } from '@zephyr3d/device'; import type { DrawContext } from '../render'; import { RENDER_PASS_TYPE_LIGHT } from '../values'; import { ShaderHelper } from './shader/helper'; @@ -56,6 +56,13 @@ export class GrassMaterial extends applyMaterialMixins( supportLighting(): boolean { return true; } + /** + * {@inheritDoc Material.supportInstancing} + * @override + */ + supportInstancing(): boolean { + return false; + } applyUniformValues(bindGroup: BindGroup, ctx: DrawContext, pass: number): void { super.applyUniformValues(bindGroup, ctx, pass); bindGroup.setTexture('terrainNormalMap', this._terrainNormalMap); @@ -126,4 +133,13 @@ export class GrassMaterial extends applyMaterialMixins( this.outputFragmentColor(scope, scope.$inputs.worldPos, null); } } + apply(ctx: DrawContext): boolean { + this.alphaToCoverage = ctx.device.getFrameBufferSampleCount() > 1; + this.alphaCutoff = this.alphaToCoverage ? 1 : 0.8; + return super.apply(ctx); + } + protected updateRenderStates(pass: number, stateSet: RenderStateSet, ctx: DrawContext): void { + super.updateRenderStates(pass, stateSet, ctx); + stateSet.useRasterizerState().setCullMode('none'); + } } diff --git a/libs/scene/src/material/material.ts b/libs/scene/src/material/material.ts index 0ffba53b..5beecd77 100644 --- a/libs/scene/src/material/material.ts +++ b/libs/scene/src/material/material.ts @@ -1,44 +1,14 @@ -import type { ListIterator } from '@zephyr3d/base'; -import { List } from '@zephyr3d/base'; -import type { - BindGroup, - GPUProgram, - RenderStateSet, - BindGroupLayout, - TextureSampler -} from '@zephyr3d/device'; +import type { AbstractDevice, BindGroup, GPUProgram, RenderStateSet } from '@zephyr3d/device'; import { ProgramBuilder } from '@zephyr3d/device'; import type { Primitive } from '../render/primitive'; -import type { Drawable, DrawContext } from '../render/drawable'; -import { Application } from '../app'; -import { - QUEUE_OPAQUE, - RENDER_PASS_TYPE_DEPTH, - RENDER_PASS_TYPE_LIGHT, - RENDER_PASS_TYPE_SHADOWMAP -} from '../values'; -import { ShaderHelper } from './shader/helper'; +import type { DrawContext } from '../render/drawable'; +import { QUEUE_OPAQUE } from '../values'; -/** - * Garbage collection options for material - * @public - */ -export type MaterialGCOptions = { - /** Whether garbage collection for materials should be disabled */ - disabled?: boolean; - /** Threshold for drawable count */ - drawableCountThreshold?: number; - /** Threshold for material count */ - materialCountThreshold?: number; - /** How long after the bind groups can be garbage collected */ - inactiveTimeDuration?: number; - /** Whether to produce verbose output */ - verbose?: boolean; -}; - -type ProgramInfo = { - programs: GPUProgram[]; - hash: string; +type MaterialState = { + program: GPUProgram; + bindGroup: BindGroup; + renderStateSet: RenderStateSet; + materialTag: number; }; /** @@ -50,74 +20,27 @@ export class Material { /** @internal */ private static _nextId = 0; /** @internal */ - private static _programMap: { - [hash: string]: ProgramInfo; - } = {}; - /** @internal */ - private static _drawableTimestamps: WeakMap = new WeakMap(); - /** @internal */ - private static _drawableIterators: WeakMap> = new WeakMap(); - /** @internal */ - private static _drawableLRU: List = new List(); - /** @internal */ - private static _materialTimestamps: WeakMap = new WeakMap(); - /** @internal */ - private static _materialIterators: WeakMap> = new WeakMap(); - /** @internal */ - private static _materialLRU: List = new List(); - /** @internal */ - private static _gcOptions: MaterialGCOptions = { - disabled: true, - drawableCountThreshold: 500, - materialCountThreshold: 200, - inactiveTimeDuration: 30000 - }; - /** @internal */ - private static _boneMatrixTextureSampler: TextureSampler = null; - /** @internal */ - //private static _instanceBindGroupPool: InstanceBindGroupPool = new InstanceBindGroupPool(); - /** @internal */ - private static _drawableBindGroupMap: WeakMap< - Drawable, - { - [hash: string]: { - bindGroup: BindGroup[]; - xformTag: number[]; - bindGroupTag: number[]; - }; - } - > = new WeakMap(); + private _states: { [hash: string]: MaterialState }; /** @internal */ protected _numPasses: number; /** @internal */ - protected _hash: string[][]; - /** @internal */ - protected _renderStateSet: RenderStateSet; - /** @internal */ - private _bindGroupMap: { - [hash: string]: { - materialBindGroup: BindGroup[]; - materialTag: number[]; - bindGroupTag: number[]; - }; - }; + protected _hash: string[]; /** @internal */ private _optionTag: number; /** @internal */ - private _materialBindGroup: BindGroup; - /** @internal */ private _id: number; + /** @internal */ + private _currentHash: string[]; /** * Creates an instance of material */ constructor() { this._id = ++Material._nextId; + this._states = {}; this._numPasses = 1; - this._hash = [[]]; - this._renderStateSet = null; - this._bindGroupMap = {}; + this._hash = [null]; this._optionTag = 0; - this._materialBindGroup = null; + this._currentHash = []; } /** Unique identifier of the material */ get instanceId(): number { @@ -128,31 +51,21 @@ export class Material { } set numPasses(val: number) { while (this._hash.length < val) { - this._hash.push([]); + this._hash.push(null); } this._numPasses = val; } /** @internal */ - protected getHash(renderPassType: number, pass: number): string { - if (this._hash[pass][renderPassType] === void 0) { - this._hash[pass][renderPassType] = this.createHash(renderPassType, pass); + protected getHash(pass: number): string { + if (this._hash[pass] === null) { + this._hash[pass] = this.createHash(pass); } - return this._hash[pass][renderPassType]; - } - /** Render states associated to this material */ - get stateSet(): RenderStateSet { - if (!this._renderStateSet) { - this._renderStateSet = this.createRenderStateSet(); - } - return this._renderStateSet; - } - set stateSet(stateset: RenderStateSet) { - this._renderStateSet = stateset; + return this._hash[pass]; } getQueueType(): number { return QUEUE_OPAQUE; } - /** Returns true if this is a transparency material */ + /** Returns true if given pass is transparent */ isTransparentPass(pass: number): boolean { return false; } @@ -161,6 +74,10 @@ export class Material { return true; } /** Returns true if this material supports geometry instancing */ + supportInstancing(): boolean { + return true; + } + /** Returns true if this material supports geometry instancing */ isBatchable(): boolean { return false; } @@ -169,66 +86,71 @@ export class Material { return this; } /** - * Draws a primitive using this material - * - * @param primitive - The prmitive to be drawn - * @param ctx - The context of current drawing task - * @param numInstances - How many instances should be drawn. if zero, the instance count will be automatically detected. + * Apply material + * @param ctx - Draw context + * @returns true if no error, otherwise false */ - draw(primitive: Primitive, ctx: DrawContext, numInstances = 0) { - for (let i = 0; i < this._numPasses; i++) { - if (this.beginDraw(i, ctx)) { - this.drawPrimitive(i, primitive, ctx, numInstances); - this.endDraw(i); + apply(ctx: DrawContext): boolean { + for (let pass = 0; pass < this._numPasses; pass++) { + const hash = this.calcGlobalHash(ctx, pass); + let state = this._states[hash]; + if (!state) { + const program = this.createProgram(ctx, pass) ?? null; + const bindGroup = + program.bindGroupLayouts.length > 2 + ? ctx.device.createBindGroup(program.bindGroupLayouts[2]) + : null; + state = { + program, + bindGroup, + renderStateSet: ctx.device.createRenderStateSet(), + materialTag: -1 + }; + this._states[hash] = state; } - } - } - /** - * Prepares for drawing - * @param ctx - The context of current drawing task - * @returns true if succeeded, otherwise false - */ - beginDraw(pass: number, ctx: DrawContext): boolean { - const device = Application.instance.device; - const programInfo = this.getOrCreateProgram(ctx, pass); - if (programInfo) { - const hash = programInfo.hash; - if (!programInfo.programs[ctx.renderPass.type]) { + if (!state.program) { return false; } - if (pass > 0) { - this.optionChanged(false); - } - this._materialBindGroup = this.applyMaterialBindGroups(ctx, hash, pass); - if (pass === 0) { - if (ctx.instanceData) { - this.applyInstanceBindGroups(ctx, hash); - } else { - this.applyDrawableBindGroups(ctx, hash); - } - } - ctx.renderPass.applyRenderStates(device, this.stateSet, ctx); - device.setProgram(programInfo.programs[ctx.renderPass.type]); - Material._drawableTimestamps.set(ctx.target, ctx.timestamp); - Material.lruPutDrawable(ctx.target); - Material._materialTimestamps.set(this, ctx.timestamp); - Material.lruPutMaterial(this); - return true; + this.applyUniforms(state.bindGroup, ctx, state.materialTag !== this._optionTag, pass); + state.materialTag = this._optionTag; + this.updateRenderStates(pass, state.renderStateSet, ctx); + this._currentHash[pass] = hash; } - return false; } - /** - * Ends drawing a primitive - */ - endDraw(pass: number): void { - this._materialBindGroup = null; + /** @internal */ + bind(device: AbstractDevice, pass: number): boolean { + const hash = this._currentHash[pass]; + const state = this._states[hash]; + if (!state) { + console.error('Material.bind() failed: state not found'); + return false; + } + if (!state.program) { + return false; + } + device.setProgram(state.program); + device.setBindGroup(2, state.bindGroup); + device.setRenderStates(state.renderStateSet); + } + /** @internal */ + private calcGlobalHash(ctx: DrawContext, pass: number): string { + return `${this.getHash(pass)}:${Number(!!ctx.skinAnimation)}:${Number(!!ctx.instancing)}:${ + ctx.renderPassHash + }`; } /** - * Gets the bind group of this material - * @returns The bind group of this material + * Draws a primitive using this material + * @internal + * + * @param primitive - The prmitive to be drawn + * @param ctx - The context of current drawing task + * @param numInstances - How many instances should be drawn. if zero, the instance count will be automatically detected. */ - getMaterialBindGroup(): BindGroup { - return this._materialBindGroup; + draw(primitive: Primitive, ctx: DrawContext, numInstances = 0) { + for (let pass = 0; pass < this._numPasses; pass++) { + this.bind(ctx.device, pass); + this.drawPrimitive(pass, primitive, ctx, numInstances); + } } /** * Sets all uniform values to the bind group of the material if needed @@ -241,237 +163,14 @@ export class Material { this._applyUniforms(bindGroup, ctx, pass); } } - /** - * Fetch the gpu program of the material for drawing - * @param ctx - The context for current drawing task - * @returns Information of the gpu program - */ - getOrCreateProgram(ctx: DrawContext, pass: number): ProgramInfo { - const programMap = Material._programMap; - const renderPassType = ctx.renderPass.type; - const hash = `${this.getHash(renderPassType, pass)}:${!!ctx.target.getBoneMatrices()}:${Number( - !!ctx.instanceData - )}:${ctx.renderPassHash}`; - let programInfo = programMap[hash]; - if (!programInfo || programInfo.programs[renderPassType] === undefined) { - const program = this.createProgram(ctx, pass) ?? null; - if (!programInfo) { - programInfo = { - programs: [null, null, null], - hash - }; - programMap[hash] = programInfo; - } - programInfo.programs[renderPassType] = program; - } - return programInfo; - } - dispose(): void { - this.clearBindGroupCache(); - } - /** - * Sets the options of garbage collection - * @param opt - The options to set - */ - static setGCOptions(opt: MaterialGCOptions) { - this._gcOptions = Object.assign({}, this._gcOptions, opt || {}); - } - /** - * Gets the options of garbage collection - * @returns The options of garbage collection - */ - static getGCOptions(): MaterialGCOptions { - return this._gcOptions; - } - /** - * Performs a garbage collection for this material - * @param ts - Current time stamp - * @returns How many bind groups have been garbage collected - */ - static garbageCollect(ts: number): number { - let n = 0; - ts -= this._gcOptions.inactiveTimeDuration; - while (this._drawableLRU.length > this._gcOptions.drawableCountThreshold) { - const iter = this._drawableLRU.begin(); - if (this._drawableTimestamps.get(iter.data) < ts) { - const bindGroups = this._drawableBindGroupMap.get(iter.data); - if (bindGroups) { - for (const k in bindGroups) { - for (const bindGroup of bindGroups[k].bindGroup) { - if (bindGroup) { - this.bindGroupGarbageCollect(bindGroup); - n++; - } - } - } - } - this._drawableBindGroupMap.delete(iter.data); - this._drawableIterators.delete(iter.data); - this._drawableLRU.remove(iter); - } else { - break; - } - } - while (this._materialLRU.length > this._gcOptions.materialCountThreshold) { - const iter = this._materialLRU.begin(); - const mat = iter.data as Material; - if (this._materialTimestamps.get(mat) < ts && mat._bindGroupMap) { - n += mat.clearBindGroupCache(); - this._materialIterators.delete(mat); - this._materialLRU.remove(iter); - } else { - break; - } - } - if (n > 0 && this._gcOptions.verbose) { - console.log(`INFO: ${n} bind groups have been garbage collected`); - } - return n; - } /** @internal */ optionChanged(changeHash: boolean) { this._optionTag++; if (changeHash) { for (let i = 0; i < this._numPasses; i++) { - this._hash[i] = []; - } - } - } - /** @internal */ - static getProgramByHashIndex(hash: string, index: number) { - return this._programMap[hash].programs[index]; - } - /** @internal */ - private applyMaterialBindGroups(ctx: DrawContext, hash: string, pass: number): BindGroup { - const index = ctx.renderPass.type; - let bindGroupInfo = this._bindGroupMap[hash]; - if (!bindGroupInfo) { - // bindGroups not created or have been garbage collected - const materialBindGroup = [ - RENDER_PASS_TYPE_LIGHT, - RENDER_PASS_TYPE_SHADOWMAP, - RENDER_PASS_TYPE_DEPTH - ].map((k) => { - const program = Material._programMap[hash].programs[k]; - return program?.bindGroupLayouts[2] - ? Application.instance.device.createBindGroup(program.bindGroupLayouts[2]) - : null; - }); - bindGroupInfo = this._bindGroupMap[hash] = { - materialBindGroup, - bindGroupTag: [0, 0, 0], - materialTag: [-1, -1, -1] - }; - } - const bindGroup = bindGroupInfo.materialBindGroup[index]; - if (bindGroup) { - this.applyUniforms( - bindGroup, - ctx, - bindGroupInfo.materialTag[index] < this._optionTag || - bindGroupInfo.bindGroupTag[index] !== bindGroup.cid, - pass - ); - bindGroupInfo.materialTag[index] = this._optionTag; - bindGroupInfo.bindGroupTag[index] = bindGroup.cid; - Application.instance.device.setBindGroup(2, bindGroup); - } else { - Application.instance.device.setBindGroup(2, null); - } - return bindGroup; - } - /** @internal */ - private getDrawableBindGroup( - ctx: DrawContext, - hash: string - ): { - bindGroup: BindGroup[]; - xformTag: number[]; - bindGroupTag: number[]; - } { - let drawableBindGroups = Material._drawableBindGroupMap.get(ctx.target); - if (!drawableBindGroups) { - drawableBindGroups = {}; - Material._drawableBindGroupMap.set(ctx.target, drawableBindGroups); - } - let drawableBindGroup = drawableBindGroups[hash]; - if (!drawableBindGroup) { - const bindGroup = [RENDER_PASS_TYPE_LIGHT, RENDER_PASS_TYPE_SHADOWMAP, RENDER_PASS_TYPE_DEPTH].map( - (k) => { - const program = Material._programMap[hash].programs[k]; - return program?.bindGroupLayouts[1] - ? Application.instance.device.createBindGroup(program.bindGroupLayouts[1]) - : null; - } - ); - drawableBindGroup = drawableBindGroups[hash] = { - bindGroup, - bindGroupTag: [0, 0, 0], - xformTag: [-1, -1, -1] - }; - } - return drawableBindGroup; - } - /** @internal */ - private applyInstanceBindGroups(ctx: DrawContext, hash: string): void { - if (ctx.instanceData) { - if (ctx.instanceData.bindGroup.dirty) { - ctx.instanceData.bindGroup.bindGroup.setRawData( - ShaderHelper.getWorldMatricesUniformName(), - 0, - ctx.instanceData.bindGroup.buffer, - 0, - ctx.instanceData.currentSize * 4 - ); - ctx.instanceData.bindGroup.dirty = false; - } - Application.instance.device.setBindGroup(3, ctx.instanceData.bindGroup.bindGroup ?? null); - } else { - Application.instance.device.setBindGroup(3, null); - } - const bindGroup = this.getDrawableBindGroup(ctx, hash).bindGroup?.[ctx.renderPass.type]; - if (bindGroup) { - if (ctx.instanceData) { - bindGroup.setValue(ShaderHelper.getInstanceBufferStrideUniformName(), ctx.instanceData.stride); - } - Application.instance.device.setBindGroup(1, bindGroup); - } else { - Application.instance.device.setBindGroup(1, null); - } - } - /** @internal */ - private applyDrawableBindGroups(ctx: DrawContext, hash: string): void { - const device = Application.instance.device; - const index = ctx.renderPass.type; - const drawableBindGroup = this.getDrawableBindGroup(ctx, hash); - if (drawableBindGroup.bindGroup) { - const bindGroup = drawableBindGroup.bindGroup[index]; - if ( - drawableBindGroup.xformTag[index] < ctx.target.getXForm().getTag() || - drawableBindGroup.bindGroupTag[index] !== bindGroup.cid - ) { - bindGroup.setValue(ShaderHelper.getWorldMatrixUniformName(), ctx.target.getXForm().worldMatrix); - drawableBindGroup.xformTag[index] = ctx.target.getXForm().getTag(); - drawableBindGroup.bindGroupTag[index] = bindGroup.cid; + this._hash[i] = null; } - const boneMatrices = ctx.target.getBoneMatrices(); - if (boneMatrices) { - if (!Material._boneMatrixTextureSampler) { - Material._boneMatrixTextureSampler = device.createSampler({ - magFilter: 'nearest', - minFilter: 'nearest', - mipFilter: 'none' - }); - } - bindGroup.setTexture(ShaderHelper.getBoneMatricesUniformName(), boneMatrices); - bindGroup.setValue(ShaderHelper.getBoneTextureSizeUniformName(), boneMatrices.width); - bindGroup.setValue(ShaderHelper.getBoneInvBindMatrixUniformName(), ctx.target.getInvBindMatrix()); - } - device.setBindGroup(1, bindGroup); - } else { - device.setBindGroup(1, null); } - device.setBindGroup(3, null); } /** * Convert pass to hash @@ -482,51 +181,8 @@ export class Material { return String(pass); } /** @internal */ - createHash(renderPassType: number, pass: number): string { - return `${this.constructor.name}|${this.passToHash(pass)}|${this._createHash(renderPassType)}`; - } - /** @internal */ - clearBindGroupCache(): number { - let n = 0; - for (const k in this._bindGroupMap) { - for (const bindGroup of this._bindGroupMap[k].materialBindGroup) { - if (bindGroup) { - Material.bindGroupGarbageCollect(bindGroup); - n++; - } - } - } - this._bindGroupMap = {}; - return n; - } - /** @internal */ - static bindGroupGarbageCollect(bindGroup: BindGroup) { - const layout: BindGroupLayout = bindGroup.getLayout(); - for (const entry of layout.entries) { - if (entry.buffer) { - const buffer = bindGroup.getBuffer(entry.name); - if (buffer) { - buffer.dispose(); - bindGroup.setBuffer(entry.name, null); - } - } - } - } - /** @internal */ - private static lruPutDrawable(drawable: Drawable) { - const iter = this._drawableIterators.get(drawable); - if (iter) { - this._drawableLRU.remove(iter); - } - this._drawableIterators.set(drawable, this._drawableLRU.append(drawable)); - } - /** @internal */ - private static lruPutMaterial(material: Material) { - const iter = this._materialIterators.get(material); - if (iter) { - this._materialLRU.remove(iter); - } - this._materialIterators.set(material, this._materialLRU.append(material)); + createHash(pass: number): string { + return `${this.constructor.name}|${pass}|${this._createHash()}`; } /** * Draw primitve @@ -538,20 +194,16 @@ export class Material { if (numInstances > 0) { primitive.drawInstanced(numInstances); } else if (ctx.instanceData) { - primitive.drawInstanced(ctx.instanceData.currentSize / ctx.instanceData.stride); + primitive.drawInstanced(ctx.instanceData.numInstances); } else { primitive.draw(); } } /** @internal */ protected createProgram(ctx: DrawContext, pass: number): GPUProgram { - const pb = new ProgramBuilder(Application.instance.device); + const pb = new ProgramBuilder(ctx.device); return this._createProgram(pb, ctx, pass); } - /** @internal */ - protected createRenderStateSet(): RenderStateSet { - return Application.instance.device.createRenderStateSet(); - } /** * Creates the shader program * @param pb - The program builder @@ -568,11 +220,18 @@ export class Material { * @param ctx - The drawing context */ protected _applyUniforms(bindGroup: BindGroup, ctx: DrawContext, pass: number) {} + /** + * Update render states according to draw context and current material pass + * @param pass - Current material pass + * @param renderStates - Render state set to be updated + * @param ctx - Draw context + */ + protected updateRenderStates(pass: number, renderStates: RenderStateSet, ctx: DrawContext): void {} /** * Calculates the hash code of the shader program * @returns The hash code */ - protected _createHash(renderPassType: number): string { + protected _createHash(): string { return ''; } /** diff --git a/libs/scene/src/material/meshmaterial.ts b/libs/scene/src/material/meshmaterial.ts index d54dcc73..526e4a2e 100644 --- a/libs/scene/src/material/meshmaterial.ts +++ b/libs/scene/src/material/meshmaterial.ts @@ -4,7 +4,8 @@ import type { GPUProgram, PBFunctionScope, PBInsideFunctionScope, - PBShaderExp + PBShaderExp, + RenderStateSet } from '@zephyr3d/device'; import { ProgramBuilder } from '@zephyr3d/device'; import { @@ -19,7 +20,7 @@ import type { DrawContext, ShadowMapPass } from '../render'; import { encodeNormalizedFloatToRGBA } from '../shaders'; import { Application } from '../app'; import { ShaderHelper } from './shader/helper'; -import { Vector2, Vector3, Vector4 } from '@zephyr3d/base'; +import { Vector2, Vector3, Vector4, applyMixins } from '@zephyr3d/base'; /** * Blending mode for mesh material @@ -27,14 +28,6 @@ import { Vector2, Vector3, Vector4 } from '@zephyr3d/base'; */ export type BlendMode = 'none' | 'blend' | 'additive'; -/** - * Convert union type to intersection - * @public - */ -export type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void - ? I - : never; - /** * Extract mixin return type * @public @@ -63,11 +56,7 @@ export function applyMaterialMixins any)[], T>( target: T, ...mixins: M ): ExtractMixinType { - let r: any = target; - for (const m of mixins) { - r = m(r); - } - return r; + return applyMixins(target, ...mixins); } let FEATURE_ALPHATEST = 0; @@ -137,26 +126,45 @@ export class MeshMaterial extends Material { return this.INSTANCE_UNIFORMS.length - 1; } getInstancedUniform(scope: PBInsideFunctionScope, uniformIndex: number): PBShaderExp { - //return ShaderHelper.getInstancedUniform(scope, 4 + uniformIndex); const pb = scope.$builder; - const instanceID = pb.shaderKind === 'vertex' ? scope.$builtins.instanceIndex : scope.$inputs.zInstanceID; - const uniformName = ShaderHelper.getWorldMatricesUniformName(); - const strideName = ShaderHelper.getInstanceBufferStrideUniformName(); - return scope[uniformName].at(pb.add(pb.mul(scope[strideName], instanceID), 4 + uniformIndex)); + const instanceID = scope.$builtins.instanceIndex; + const uniformName = ShaderHelper.getInstanceDataUniformName(); + const strideName = ShaderHelper.getInstanceDataStrideUniformName(); + const offsetName = ShaderHelper.getInstanceDataOffsetUniformName(); + return scope[uniformName].at( + pb.add(pb.mul(scope[strideName], instanceID), 4 + uniformIndex, scope[offsetName]) + ); } /** Create material instance */ createInstance(): this { + if (this.$isInstance) { + return this.coreMaterial.createInstance(); + } const instanceUniforms = (this.constructor as typeof MeshMaterial).INSTANCE_UNIFORMS; const uniformsHolder = instanceUniforms.length > 0 ? new Float32Array(4 * instanceUniforms.length) : null; - const batchable = Application.instance.device.type !== 'webgl'; - const coreMaterial = this.coreMaterial; + const isWebGL1 = Application.instance.device.type === 'webgl'; + const instance = {} as any; + const that = this; + instance.isBatchable = () => !isWebGL1 && that.supportInstancing(); + instance.$instanceUniforms = uniformsHolder; + instance.$isInstance = true; + instance.coreMaterial = that; // Copy original uniform values for (let i = 0; i < instanceUniforms.length; i++) { const [prop, type] = instanceUniforms[i]; - const value = coreMaterial[prop]; + const value = that[prop]; switch (type) { case 'float': { uniformsHolder[i * 4] = Number(value); + Object.defineProperty(instance, prop, { + get() { + return uniformsHolder[i * 4]; + }, + set(value) { + uniformsHolder[i * 4 + 0] = value; + that[prop] = value; + } + }); break; } case 'vec2': { @@ -165,6 +173,15 @@ export class MeshMaterial extends Material { } uniformsHolder[i * 4] = value.x; uniformsHolder[i * 4 + 1] = value.y; + Object.defineProperty(instance, prop, { + get() { + return new Vector2(uniformsHolder[i * 4], uniformsHolder[i * 4 + 1]); + }, + set(value) { + uniformsHolder.set(value); + that[prop] = value; + } + }); break; } case 'vec3': { @@ -174,6 +191,15 @@ export class MeshMaterial extends Material { uniformsHolder[i * 4] = value.x; uniformsHolder[i * 4 + 1] = value.y; uniformsHolder[i * 4 + 2] = value.z; + Object.defineProperty(instance, prop, { + get() { + return new Vector3(uniformsHolder[i * 4], uniformsHolder[i * 4 + 1], uniformsHolder[i * 4 + 2]); + }, + set(value) { + uniformsHolder.set(value); + that[prop] = value; + } + }); break; } case 'vec4': { @@ -184,89 +210,26 @@ export class MeshMaterial extends Material { uniformsHolder[i * 4 + 1] = value.y; uniformsHolder[i * 4 + 2] = value.z; uniformsHolder[i * 4 + 3] = value.w; + Object.defineProperty(instance, prop, { + get() { + return new Vector4( + uniformsHolder[i * 4], + uniformsHolder[i * 4 + 1], + uniformsHolder[i * 4 + 2], + uniformsHolder[i * 4 + 3] + ); + }, + set(value) { + uniformsHolder.set(value); + that[prop] = value; + } + }); break; } } } - const handler: ProxyHandler = { - get(target, prop, receiver) { - if (prop === 'isBatchable') { - return () => batchable; - } else if (prop === '$instanceUniforms') { - return uniformsHolder; - } else if (prop === '$isInstance') { - return true; - } else if (prop === 'beginDraw') { - if (!batchable || !target.isBatchable()) { - for (let i = 0; i < instanceUniforms.length; i++) { - const name = instanceUniforms[i][0]; - const type = instanceUniforms[i][1]; - switch (type) { - case 'float': - target[name] = uniformsHolder[i * 4]; - break; - case 'vec2': - target[name] = new Vector2(uniformsHolder[i * 4], uniformsHolder[i * 4 + 1]); - break; - case 'vec3': - target[name] = new Vector3( - uniformsHolder[i * 4], - uniformsHolder[i * 4 + 1], - uniformsHolder[i * 4 + 2] - ); - case 'vec4': - target[name] = new Vector4( - uniformsHolder[i * 4], - uniformsHolder[i * 4 + 1], - uniformsHolder[i * 4 + 2], - uniformsHolder[i * 4 + 3] - ); - } - } - } - } else if (prop === 'coreMaterial') { - return target; - } else if (typeof prop === 'string') { - const index = instanceUniforms.findIndex((val) => val[0] === prop); - if (index >= 0) { - switch (instanceUniforms[index][1]) { - case 'float': - return uniformsHolder[index * 4]; - case 'vec2': - return new Vector2(uniformsHolder[index * 4], uniformsHolder[index * 4 + 1]); - case 'vec3': - return new Vector3( - uniformsHolder[index * 4], - uniformsHolder[index * 4 + 1], - uniformsHolder[index * 4 + 2] - ); - case 'vec4': - return new Vector4( - uniformsHolder[index * 4], - uniformsHolder[index * 4 + 1], - uniformsHolder[index * 4 + 2], - uniformsHolder[index * 4 + 3] - ); - } - } - } - return Reflect.get(target, prop, receiver); - }, - set(target, prop, value, receiver) { - const i = instanceUniforms.findIndex((val) => val[0] === prop); - if (i >= 0) { - if (typeof value === 'number') { - uniformsHolder[i * 4 + 0] = value; - } else if (value instanceof Float32Array) { - uniformsHolder.set(value); - } - return true; - } else { - return Reflect.set(target, prop, value, receiver); - } - } - }; - return new Proxy(coreMaterial, handler); + Object.setPrototypeOf(instance, that); + return instance; } /** Draw context for shader creation */ get drawContext(): DrawContext { @@ -327,15 +290,13 @@ export class MeshMaterial extends Material { return true; } /** - * Update render states according to draw context and current material pass - * @param pass - Current material pass - * @param ctx - Draw context + * {@inheritDoc Material.updateRenderStates} */ - protected updateRenderStates(pass: number, ctx: DrawContext): void { + protected updateRenderStates(pass: number, stateSet: RenderStateSet, ctx: DrawContext): void { const blending = this.featureUsed(FEATURE_ALPHABLEND) || ctx.lightBlending; const a2c = this.featureUsed(FEATURE_ALPHATOCOVERAGE); if (blending || a2c) { - const blendingState = this.stateSet.useBlendingState(); + const blendingState = stateSet.useBlendingState(); if (blending) { blendingState.enable(true); blendingState.setBlendFuncAlpha('zero', 'one'); @@ -350,18 +311,23 @@ export class MeshMaterial extends Material { } blendingState.enableAlphaToCoverage(a2c); if (blendingState.enabled) { - this.stateSet.useDepthState().enableTest(true).enableWrite(false); + stateSet.useDepthState().enableTest(true).enableWrite(false); } else { - this.stateSet.defaultDepthState(); + stateSet.defaultDepthState(); } - } else if (this.stateSet.blendingState?.enabled && !blending) { - this.stateSet.defaultBlendingState(); - this.stateSet.defaultDepthState(); + } else if (stateSet.blendingState?.enabled && !blending) { + stateSet.defaultBlendingState(); + stateSet.defaultDepthState(); } if (this._cullMode !== 'back') { - this.stateSet.useRasterizerState().cullMode = this._cullMode; + stateSet.useRasterizerState().cullMode = this._cullMode; } else { - this.stateSet.defaultRasterizerState(); + stateSet.defaultRasterizerState(); + } + stateSet.defaultColorState(); + stateSet.defaultStencilState(); + if (ctx.oit) { + ctx.oit.setRenderStates(stateSet); } } /** @@ -375,9 +341,12 @@ export class MeshMaterial extends Material { if (this.featureUsed(FEATURE_ALPHATEST)) { bindGroup.setValue('zAlphaCutoff', this._alphaCutoff); } - if (this.featureUsed(FEATURE_ALPHABLEND)) { + if (this.isTransparentPass(pass)) { bindGroup.setValue('zOpacity', this._opacity); } + if (ctx.oit) { + ctx.oit.applyUniforms(ctx, bindGroup); + } } /** * Determine which queue should be used to render this material. @@ -394,16 +363,9 @@ export class MeshMaterial extends Material { isTransparentPass(pass: number): boolean { return this.featureUsed(FEATURE_ALPHABLEND); } - /** - * {@inheritdoc Material.beginDraw} - */ - beginDraw(pass: number, ctx: DrawContext): boolean { - this.updateRenderStates(pass, ctx); - return super.beginDraw(pass, ctx); - } /** @internal */ protected createProgram(ctx: DrawContext, pass: number): GPUProgram { - const pb = new ProgramBuilder(Application.instance.device); + const pb = new ProgramBuilder(ctx.device); if (ctx.renderPass.type === RENDER_PASS_TYPE_SHADOWMAP) { const shadowMapParams = ctx.shadowMapInfo.get((ctx.renderPass as ShadowMapPass).light); pb.emulateDepthClamp = !!shadowMapParams.depthClampEnabled; @@ -437,7 +399,7 @@ export class MeshMaterial extends Material { * * @internal */ - protected _createHash(renderPassType: number): string { + protected _createHash(): string { return this._featureStates.map((val) => (val === undefined ? '' : val)).join('|'); } /** @@ -468,13 +430,10 @@ export class MeshMaterial extends Material { vertexShader(scope: PBFunctionScope): void { const pb = scope.$builder; ShaderHelper.prepareVertexShader(pb, this.drawContext); - if (this.drawContext.target.getBoneMatrices()) { + if (this.drawContext.skinAnimation) { scope.$inputs.zBlendIndices = pb.vec4().attrib('blendIndices'); scope.$inputs.zBlendWeights = pb.vec4().attrib('blendWeights'); } - if (this.drawContext.instanceData) { - scope.$outputs.zInstanceID = scope.$builtins.instanceIndex; - } } /** * Fragment shader implementation of this material @@ -509,7 +468,11 @@ export class MeshMaterial extends Material { }); }, fragment(pb) { - this.$outputs.zFragmentOutput = pb.vec4(); + if (that.drawContext.oit) { + that.drawContext.oit.setupFragmentOutput(this); + } else { + this.$outputs.zFragmentOutput = pb.vec4(); + } pb.main(function () { that.fragmentShader(this); }); @@ -523,6 +486,7 @@ export class MeshMaterial extends Material { */ return program; } + doAlphaTest(scope: PBInsideFunctionScope, color: PBShaderExp) {} /** * Calculate final fragment color for output. * @@ -550,7 +514,9 @@ export class MeshMaterial extends Material { }); } if (that.isTransparentPass(that.pass)) { - this.outColor = pb.vec4(pb.mul(this.outColor.rgb, this.outColor.a), this.outColor.a); + if (!that.drawContext.oit || !that.drawContext.oit.outputFragmentColor(this, this.outColor)) { + this.outColor = pb.vec4(pb.mul(this.outColor.rgb, this.outColor.a), this.outColor.a); + } } ShaderHelper.applyFog(this, this.worldPos, this.outColor, that.drawContext); this.$outputs.zFragmentOutput = ShaderHelper.encodeColorOutput(this, this.outColor); @@ -562,7 +528,7 @@ export class MeshMaterial extends Material { } ShaderHelper.discardIfClipped(this, this.worldPos); this.$l.depth = ShaderHelper.nonLinearDepthToLinearNormalized(this, this.$builtins.fragCoord.z); - if (Application.instance.device.type === 'webgl') { + if (that.drawContext.device.type === 'webgl') { this.$outputs.zFragmentOutput = encodeNormalizedFloatToRGBA(this, this.depth); } else { this.$outputs.zFragmentOutput = pb.vec4(this.depth, 0, 0, 1); diff --git a/libs/scene/src/material/mixins/albedocolor.ts b/libs/scene/src/material/mixins/albedocolor.ts index 09980e72..d70d8541 100644 --- a/libs/scene/src/material/mixins/albedocolor.ts +++ b/libs/scene/src/material/mixins/albedocolor.ts @@ -44,9 +44,7 @@ function mixinAlbedoColor(BaseCls: T) { this.uniformChanged(); } getUniformValueAlbedoColor(scope: PBInsideFunctionScope): PBShaderExp { - return this.drawContext.instanceData - ? this.getInstancedUniform(scope, ALBEDO_COLOR_UNIFORM) - : scope.zAlbedo; + return this.drawContext.instancing ? scope.$inputs.zAlbedo : scope.zAlbedo; } calculateAlbedoColor(scope: PBInsideFunctionScope, uv?: PBShaderExp): PBShaderExp { const pb = scope.$builder; @@ -62,16 +60,24 @@ function mixinAlbedoColor(BaseCls: T) { } return color; } + vertexShader(scope: PBFunctionScope): void { + super.vertexShader(scope); + if (this.needFragmentColor()) { + if (this.drawContext.instancing) { + scope.$outputs.zAlbedo = this.getInstancedUniform(scope, ALBEDO_COLOR_UNIFORM); + } + } + } fragmentShader(scope: PBFunctionScope): void { super.fragmentShader(scope); - if (this.needFragmentColor()) { + if (this.needFragmentColor() && !this.drawContext.instancing) { const pb = scope.$builder; scope.zAlbedo = pb.vec4().uniform(2); } } applyUniformValues(bindGroup: BindGroup, ctx: DrawContext, pass: number): void { super.applyUniformValues(bindGroup, ctx, pass); - if (this.needFragmentColor(ctx)) { + if (this.needFragmentColor(ctx) && !this.drawContext.instancing) { bindGroup.setValue('zAlbedo', this._albedoColor); } } diff --git a/libs/scene/src/material/mixins/lightmodel/blinnphong.ts b/libs/scene/src/material/mixins/lightmodel/blinnphong.ts index 824956ad..a0b05559 100644 --- a/libs/scene/src/material/mixins/lightmodel/blinnphong.ts +++ b/libs/scene/src/material/mixins/lightmodel/blinnphong.ts @@ -57,7 +57,7 @@ export function mixinBlinnPhong(BaseCls: T) { } applyUniformValues(bindGroup: BindGroup, ctx: DrawContext, pass: number): void { super.applyUniformValues(bindGroup, ctx, pass); - if (this.needFragmentColor()) { + if (this.needFragmentColor(ctx)) { bindGroup.setValue('zShininess', this._shininess); } } diff --git a/libs/scene/src/material/shader/helper.ts b/libs/scene/src/material/shader/helper.ts index 15578172..7f4eadff 100644 --- a/libs/scene/src/material/shader/helper.ts +++ b/libs/scene/src/material/shader/helper.ts @@ -6,7 +6,6 @@ import { RENDER_PASS_TYPE_LIGHT, RENDER_PASS_TYPE_SHADOWMAP } from '../../values'; -import { Application } from '../../app'; import { ScatteringLut } from '../../render/scatteringlut'; import type { ProgramBuilder, @@ -14,19 +13,22 @@ import type { PBShaderExp, PBInsideFunctionScope, StructuredBuffer, - Texture2D + Texture2D, + PBGlobalScope } from '@zephyr3d/device'; import type { PunctualLight } from '../../scene/light'; import { linearToGamma } from '../../shaders'; +import type { Camera } from '../../camera'; const UNIFORM_NAME_GLOBAL = 'Z_UniformGlobal'; const UNIFORM_NAME_LIGHT_BUFFER = 'Z_UniformLightBuffer'; const UNIFORM_NAME_LIGHT_INDEX_TEXTURE = 'Z_UniformLightIndexTex'; const UNIFORM_NAME_AERIALPERSPECTIVE_LUT = 'Z_UniformAerialPerspectiveLUT'; const UNIFORM_NAME_SHADOW_MAP = 'Z_UniformShadowMap'; -const UNIFORM_NAME_INSTANCE_BUFFER_STRIDE = 'Z_UniformInstanceBufferStride'; const UNIFORM_NAME_WORLD_MATRIX = 'Z_UniformWorldMatrix'; -const UNIFORM_NAME_WORLD_MATRICES = 'Z_UniformWorldMatrices'; +const UNIFORM_NAME_INSTANCE_DATA_STRIDE = 'Z_UniformInstanceDataStride'; +const UNIFORM_NAME_INSTANCE_DATA = 'Z_UniformInstanceData'; +const UNIFORM_NAME_INSTANCE_DATA_OFFSET = 'Z_UniformInstanceDataOffset'; const UNIFORM_NAME_BONE_MATRICES = 'Z_UniformBoneMatrices'; const UNIFORM_NAME_BONE_TEXTURE_SIZE = 'Z_UniformBoneTexSize'; const UNIFORM_NAME_BONE_INV_BIND_MATRIX = 'Z_UniformBoneInvBindMatrix'; @@ -77,11 +79,14 @@ export class ShaderHelper { static getWorldMatrixUniformName(): string { return UNIFORM_NAME_WORLD_MATRIX; } - static getWorldMatricesUniformName(): string { - return UNIFORM_NAME_WORLD_MATRICES; + static getInstanceDataUniformName(): string { + return UNIFORM_NAME_INSTANCE_DATA; } - static getInstanceBufferStrideUniformName(): string { - return UNIFORM_NAME_INSTANCE_BUFFER_STRIDE; + static getInstanceDataOffsetUniformName(): string { + return UNIFORM_NAME_INSTANCE_DATA_OFFSET; + } + static getInstanceDataStrideUniformName(): string { + return UNIFORM_NAME_INSTANCE_DATA_STRIDE; } static getBoneMatricesUniformName(): string { return UNIFORM_NAME_BONE_MATRICES; @@ -106,11 +111,6 @@ export class ShaderHelper { */ static prepareFragmentShader(pb: ProgramBuilder, ctx: DrawContext) { this.setupGlobalUniforms(pb, ctx); - if (ctx.instanceData) { - const scope = pb.getGlobalScope(); - scope[UNIFORM_NAME_INSTANCE_BUFFER_STRIDE] = pb.uint().uniform(1); - scope[UNIFORM_NAME_WORLD_MATRICES] = pb.vec4[65536 >> 4]().uniformBuffer(3); - } } /** * Prepares the vertex shader which is going to be used in our material system @@ -186,7 +186,7 @@ export class ShaderHelper { pb.getDevice().type === 'webgl' ? pb.tex2D() : pb.utex2D() ).uniform(0); } - if (ctx.applyFog && ctx.scene.env.sky.drawScatteredFog(ctx)) { + if (ctx.applyFog === 'scatter') { scope[UNIFORM_NAME_AERIALPERSPECTIVE_LUT] = pb.tex2D().uniform(0); } if (ctx.currentShadowLight) { @@ -205,9 +205,8 @@ export class ShaderHelper { : scope.$builder.tex2DArray(); if ( !shadowMapParams.shadowMap.isDepth() && - !Application.instance.device - .getDeviceCaps() - .textureCaps.getTextureFormatInfo(shadowMapParams.shadowMap.format).filterable + !ctx.device.getDeviceCaps().textureCaps.getTextureFormatInfo(shadowMapParams.shadowMap.format) + .filterable ) { tex.sampleType('unfilterable-float'); } @@ -395,28 +394,55 @@ export class ShaderHelper { */ static getInstancedUniform(scope: PBInsideFunctionScope, uniformIndex: number): PBShaderExp { const pb = scope.$builder; - return scope[UNIFORM_NAME_WORLD_MATRICES].at( + return scope[UNIFORM_NAME_INSTANCE_DATA].at( pb.add( - pb.mul(scope[UNIFORM_NAME_INSTANCE_BUFFER_STRIDE], pb.uint(scope.$builtins.instanceIndex)), + pb.mul(scope[UNIFORM_NAME_INSTANCE_DATA_STRIDE], pb.uint(scope.$builtins.instanceIndex)), + scope[UNIFORM_NAME_INSTANCE_DATA_OFFSET], uniformIndex ) ); } /** * Gets the uniform variable of type mat4 which holds the normal matrix of current object to be drawn + * * @param scope - Current shader scope * @returns The normal matrix of current object to be drawn */ static getNormalMatrix(scope: PBInsideFunctionScope): PBShaderExp { return this.getWorldMatrix(scope); } + /** + * Vertex shader drawable stuff + * + * @param scope - Current shader scope + * @param skinning - true if skinning is used, otherwise false. + * @param instanced - true if instancing is used, otherwise false. + */ + static vertexShaderDrawableStuff(scope: PBGlobalScope, skinning: boolean, instanced: boolean): void { + const pb = scope.$builder; + if (instanced) { + scope[UNIFORM_NAME_INSTANCE_DATA_STRIDE] = pb.uint().uniform(1); + scope[UNIFORM_NAME_INSTANCE_DATA_OFFSET] = pb.uint().uniform(1); + scope[UNIFORM_NAME_INSTANCE_DATA] = pb.vec4[65536 >> 4]().uniformBuffer(3); + } else { + scope[UNIFORM_NAME_WORLD_MATRIX] = pb.mat4().uniform(1); + } + if (skinning) { + scope[UNIFORM_NAME_BONE_MATRICES] = pb.tex2D().uniform(1).sampleType('unfilterable-float'); + scope[UNIFORM_NAME_BONE_INV_BIND_MATRIX] = pb.mat4().uniform(1); + scope[UNIFORM_NAME_BONE_TEXTURE_SIZE] = pb.int().uniform(1); + } + } /** @internal */ static prepareVertexShaderCommon(pb: ProgramBuilder, ctx: DrawContext) { + this.vertexShaderDrawableStuff(pb.getGlobalScope(), !!ctx.skinAnimation, !!ctx.instancing); + /* const skinning = !!ctx.target?.getBoneMatrices(); const scope = pb.getGlobalScope(); if (ctx.instanceData) { - scope[UNIFORM_NAME_INSTANCE_BUFFER_STRIDE] = pb.uint().uniform(1); - scope[UNIFORM_NAME_WORLD_MATRICES] = pb.vec4[65536 >> 4]().uniformBuffer(3); + scope[UNIFORM_NAME_INSTANCE_DATA_STRIDE] = pb.uint().uniform(1); + scope[UNIFORM_NAME_INSTANCE_DATA_OFFSET] = pb.uint().uniform(1); + scope[UNIFORM_NAME_INSTANCE_DATA] = pb.vec4[65536 >> 4]().uniformBuffer(3); } else { scope[UNIFORM_NAME_WORLD_MATRIX] = pb.mat4().uniform(1); } @@ -425,22 +451,18 @@ export class ShaderHelper { scope[UNIFORM_NAME_BONE_INV_BIND_MATRIX] = pb.mat4().uniform(1); scope[UNIFORM_NAME_BONE_TEXTURE_SIZE] = pb.int().uniform(1); } + */ } /** @internal */ - static setCameraUniforms(bindGroup: BindGroup, ctx: DrawContext, linear: boolean) { - const pos = ctx.camera.getWorldPosition(); + static setCameraUniforms(bindGroup: BindGroup, camera: Camera, flip: boolean, linear: boolean) { + const pos = camera.getWorldPosition(); const cameraStruct = { - position: new Vector4(pos.x, pos.y, pos.z, ctx.camera.clipPlane ? 1 : 0), - clipPlane: ctx.camera.clipPlane ?? Vector4.zero(), - viewProjectionMatrix: ctx.camera.viewProjectionMatrix, - viewMatrix: ctx.camera.viewMatrix, - projectionMatrix: ctx.camera.getProjectionMatrix(), - params: new Vector4( - ctx.camera.getNearPlane(), - ctx.camera.getFarPlane(), - ctx.flip ? -1 : 1, - linear ? 0 : 1 - ) + position: new Vector4(pos.x, pos.y, pos.z, camera.clipPlane ? 1 : 0), + clipPlane: camera.clipPlane ?? Vector4.zero(), + viewProjectionMatrix: camera.viewProjectionMatrix, + viewMatrix: camera.viewMatrix, + projectionMatrix: camera.getProjectionMatrix(), + params: new Vector4(camera.getNearPlane(), camera.getFarPlane(), flip ? -1 : 1, linear ? 0 : 1) }; bindGroup.setValue(UNIFORM_NAME_GLOBAL, { camera: cameraStruct @@ -845,53 +867,6 @@ export class ShaderHelper { scope.$builtins.position = pos; } } - /** @internal */ - static getSkinMatrix(scope: PBInsideFunctionScope): PBShaderExp { - const pb = scope.$builder; - const funcNameGetBoneMatrixFromTexture = 'Z_getBoneMatrixFromTexture'; - pb.func(funcNameGetBoneMatrixFromTexture, [pb.int('boneIndex')], function () { - const boneTexture = this[UNIFORM_NAME_BONE_MATRICES]; - this.$l.w = pb.float(this[UNIFORM_NAME_BONE_TEXTURE_SIZE]); - this.$l.pixelIndex = pb.float(pb.mul(this.boneIndex, 4)); - this.$l.xIndex = pb.mod(this.pixelIndex, this.w); - this.$l.yIndex = pb.floor(pb.div(this.pixelIndex, this.w)); - this.$l.u1 = pb.div(pb.add(this.xIndex, 0.5), this.w); - this.$l.u2 = pb.div(pb.add(this.xIndex, 1.5), this.w); - this.$l.u3 = pb.div(pb.add(this.xIndex, 2.5), this.w); - this.$l.u4 = pb.div(pb.add(this.xIndex, 3.5), this.w); - this.$l.v = pb.div(pb.add(this.yIndex, 0.5), this.w); - if (Application.instance.device.type !== 'webgl') { - this.$l.row1 = pb.textureSampleLevel(boneTexture, pb.vec2(this.u1, this.v), 0); - this.$l.row2 = pb.textureSampleLevel(boneTexture, pb.vec2(this.u2, this.v), 0); - this.$l.row3 = pb.textureSampleLevel(boneTexture, pb.vec2(this.u3, this.v), 0); - this.$l.row4 = pb.textureSampleLevel(boneTexture, pb.vec2(this.u4, this.v), 0); - } else { - this.$l.row1 = pb.textureSample(boneTexture, pb.vec2(this.u1, this.v)); - this.$l.row2 = pb.textureSample(boneTexture, pb.vec2(this.u2, this.v)); - this.$l.row3 = pb.textureSample(boneTexture, pb.vec2(this.u3, this.v)); - this.$l.row4 = pb.textureSample(boneTexture, pb.vec2(this.u4, this.v)); - } - this.$return(pb.mat4(this.row1, this.row2, this.row3, this.row4)); - }); - const funcNameGetSkinningMatrix = 'Z_getSkinningMatrix'; - pb.func(funcNameGetSkinningMatrix, [], function () { - const invBindMatrix = this[UNIFORM_NAME_BONE_INV_BIND_MATRIX]; - const blendIndices = pb.getGlobalScope().$getVertexAttrib('blendIndices'); - const blendWeights = pb.getGlobalScope().$getVertexAttrib('blendWeights'); - this.$l.m0 = pb.getGlobalScope()[funcNameGetBoneMatrixFromTexture](pb.int(blendIndices[0])); - this.$l.m1 = pb.getGlobalScope()[funcNameGetBoneMatrixFromTexture](pb.int(blendIndices[1])); - this.$l.m2 = pb.getGlobalScope()[funcNameGetBoneMatrixFromTexture](pb.int(blendIndices[2])); - this.$l.m3 = pb.getGlobalScope()[funcNameGetBoneMatrixFromTexture](pb.int(blendIndices[3])); - this.$l.m = pb.add( - pb.mul(this.m0, blendWeights.x), - pb.mul(this.m1, blendWeights.y), - pb.mul(this.m2, blendWeights.z), - pb.mul(this.m3, blendWeights.w) - ); - this.$return(pb.mul(invBindMatrix, this.m)); - }); - return pb.getGlobalScope()[funcNameGetSkinningMatrix](); - } /** * Get global uniforms * @@ -940,7 +915,7 @@ export class ShaderHelper { pb.float(pb.greaterThan(this.shadowCascades, 3)) ); this.$l.split = pb.int(pb.dot(this.comparison, this.cascadeFlags)); - if (Application.instance.device.type === 'webgl') { + if (ctx.device.type === 'webgl') { this.$l.shadowVertex = pb.vec4(); this.$for(pb.int('cascade'), 0, 4, function () { this.$if(pb.equal(this.cascade, this.split), function () { @@ -999,63 +974,57 @@ export class ShaderHelper { return pb.getGlobalScope()[funcName](worldPos, NoL); } static applyFog(scope: PBInsideFunctionScope, worldPos: PBShaderExp, color: PBShaderExp, ctx: DrawContext) { - if (ctx.applyFog) { - const pb = scope.$builder; - const that = this; - if (ctx.env.sky.drawScatteredFog(ctx)) { - const funcName = 'Z_applySkyFog'; - pb.func(funcName, [pb.vec3('worldPos'), pb.vec4('color').inout()], function () { - this.$l.viewDir = pb.sub(this.worldPos, that.getCameraPosition(this)); - this.viewDir.y = pb.max(this.viewDir.y, 0); - this.$l.distance = pb.mul(pb.length(this.viewDir), that.getAPDensity(this)); - this.$l.sliceDist = pb.div( - pb.mul(that.getCameraParams(this).y, that.getAPDensity(this)), - ScatteringLut.aerialPerspectiveSliceZ - ); - this.$l.slice0 = pb.floor(pb.div(this.distance, this.sliceDist)); - this.$l.slice1 = pb.add(this.slice0, 1); - this.$l.factor = pb.sub(pb.div(this.distance, this.sliceDist), this.slice0); - this.$l.viewNormal = pb.normalize(this.viewDir); - this.$l.horizonAngle = pb.acos( - pb.clamp( - pb.dot(pb.normalize(that.getSunLightDir(this).xz), pb.normalize(this.viewNormal.xz)), - 0, - 1 - ) - ); - this.$l.zenithAngle = pb.asin(this.viewNormal.y); - this.$l.sliceU = pb.max( - pb.div(this.horizonAngle, Math.PI * 2), - 0.5 / ScatteringLut.aerialPerspectiveSliceZ - ); - this.$l.u0 = pb.div(pb.add(this.slice0, this.sliceU), ScatteringLut.aerialPerspectiveSliceZ); - this.$l.u1 = pb.add(this.u0, 1 / ScatteringLut.aerialPerspectiveSliceZ); - this.$l.v = pb.div(this.zenithAngle, Math.PI / 2); - this.$l.t0 = pb.textureSampleLevel(that.getAerialPerspectiveLUT(this), pb.vec2(this.u0, this.v), 0); - this.$l.t1 = pb.textureSampleLevel(that.getAerialPerspectiveLUT(this), pb.vec2(this.u1, this.v), 0); - this.$l.t = pb.mix(this.t0, this.t1, this.factor); + const pb = scope.$builder; + const that = this; + if (ctx.applyFog === 'scatter') { + const funcName = 'Z_applySkyFog'; + pb.func(funcName, [pb.vec3('worldPos'), pb.vec4('color').inout()], function () { + this.$l.viewDir = pb.sub(this.worldPos, that.getCameraPosition(this)); + this.viewDir.y = pb.max(this.viewDir.y, 0); + this.$l.distance = pb.mul(pb.length(this.viewDir), that.getAPDensity(this)); + this.$l.sliceDist = pb.div( + pb.mul(that.getCameraParams(this).y, that.getAPDensity(this)), + ScatteringLut.aerialPerspectiveSliceZ + ); + this.$l.slice0 = pb.floor(pb.div(this.distance, this.sliceDist)); + this.$l.slice1 = pb.add(this.slice0, 1); + this.$l.factor = pb.sub(pb.div(this.distance, this.sliceDist), this.slice0); + this.$l.viewNormal = pb.normalize(this.viewDir); + this.$l.horizonAngle = pb.acos( + pb.clamp(pb.dot(pb.normalize(that.getSunLightDir(this).xz), pb.normalize(this.viewNormal.xz)), 0, 1) + ); + this.$l.zenithAngle = pb.asin(this.viewNormal.y); + this.$l.sliceU = pb.max( + pb.div(this.horizonAngle, Math.PI * 2), + 0.5 / ScatteringLut.aerialPerspectiveSliceZ + ); + this.$l.u0 = pb.div(pb.add(this.slice0, this.sliceU), ScatteringLut.aerialPerspectiveSliceZ); + this.$l.u1 = pb.add(this.u0, 1 / ScatteringLut.aerialPerspectiveSliceZ); + this.$l.v = pb.div(this.zenithAngle, Math.PI / 2); + this.$l.t0 = pb.textureSampleLevel(that.getAerialPerspectiveLUT(this), pb.vec2(this.u0, this.v), 0); + this.$l.t1 = pb.textureSampleLevel(that.getAerialPerspectiveLUT(this), pb.vec2(this.u1, this.v), 0); + this.$l.t = pb.mix(this.t0, this.t1, this.factor); - this.color = pb.vec4(pb.add(pb.mul(this.color.rgb, this.t.a), this.t.rgb), this.color.a); - //this.color = pb.vec4(pb.vec3(pb.mix(this.u0, this.u1, this.factor)), this.color.a); - }); - scope[funcName](worldPos, color); - } else { - const funcName = 'Z_applyFog'; - pb.func(funcName, [pb.vec3('worldPos'), pb.vec4('color').inout()], function () { - this.$l.viewDir = pb.sub(this.worldPos, that.getCameraPosition(this)); - this.$l.fogFactor = that.computeFogFactor( - this, - this.viewDir, - that.getFogType(this), - that.getFogParams(this) - ); - this.color = pb.vec4( - pb.mix(this.color.rgb, that.getFogColor(this).rgb, this.fogFactor), - this.color.a - ); - }); - scope[funcName](worldPos, color); - } + this.color = pb.vec4(pb.add(pb.mul(this.color.rgb, this.t.a), this.t.rgb), this.color.a); + //this.color = pb.vec4(pb.vec3(pb.mix(this.u0, this.u1, this.factor)), this.color.a); + }); + scope[funcName](worldPos, color); + } else if (ctx.applyFog) { + const funcName = 'Z_applyFog'; + pb.func(funcName, [pb.vec3('worldPos'), pb.vec4('color').inout()], function () { + this.$l.viewDir = pb.sub(this.worldPos, that.getCameraPosition(this)); + this.$l.fogFactor = that.computeFogFactor( + this, + this.viewDir, + that.getFogType(this), + that.getFogParams(this) + ); + this.color = pb.vec4( + pb.mix(this.color.rgb, that.getFogColor(this).rgb, this.fogFactor), + this.color.a + ); + }); + scope[funcName](worldPos, color); } } /** diff --git a/libs/scene/src/material/terrainmaterial.ts b/libs/scene/src/material/terrainmaterial.ts index 164b04eb..67a9d98e 100644 --- a/libs/scene/src/material/terrainmaterial.ts +++ b/libs/scene/src/material/terrainmaterial.ts @@ -5,6 +5,7 @@ import type { PBFunctionScope, PBInsideFunctionScope, PBShaderExp, + RenderStateSet, Texture2D, Texture2DArray } from '@zephyr3d/device'; @@ -15,6 +16,7 @@ import { Application } from '../app'; import { Vector4 } from '@zephyr3d/base'; import { drawFullscreenQuad } from '../render/fullscreenquad'; import { ShaderHelper } from './shader/helper'; +import { RENDER_PASS_TYPE_SHADOWMAP } from '../values'; /** * Terrain detail map information @@ -186,6 +188,13 @@ export class TerrainMaterial extends applyMaterialMixins( supportLighting(): boolean { return true; } + /** + * {@inheritDoc Material.supportInstancing} + * @override + */ + supportInstancing(): boolean { + return false; + } /** * {@inheritDoc Material.isBatchable} * @override @@ -427,4 +436,13 @@ export class TerrainMaterial extends applyMaterialMixins( fb.dispose(); return tex; } + protected updateRenderStates(pass: number, stateSet: RenderStateSet, ctx: DrawContext): void { + super.updateRenderStates(pass, stateSet, ctx); + const isShadowMapPass = ctx.renderPass.type === RENDER_PASS_TYPE_SHADOWMAP; + if (isShadowMapPass) { + stateSet.useRasterizerState().setCullMode('front'); + } else { + stateSet.defaultRasterizerState(); + } + } } diff --git a/libs/scene/src/posteffect/bloom.ts b/libs/scene/src/posteffect/bloom.ts index 3c304c60..2acf9739 100644 --- a/libs/scene/src/posteffect/bloom.ts +++ b/libs/scene/src/posteffect/bloom.ts @@ -7,7 +7,6 @@ import type { Texture2D, TextureSampler } from '@zephyr3d/device'; -import { Application } from '../app'; import { AbstractPostEffect } from './posteffect'; import { TemporalCache, type DrawContext } from '../render'; import { Vector2, Vector4 } from '@zephyr3d/base'; @@ -100,7 +99,7 @@ export class Bloom extends AbstractPostEffect { } /** {@inheritDoc AbstractPostEffect.apply} */ apply(ctx: DrawContext, inputColorTexture: Texture2D, sceneDepthTexture: Texture2D, srgbOutput: boolean) { - const device = Application.instance.device; + const device = ctx.device; const downsampleFramebuffers: FrameBuffer[] = []; this._prepare(device, inputColorTexture); device.pushDeviceStates(); diff --git a/libs/scene/src/posteffect/compositor.ts b/libs/scene/src/posteffect/compositor.ts index 84fd6065..a4b27967 100644 --- a/libs/scene/src/posteffect/compositor.ts +++ b/libs/scene/src/posteffect/compositor.ts @@ -1,4 +1,3 @@ -import { Application } from '../app'; import { linearToGamma } from '../shaders/misc'; import type { DrawContext } from '../render'; import { TemporalCache } from '../render'; @@ -112,7 +111,7 @@ export class Compositor { } /** @internal */ begin(ctx: DrawContext) { - const device = Application.instance.device; + const device = ctx.device; const format = device.getDeviceCaps().textureCaps.supportHalfFloatColorBuffer ? 'rgba16f' : 'rgba8unorm'; const finalFramebuffer = device.getFramebuffer(); const depth = finalFramebuffer?.getDepthAttachment() as Texture2D; @@ -220,7 +219,7 @@ export class Compositor { drawPostEffects(ctx: DrawContext, opaque: boolean, sceneDepthTexture: Texture2D) { const postEffects = opaque ? this._postEffectsOpaque : this._postEffectsTransparency; if (postEffects.length > 0) { - const device = Application.instance.device; + const device = ctx.device; for (let i = 0; i < postEffects.length; i++) { const postEffect = postEffects[i]; if (!postEffect.enabled) { @@ -246,7 +245,7 @@ export class Compositor { } /** @internal */ end(ctx: DrawContext) { - const device = Application.instance.device; + const device = ctx.device; if (device.getFramebuffer() !== ctx.compositorContex.finalFramebuffer) { const srcTex = device.getFramebuffer().getColorAttachments()[0] as Texture2D; device.setFramebuffer(ctx.compositorContex.finalFramebuffer); diff --git a/libs/scene/src/posteffect/fxaa.ts b/libs/scene/src/posteffect/fxaa.ts index ac8a3c0e..6d11e088 100644 --- a/libs/scene/src/posteffect/fxaa.ts +++ b/libs/scene/src/posteffect/fxaa.ts @@ -1,5 +1,4 @@ import { Vector2 } from '@zephyr3d/base'; -import { Application } from '../app'; import { AbstractPostEffect } from './posteffect'; import { linearToGamma } from '../shaders/misc'; import type { AbstractDevice, BindGroup, GPUProgram, Texture2D, TextureSampler } from '@zephyr3d/device'; @@ -39,7 +38,7 @@ export class FXAA extends AbstractPostEffect { } /** {@inheritDoc AbstractPostEffect.apply} */ apply(ctx: DrawContext, inputColorTexture: Texture2D, sceneDepthTexture: Texture2D, srgbOutput: boolean) { - const device = Application.instance.device; + const device = ctx.device; this._prepare(device); this._invTexSize.setXY(1 / inputColorTexture.width, 1 / inputColorTexture.height); this._bindgroup.setTexture('srcTex', inputColorTexture, FXAA._sampler); diff --git a/libs/scene/src/posteffect/grayscale.ts b/libs/scene/src/posteffect/grayscale.ts index b4b33ea6..144a53db 100644 --- a/libs/scene/src/posteffect/grayscale.ts +++ b/libs/scene/src/posteffect/grayscale.ts @@ -1,4 +1,3 @@ -import { Application } from '../app'; import { AbstractPostEffect } from './posteffect'; import { linearToGamma } from '../shaders/misc'; import type { AbstractDevice, BindGroup, GPUProgram, Texture2D, TextureSampler } from '@zephyr3d/device'; @@ -36,7 +35,7 @@ export class Grayscale extends AbstractPostEffect { } /** {@inheritDoc AbstractPostEffect.apply} */ apply(ctx: DrawContext, inputColorTexture: Texture2D, sceneDepthTexture: Texture2D, srgbOutput: boolean) { - const device = Application.instance.device; + const device = ctx.device; this._prepare(device); this._bindgroup.setTexture('srcTex', inputColorTexture, Grayscale._sampler); this._bindgroup.setValue('flip', this.needFlip(device) ? 1 : 0); diff --git a/libs/scene/src/posteffect/posteffect.ts b/libs/scene/src/posteffect/posteffect.ts index b58a88b5..46105c37 100644 --- a/libs/scene/src/posteffect/posteffect.ts +++ b/libs/scene/src/posteffect/posteffect.ts @@ -1,13 +1,4 @@ -import { Application } from '../app'; -import type { - AbstractDevice, - BaseTexture, - FrameBuffer, - RenderStateSet, - Texture2D, - TextureFormat, - VertexLayout -} from '@zephyr3d/device'; +import type { AbstractDevice, RenderStateSet, Texture2D, VertexLayout } from '@zephyr3d/device'; import type { DrawContext } from '../render'; import { drawFullscreenQuad } from '../render/fullscreenquad'; @@ -21,12 +12,6 @@ export abstract class AbstractPostEffect { protected _quadRenderStateSet: RenderStateSet; protected _enabled: boolean; protected _opaque: boolean; - protected _intermediateFramebuffers: { - [name: string]: { - framebuffer: FrameBuffer; - depth: 'none' | 'current' | 'temporal'; - }; - }; /** * Creates an instance of a post effect * @param name - Name of the post effect @@ -37,7 +22,6 @@ export abstract class AbstractPostEffect { this._quadRenderStateSet = null; this._enabled = true; this._opaque = false; - this._intermediateFramebuffers = {}; } /** Whether this post effect is enabled */ get enabled(): boolean { @@ -58,71 +42,6 @@ export abstract class AbstractPostEffect { needFlip(device: AbstractDevice): boolean { return device.type === 'webgpu' && !!device.getFramebuffer(); } - /** - * Adds an intermediate frame buffer - * @param name - Name of the frame buffer - * @param format - Render target texture format - * @param useDepth - Whether the scene depth buffer should be attached to the frame buffer - */ - protected addIntermediateFramebuffer(name: string, depth: 'none' | 'current' | 'temporal') { - if (this._intermediateFramebuffers[name]) { - throw new Error(`Intermediate framebuffer already exists: ${name}`); - } - this._intermediateFramebuffers[name] = { - depth, - framebuffer: null - }; - } - /** - * Gets the intermediate frame buffer by name - * @param name - Name of the intermediate frame buffer - * @param width - Width of the frame buffer - * @param height - Height of the frame buffer - * @returns The intermediate frame buffer or null if not exists - * - * @remarks - * The intemediate buffer will be resized to fit the given size if needed - */ - protected getIntermediateFramebuffer( - name: string, - format: TextureFormat, - width: number, - height: number - ): FrameBuffer { - const fb = this._intermediateFramebuffers[name]; - if (!fb) { - return null; - } - const device = Application.instance.device; - const currentDepthBuffer = device.getFramebuffer().getDepthAttachment(); - if (fb.framebuffer) { - const colorTex = fb.framebuffer.getColorAttachments()[0]; - const depthTex = fb.framebuffer.getDepthAttachment(); - if (colorTex.width !== width || colorTex.height !== height || colorTex.format !== format) { - fb.framebuffer.dispose(); - colorTex.dispose(); - if (depthTex && depthTex !== currentDepthBuffer) { - depthTex.dispose(); - } - fb.framebuffer = null; - } - } - if (!fb.framebuffer) { - const colorTex = device.createTexture2D(format, width, height, { - samplerOptions: { mipFilter: 'none' } - }); - colorTex.name = `Intermediate-<${name}>`; - let depthTex: BaseTexture = null; - if (fb.depth === 'current') { - depthTex = currentDepthBuffer; - } else if (fb.depth === 'temporal') { - depthTex = device.createTexture2D('d24s8', width, height); - depthTex.name = `Intermediate-<${name}>-depth`; - } - fb.framebuffer = device.createFrameBuffer([colorTex], depthTex); - } - return fb.framebuffer; - } /** * Checks whether this post effect requires the linear depth texture * @returns true if the linear depth texture is required. @@ -156,15 +75,6 @@ export abstract class AbstractPostEffect { this._quadVertexLayout?.dispose(); this._quadVertexLayout = null; this._quadRenderStateSet = null; - for (const k in this._intermediateFramebuffers) { - const fb = this._intermediateFramebuffers[k]; - if (fb) { - const colorAttachment = fb.framebuffer.getColorAttachments()[0]; - fb.framebuffer.dispose(); - colorAttachment.dispose(); - } - } - this._intermediateFramebuffers = {}; } /** * Draws a fullscreen quad diff --git a/libs/scene/src/posteffect/sao.ts b/libs/scene/src/posteffect/sao.ts index 9c50d360..a412bbc9 100644 --- a/libs/scene/src/posteffect/sao.ts +++ b/libs/scene/src/posteffect/sao.ts @@ -7,7 +7,6 @@ import type { TextureSampler } from '@zephyr3d/device'; import { isFloatTextureFormat } from '@zephyr3d/device'; -import { Application } from '../app'; import { AbstractPostEffect } from './posteffect'; import { decodeNormalizedFloatFromRGBA, encodeNormalizedFloatToRGBA } from '../shaders/misc'; import { Matrix4x4, Vector2, Vector4 } from '@zephyr3d/base'; @@ -64,8 +63,6 @@ export class SAO extends AbstractPostEffect { this._blitterV.kernelRadius = 8; this._blitterV.stdDev = 10; this._copyBlitter = new CopyBlitter(); - this.addIntermediateFramebuffer('ao', 'current'); - this.addIntermediateFramebuffer('blur', 'current'); } /** Scale value */ get scale(): number { @@ -136,7 +133,7 @@ export class SAO extends AbstractPostEffect { } /** {@inheritDoc AbstractPostEffect.apply} */ apply(ctx: DrawContext, inputColorTexture: Texture2D, sceneDepthTexture: Texture2D, srgbOutput: boolean) { - const device = Application.instance.device; + const device = ctx.device; const viewport = device.getViewport(); this._prepare(device, inputColorTexture); this._copyBlitter.srgbOut = srgbOutput; diff --git a/libs/scene/src/posteffect/tonemap.ts b/libs/scene/src/posteffect/tonemap.ts index a2a481e0..5ab2f4c9 100644 --- a/libs/scene/src/posteffect/tonemap.ts +++ b/libs/scene/src/posteffect/tonemap.ts @@ -1,4 +1,3 @@ -import { Application } from '../app'; import { AbstractPostEffect } from './posteffect'; import { linearToGamma } from '../shaders/misc'; import type { AbstractDevice, BindGroup, GPUProgram, Texture2D, TextureSampler } from '@zephyr3d/device'; @@ -39,7 +38,7 @@ export class Tonemap extends AbstractPostEffect { } /** {@inheritDoc AbstractPostEffect.apply} */ apply(ctx: DrawContext, inputColorTexture: Texture2D, sceneDepthTexture: Texture2D, srgbOutput: boolean) { - const device = Application.instance.device; + const device = ctx.device; this._prepare(device, inputColorTexture); this._tonemap(device, inputColorTexture, srgbOutput); } diff --git a/libs/scene/src/posteffect/water.ts b/libs/scene/src/posteffect/water.ts index 7e942bfe..0c664971 100644 --- a/libs/scene/src/posteffect/water.ts +++ b/libs/scene/src/posteffect/water.ts @@ -9,7 +9,6 @@ import type { DrawContext } from '../render'; import { TemporalCache, WaterMesh } from '../render'; import { AbstractPostEffect } from './posteffect'; import { decodeNormalizedFloatFromRGBA, linearToGamma } from '../shaders'; -import { Application } from '../app'; import { Interpolator, Matrix4x4, Plane, Vector2, Vector3, Vector4 } from '@zephyr3d/base'; import { Camera } from '../camera'; import { CopyBlitter } from '../blitter'; @@ -79,7 +78,6 @@ export class PostWater extends AbstractPostEffect { new Float32Array([0, 0, 0, 0.08, 0.41, 0.34, 0.13, 0.4, 0.45, 0.21, 0.5, 0.6]) ); this._rampTex = null; - this.addIntermediateFramebuffer('reflection', 'temporal'); this._foamWidth = 1.2; this._foamContrast = 7.2; this._waterWireframe = false; @@ -286,7 +284,7 @@ export class PostWater extends AbstractPostEffect { } /** {@inheritDoc AbstractPostEffect.apply} */ apply(ctx: DrawContext, inputColorTexture: Texture2D, sceneDepthTexture: Texture2D, srgbOutput: boolean) { - const device = Application.instance.device; + const device = ctx.device; const rampTex = this._getRampTexture(device); this._copyBlitter.srgbOut = srgbOutput; this._copyBlitter.blit( @@ -367,7 +365,6 @@ export class PostWater extends AbstractPostEffect { 'targetSize', new Vector2(device.getViewport().width, device.getViewport().height) ); - waterMesh.bindGroup.setValue('envLightStrength', ctx.env.light.strength); waterMesh.bindGroup.setValue('waterLevel', this._elevation); waterMesh.bindGroup.setValue('srgbOut', srgbOutput ? 1 : 0); if (ctx.sunLight) { @@ -375,7 +372,10 @@ export class PostWater extends AbstractPostEffect { waterMesh.bindGroup.setValue('lightShininess', 0.7); waterMesh.bindGroup.setValue('lightDiffuseAndIntensity', ctx.sunLight.diffuseAndIntensity); } - ctx.env.light.envLight.updateBindGroup(waterMesh.bindGroup); + if (ctx.env.light.envLight) { + waterMesh.bindGroup.setValue('envLightStrength', ctx.env.light.strength); + ctx.env.light.envLight.updateBindGroup(waterMesh.bindGroup); + } waterMesh.render(ctx.camera, this.needFlip(device)); TemporalCache.releaseFramebuffer(fbRefl); } @@ -408,7 +408,7 @@ export class PostWater extends AbstractPostEffect { const hash = `${ctx.sunLight ? 1 : 0}:${ctx.env.light.getHash(ctx)}`; let waterMesh = this._waterMeshes[hash]; if (!waterMesh) { - const device = Application.instance.device; + const device = ctx.device; waterMesh = new WaterMesh(device, { setupUniforms(scope: PBGlobalScope) { const pb = scope.$builder; @@ -424,7 +424,6 @@ export class PostWater extends AbstractPostEffect { scope.cameraPos = pb.vec3().uniform(0); scope.invViewProj = pb.mat4().uniform(0); scope.targetSize = pb.vec2().uniform(0); - scope.envLightStrength = pb.float().uniform(0); scope.waterLevel = pb.float().uniform(0); scope.srgbOut = pb.int().uniform(0); if (ctx.sunLight) { @@ -433,7 +432,10 @@ export class PostWater extends AbstractPostEffect { scope.lightDiffuseAndIntensity = pb.vec4().uniform(0); } } - ctx.env.light.envLight.initShaderBindings(pb); + if (ctx.env.light.envLight) { + scope.envLightStrength = pb.float().uniform(0); + ctx.env.light.envLight.initShaderBindings(pb); + } }, shading( scope: PBInsideFunctionScope, @@ -548,10 +550,12 @@ export class PostWater extends AbstractPostEffect { ); this.finalColor = pb.add(this.finalColor, this.specular); } - const irradiance = ctx.env.light.envLight.getIrradiance(this, this.myNormal); - if (irradiance) { - this.$l.sss = pb.mul(this.getScattering(this.depth), irradiance, this.envLightStrength); - this.finalColor = pb.add(this.finalColor, this.sss); + if (ctx.env.light.envLight) { + const irradiance = ctx.env.light.envLight.getIrradiance(this, this.myNormal); + if (irradiance) { + this.$l.sss = pb.mul(this.getScattering(this.depth), irradiance, this.envLightStrength); + this.finalColor = pb.add(this.finalColor, this.sss); + } } this.$if(pb.notEqual(this.srgbOut, 0), function () { this.finalColor = linearToGamma(this, this.finalColor); diff --git a/libs/scene/src/render/abuffer_oit.ts b/libs/scene/src/render/abuffer_oit.ts new file mode 100644 index 00000000..a37b3bdd --- /dev/null +++ b/libs/scene/src/render/abuffer_oit.ts @@ -0,0 +1,390 @@ +import type { + AbstractDevice, + BindGroup, + DeviceViewport, + GPUDataBuffer, + GPUProgram, + PBGlobalScope, + PBInsideFunctionScope, + PBShaderExp, + RenderStateSet +} from '@zephyr3d/device'; +import { OIT } from './oit'; +import type { DrawContext } from './drawable'; +import { drawFullscreenQuad } from './fullscreenquad'; +import { ShaderHelper } from '../material'; + +/** + * per-pixel linked list OIT renderer using ABuffer. + * + * @remarks + * The ABuffer OIT renderer only supports WebGPU device. + * + * @public + */ +export class ABufferOIT extends OIT { + /** Type name of ABufferOIT */ + public static readonly type = 'ab'; + private static MAX_FRAGMENT_LAYERS = 75; + private static _compositeProgram: GPUProgram = null; + private static _compositeBindGroup: BindGroup = null; + private static _compositeRenderStates: RenderStateSet = null; + private static _ubAlignment = 0; + private _nodeBuffer: GPUDataBuffer; + private _headStagingBuffer: GPUDataBuffer; + private _headBuffer: GPUDataBuffer; + private _scissorOffsetBuffer: GPUDataBuffer; + private _numLayers: number; + private _screenSize: Uint32Array; + private _hash: string; + private _debug: boolean; + private _scissorSlices: number; + private _scissorHeight: number; + private _currentPass: number; + private _savedScissor: DeviceViewport; + /** + * Creates an instance of ABufferOIT class + * + * @param numLayers - How many transparent layers, default is 16 + */ + constructor(numLayers = 16) { + super(); + this._nodeBuffer = null; + this._headStagingBuffer = null; + this._headBuffer = null; + this._scissorOffsetBuffer = null; + this._numLayers = numLayers; + this._screenSize = new Uint32Array([0xffffffff, 0xffffffff]); + this._hash = null; + this._debug = false; + this._scissorSlices = 0; + this._scissorHeight = 0; + this._savedScissor = null; + this._currentPass = 0; + } + /** + * {@inheritDoc OIT.getType} + */ + getType(): string { + return ABufferOIT.type; + } + /** + * {@inheritDoc OIT.supportDevice} + */ + supportDevice(deviceType: string): boolean { + return deviceType === 'webgpu'; + } + /** + * {@inheritDoc OIT.dispose} + */ + dispose() { + this._nodeBuffer?.dispose(); + this._nodeBuffer = null; + this._headStagingBuffer?.dispose(); + this._headStagingBuffer = null; + this._headBuffer?.dispose(); + this._headBuffer = null; + this._scissorOffsetBuffer?.dispose(); + this._scissorOffsetBuffer = null; + this._hash = null; + this._savedScissor = null; + } + /** + * {@inheritDoc OIT.begin} + */ + begin(ctx: DrawContext): number { + const device = ctx.device; + this._savedScissor = device.getScissor(); + const ubAlignment = (ABufferOIT._ubAlignment = + device.getDeviceCaps().shaderCaps.uniformBufferOffsetAlignment); + const viewport = device.getViewport(); + const screenWidth = device.screenToDevice(viewport.width); + const screenHeight = device.screenToDevice(Math.max(viewport.height, 1)); + if (screenWidth !== this._screenSize[0] || screenHeight !== this._screenSize[1]) { + // Resize buffers if viewport was changed + this._screenSize[0] = screenWidth; + this._screenSize[1] = screenHeight; + // compute scissor slices + const maxBufferSize = device.getDeviceCaps().shaderCaps.maxStorageBufferSize; + const bytesPerLine = screenWidth * 4 * 4 * this._numLayers; + this._scissorHeight = (maxBufferSize / bytesPerLine) >> 0; + this._scissorSlices = Math.ceil(screenHeight / this._scissorHeight); + const offsetBufferSize = ubAlignment * this._scissorSlices; + const offsetBuffer = new Uint32Array(offsetBufferSize >> 2); + for (let i = 0; i < this._scissorSlices; i++) { + offsetBuffer[i * (ubAlignment >> 2)] = i * this._scissorHeight; + } + if (!this._scissorOffsetBuffer || this._scissorOffsetBuffer.byteLength < offsetBuffer.byteLength) { + this._scissorOffsetBuffer?.dispose(); + this._scissorOffsetBuffer = device.createBuffer(offsetBuffer.byteLength, { usage: 'uniform' }); + } + this._scissorOffsetBuffer.bufferSubData(0, offsetBuffer); + // resize node buffer + const size = screenWidth * this._scissorHeight * 4; + const nodeBufferSize = size * 4 * this._numLayers; + if (!this._nodeBuffer || nodeBufferSize > this._nodeBuffer.byteLength) { + this._nodeBuffer?.dispose(); + this._nodeBuffer = device.createBuffer(nodeBufferSize, { storage: true, usage: 'uniform' }); + } + // resize head buffer + const headBufferSize = size + 4; + if (!this._headBuffer || headBufferSize > this._headBuffer.byteLength) { + this._headBuffer?.dispose(); + this._headBuffer = device.createBuffer(headBufferSize, { storage: true, usage: 'uniform' }); + this._headStagingBuffer?.dispose(); + this._headStagingBuffer = device.createBuffer(headBufferSize, { storage: true, usage: 'uniform' }); + const tmpArray = new Uint32Array(headBufferSize >> 2); + tmpArray.fill(0xffffffff); + this._headStagingBuffer.bufferSubData(0, tmpArray); + } + } + return this._scissorSlices; + } + /** + * {@inheritDoc OIT.end} + */ + end(ctx: DrawContext) { + // Restore scissor rect + ctx.device.setScissor(this._savedScissor); + } + /** + * {@inheritDoc OIT.setupFragmentOutput} + */ + setupFragmentOutput(scope: PBGlobalScope) { + const pb = scope.$builder; + scope.Z_AB_scissorOffset = pb.uint().uniformBuffer(2); + scope.Z_AB_nodeBuffer = pb.uvec4[0]().storageBuffer(2); + scope.Z_AB_headImage = pb.atomic_uint[0]().storageBuffer(2); + scope.Z_AB_screenSize = pb.uint().uniform(2); + scope.Z_AB_depthTexture = pb.tex2D().uniform(2); + scope.$outputs.outColor = pb.vec4(); + } + /** + * {@inheritDoc OIT.beginPass} + */ + beginPass(ctx: DrawContext, pass: number) { + this._currentPass = pass; + const device = ctx.device; + const scissorY = pass * this._scissorHeight; + const scissorH = + Math.min((pass + 1) * this._scissorHeight, this._screenSize[1]) - pass * this._scissorHeight; + device.setScissor([ + 0, + device.deviceToScreen(this._screenSize[1] - scissorY - scissorH), + device.deviceToScreen(this._screenSize[0]), + device.deviceToScreen(scissorH) + ]); + device.copyBuffer(this._headStagingBuffer, this._headBuffer, 0, 0, this._headStagingBuffer.byteLength); + // Update render hash + this._hash = `${this.getType()}#${this._nodeBuffer.uid}#${this._headBuffer.uid}#${ + this._scissorOffsetBuffer.uid + }#${pass}`; + if (this._debug) { + const data = new Uint8Array(this._headBuffer.byteLength); + const readBuffer = device.createBuffer(this._headBuffer.byteLength, { usage: 'read' }); + device.copyBuffer(this._headBuffer, readBuffer, 0, 0, this._headBuffer.byteLength); + readBuffer.getBufferSubData(data).then(() => { + const uint = new Uint32Array(data.buffer); + for (let i = 0; i < uint.length; i++) { + if (uint[i] !== 0) { + console.error('Clear head buffer failed'); + break; + } + } + readBuffer.dispose(); + }); + } + return true; + } + /** + * {@inheritDoc OIT.endPass} + */ + endPass(ctx: DrawContext, pass: number) { + const device = ctx.device; + ABufferOIT.getCompositeProgram(device); + const lastBindGroup = device.getBindGroup(0); + device.setProgram(ABufferOIT.getCompositeProgram(device)); + const bindGroup = ABufferOIT._compositeBindGroup; + bindGroup.setBuffer( + 'scissorOffset', + this._scissorOffsetBuffer, + 0, + pass * ABufferOIT._ubAlignment, + ABufferOIT._ubAlignment + ); + bindGroup.setBuffer('headBuffer', this._headBuffer); + bindGroup.setBuffer('nodeBuffer', this._nodeBuffer); + bindGroup.setValue('screenWidth', this._screenSize[0]); + device.setBindGroup(0, bindGroup); + drawFullscreenQuad(ABufferOIT._compositeRenderStates); + device.setBindGroup(0, lastBindGroup[0], lastBindGroup[1]); + } + /** + * {@inheritDoc OIT.calculateHash} + */ + calculateHash(): string { + return this._hash; + } + /** + * {@inheritDoc OIT.applyUniforms} + */ + applyUniforms(ctx: DrawContext, bindGroup: BindGroup) { + bindGroup.setBuffer('Z_AB_nodeBuffer', this._nodeBuffer); + bindGroup.setBuffer( + 'Z_AB_scissorOffset', + this._scissorOffsetBuffer, + 0, + this._currentPass * ABufferOIT._ubAlignment, + ABufferOIT._ubAlignment + ); + bindGroup.setBuffer('Z_AB_headImage', this._headBuffer); + bindGroup.setValue('Z_AB_screenSize', this._screenSize[0]); + bindGroup.setTexture('Z_AB_depthTexture', ctx.linearDepthTexture); + } + /** + * {@inheritDoc OIT.outputFragmentColor} + */ + outputFragmentColor(scope: PBInsideFunctionScope, color: PBShaderExp) { + const pb = scope.$builder; + // linear depth of current fragment + scope.$l.fragDepth = ShaderHelper.nonLinearDepthToLinearNormalized(scope, scope.$builtins.fragCoord.z); + // linear depth in depth texture + scope.$l.linearDepth = pb.textureLoad( + scope.Z_AB_depthTexture, + pb.ivec2(scope.$builtins.fragCoord.xy), + 0 + ).r; + // saved to buffer only if nothing is infront + scope.$if(pb.lessThan(scope.fragDepth, scope.linearDepth), function () { + this.$l.Z_AB_pixelCount = pb.atomicAdd(this.Z_AB_headImage.at(0), 1); + this.$l.Z_AB_nodeOffset = this.Z_AB_pixelCount; + // save if index not exceeded + this.$if(pb.lessThan(this.Z_AB_nodeOffset, pb.arrayLength(this.Z_AB_nodeBuffer)), function () { + this.$l.Z_AB_headOffset = pb.add( + pb.mul(this.Z_AB_screenSize, pb.sub(pb.uint(this.$builtins.fragCoord.y), this.Z_AB_scissorOffset)), + pb.uint(this.$builtins.fragCoord.x) + ); + this.$l.Z_AB_oldHead = pb.atomicExchange( + this.Z_AB_headImage.at(pb.add(this.Z_AB_headOffset, 1)), + this.Z_AB_nodeOffset + ); + this.$l.Z_AB_colorScale = pb.floatBitsToUint(pb.length(color.rgb)); + this.$l.Z_AB_color = pb.pack4x8unorm(pb.vec4(pb.normalize(color.rgb), pb.clamp(color.a, 0, 1))); + this.$l.Z_AB_depth = pb.floatBitsToUint(this.fragDepth); + this.Z_AB_nodeBuffer.setAt( + this.Z_AB_nodeOffset, + pb.uvec4(this.Z_AB_color, this.Z_AB_colorScale, this.Z_AB_depth, this.Z_AB_oldHead) + ); + pb.discard; + }); + }); + return true; + } + /** + * {@inheritDoc OIT.setRenderStates} + */ + setRenderStates(rs: RenderStateSet) { + const stencilStates = rs.useStencilState(); + stencilStates.enable(true); + stencilStates.setFrontCompareFunc('always'); + stencilStates.setBackCompareFunc('always'); + stencilStates.setFrontOp('keep', 'keep', 'replace'); + stencilStates.setBackOp('keep', 'keep', 'replace'); + stencilStates.setReference(1); + stencilStates.setReadMask(0xff); + } + /** @internal */ + private static getCompositeProgram(device: AbstractDevice) { + if (!this._compositeProgram) { + this._compositeProgram = device.buildRenderProgram({ + vertex(pb) { + this.$inputs.pos = pb.vec2().attrib('position'); + this.$outputs.uv = pb.vec2(); + pb.main(function () { + this.$builtins.position = pb.vec4(this.$inputs.pos, 1, 1); + this.$outputs.uv = pb.add(pb.mul(this.$inputs.pos.xy, 0.5), pb.vec2(0.5)); + if (device.type === 'webgpu') { + this.$builtins.position.y = pb.neg(this.$builtins.position.y); + } + }); + }, + fragment(pb) { + this.$outputs.outColor = pb.vec4(); + this.scissorOffset = pb.uint().uniformBuffer(0); + this.headBuffer = pb.uint[0]().storageBuffer(0); + this.nodeBuffer = pb.uvec4[0]().storageBuffer(0); + this.screenWidth = pb.uint().uniform(0); + pb.func('unpackColor', [pb.uvec4('x')], function () { + this.$l.colorNorm = pb.unpack4x8unorm(this.x.x); + this.$l.colorScale = pb.uintBitsToFloat(this.x.y); + this.$return(pb.vec4(pb.mul(this.colorNorm.rgb, this.colorScale), this.colorNorm.a)); + }); + pb.main(function () { + this.$l.fragmentArray = pb.uvec4[ABufferOIT.MAX_FRAGMENT_LAYERS](); + this.$l.fragmentArrayLen = pb.uint(0); + this.$l.offset = pb.add( + pb.mul(this.screenWidth, pb.sub(pb.uint(this.$builtins.fragCoord.y), this.scissorOffset)), + pb.uint(this.$builtins.fragCoord.x) + ); + //this.$l.head = this.headBuffer.at(this.offset); + this.$l.head = this.headBuffer.at(pb.add(this.offset, 1)); + this.$while( + pb.and( + pb.lessThan(this.fragmentArrayLen, ABufferOIT.MAX_FRAGMENT_LAYERS), + pb.notEqual(this.head, 0xffffffff) + ), + function () { + this.fragmentArray.setAt(this.fragmentArrayLen, this.nodeBuffer.at(this.head)); + this.head = this.fragmentArray.at(this.fragmentArrayLen).w; + this.fragmentArrayLen = pb.add(this.fragmentArrayLen, 1); + } + ); + this.$if(pb.equal(this.fragmentArrayLen, 0), function () { + this.$outputs.outColor = pb.vec4(0, 0, 0, 1); + }).$else(function () { + // bubble sort + this.$for(pb.uint('i'), 0, pb.sub(this.fragmentArrayLen, 1), function () { + this.$for(pb.uint('j'), 0, pb.sub(pb.sub(this.fragmentArrayLen, 1), this.i), function () { + this.$l.a = this.fragmentArray.at(this.j); + this.$l.b = this.fragmentArray.at(pb.add(this.j, 1)); + this.$if(pb.greaterThan(this.a.z, this.b.z), function () { + this.fragmentArray.setAt(this.j, this.b); + this.fragmentArray.setAt(pb.add(this.j, 1), this.a); + }); + }); + }); + // under operator blending + this.$l.c0 = this.unpackColor(this.fragmentArray[0]); + this.$l.c_dst = pb.mul(this.c0.rgb, this.c0.a); + this.$l.a_dst = pb.sub(1, this.c0.a); + this.$for(pb.uint('i'), 1, this.fragmentArrayLen, function () { + this.$l.c = this.unpackColor(this.fragmentArray.at(this.i)); + this.c_dst = pb.add(pb.mul(this.c.rgb, this.c.a, this.a_dst), this.c_dst); + this.a_dst = pb.mul(this.a_dst, pb.sub(1, this.c.a)); + }); + this.$outputs.outColor = pb.vec4(this.c_dst, this.a_dst); + }); + }); + } + }); + this._compositeBindGroup = device.createBindGroup(this._compositeProgram.bindGroupLayouts[0]); + this._compositeRenderStates = device.createRenderStateSet(); + this._compositeRenderStates + .useBlendingState() + .enable(true) + .setBlendFuncRGB('one', 'src-alpha') + .setBlendFuncAlpha('zero', 'one'); + this._compositeRenderStates.useDepthState().enableTest(false).enableWrite(false); + const stencilStates = this._compositeRenderStates.useStencilState(); + stencilStates.enable(false); + stencilStates.setFrontCompareFunc('always'); + stencilStates.setBackCompareFunc('always'); + stencilStates.setFrontOp('keep', 'keep', 'replace'); + stencilStates.setBackOp('keep', 'keep', 'replace'); + stencilStates.setReference(0); + stencilStates.setReadMask(0xff); + console.log(this._compositeProgram.getShaderSource('fragment')); + } + return this._compositeProgram; + } +} diff --git a/libs/scene/src/render/depthpass.ts b/libs/scene/src/render/depthpass.ts index 26ed35f1..ed56f28b 100644 --- a/libs/scene/src/render/depthpass.ts +++ b/libs/scene/src/render/depthpass.ts @@ -1,6 +1,5 @@ import { RenderPass } from './renderpass'; import { RENDER_PASS_TYPE_DEPTH } from '../values'; -import { Application } from '../app'; import type { RenderQueue } from './render_queue'; import type { DrawContext } from './drawable'; import { ShaderHelper } from '../material/shader/helper'; @@ -25,26 +24,24 @@ export class DepthPass extends RenderPass { } /** @internal */ protected renderItems(ctx: DrawContext, renderQueue: RenderQueue) { - ctx.target = null; - ctx.applyFog = false; + ctx.applyFog = null; ctx.drawEnvLight = false; ctx.env = null; - ctx.renderPassHash = null; - ctx.flip = this.isAutoFlip(); - const device = Application.instance.device; - const bindGroup = this.getGlobalBindGroupInfo(ctx).bindGroup; - device.setBindGroup(0, bindGroup); - ShaderHelper.setCameraUniforms(bindGroup, ctx, true); + ctx.flip = this.isAutoFlip(ctx); ctx.renderPassHash = this.getGlobalBindGroupHash(ctx); + const bindGroup = ctx.globalBindGroupAllocator.getGlobalBindGroup(ctx); + ctx.device.setBindGroup(0, bindGroup); + ShaderHelper.setCameraUniforms(bindGroup, ctx.camera, ctx.flip, true); const reverseWinding = ctx.camera.worldMatrixDet < 0; for (const order of Object.keys(renderQueue.items) .map((val) => Number(val)) .sort((a, b) => a - b)) { const renderItems = renderQueue.items[order]; - for (const item of renderItems.opaqueList) { - ctx.instanceData = item.instanceData; - ctx.target = item.drawable; - this.drawItem(device, item, ctx, reverseWinding); + for (const lit of renderItems.opaque.lit) { + this.drawItemList(lit, ctx, reverseWinding); + } + for (const unlit of renderItems.opaque.unlit) { + this.drawItemList(unlit, ctx, reverseWinding); } } } diff --git a/libs/scene/src/render/drawable.ts b/libs/scene/src/render/drawable.ts index 0ecf8bb0..a812c63b 100644 --- a/libs/scene/src/render/drawable.ts +++ b/libs/scene/src/render/drawable.ts @@ -1,35 +1,44 @@ import type { Matrix4x4, Vector4 } from '@zephyr3d/base'; -import type { Texture2D, TextureFormat } from '@zephyr3d/device'; +import type { AbstractDevice, Texture2D, TextureFormat } from '@zephyr3d/device'; import type { XForm } from '../scene/xform'; import type { Camera } from '../camera/camera'; -import type { RenderPass } from '.'; -import type { CachedBindGroup, InstanceData } from './render_queue'; +import type { FogType, RenderPass } from '.'; +import type { DrawableInstanceInfo, InstanceData, RenderQueue, RenderQueueRef } from './render_queue'; import type { ShadowMapParams } from '../shadow'; import type { Environment } from '../scene/environment'; import type { DirectionalLight, GraphNode, PunctualLight, Scene } from '../scene'; import type { Compositor, CompositorContext } from '../posteffect'; import type { RenderLogger } from '../logger/logger'; import type { ClusteredLight } from './cluster_light'; +import type { Material } from '../material'; +import type { GlobalBindGroupAllocator } from './globalbindgroup_allocator'; +import type { OIT } from './oit'; /** * The context for drawing objects * @public */ export interface DrawContext { + /** Render device */ + device: AbstractDevice; /** The camera position of the primary render pass */ primaryCamera: Camera; + /** The render queue which is currently being rendered */ + renderQueue?: RenderQueue; + /** Global bind group allocator */ + globalBindGroupAllocator: GlobalBindGroupAllocator; /** The camera for current drawing task */ camera: Camera; + /** OIT */ + oit: OIT; /** The scene that is currently been drawing */ scene: Scene; - /** The object that is currently being drawn */ - target: Drawable; /** The render pass to which the current drawing task belongs */ renderPass: RenderPass; /** Hash value for the drawing task */ renderPassHash: string; /** Whether should apply fog to fragment */ - applyFog: boolean; + applyFog: FogType; /** Wether should flip upside down */ flip: boolean; /** Whether current render pass is base light pass */ @@ -74,6 +83,10 @@ export interface DrawContext { clusteredLight?: ClusteredLight; /** render logger */ logger?: RenderLogger; + /** Whether skin animation is used */ + skinAnimation?: boolean; + /** Whehter instance rendering is used */ + instancing?: boolean; } /** @@ -99,6 +112,11 @@ export interface Drawable { getQueueType(): number; /** true if the shading of this object is independent of lighting */ isUnlit(): boolean; + /** Gets the associated material */ + getMaterial(): Material; + /** Set render queue reference */ + pushRenderQueueRef(ref: RenderQueueRef); + applyTransformUniforms(renderQueue: RenderQueue): void; /** * Draw the object * @param ctx - Context of the drawing task @@ -124,12 +142,6 @@ export interface BatchDrawable extends Drawable { * Gets the instance uniforms */ getInstanceUniforms(): Float32Array; - /** - * Sets the uniform data buffer and offset - */ - setInstanceDataBuffer(renderPass: RenderPass, bindGroup: CachedBindGroup, offset: number); - /** - * Gets the uniform data buffer offset - */ - getInstanceDataBuffer(renderPass: RenderPass): { bindGroup: CachedBindGroup; offset: number }; + applyInstanceOffsetAndStride(renderQueue: RenderQueue, stride: number, offset: number): void; + applyMaterialUniforms(instanceInfo: DrawableInstanceInfo); } diff --git a/libs/scene/src/render/drawable_mixin.ts b/libs/scene/src/render/drawable_mixin.ts new file mode 100644 index 00000000..8a54f0a2 --- /dev/null +++ b/libs/scene/src/render/drawable_mixin.ts @@ -0,0 +1,140 @@ +import type { GenericConstructor } from '@zephyr3d/base'; +import type { AbstractDevice } from '@zephyr3d/device'; +import { ProgramBuilder, type BindGroup } from '@zephyr3d/device'; +import type { BatchDrawable, DrawContext, Drawable } from './drawable'; +import { ShaderHelper } from '../material'; +import type { DrawableInstanceInfo, RenderQueue, RenderQueueRef } from './render_queue'; +import { Application } from '../app'; +import type { Mesh, XForm } from '../scene'; + +export interface IMixinDrawable { + pushRenderQueueRef(ref: RenderQueueRef): void; + applyInstanceOffsetAndStride(renderQueue: RenderQueue, stride: number, offset: number): void; + applyTransformUniforms(renderQueue: RenderQueue): void; + applyMaterialUniforms(instanceInfo: DrawableInstanceInfo): void; + bind(ctx: DrawContext): void; +} + +export function mixinDrawable< + T extends GenericConstructor<{ + getXForm(): XForm; + }> +>(baseCls?: T): T & { new (...args: any[]): IMixinDrawable } { + const cls = class extends baseCls { + private _mdRenderQueueRef: RenderQueueRef[]; + private _mdDrawableBindGroup: BindGroup; + private _mdDrawableBindGroupInstanced: Map; + private _mdDrawableBindGroupSkin: BindGroup; + constructor(...args: any[]) { + super(...args); + this._mdRenderQueueRef = []; + this._mdDrawableBindGroup = null; + this._mdDrawableBindGroupInstanced = new Map(); + this._mdDrawableBindGroupSkin = null; + this.getXForm().on('transformchanged', (node) => { + for (const ref of this._mdRenderQueueRef) { + if (ref.ref) { + this.applyTransformUniforms(ref.ref); + } + } + }); + } + pushRenderQueueRef(ref: RenderQueueRef): void { + this.renderQueueRefPrune(); + this._mdRenderQueueRef.push(ref); + } + renderQueueRefPrune() { + while (this._mdRenderQueueRef.length > 0) { + const ref = this._mdRenderQueueRef[this._mdRenderQueueRef.length - 1].ref; + if (!ref) { + this._mdRenderQueueRef.pop(); + } else { + return ref; + } + } + return null; + } + applyInstanceOffsetAndStride(renderQueue: RenderQueue, stride: number, offset: number): void { + const drawableBindGroup = this.getDrawableBindGroup(Application.instance.device, true, renderQueue); + drawableBindGroup.setValue(ShaderHelper.getInstanceDataStrideUniformName(), stride >> 2); + drawableBindGroup.setValue(ShaderHelper.getInstanceDataOffsetUniformName(), offset >> 2); + } + applyTransformUniforms(renderQueue: RenderQueue): void { + const instanceInfo = renderQueue.getInstanceInfo(this as unknown as Drawable); + const drawableBindGroup = this.getDrawableBindGroup( + Application.instance.device, + !!instanceInfo, + renderQueue + ); + if (instanceInfo) { + instanceInfo.bindGroup.bindGroup.setRawData( + ShaderHelper.getInstanceDataUniformName(), + instanceInfo.offset * 4, + this.getXForm().worldMatrix, + 0, + 16 + ); + } else { + drawableBindGroup.setValue(ShaderHelper.getWorldMatrixUniformName(), this.getXForm().worldMatrix); + } + } + applyMaterialUniforms(instanceInfo: DrawableInstanceInfo): void { + const uniforms = (this as unknown as BatchDrawable).getInstanceUniforms(); + if (uniforms) { + instanceInfo.bindGroup.bindGroup.setRawData( + ShaderHelper.getInstanceDataUniformName(), + (instanceInfo.offset + 16) * 4, + uniforms, + 0, + uniforms.length + ); + } + } + /** @internal */ + bind(ctx: DrawContext): void { + const device = ctx.device; + const drawableBindGroup = this.getDrawableBindGroup(device, !!ctx.instanceData, ctx.renderQueue); + device.setBindGroup(1, drawableBindGroup); + device.setBindGroup(3, ctx.instanceData ? ctx.instanceData.bindGroup.bindGroup : null); + if (ctx.skinAnimation) { + const boneTexture = (this as unknown as Mesh).getBoneMatrices(); + drawableBindGroup.setTexture(ShaderHelper.getBoneMatricesUniformName(), boneTexture); + drawableBindGroup.setValue( + ShaderHelper.getBoneInvBindMatrixUniformName(), + (this as unknown as Mesh).getInvBindMatrix() + ); + drawableBindGroup.setValue(ShaderHelper.getBoneTextureSizeUniformName(), boneTexture.width); + } + } + /** @internal */ + getDrawableBindGroup(device: AbstractDevice, instancing: boolean, renderQueue: RenderQueue): BindGroup { + const skinning = !!(this as unknown as Drawable).getBoneMatrices(); + let bindGroup = skinning + ? this._mdDrawableBindGroupSkin + : instancing + ? this._mdDrawableBindGroupInstanced.get(renderQueue) + : this._mdDrawableBindGroup; + if (!bindGroup) { + const buildInfo = new ProgramBuilder(device).buildRender({ + vertex(pb) { + ShaderHelper.vertexShaderDrawableStuff(this, skinning, instancing); + pb.main(function () {}); + }, + fragment(pb) { + pb.main(function () {}); + } + }); + bindGroup = device.createBindGroup(buildInfo[2][1]); + if (skinning) { + this._mdDrawableBindGroupSkin = bindGroup; + } else if (instancing) { + this._mdDrawableBindGroupInstanced.set(renderQueue, bindGroup); + } else { + this._mdDrawableBindGroup = bindGroup; + } + } + return bindGroup; + } + }; + return cls as unknown as T & { new (...args: any[]): IMixinDrawable }; +} diff --git a/libs/scene/src/render/globalbindgroup_allocator.ts b/libs/scene/src/render/globalbindgroup_allocator.ts new file mode 100644 index 00000000..4fcaaf7f --- /dev/null +++ b/libs/scene/src/render/globalbindgroup_allocator.ts @@ -0,0 +1,47 @@ +import type { BindGroup, BindGroupLayout } from '@zephyr3d/device'; +import type { DrawContext } from './drawable'; +import { ShaderHelper } from '../material'; + +export class GlobalBindGroupAllocator { + static _layouts: Record = {}; + static _allocators: GlobalBindGroupAllocator[] = []; + private _bindGroups: Record; + constructor() { + this._bindGroups = {}; + } + static get(): GlobalBindGroupAllocator { + return this._allocators.pop() ?? new GlobalBindGroupAllocator(); + } + static release(allocator: GlobalBindGroupAllocator) { + this._allocators.push(allocator); + } + /** + * Allocate global bind group according to current draw context + * @param ctx - Draw context + * @returns Global bind group + */ + getGlobalBindGroup(ctx: DrawContext): BindGroup { + const hash = ctx.renderPassHash; + let bindGroup = this._bindGroups[hash]; + if (!bindGroup) { + let layout = GlobalBindGroupAllocator._layouts[hash]; + if (!layout) { + const ret = ctx.device.programBuilder.buildRender({ + vertex(pb) { + ShaderHelper.prepareVertexShader(pb, ctx); + pb.main(function () {}); + }, + fragment(pb) { + ShaderHelper.prepareFragmentShader(pb, ctx); + pb.main(function () {}); + } + }); + layout = ret[2][0]; + GlobalBindGroupAllocator._layouts[hash] = layout; + } + bindGroup = ctx.device.createBindGroup(layout); + this._bindGroups[hash] = bindGroup; + } + return bindGroup; + } +} diff --git a/libs/scene/src/render/index.ts b/libs/scene/src/render/index.ts index ef80e740..f6460de6 100644 --- a/libs/scene/src/render/index.ts +++ b/libs/scene/src/render/index.ts @@ -1,4 +1,3 @@ -export * from './renderscheme'; export * from './drawable'; export * from './renderpass'; export * from './renderer'; @@ -13,3 +12,6 @@ export * from './primitive'; export * from './cull_visitor'; export * from './temporalcache'; export * from './watermesh'; +export * from './oit'; +export * from './weightedblended_oit'; +export * from './abuffer_oit'; diff --git a/libs/scene/src/render/lightpass.ts b/libs/scene/src/render/lightpass.ts index ad8a0ac5..b75b93fc 100644 --- a/libs/scene/src/render/lightpass.ts +++ b/libs/scene/src/render/lightpass.ts @@ -1,9 +1,7 @@ import { RenderPass } from './renderpass'; import { QUEUE_OPAQUE, QUEUE_TRANSPARENT, RENDER_PASS_TYPE_LIGHT } from '../values'; -import { Application } from '../app'; import { Vector4 } from '@zephyr3d/base'; -import type { RenderQueueItem } from './render_queue'; -import type { RenderQueue } from './render_queue'; +import type { RenderItemListBundle, RenderQueue } from './render_queue'; import type { PunctualLight } from '../scene/light'; import type { DrawContext } from './drawable'; import { ShaderHelper } from '../material/shader/helper'; @@ -24,89 +22,125 @@ export class LightPass extends RenderPass { } /** @internal */ protected _getGlobalBindGroupHash(ctx: DrawContext) { - return `${this._shadowMapHash}:${ctx.env.getHash(ctx)}`; + return `lp:${this._shadowMapHash}:${ctx.linearDepthTexture?.uid ?? ''}:${ + ctx.oit?.calculateHash() ?? '' + }:${ctx.env.getHash(ctx)}`; } /** @internal */ - protected renderLightPass(ctx: DrawContext, items: RenderQueueItem[], lights: PunctualLight[]) { - const device = Application.instance.device; + protected renderLightPass( + ctx: DrawContext, + itemList: RenderItemListBundle, + lights: PunctualLight[], + flags: any + ) { const baseLightPass = !ctx.lightBlending; ctx.drawEnvLight = baseLightPass && ctx.env.light.type !== 'none' && (ctx.env.light.envLight.hasRadiance() || ctx.env.light.envLight.hasIrradiance()); ctx.renderPassHash = this.getGlobalBindGroupHash(ctx); - const info = this.getGlobalBindGroupInfo(ctx); - ShaderHelper.setCameraUniforms(info.bindGroup, ctx, !!device.getFramebuffer()); + const bindGroup = ctx.globalBindGroupAllocator.getGlobalBindGroup(ctx); + if (!flags.cameraSet[ctx.renderPassHash]) { + ShaderHelper.setCameraUniforms(bindGroup, ctx.camera, ctx.flip, !!ctx.device.getFramebuffer()); + flags.cameraSet[ctx.renderPassHash] = 1; + } if (ctx.currentShadowLight) { - ShaderHelper.setLightUniformsShadow(info.bindGroup, ctx, lights[0]); + ShaderHelper.setLightUniformsShadow(bindGroup, ctx, lights[0]); } else { - ShaderHelper.setLightUniforms( - info.bindGroup, - ctx, - ctx.clusteredLight.clusterParam, - ctx.clusteredLight.countParam, - ctx.clusteredLight.lightBuffer, - ctx.clusteredLight.lightIndexTexture - ); + if (!flags.lightSet[ctx.renderPassHash]) { + ShaderHelper.setLightUniforms( + bindGroup, + ctx, + ctx.clusteredLight.clusterParam, + ctx.clusteredLight.countParam, + ctx.clusteredLight.lightBuffer, + ctx.clusteredLight.lightIndexTexture + ); + flags.lightSet[ctx.renderPassHash] = 1; + } } - if (ctx.applyFog) { + if (ctx.applyFog && !flags.fogSet[ctx.renderPassHash]) { ShaderHelper.setFogUniforms( - info.bindGroup, + bindGroup, ctx.env.sky.mappedFogType, baseLightPass ? ctx.env.sky.fogColor : Vector4.zero(), ctx.env.sky.fogParams, ctx.env.sky.aerialPerspectiveDensity * ctx.env.sky.aerialPerspectiveDensity, ctx.env.sky.getAerialPerspectiveLUT(ctx) ); + flags.fogSet[ctx.renderPassHash] = 1; } - device.setBindGroup(0, info.bindGroup); + ctx.device.setBindGroup(0, bindGroup); const reverseWinding = ctx.camera.worldMatrixDet < 0; - for (const item of items) { - // unlit objects should only be drawn once - if (!ctx.lightBlending || !item.drawable.isUnlit()) { - ctx.instanceData = item.instanceData; - ctx.target = item.drawable; - this.drawItem(device, item, ctx, reverseWinding); + for (const lit of itemList.lit) { + this.drawItemList(lit, ctx, reverseWinding); + } + if (!ctx.lightBlending) { + for (const unlit of itemList.unlit) { + this.drawItemList(unlit, ctx, reverseWinding); } } } /** @internal */ protected renderItems(ctx: DrawContext, renderQueue: RenderQueue) { - ctx.applyFog = false; - ctx.target = null; + ctx.applyFog = null; ctx.renderPassHash = null; ctx.env = ctx.scene.env; ctx.drawEnvLight = false; - ctx.flip = this.isAutoFlip(); - renderQueue.sortItems(); - + ctx.flip = this.isAutoFlip(ctx); + const oit = + ctx.primaryCamera.oit && ctx.primaryCamera.oit.supportDevice(ctx.device.type) + ? ctx.primaryCamera.oit + : null; + if (!oit) { + renderQueue.sortTransparentItems(ctx.primaryCamera.getWorldPosition()); + } + const flags: any = { + lightSet: {}, + cameraSet: {}, + fogSet: {} + }; const orders = Object.keys(renderQueue.items) .map((val) => Number(val)) .sort((a, b) => a - b); for (let i = 0; i < 2; i++) { - ctx.applyFog = i === 1 && ctx.env.sky.fogType !== 'none'; + ctx.applyFog = i === 1 && ctx.env.sky.fogType !== 'none' ? ctx.env.sky.fogType : null; ctx.queue = i === 0 ? QUEUE_OPAQUE : QUEUE_TRANSPARENT; - for (const order of orders) { - const items = renderQueue.items[order]; - const lists = [items.opaqueList, items.transList]; - const list = lists[i]; - let lightIndex = 0; - if (ctx.shadowMapInfo) { - for (const k of ctx.shadowMapInfo.keys()) { - ctx.currentShadowLight = k; + ctx.oit = i === 0 ? null : oit; + const numOitPasses = ctx.oit ? ctx.oit.begin(ctx) : 1; + for (let p = 0; p < numOitPasses; p++) { + if (ctx.oit) { + if (!ctx.oit.beginPass(ctx, p)) { + continue; + } + } + for (const order of orders) { + const items = renderQueue.items[order]; + const lists = [items.opaque, items.transparent]; + let lightIndex = 0; + if (ctx.shadowMapInfo) { + for (const k of ctx.shadowMapInfo.keys()) { + ctx.currentShadowLight = k; + ctx.lightBlending = lightIndex > 0; + this._shadowMapHash = ctx.shadowMapInfo.get(k).shaderHash; + this.renderLightPass(ctx, lists[i], [k], flags); + lightIndex++; + } + } + if (lightIndex === 0 || renderQueue.unshadowedLights.length > 0) { + ctx.currentShadowLight = null; ctx.lightBlending = lightIndex > 0; - this._shadowMapHash = ctx.shadowMapInfo.get(k).shaderHash; - this.renderLightPass(ctx, list, [k]); - lightIndex++; + this._shadowMapHash = ''; + this.renderLightPass(ctx, lists[i], renderQueue.unshadowedLights, flags); } } - if (lightIndex === 0 || renderQueue.unshadowedLights.length > 0) { - ctx.currentShadowLight = null; - ctx.lightBlending = lightIndex > 0; - this._shadowMapHash = ''; - this.renderLightPass(ctx, list, renderQueue.unshadowedLights); + if (ctx.oit) { + ctx.oit.endPass(ctx, p); } } + if (ctx.oit) { + ctx.oit.end(ctx); + } if (i === 0) { ctx.env.sky.skyWorldMatrix = ctx.scene.rootNode.worldMatrix; ctx.env.sky.renderSky(ctx); diff --git a/libs/scene/src/render/oit.ts b/libs/scene/src/render/oit.ts new file mode 100644 index 00000000..fc43a400 --- /dev/null +++ b/libs/scene/src/render/oit.ts @@ -0,0 +1,113 @@ +import type { + BindGroup, + PBGlobalScope, + PBInsideFunctionScope, + PBShaderExp, + RenderStateSet +} from '@zephyr3d/device'; +import type { DrawContext } from './drawable'; + +/** + * Abstract class for order-independent transparency renderers. + * + * Order-independent transparency (OIT) renderers allow for rendering + * of transparent objects in any order, regardless of their depth. + * + * This abstract class defines the common interface for all OIT renderers. + * Specific implementations of OIT renderers should extend this class and + * provide concrete implementations for the abstract methods. + * + * @public + */ +export abstract class OIT { + /** + * Returns the type of the renderer. + * + * @returns The type of the renderer. + */ + abstract getType(): string; + /** + * Checks whether the renderer supports the given device type. + * + * @param deviceType - The device type. + * @returns True if the renderer supports the device type, false otherwise. + */ + abstract supportDevice(deviceType: string): boolean; + /** + * Begins rendering the transparent objects. + * + * @param ctx - The draw context. + * @returns The number of passes required for rendering. + */ + abstract begin(ctx: DrawContext): number; + /** + * Ends rendering the transparent objects. + * + * @param ctx - The draw context. + */ + abstract end(ctx: DrawContext); + /** + * Begins rendering for the given pass. + * + * @param ctx - The draw context. + * @param pass - The pass number. + * @returns True if the transparent objects should be rendered, false otherwise. + */ + abstract beginPass(ctx: DrawContext, pass: number): boolean; + /** + * Ends rendering for the given pass. + * + * @param ctx - The draw context. + * @param pass - The pass number. + */ + abstract endPass(ctx: DrawContext, pass: number); + /** + * Sets up the fragment output. + * + * @remarks + * This method declares necessary uniform variables for OIT rendering and injects it into the object's material. + * + * @param scope - The global shader scope. + */ + abstract setupFragmentOutput(scope: PBGlobalScope); + /** + * Do the fragment color output. + * + * @remarks + * This method outputs the calculated fragment color for OIT rendering and injects it into the object's material. + * + * @param scope - The global shader scope. + * @param color - The calculated fragment color. + */ + abstract outputFragmentColor(scope: PBInsideFunctionScope, color: PBShaderExp): boolean; + /** + * Applies the uniforms for the given draw context and bind group. + * + * This function will be called when ever the transparent material will upload uniform variables. + * + * @param ctx - The draw context. + * @param bindGroup - The bind group. + */ + abstract applyUniforms(ctx: DrawContext, bindGroup: BindGroup); + /** + * Calculates the hash of the renderer. + * + * @remarks + * When this hash value was changed, material shader will be forced recreate. + * + * @returns The hash of the renderer. + */ + abstract calculateHash(): string; + /** + * Sets the render states for the renderer. + * + * This function will be called when the transparent object will be rendered. + * + * @param rs - The render states. + */ + abstract setRenderStates(rs: RenderStateSet); + /** + * Disposes the renderer. + */ + abstract dispose(): void; +} diff --git a/libs/scene/src/render/render_queue.ts b/libs/scene/src/render/render_queue.ts index a3998897..2f17949c 100644 --- a/libs/scene/src/render/render_queue.ts +++ b/libs/scene/src/render/render_queue.ts @@ -1,70 +1,75 @@ import { Application } from '../app'; import type { Vector4 } from '@zephyr3d/base'; +import { Vector3 } from '@zephyr3d/base'; import type { Camera } from '../camera/camera'; -import type { Drawable } from './drawable'; +import type { BatchDrawable, Drawable } from './drawable'; import type { DirectionalLight, PunctualLight } from '../scene/light'; import type { RenderPass } from '.'; import { QUEUE_TRANSPARENT } from '../values'; import type { BindGroup, BindGroupLayout } from '@zephyr3d/device'; import { ProgramBuilder } from '@zephyr3d/device'; +import type { Material } from '../material'; import { ShaderHelper } from '../material'; +import { RenderBundleWrapper } from './renderbundle_wrapper'; /** @internal */ export type CachedBindGroup = { bindGroup: BindGroup; buffer: Float32Array; + offset: number; dirty: boolean; }; +const maxBufferSizeInFloats = 65536 / 4; + /** @internal */ export class InstanceBindGroupAllocator { private static _instanceBindGroupLayout: BindGroupLayout = null; - _usedBindGroupList: CachedBindGroup[] = []; - _freeBindGroupList: CachedBindGroup[] = []; + _bindGroupList: CachedBindGroup[] = []; private _allocFrameStamp: number; constructor() { this._allocFrameStamp = -1; - this._usedBindGroupList = []; - this._freeBindGroupList = []; + this._bindGroupList = []; } - allocateInstanceBindGroup(framestamp: number): CachedBindGroup { + allocateInstanceBindGroup(framestamp: number, sizeInFloats: number): CachedBindGroup { // Reset if render frame changed if (this._allocFrameStamp !== framestamp) { this._allocFrameStamp = framestamp; - this._freeBindGroupList.push(...this._usedBindGroupList); - this._usedBindGroupList.length = 0; + for (const k of this._bindGroupList) { + k.offset = 0; + } } - let bindGroup = this._freeBindGroupList.pop(); - if (!bindGroup) { - if (!InstanceBindGroupAllocator._instanceBindGroupLayout) { - const buildInfo = new ProgramBuilder(Application.instance.device).buildRender({ - vertex(pb) { - this[ShaderHelper.getWorldMatricesUniformName()] = pb.vec4[65536 >> 4]().uniformBuffer(3); - pb.main(function () {}); - }, - fragment(pb) { - this[ShaderHelper.getWorldMatricesUniformName()] = pb.vec4[65536 >> 4]().uniformBuffer(3); - pb.main(function () {}); - } - }); - InstanceBindGroupAllocator._instanceBindGroupLayout = buildInfo[2][3]; + for (const k of this._bindGroupList) { + if (k.offset + sizeInFloats <= maxBufferSizeInFloats) { + k.dirty = true; + return k; } - bindGroup = { - bindGroup: Application.instance.device.createBindGroup( - InstanceBindGroupAllocator._instanceBindGroupLayout - ), - buffer: new Float32Array(65536 >> 2), - dirty: true - }; - } else { - bindGroup.dirty = true; } - this._usedBindGroupList.push(bindGroup); + if (!InstanceBindGroupAllocator._instanceBindGroupLayout) { + const buildInfo = new ProgramBuilder(Application.instance.device).buildRender({ + vertex(pb) { + this[ShaderHelper.getInstanceDataUniformName()] = pb.vec4[65536 >> 4]().uniformBuffer(3); + pb.main(function () {}); + }, + fragment(pb) { + pb.main(function () {}); + } + }); + InstanceBindGroupAllocator._instanceBindGroupLayout = buildInfo[2][3]; + } + const bindGroup = { + bindGroup: Application.instance.device.createBindGroup( + InstanceBindGroupAllocator._instanceBindGroupLayout + ), + buffer: new Float32Array(maxBufferSizeInFloats), + offset: 0, + dirty: true + }; + this._bindGroupList.push(bindGroup); return bindGroup; } } -const maxUniformSize = 65536 >> 2; const defaultInstanceBindGroupAlloator = new InstanceBindGroupAllocator(); /** @@ -74,14 +79,13 @@ const defaultInstanceBindGroupAlloator = new InstanceBindGroupAllocator(); export interface InstanceData { bindGroup: CachedBindGroup; stride: number; - currentSize: number; - maxSize: number; - hash: string; + offset: number; + numInstances: number; } /** * Render queue item - * @public + * @internal */ export interface RenderQueueItem { drawable: Drawable; @@ -90,15 +94,49 @@ export interface RenderQueueItem { instanceData: InstanceData; } +/** @internal */ +export interface RenderItemListInfo { + itemList: RenderQueueItem[]; + renderBundle?: RenderBundleWrapper; + skinItemList: RenderQueueItem[]; + skinRenderBundle?: RenderBundleWrapper; + instanceItemList: RenderQueueItem[]; + instanceRenderBundle?: RenderBundleWrapper; + instanceList: Record; + materialList: Set; + renderQueue: RenderQueue; +} + +/** @internal */ +export interface RenderItemListBundle { + lit: RenderItemListInfo[]; + unlit: RenderItemListInfo[]; +} + /** * Item list of render queue - * @public + * @internal */ -interface RenderItemList { - opaqueList: RenderQueueItem[]; - opaqueInstanceList: Record; - transList: RenderQueueItem[]; - transInstanceList: Record; +export interface RenderItemList { + opaque: RenderItemListBundle; + transparent: RenderItemListBundle; +} + +/** + * Render queue reference + * @internal + */ +export interface RenderQueueRef { + ref: RenderQueue; +} + +/** + * Drawable instance information + * @internal + */ +export interface DrawableInstanceInfo { + bindGroup: CachedBindGroup; + offset: number; } /** @@ -118,6 +156,10 @@ export class RenderQueue { private _sunLight: DirectionalLight; /** @internal */ private _bindGroupAllocator: InstanceBindGroupAllocator; + /** @internal */ + private _ref: RenderQueueRef; + /** @internal */ + private _instanceInfo: Map; /** * Creates an instance of a render queue * @param renderPass - The render pass to which the render queue belongs @@ -129,6 +171,8 @@ export class RenderQueue { this._shadowedLightList = []; this._unshadowedLightList = []; this._sunLight = null; + this._ref = { ref: this }; + this._instanceInfo = new Map(); } /** The sun light */ get sunLight(): DirectionalLight { @@ -159,6 +203,20 @@ export class RenderQueue { get unshadowedLights() { return this._unshadowedLightList; } + /** + * Gets the indirect reference of this + */ + get ref(): RenderQueueRef { + return this._ref; + } + /** + * Gets the instance information for given drawable object + * @param drawable - The drawable object + * @returns The instane information for given drawable object, null if no exists + */ + getInstanceInfo(drawable: Drawable) { + return this._instanceInfo.get(drawable); + } /** * Gets the maximum batch size of a given device * @returns The maximum batch size of the device @@ -187,24 +245,17 @@ export class RenderQueue { * @param queue - The render queue to be pushed */ pushRenderQueue(queue: RenderQueue) { - if (queue && queue !== this) { - for (const k in queue._itemLists) { - const l = queue._itemLists[k]; - if (l) { - let list = this._itemLists[k]; - if (!list) { - list = { - opaqueList: [], - opaqueInstanceList: {}, - transList: [], - transInstanceList: {} - }; - this._itemLists[k] = list; - } - list.opaqueList.push(...l.opaqueList); - list.transList.push(...l.transList); - } + for (const order in queue._itemLists) { + let itemLists = this._itemLists[order]; + if (!itemLists) { + itemLists = this.newRenderItemList(true); + this._itemLists[order] = itemLists; } + const newItemLists = queue._itemLists[order]; + itemLists.opaque.lit.push(...newItemLists.opaque.lit); + itemLists.opaque.unlit.push(...newItemLists.opaque.unlit); + itemLists.transparent.lit.push(...newItemLists.transparent.lit); + itemLists.transparent.unlit.push(...newItemLists.transparent.unlit); } } /** @@ -217,71 +268,46 @@ export class RenderQueue { if (drawable) { let itemList = this._itemLists[renderOrder]; if (!itemList) { - itemList = { - opaqueList: [], - opaqueInstanceList: {}, - transList: [], - transInstanceList: {} - }; + itemList = this.newRenderItemList(false); this._itemLists[renderOrder] = itemList; } const trans = drawable.getQueueType() === QUEUE_TRANSPARENT; - const list = trans ? itemList.transList : itemList.opaqueList; + const unlit = drawable.isUnlit(); if (drawable.isBatchable()) { - const instanceList = trans ? itemList.transInstanceList : itemList.opaqueInstanceList; + const instanceList = trans + ? unlit + ? itemList.transparent.unlit[0].instanceList + : itemList.transparent.lit[0].instanceList + : unlit + ? itemList.opaque.unlit[0].instanceList + : itemList.opaque.lit[0].instanceList; const hash = drawable.getInstanceId(this._renderPass); - const index = instanceList[hash]; - if ( - index === undefined || - list[index].instanceData.currentSize + list[index].instanceData.stride > - list[index].instanceData.maxSize - ) { - instanceList[hash] = list.length; - const bindGroup = this._bindGroupAllocator.allocateInstanceBindGroup( - Application.instance.device.frameInfo.frameCounter - ); - drawable.setInstanceDataBuffer(this._renderPass, bindGroup, 0); - bindGroup.buffer.set(drawable.getXForm().worldMatrix); - let currentSize = 4; - const instanceUniforms = drawable.getInstanceUniforms(); - if (instanceUniforms) { - bindGroup.buffer.set(instanceUniforms, currentSize * 4); - currentSize += instanceUniforms.length >> 2; - } - const maxSize = Math.floor(maxUniformSize / currentSize); - list.push({ - drawable, - sortDistance: drawable.getSortDistance(camera), - instanceData: { - bindGroup, - currentSize, - maxSize, - stride: currentSize, - hash: hash - } - }); - } else { - const instanceData = list[index].instanceData; - drawable.setInstanceDataBuffer( - this._renderPass, - instanceData.bindGroup, - instanceData.currentSize * 4 - ); - instanceData.bindGroup.buffer.set(drawable.getXForm().worldMatrix, instanceData.currentSize * 4); - instanceData.currentSize += 4; - const instanceUniforms = drawable.getInstanceUniforms(); - if (instanceUniforms) { - instanceData.bindGroup.buffer.set(instanceUniforms, instanceData.currentSize * 4); - instanceData.currentSize += instanceUniforms.length >> 2; - } + let drawableList = instanceList[hash]; + if (!drawableList) { + drawableList = []; + instanceList[hash] = drawableList; } + drawableList.push(drawable); } else { - list.push({ + const list = trans + ? unlit + ? itemList.transparent.unlit[0] + : itemList.transparent.lit[0] + : unlit + ? itemList.opaque.unlit[0] + : itemList.opaque.lit[0]; + this.binaryInsert(drawable.getBoneMatrices() ? list.skinItemList : list.itemList, { drawable, sortDistance: drawable.getSortDistance(camera), instanceData: null }); + drawable.applyTransformUniforms(this); + const mat = drawable.getMaterial(); + if (mat) { + list.materialList.add(mat.coreMaterial); + } } + drawable.pushRenderQueueRef(this._ref); } } /** @@ -293,15 +319,209 @@ export class RenderQueue { this._unshadowedLightList = []; this._sunLight = null; } + /** @internal */ + dispose() { + this._ref.ref = null; + this._ref = null; + this.reset(); + } + /** @internal */ + end(camera: Camera, createRenderBundles?: boolean): this { + const frameCounter = Application.instance.device.frameInfo.frameCounter; + for (const k in this._itemLists) { + const itemList = this._itemLists[k]; + const lists = [ + itemList.opaque.lit, + itemList.opaque.unlit, + itemList.transparent.lit, + itemList.transparent.unlit + ]; + for (let i = 0; i < 4; i++) { + const list = lists[i]; + for (const info of list) { + if (info.renderQueue !== this) { + continue; + } + const instanceList = info.instanceList; + for (const x in instanceList) { + const drawables = instanceList[x]; + if (drawables.length === 1) { + this.binaryInsert(info.itemList, { + drawable: drawables[0], + sortDistance: drawables[0].getSortDistance(camera), + instanceData: null + }); + drawables[0].applyTransformUniforms(this); + const mat = drawables[0].getMaterial(); + if (mat) { + info.materialList.add(mat.coreMaterial); + } + } else { + let bindGroup: CachedBindGroup = null; + let item: RenderQueueItem = null; + for (let i = 0; i < drawables.length; i++) { + const drawable = drawables[i]; + const instanceUniforms = drawable.getInstanceUniforms(); + const instanceUniformsSize = instanceUniforms?.length ?? 0; + const stride = 16 + instanceUniformsSize; + if (!bindGroup || bindGroup.offset + stride > maxBufferSizeInFloats) { + bindGroup = this._bindGroupAllocator.allocateInstanceBindGroup(frameCounter, stride); + item = { + drawable, + sortDistance: drawable.getSortDistance(camera), + instanceData: { + bindGroup, + offset: bindGroup.offset, + numInstances: 0, + stride + } + }; + this.binaryInsert(info.instanceItemList, item); + drawable.applyInstanceOffsetAndStride(this, stride, bindGroup.offset); + } + const instanceInfo = { bindGroup, offset: bindGroup.offset }; + this._instanceInfo.set(drawable, instanceInfo); + drawable.applyTransformUniforms(this); + drawable.applyMaterialUniforms(instanceInfo); + bindGroup.offset += stride; + item.instanceData.numInstances++; + const mat = drawable.getMaterial(); + if (mat) { + info.materialList.add(mat.coreMaterial); + } + } + } + } + info.instanceList = {}; + if (createRenderBundles) { + if (info.itemList.length > 0) { + info.renderBundle = new RenderBundleWrapper(); + } + if (info.skinItemList.length > 0) { + info.skinRenderBundle = new RenderBundleWrapper(); + } + if (info.instanceItemList.length > 0) { + info.instanceRenderBundle = new RenderBundleWrapper(); + } + } + } + } + /* + itemList.opaque.lit.forEach(info => { + info.itemList.sort((a, b) => (a.drawable.getMaterial()?.instanceId ?? 0) - (b.drawable.getMaterial()?.instanceId ?? 0)) + }); + itemList.opaque.unlit.forEach(info => { + info.itemList.sort((a, b) => (a.drawable.getMaterial()?.instanceId ?? 0) - (b.drawable.getMaterial()?.instanceId ?? 0)) + }); + */ + } + return this; + } + binaryInsert(itemList: RenderQueueItem[], item: RenderQueueItem) { + let left = 0; + let right = itemList.length - 1; + const newInstanceId = item.drawable.getMaterial().instanceId; + while (left <= right) { + const mid = Math.floor((left + right) / 2); + const instanceId = itemList[mid].drawable.getMaterial().instanceId; + if (instanceId === newInstanceId) { + itemList.splice(mid + 1, 0, item); + return; + } else if (instanceId < newInstanceId) { + left = mid + 1; + } else { + right = mid - 1; + } + } + itemList.splice(left, 0, item); + } /** * Sorts the items in the render queue for rendering */ - sortItems() { + sortTransparentItems(cameraPos: Vector3) { for (const list of Object.values(this._itemLists)) { - list.opaqueList.sort((a, b) => a.sortDistance - b.sortDistance); - list.transList.sort((a, b) => b.sortDistance - a.sortDistance); + list.transparent.lit[0].itemList.sort( + (a, b) => + this.drawableDistanceToCamera(b.drawable, cameraPos) - + this.drawableDistanceToCamera(a.drawable, cameraPos) + ); + list.transparent.lit[0].skinItemList.sort( + (a, b) => + this.drawableDistanceToCamera(b.drawable, cameraPos) - + this.drawableDistanceToCamera(a.drawable, cameraPos) + ); + list.transparent.unlit[0].itemList.sort( + (a, b) => + this.drawableDistanceToCamera(b.drawable, cameraPos) - + this.drawableDistanceToCamera(a.drawable, cameraPos) + ); + list.transparent.unlit[0].skinItemList.sort( + (a, b) => + this.drawableDistanceToCamera(b.drawable, cameraPos) - + this.drawableDistanceToCamera(a.drawable, cameraPos) + ); } } + private drawableDistanceToCamera(drawable: Drawable, cameraPos: Vector3) { + const drawablePos = drawable.getXForm().position; + return Vector3.distanceSq(drawablePos, cameraPos); + } + private newRenderItemList(empty: boolean): RenderItemList { + return { + opaque: { + lit: empty + ? [] + : [ + { + itemList: [], + skinItemList: [], + instanceItemList: [], + materialList: new Set(), + instanceList: {}, + renderQueue: this + } + ], + unlit: empty + ? [] + : [ + { + itemList: [], + skinItemList: [], + instanceItemList: [], + materialList: new Set(), + instanceList: {}, + renderQueue: this + } + ] + }, + transparent: { + lit: empty + ? [] + : [ + { + itemList: [], + skinItemList: [], + instanceItemList: [], + materialList: new Set(), + instanceList: {}, + renderQueue: this + } + ], + unlit: empty + ? [] + : [ + { + itemList: [], + skinItemList: [], + instanceItemList: [], + materialList: new Set(), + instanceList: {}, + renderQueue: this + } + ] + } + }; + } /* private encodeInstanceColor(index: number, outColor: Float32Array) { outColor[0] = ((index >> 24) & 255) / 255; diff --git a/libs/scene/src/render/renderbundle_wrapper.ts b/libs/scene/src/render/renderbundle_wrapper.ts new file mode 100644 index 00000000..cca443df --- /dev/null +++ b/libs/scene/src/render/renderbundle_wrapper.ts @@ -0,0 +1,18 @@ +import type { RenderBundle } from '@zephyr3d/device'; +import { Application } from '../app'; + +export class RenderBundleWrapper { + private _renderBundles: Record; + constructor() { + this._renderBundles = {}; + } + getRenderBundle(hash: string) { + return this._renderBundles[hash] ?? null; + } + beginRenderBundle() { + Application.instance.device.beginCapture(); + } + endRenderBundle(hash: string) { + this._renderBundles[hash] = Application.instance.device.endCapture(); + } +} diff --git a/libs/scene/src/render/renderer.ts b/libs/scene/src/render/renderer.ts index 1084e791..9f8f3386 100644 --- a/libs/scene/src/render/renderer.ts +++ b/libs/scene/src/render/renderer.ts @@ -13,6 +13,7 @@ import type { Camera } from '../camera'; import type { Compositor } from '../posteffect'; import type { RenderLogger } from '../logger/logger'; import { ClusteredLight } from './cluster_light'; +import { GlobalBindGroupAllocator } from './globalbindgroup_allocator'; /** * Forward render scheme @@ -26,8 +27,6 @@ export class SceneRenderer { /** @internal */ private static _shadowMapPass = new ShadowMapPass(); /** @internal */ - private static _enableDepthPass = false; - /** @internal */ private static _clusters: ClusteredLight[] = []; /** lighting render pass */ static get sceneRenderPass(): LightPass { @@ -65,18 +64,20 @@ export class SceneRenderer { static renderScene(scene: Scene, camera: Camera, compositor?: Compositor, logger?: RenderLogger): void { const device = Application.instance.device; const ctx: DrawContext = { + device, scene, primaryCamera: camera, + oit: null, + globalBindGroupAllocator: GlobalBindGroupAllocator.get(), camera, compositor: compositor?.needDrawPostEffects() ? compositor : null, timestamp: device.frameInfo.frameTimestamp, logger, queue: 0, lightBlending: false, - target: null, renderPass: null, renderPassHash: null, - applyFog: false, + applyFog: null, flip: false, drawEnvLight: false, env: null @@ -85,6 +86,7 @@ export class SceneRenderer { if (camera && !device.isContextLost()) { this._renderScene(ctx); } + GlobalBindGroupAllocator.release(ctx.globalBindGroupAllocator); } /** @internal */ protected static _renderSceneDepth( @@ -92,7 +94,7 @@ export class SceneRenderer { renderQueue: RenderQueue, depthFramebuffer: FrameBuffer ) { - const device = Application.instance.device; + const device = ctx.device; device.pushDeviceStates(); device.setFramebuffer(depthFramebuffer); this._depthPass.clearColor = device.type === 'webgl' ? new Vector4(0, 0, 0, 1) : new Vector4(1, 1, 1, 1); @@ -101,7 +103,7 @@ export class SceneRenderer { } /** @internal */ protected static _renderScene(ctx: DrawContext): void { - const device = Application.instance.device; + const device = ctx.device; const vp = ctx.camera.viewport; const scissor = ctx.camera.scissor; const finalFramebuffer = device.getFramebuffer(); @@ -142,9 +144,10 @@ export class SceneRenderer { this.renderShadowMaps(ctx, renderQueue.shadowedLights); const sampleCount = ctx.compositor ? 1 : ctx.primaryCamera.sampleCount; if ( - this._enableDepthPass || + ctx.primaryCamera.depthPrePass || oversizedViewport || ctx.scene.env.needSceneDepthTexture() || + ctx.primaryCamera.oit || ctx.compositor?.requireLinearDepth() ) { const format: TextureFormat = device.type === 'webgl' ? 'rgba8unorm' : 'r32f'; @@ -238,6 +241,8 @@ export class SceneRenderer { this._scenePass.render(ctx, null, renderQueue); ctx.compositor?.end(ctx); + renderQueue.dispose(); + if (tempFramebuffer && tempFramebuffer !== finalFramebuffer) { const blitter = new CopyBlitter(); if (oversizedViewport) { @@ -271,10 +276,10 @@ export class SceneRenderer { /** @internal */ private static renderShadowMaps(ctx: DrawContext, lights: PunctualLight[]) { ctx.renderPass = this._shadowMapPass; - Application.instance.device.pushDeviceStates(); + ctx.device.pushDeviceStates(); for (const light of lights) { light.shadow.render(ctx, this._shadowMapPass); } - Application.instance.device.popDeviceStates(); + ctx.device.popDeviceStates(); } } diff --git a/libs/scene/src/render/renderpass.ts b/libs/scene/src/render/renderpass.ts index 0b987b9b..4de3fa0c 100644 --- a/libs/scene/src/render/renderpass.ts +++ b/libs/scene/src/render/renderpass.ts @@ -1,13 +1,13 @@ import { Vector4 } from '@zephyr3d/base'; import { CullVisitor } from './cull_visitor'; -import { Material } from '../material'; import { Application } from '../app'; -import type { RenderQueueItem } from './render_queue'; +import type { RenderItemListInfo, RenderQueueItem } from './render_queue'; import { RenderQueue } from './render_queue'; import type { Camera } from '../camera/camera'; import type { DrawContext } from './drawable'; -import type { AbstractDevice, BindGroup, BindGroupLayout, RenderStateSet } from '@zephyr3d/device'; +import type { AbstractDevice, BindGroup } from '@zephyr3d/device'; import { ShaderHelper } from '../material/shader/helper'; +import type { RenderBundleWrapper } from './renderbundle_wrapper'; /** * Base class for any kind of render passes @@ -17,7 +17,7 @@ export abstract class RenderPass { /** @internal */ protected _type: number; /** @internal */ - protected _globalBindGroups: Record; + protected _globalBindGroups: Record; /** @internal */ protected _clearColor: Vector4; /** @internal */ @@ -63,8 +63,8 @@ export abstract class RenderPass { return this._type; } /** @internal */ - isAutoFlip(): boolean { - return !!(Application.instance.device.getFramebuffer() && Application.instance.device.type === 'webgpu'); + isAutoFlip(ctx: DrawContext): boolean { + return !!(ctx.device.getFramebuffer() && ctx.device.type === 'webgpu'); } /** * Renders a scene @@ -75,16 +75,11 @@ export abstract class RenderPass { this.drawScene(ctx, cullCamera ?? ctx.camera, renderQueue); } /** @internal */ - applyRenderStates(device: AbstractDevice, stateSet: RenderStateSet, ctx: DrawContext) { - device.setRenderStates(stateSet); - } - /** @internal */ - protected getGlobalBindGroupInfo(ctx: DrawContext): { bindGroup: BindGroup; layout: BindGroupLayout } { + protected getGlobalBindGroup(ctx: DrawContext): BindGroup { const hash = this.getGlobalBindGroupHash(ctx); let bindGroup = this._globalBindGroups[hash]; if (!bindGroup) { - //const programBuilder = new ProgramBuilder(Application.instance.device); - const ret = Application.instance.device.programBuilder.buildRender({ + const ret = ctx.device.programBuilder.buildRender({ vertex(pb) { ShaderHelper.prepareVertexShader(pb, ctx); pb.main(function () {}); @@ -94,24 +89,15 @@ export abstract class RenderPass { pb.main(function () {}); } }); - bindGroup = { - bindGroup: Application.instance.device.createBindGroup(ret[2][0]), - layout: ret[2][0] - }; + bindGroup = ctx.device.createBindGroup(ret[2][0]); this._globalBindGroups[hash] = bindGroup; } - if (bindGroup.bindGroup.disposed) { - bindGroup.bindGroup.reload(); - } return bindGroup; } /** * Disposes the render pass */ dispose() { - for (const k in this._globalBindGroups) { - Material.bindGroupGarbageCollect(this._globalBindGroups[k].bindGroup); - } this._globalBindGroups = {}; } /** @internal */ @@ -124,14 +110,17 @@ export abstract class RenderPass { protected abstract renderItems(ctx: DrawContext, renderQueue: RenderQueue); /** @internal */ protected drawScene(ctx: DrawContext, cullCamera: Camera, renderQueue?: RenderQueue) { - const device = Application.instance.device; + const device = ctx.device; this.clearFramebuffer(); - renderQueue = renderQueue ?? this.cullScene(ctx, cullCamera); - if (renderQueue) { + const rq = renderQueue ?? this.cullScene(ctx, cullCamera); + if (rq) { const windingReversed = device.isWindingOrderReversed(); - device.reverseVertexWindingOrder(this.isAutoFlip() ? !windingReversed : windingReversed); - this.renderItems(ctx, renderQueue); + device.reverseVertexWindingOrder(this.isAutoFlip(ctx) ? !windingReversed : windingReversed); + this.renderItems(ctx, rq); device.reverseVertexWindingOrder(windingReversed); + if (rq !== renderQueue) { + rq.dispose(); + } } } /** @@ -149,7 +138,7 @@ export abstract class RenderPass { } else { ctx.scene.rootNode.traverse(cullVisitor); } - return renderQueue; + return renderQueue.end(cullCamera); } return null; } @@ -170,6 +159,105 @@ export abstract class RenderPass { } } /** @internal */ + private internalDrawItemList( + ctx: DrawContext, + items: RenderQueueItem[], + renderBundle: RenderBundleWrapper, + reverseWinding: boolean, + hash: string + ) { + if (renderBundle && ctx.primaryCamera.commandBufferReuse) { + const bundle = renderBundle.getRenderBundle(hash); + if (bundle) { + ctx.device.executeRenderBundle(bundle); + return; + } + renderBundle.beginRenderBundle(); + } + for (const item of items) { + ctx.instanceData = item.instanceData; + const reverse = reverseWinding !== item.drawable.getXForm().worldMatrixDet < 0; + if (reverse) { + ctx.device.reverseVertexWindingOrder(!ctx.device.isWindingOrderReversed()); + } + item.drawable.draw(ctx); + if (reverse) { + ctx.device.reverseVertexWindingOrder(!ctx.device.isWindingOrderReversed()); + } + } + if (renderBundle && ctx.primaryCamera.commandBufferReuse) { + renderBundle.endRenderBundle(hash); + } + } + /** @internal */ + protected drawItemList(itemList: RenderItemListInfo, ctx: DrawContext, reverseWinding: boolean) { + ctx.renderQueue = itemList.renderQueue; + ctx.instanceData = null; + const windingHash = reverseWinding ? '1' : '0'; + const bindGroupHash = ctx.device.getBindGroup(0)[0].getGPUId(); + const framebufferHash = ctx.device.getFramebuffer()?.getHash() ?? ''; + const hash = `${windingHash}-${bindGroupHash}-${framebufferHash}-${ctx.renderPassHash}`; + if (itemList) { + if (itemList.itemList.length > 0) { + ctx.skinAnimation = false; + ctx.instancing = false; + itemList.materialList.forEach((mat) => mat.apply(ctx)); + this.internalDrawItemList(ctx, itemList.itemList, itemList.renderBundle, reverseWinding, hash); + } + if (itemList.skinItemList.length > 0) { + ctx.skinAnimation = true; + ctx.instancing = false; + itemList.materialList.forEach((mat) => mat.apply(ctx)); + this.internalDrawItemList( + ctx, + itemList.skinItemList, + itemList.skinRenderBundle, + reverseWinding, + hash + ); + /* + for (const item of itemList.skinItemList) { + ctx.instanceData = item.instanceData; + const reverse = reverseWinding !== item.drawable.getXForm().worldMatrixDet < 0; + if (reverse) { + device.reverseVertexWindingOrder(!device.isWindingOrderReversed()); + } + item.drawable.draw(ctx); + if (reverse) { + device.reverseVertexWindingOrder(!device.isWindingOrderReversed()); + } + } + */ + } + if (itemList.instanceItemList.length > 0) { + ctx.skinAnimation = false; + ctx.instancing = true; + itemList.materialList.forEach((mat) => mat.apply(ctx)); + this.internalDrawItemList( + ctx, + itemList.instanceItemList, + itemList.instanceRenderBundle, + reverseWinding, + hash + ); + /* + for (const item of itemList.instanceItemList) { + ctx.instanceData = item.instanceData; + const reverse = reverseWinding !== item.drawable.getXForm().worldMatrixDet < 0; + if (reverse) { + device.reverseVertexWindingOrder(!device.isWindingOrderReversed()); + } + item.drawable.draw(ctx); + if (reverse) { + device.reverseVertexWindingOrder(!device.isWindingOrderReversed()); + } + } + */ + } + } + ctx.renderQueue = null; + } + /** @internal */ private clearFramebuffer() { Application.instance.device.clearFrameBuffer(this._clearColor, this._clearDepth, this._clearStencil); } diff --git a/libs/scene/src/render/renderscheme.ts b/libs/scene/src/render/renderscheme.ts deleted file mode 100644 index b74d70d5..00000000 --- a/libs/scene/src/render/renderscheme.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { Application } from '../app'; -import type { Vector4 } from '@zephyr3d/base'; -import type { TextureFormat } from '@zephyr3d/device'; -import type { Camera } from '../camera/camera'; -import type { Scene } from '../scene/scene'; -import type { Compositor } from '../posteffect'; -import type { DrawContext } from '.'; - -/** - * Base class for any kind of render scheme - * @public - */ -export abstract class RenderScheme { - /** @internal */ - protected _shadowMapFormat: TextureFormat; - /** @internal */ - protected _enableDepthPass: boolean; - /** @internal */ - protected _currentScene: Scene; - /** - * Creates an instance of RenderScheme - */ - constructor() { - this._shadowMapFormat = null; - this._enableDepthPass = false; - this._currentScene = null; - } - /** True if an early depth pass is required */ - get requireDepthPass(): boolean { - return this._enableDepthPass; - } - set requireDepthPass(val: boolean) { - this._enableDepthPass = !!val; - } - /** The scene that is currently been rendered */ - get currentScene(): Scene { - return this._currentScene; - } - /** - * Renders a scene by given camera - * @param scene - The scene to be rendered - * @param camera - The camera that will be used to render the scene - */ - renderScene(scene: Scene, camera: Camera, compositor?: Compositor): void { - this._currentScene = scene; - const ctx = { scene, camera, compositor } as DrawContext; - scene.frameUpdate(); - if (camera && !Application.instance.device.isContextLost()) { - this._renderScene(ctx); - } - this._currentScene = null; - } - /** - * Disposes the render scheme - */ - dispose(): void { - this._dispose(); - } - /** - * Gets the texture format for shadow maps - * @returns Texture format for shadow maps - */ - getShadowMapFormat(): TextureFormat { - if (!this._shadowMapFormat) { - const device = Application.instance.device; - this._shadowMapFormat = device.getDeviceCaps().textureCaps.supportHalfFloatColorBuffer - ? device.type === 'webgl' - ? 'rgba16f' - : 'r16f' - : device.getDeviceCaps().textureCaps.supportFloatColorBuffer - ? device.type === 'webgl' - ? 'rgba32f' - : 'r32f' - : 'rgba8unorm'; - } - return this._shadowMapFormat; - } - /** @internal */ - abstract setClearParams(color: Vector4, depth: number, stencil: number): void; - /** @internal */ - protected abstract _renderScene(ctx: DrawContext): void; - /** @internal */ - protected abstract _dispose(): void; -} diff --git a/libs/scene/src/render/shadowmap_pass.ts b/libs/scene/src/render/shadowmap_pass.ts index 534e6c2e..edefe9eb 100644 --- a/libs/scene/src/render/shadowmap_pass.ts +++ b/libs/scene/src/render/shadowmap_pass.ts @@ -1,10 +1,8 @@ import { RenderPass } from './renderpass'; import { RENDER_PASS_TYPE_SHADOWMAP } from '../values'; -import { Application } from '../app'; import type { PunctualLight } from '../scene/light'; import type { RenderQueue } from './render_queue'; import type { DrawContext } from './drawable'; -import type { AbstractDevice, RenderStateSet } from '@zephyr3d/device'; import { ShaderHelper } from '../material/shader/helper'; /** @@ -15,15 +13,12 @@ import { ShaderHelper } from '../material/shader/helper'; export class ShadowMapPass extends RenderPass { /** @internal */ protected _currentLight: PunctualLight; - /** @internal */ - protected _stateOverriden: RenderStateSet; /** * Creates an instance of ShadowMapPass */ constructor() { super(RENDER_PASS_TYPE_SHADOWMAP); this._currentLight = null; - this._stateOverriden = null; } /** The light that will be used to render shadow map */ get light(): PunctualLight { @@ -33,48 +28,30 @@ export class ShadowMapPass extends RenderPass { this._currentLight = light; } /** @internal */ - private get stateOverriden(): RenderStateSet { - if (!this._stateOverriden) { - this._stateOverriden = Application.instance.device.createRenderStateSet(); - this._stateOverriden.useRasterizerState().setCullMode('none'); - } - return this._stateOverriden; - } - /** @internal */ - applyRenderStates(device: AbstractDevice, stateSet: RenderStateSet, ctx: DrawContext) { - const stateOverriden = this.stateOverriden; - const state = stateOverriden.rasterizerState; - stateOverriden.copyFrom(stateSet); - stateOverriden.useRasterizerState(state); - device.setRenderStates(stateOverriden); - } - /** @internal */ protected _getGlobalBindGroupHash(ctx: DrawContext) { - return ctx.shadowMapInfo.get(this.light).shaderHash; + return `sm:${ctx.shadowMapInfo.get(this.light).shaderHash}`; } /** @internal */ protected renderItems(ctx: DrawContext, renderQueue: RenderQueue) { - ctx.target = null; ctx.drawEnvLight = false; ctx.env = null; - ctx.applyFog = false; - ctx.renderPassHash = null; - ctx.flip = this.isAutoFlip(); - const device = Application.instance.device; - const bindGroup = this.getGlobalBindGroupInfo(ctx).bindGroup; - device.setBindGroup(0, bindGroup); - ShaderHelper.setLightUniformsShadowMap(bindGroup, ctx, this._currentLight); - ShaderHelper.setCameraUniforms(bindGroup, ctx, true); + ctx.applyFog = null; + ctx.flip = this.isAutoFlip(ctx); ctx.renderPassHash = this.getGlobalBindGroupHash(ctx); + const bindGroup = ctx.globalBindGroupAllocator.getGlobalBindGroup(ctx); + ctx.device.setBindGroup(0, bindGroup); + ShaderHelper.setLightUniformsShadowMap(bindGroup, ctx, this._currentLight); + ShaderHelper.setCameraUniforms(bindGroup, ctx.camera, ctx.flip, true); const reverseWinding = ctx.camera.worldMatrixDet < 0; for (const order of Object.keys(renderQueue.items) .map((val) => Number(val)) .sort((a, b) => a - b)) { const renderItems = renderQueue.items[order]; - for (const item of renderItems.opaqueList) { - ctx.instanceData = item.instanceData; - ctx.target = item.drawable; - this.drawItem(device, item, ctx, reverseWinding); + for (const lit of renderItems.opaque.lit) { + this.drawItemList(lit, ctx, reverseWinding); + } + for (const unlit of renderItems.opaque.unlit) { + this.drawItemList(unlit, ctx, reverseWinding); } } } diff --git a/libs/scene/src/render/sky.ts b/libs/scene/src/render/sky.ts index ff76c3bd..c5433edd 100644 --- a/libs/scene/src/render/sky.ts +++ b/libs/scene/src/render/sky.ts @@ -131,7 +131,7 @@ export class SkyRenderer { } /** @internal */ getHash(ctx: DrawContext): string { - return ctx.applyFog ? (this._fogType === 'none' ? '0' : this.drawScatteredFog(ctx) ? '1' : '2') : ''; + return ctx.applyFog === 'scatter' ? '1' : ctx.applyFog ? '2' : '0'; } /** Which type of the sky should be rendered */ get skyType(): SkyType { @@ -398,7 +398,7 @@ export class SkyRenderer { renderFog(ctx: DrawContext) { const camera = ctx.camera; const sceneDepthTexture = ctx.linearDepthTexture; - const device = Application.instance.device; + const device = ctx.device; const savedRenderStates = device.getRenderStates(); this._prepareSkyBox(device); const sunLight = ctx.sunLight; diff --git a/libs/scene/src/render/watermesh.ts b/libs/scene/src/render/watermesh.ts index 51ed739f..f94391af 100644 --- a/libs/scene/src/render/watermesh.ts +++ b/libs/scene/src/render/watermesh.ts @@ -131,7 +131,6 @@ type Programs = { postfft2Program: GPUProgram; postfft2Program2: GPUProgram; postfft2Program4: GPUProgram; - waterProgram: GPUProgram; }; type Globales = { @@ -170,15 +169,21 @@ export class WaterMesh { private _hkBindGroup2: BindGroup; private _hkBindGroup4: BindGroup; private _fft2hBindGroup: BindGroup; - private _fft2hBindGroup2: BindGroup; - private _fft2hBindGroup4: BindGroup; private _fft2vBindGroup: BindGroup; - private _fft2vBindGroup2: BindGroup; - private _fft2vBindGroup4: BindGroup; + private _fft2hBindGroup2Used: BindGroup[][]; + private _fft2hBindGroup2Free: BindGroup[][]; + private _fft2hBindGroup4Used: BindGroup[][]; + private _fft2hBindGroup4Free: BindGroup[][]; + private _fft2vBindGroup2Used: BindGroup[][]; + private _fft2vBindGroup2Free: BindGroup[][]; + private _fft2vBindGroup4Used: BindGroup[][]; + private _fft2vBindGroup4Free: BindGroup[][]; private _postfft2BindGroup: BindGroup; private _postfft2BindGroup2: BindGroup; private _postfft2BindGroup4: BindGroup; private _waterBindGroup: BindGroup; + private _usedClipmapBindGroups: BindGroup[]; + private _freeClipmapBindGroups: BindGroup[]; private _nearestRepeatSampler: TextureSampler; private _linearRepeatSampler: TextureSampler; private _updateRenderStates: RenderStateSet; @@ -204,7 +209,7 @@ export class WaterMesh { private _dataTextureFormat: TextureFormat; private _renderMode: number; private _updateFrameStamp: number; - // private _impl: WaterShaderImpl; + private _waterProgram: GPUProgram; constructor(device: AbstractDevice, impl?: WaterShaderImpl) { const renderTargetFloat32 = device.getDeviceCaps().textureCaps.supportFloatColorBuffer; const linearFloat32 = renderTargetFloat32 && device.getDeviceCaps().textureCaps.supportLinearFloatTexture; @@ -254,13 +259,13 @@ export class WaterMesh { fft2vProgram4: this._renderMode === RENDER_TWO_PASS ? createProgramFFT2V(4) : null, postfft2Program: this._renderMode === RENDER_NORMAL ? createProgramPostFFT2() : null, postfft2Program2: this._renderMode === RENDER_TWO_PASS ? createProgramPostFFT2(2) : null, - postfft2Program4: this._renderMode === RENDER_TWO_PASS ? createProgramPostFFT2(4) : null, - waterProgram: createProgramOcean(impl) + postfft2Program4: this._renderMode === RENDER_TWO_PASS ? createProgramPostFFT2(4) : null }, quad: WaterMesh.createQuad(device), noiseTextures: new Map(), butterflyTextures: new Map() }; + this._waterProgram = createProgramOcean(impl); this._params = defaultBuildParams; const programs = WaterMesh._globals.programs; this._h0BindGroup = device.createBindGroup(programs.h0Program.bindGroupLayouts[0]); @@ -276,21 +281,17 @@ export class WaterMesh { this._fft2hBindGroup = programs.fft2hProgram ? device.createBindGroup(WaterMesh._globals.programs.fft2hProgram.bindGroupLayouts[0]) : null; - this._fft2hBindGroup2 = programs.fft2hProgram2 - ? device.createBindGroup(WaterMesh._globals.programs.fft2hProgram2.bindGroupLayouts[0]) - : null; - this._fft2hBindGroup4 = programs.fft2hProgram4 - ? device.createBindGroup(WaterMesh._globals.programs.fft2hProgram4.bindGroupLayouts[0]) - : null; this._fft2vBindGroup = programs.fft2vProgram ? device.createBindGroup(WaterMesh._globals.programs.fft2vProgram.bindGroupLayouts[0]) : null; - this._fft2vBindGroup2 = programs.fft2vProgram2 - ? device.createBindGroup(WaterMesh._globals.programs.fft2vProgram2.bindGroupLayouts[0]) - : null; - this._fft2vBindGroup4 = programs.fft2vProgram4 - ? device.createBindGroup(WaterMesh._globals.programs.fft2vProgram4.bindGroupLayouts[0]) - : null; + this._fft2hBindGroup2Used = [[], []]; + this._fft2hBindGroup2Free = [[], []]; + this._fft2hBindGroup4Used = [[], []]; + this._fft2hBindGroup4Free = [[], []]; + this._fft2vBindGroup2Used = [[], []]; + this._fft2vBindGroup2Free = [[], []]; + this._fft2vBindGroup4Used = [[], []]; + this._fft2vBindGroup4Free = [[], []]; this._postfft2BindGroup = programs.postfft2Program ? device.createBindGroup(WaterMesh._globals.programs.postfft2Program.bindGroupLayouts[0]) : null; @@ -300,9 +301,7 @@ export class WaterMesh { this._postfft2BindGroup4 = programs.postfft2Program4 ? device.createBindGroup(WaterMesh._globals.programs.postfft2Program4.bindGroupLayouts[0]) : null; - this._waterBindGroup = device.createBindGroup( - WaterMesh._globals.programs.waterProgram.bindGroupLayouts[0] - ); + this._waterBindGroup = device.createBindGroup(this._waterProgram.bindGroupLayouts[0]); this._instanceData = null; this._ifftTextures = null; this._wireframe = false; @@ -339,7 +338,8 @@ export class WaterMesh { this._paramsChanged = true; this._resolutionChanged = true; this._updateFrameStamp = -1; - // this._impl = impl; + this._usedClipmapBindGroups = []; + this._freeClipmapBindGroups = []; } } get params() { @@ -458,6 +458,14 @@ export class WaterMesh { set regionMax(val: Vector2) { this._regionMax.set(val); } + getClipmapBindGroup(device: AbstractDevice) { + let bindGroup = this._usedClipmapBindGroups.pop(); + if (!bindGroup) { + bindGroup = device.createBindGroup(this._waterProgram.bindGroupLayouts[1]); + } + this._freeClipmapBindGroups.push(bindGroup); + return bindGroup; + } render(camera: Camera, flip?: boolean) { if (this._renderMode === RENDER_NONE) { return; @@ -471,7 +479,7 @@ export class WaterMesh { device.popDeviceStates(); const cameraPos = camera.getWorldPosition(); const instanceData = this.getInstanceData(); - device.setProgram(WaterMesh._globals.programs.waterProgram); + device.setProgram(this._waterProgram); device.setBindGroup(0, this._waterBindGroup); device.setRenderStates(this._waterRenderStates); const sampler = this._linearRepeatSampler; @@ -501,7 +509,6 @@ export class WaterMesh { this._waterBindGroup.setValue('viewProjMatrix', camera.viewProjectionMatrix); this._waterBindGroup.setValue('level', this._level); this._waterBindGroup.setValue('pos', cameraPos); - this._waterBindGroup.setValue('scale', 1); this._waterBindGroup.setValue('flip', flip ? 1 : 0); const that = this; const position = new Vector3(cameraPos.x, cameraPos.z, 0); @@ -527,16 +534,20 @@ export class WaterMesh { gridScale: gridScale, AABBExtents: this._aabbExtents, drawPrimitive(prim, modelMatrix, offset, scale, gridScale) { - that._waterBindGroup.setValue('modelMatrix', modelMatrix); - that._waterBindGroup.setValue('offset', offset); - that._waterBindGroup.setValue('scale', scale); - that._waterBindGroup.setValue('gridScale', gridScale); + const clipmapBindGroup = that.getClipmapBindGroup(device); + clipmapBindGroup.setValue('modelMatrix', modelMatrix); + clipmapBindGroup.setValue('offset', offset); + clipmapBindGroup.setValue('scale', scale); + clipmapBindGroup.setValue('gridScale', gridScale); + device.setBindGroup(1, clipmapBindGroup); prim.primitiveType = that._wireframe ? 'line-strip' : 'triangle-list'; prim.draw(); } }, mipLevels ); + this._usedClipmapBindGroups.push(...this._freeClipmapBindGroups); + this._freeClipmapBindGroups.length = 0; } update(device: AbstractDevice, time: number): void { device.setRenderStates(this._updateRenderStates); @@ -680,7 +691,7 @@ export class WaterMesh { ); } WaterMesh._globals.quad.draw(); - pingPong = (pingPong + 1) % 2; + pingPong = 1 - pingPong; //(pingPong + 1) % 2; } // vertical ifft @@ -713,7 +724,38 @@ export class WaterMesh { this._ifftTextures = pingPongTextures[pingPong]; } - + private getFFT2hBindGroup2(device: AbstractDevice, pingpong: number): BindGroup { + let bindGroup = this._fft2hBindGroup2Used[pingpong].pop(); + if (!bindGroup) { + bindGroup = device.createBindGroup(WaterMesh._globals.programs.fft2hProgram2.bindGroupLayouts[0]); + } + this._fft2hBindGroup2Free[pingpong].push(bindGroup); + return bindGroup; + } + private getFFT2hBindGroup4(device: AbstractDevice, pingpong: number): BindGroup { + let bindGroup = this._fft2hBindGroup4Used[pingpong].pop(); + if (!bindGroup) { + bindGroup = device.createBindGroup(WaterMesh._globals.programs.fft2hProgram4.bindGroupLayouts[0]); + } + this._fft2hBindGroup4Free[pingpong].push(bindGroup); + return bindGroup; + } + private getFFT2vBindGroup2(device: AbstractDevice, pingpong: number): BindGroup { + let bindGroup = this._fft2vBindGroup2Used[pingpong].pop(); + if (!bindGroup) { + bindGroup = device.createBindGroup(WaterMesh._globals.programs.fft2vProgram2.bindGroupLayouts[0]); + } + this._fft2vBindGroup2Free[pingpong].push(bindGroup); + return bindGroup; + } + private getFFT2vBindGroup4(device: AbstractDevice, pingpong: number): BindGroup { + let bindGroup = this._fft2vBindGroup4Used[pingpong].pop(); + if (!bindGroup) { + bindGroup = device.createBindGroup(WaterMesh._globals.programs.fft2vProgram4.bindGroupLayouts[0]); + } + this._fft2vBindGroup4Free[pingpong].push(bindGroup); + return bindGroup; + } private ifft2TwoPass(): void { const device = Application.instance.device; const instanceData = this.getInstanceData(); @@ -737,31 +779,16 @@ export class WaterMesh { for (let phase = 0; phase < phases; phase++) { device.setFramebuffer(pingPongFramebuffers4[pingPong]); device.setProgram(WaterMesh._globals.programs.fft2hProgram4); - device.setBindGroup(0, this._fft2hBindGroup4); - this._fft2hBindGroup4.setTexture('butterfly', butterflyTex, this._nearestRepeatSampler); - this._fft2hBindGroup4.setValue('phase', phase); - this._fft2hBindGroup4.setTexture( - 'spectrum0', - pingPongTextures[pingPong][0], - this._nearestRepeatSampler - ); - this._fft2hBindGroup4.setTexture( - 'spectrum1', - pingPongTextures[pingPong][1], - this._nearestRepeatSampler - ); - this._fft2hBindGroup4.setTexture( - 'spectrum2', - pingPongTextures[pingPong][2], - this._nearestRepeatSampler - ); - this._fft2hBindGroup4.setTexture( - 'spectrum3', - pingPongTextures[pingPong][3], - this._nearestRepeatSampler - ); + const fft2hBindGroup4 = this.getFFT2hBindGroup4(device, pingPong); + device.setBindGroup(0, fft2hBindGroup4); + fft2hBindGroup4.setTexture('butterfly', butterflyTex, this._nearestRepeatSampler); + fft2hBindGroup4.setValue('phase', phase); + fft2hBindGroup4.setTexture('spectrum0', pingPongTextures[pingPong][0], this._nearestRepeatSampler); + fft2hBindGroup4.setTexture('spectrum1', pingPongTextures[pingPong][1], this._nearestRepeatSampler); + fft2hBindGroup4.setTexture('spectrum2', pingPongTextures[pingPong][2], this._nearestRepeatSampler); + fft2hBindGroup4.setTexture('spectrum3', pingPongTextures[pingPong][3], this._nearestRepeatSampler); if (device.type === 'webgl') { - this._fft2hBindGroup4.setValue( + fft2hBindGroup4.setValue( 'texSize', new Vector4( pingPongTextures[pingPong][0].width, @@ -774,21 +801,14 @@ export class WaterMesh { WaterMesh._globals.quad.draw(); device.setFramebuffer(pingPongFramebuffers2[pingPong]); device.setProgram(WaterMesh._globals.programs.fft2hProgram2); - device.setBindGroup(0, this._fft2hBindGroup2); - this._fft2hBindGroup2.setTexture('butterfly', butterflyTex, this._nearestRepeatSampler); - this._fft2hBindGroup2.setValue('phase', phase); - this._fft2hBindGroup2.setTexture( - 'spectrum4', - pingPongTextures[pingPong][4], - this._nearestRepeatSampler - ); - this._fft2hBindGroup2.setTexture( - 'spectrum5', - pingPongTextures[pingPong][5], - this._nearestRepeatSampler - ); + const fft2hBindGroup2 = this.getFFT2hBindGroup2(device, pingPong); + device.setBindGroup(0, fft2hBindGroup2); + fft2hBindGroup2.setTexture('butterfly', butterflyTex, this._nearestRepeatSampler); + fft2hBindGroup2.setValue('phase', phase); + fft2hBindGroup2.setTexture('spectrum4', pingPongTextures[pingPong][4], this._nearestRepeatSampler); + fft2hBindGroup2.setTexture('spectrum5', pingPongTextures[pingPong][5], this._nearestRepeatSampler); if (device.type === 'webgl') { - this._fft2hBindGroup2.setValue( + fft2hBindGroup2.setValue( 'texSize', new Vector4( pingPongTextures[pingPong][0].width, @@ -806,31 +826,16 @@ export class WaterMesh { for (let phase = 0; phase < phases; phase++) { device.setFramebuffer(pingPongFramebuffers4[pingPong]); device.setProgram(WaterMesh._globals.programs.fft2vProgram4); - device.setBindGroup(0, this._fft2vBindGroup4); - this._fft2vBindGroup4.setTexture('butterfly', butterflyTex, this._nearestRepeatSampler); - this._fft2vBindGroup4.setValue('phase', phase); - this._fft2vBindGroup4.setTexture( - 'spectrum0', - pingPongTextures[pingPong][0], - this._nearestRepeatSampler - ); - this._fft2vBindGroup4.setTexture( - 'spectrum1', - pingPongTextures[pingPong][1], - this._nearestRepeatSampler - ); - this._fft2vBindGroup4.setTexture( - 'spectrum2', - pingPongTextures[pingPong][2], - this._nearestRepeatSampler - ); - this._fft2vBindGroup4.setTexture( - 'spectrum3', - pingPongTextures[pingPong][3], - this._nearestRepeatSampler - ); + const fft2vBindGroup4 = this.getFFT2vBindGroup4(device, pingPong); + device.setBindGroup(0, fft2vBindGroup4); + fft2vBindGroup4.setTexture('butterfly', butterflyTex, this._nearestRepeatSampler); + fft2vBindGroup4.setValue('phase', phase); + fft2vBindGroup4.setTexture('spectrum0', pingPongTextures[pingPong][0], this._nearestRepeatSampler); + fft2vBindGroup4.setTexture('spectrum1', pingPongTextures[pingPong][1], this._nearestRepeatSampler); + fft2vBindGroup4.setTexture('spectrum2', pingPongTextures[pingPong][2], this._nearestRepeatSampler); + fft2vBindGroup4.setTexture('spectrum3', pingPongTextures[pingPong][3], this._nearestRepeatSampler); if (device.type === 'webgl') { - this._fft2vBindGroup4.setValue( + fft2vBindGroup4.setValue( 'texSize', new Vector4( pingPongTextures[pingPong][0].width, @@ -843,21 +848,14 @@ export class WaterMesh { WaterMesh._globals.quad.draw(); device.setFramebuffer(pingPongFramebuffers2[pingPong]); device.setProgram(WaterMesh._globals.programs.fft2vProgram2); - device.setBindGroup(0, this._fft2vBindGroup2); - this._fft2vBindGroup2.setTexture('butterfly', butterflyTex, this._nearestRepeatSampler); - this._fft2vBindGroup2.setValue('phase', phase); - this._fft2vBindGroup2.setTexture( - 'spectrum4', - pingPongTextures[pingPong][4], - this._nearestRepeatSampler - ); - this._fft2vBindGroup2.setTexture( - 'spectrum5', - pingPongTextures[pingPong][5], - this._nearestRepeatSampler - ); + const fft2vBindGroup2 = this.getFFT2vBindGroup2(device, pingPong); + device.setBindGroup(0, fft2vBindGroup2); + fft2vBindGroup2.setTexture('butterfly', butterflyTex, this._nearestRepeatSampler); + fft2vBindGroup2.setValue('phase', phase); + fft2vBindGroup2.setTexture('spectrum4', pingPongTextures[pingPong][4], this._nearestRepeatSampler); + fft2vBindGroup2.setTexture('spectrum5', pingPongTextures[pingPong][5], this._nearestRepeatSampler); if (device.type === 'webgl') { - this._fft2vBindGroup2.setValue( + fft2vBindGroup2.setValue( 'texSize', new Vector4( pingPongTextures[pingPong][0].width, @@ -871,6 +869,16 @@ export class WaterMesh { pingPong = (pingPong + 1) % 2; } this._ifftTextures = pingPongTextures[pingPong]; + for (let i = 0; i < 2; i++) { + this._fft2hBindGroup2Used[i].push(...this._fft2hBindGroup2Free[i]); + this._fft2hBindGroup2Free[i].length = 0; + this._fft2hBindGroup4Used[i].push(...this._fft2hBindGroup4Free[i]); + this._fft2hBindGroup4Free[i].length = 0; + this._fft2vBindGroup2Used[i].push(...this._fft2vBindGroup2Free[i]); + this._fft2vBindGroup2Free[i].length = 0; + this._fft2vBindGroup4Used[i].push(...this._fft2vBindGroup4Free[i]); + this._fft2vBindGroup4Free[i].length = 0; + } } private postIfft2(): void { const device = Application.instance.device; diff --git a/libs/scene/src/render/weightedblended_oit.ts b/libs/scene/src/render/weightedblended_oit.ts new file mode 100644 index 00000000..cc4bf290 --- /dev/null +++ b/libs/scene/src/render/weightedblended_oit.ts @@ -0,0 +1,227 @@ +import type { + AbstractDevice, + BindGroup, + FrameBuffer, + GPUProgram, + PBGlobalScope, + PBInsideFunctionScope, + PBShaderExp, + RenderStateSet, + Texture2D +} from '@zephyr3d/device'; +import { OIT } from './oit'; +import type { DrawContext } from './drawable'; +import { drawFullscreenQuad } from './fullscreenquad'; +import { Vector4 } from '@zephyr3d/base'; + +/** + * Weighted-blended OIT renderer. + * + * @remarks + * The weighed-blended OIT renderer supports both WebGL, WebGL2 and WebGPU device. + * + * @public + */ +export class WeightedBlendedOIT extends OIT { + /** Type name of WeightedBlendedOIT */ + public static readonly type = 'wb'; + private static _compositeProgram: GPUProgram; + private static _compositeBindGroup: BindGroup; + private static _compositeRenderStates: RenderStateSet; + private _accumBuffer: FrameBuffer; + /** + * Creates an instance of WeightedBlendedOIT class. + */ + constructor() { + super(); + this._accumBuffer = null; + } + /** + * {@inheritDoc OIT.getType} + */ + getType(): string { + return WeightedBlendedOIT.type; + } + /** + * {@inheritDoc OIT.supportDevice} + */ + supportDevice(deviceType: string): boolean { + return true; + } + /** + * {@inheritDoc OIT.dispose} + */ + dispose() { + return; + } + /** + * {@inheritDoc OIT.begin} + */ + begin(ctx: DrawContext): number { + return 1; + } + /** + * {@inheritDoc OIT.end} + */ + end(ctx: DrawContext) { + return; + } + /** + * {@inheritDoc OIT.setupFragmentOutput} + */ + setupFragmentOutput(scope: PBGlobalScope) { + const pb = scope.$builder; + scope.$outputs.outColor = pb.vec4(); + scope.$outputs.outAlpha = pb.getDevice().type === 'webgl' ? pb.vec4() : pb.float(); + } + /** + * {@inheritDoc OIT.beginPass} + */ + beginPass(ctx: DrawContext, pass: number): boolean { + const device = ctx.device; + const accumBuffer = this.getAccumFramebuffer(ctx, device); + device.pushDeviceStates(); + device.setFramebuffer(accumBuffer); + device.clearFrameBuffer(new Vector4(0, 0, 0, 1), null, null); + return true; + } + /** + * {@inheritDoc OIT.endPass} + */ + endPass(ctx: DrawContext, pass: number) { + const device = ctx.device; + const accumBuffer = device.getFramebuffer(); + device.popDeviceStates(); + const accumTargets = accumBuffer.getColorAttachments(); + this.composite(ctx, device, accumTargets[0] as Texture2D, accumTargets[1] as Texture2D); + } + /** + * {@inheritDoc OIT.calculateHash} + */ + calculateHash(): string { + return this.getType(); + } + /** + * {@inheritDoc OIT.applyUniforms} + */ + applyUniforms(ctx: DrawContext, bindGroup: BindGroup) { + return; + } + /** + * {@inheritDoc OIT.outputFragmentColor} + */ + outputFragmentColor(scope: PBInsideFunctionScope, color: PBShaderExp) { + const pb = scope.$builder; + pb.func('Z_WBOIT_depthWeight', [pb.float('z'), pb.float('a')], function () { + this.$return( + pb.clamp( + pb.mul( + pb.pow(pb.add(pb.min(1, pb.mul(this.a, 10)), 0.01), 3), + 1e8, + pb.pow(pb.sub(1, pb.mul(this.z, 0.9)), 2) + ), + 1e-2, + 3e3 + ) + ); + }); + pb.func('Z_WBOIT_output', [pb.vec4('color')], function () { + this.$l.w = this.Z_WBOIT_depthWeight(this.$builtins.fragCoord.z, this.color.a); + this.$outputs[0] = pb.vec4(pb.mul(this.color.rgb, this.w), this.color.a); + this.$outputs[1] = + pb.getDevice().type === 'webgl' + ? pb.vec4(pb.mul(this.color.a, this.w)) + : pb.mul(this.color.a, this.w); + }); + scope.Z_WBOIT_output(color); + return true; + } + /** + * {@inheritDoc OIT.setRenderStates} + */ + setRenderStates(rs: RenderStateSet) { + const blendingState = rs.useBlendingState(); + blendingState.enable(true); + blendingState.setBlendEquation('add', 'add'); + blendingState.setBlendFuncRGB('one', 'one'); + blendingState.setBlendFuncAlpha('zero', 'inv-src-alpha'); + const depthState = rs.useDepthState(); + depthState.enableWrite(false).enableTest(true); + } + /** @internal */ + private composite(ctx: DrawContext, device: AbstractDevice, accumColor: Texture2D, accumAlpha: Texture2D) { + device.setProgram(WeightedBlendedOIT.getCompositeProgram(device)); + WeightedBlendedOIT._compositeBindGroup.setTexture('accumColorTex', accumColor); + WeightedBlendedOIT._compositeBindGroup.setTexture('accumAlphaTex', accumAlpha); + device.setBindGroup(0, WeightedBlendedOIT._compositeBindGroup); + drawFullscreenQuad(WeightedBlendedOIT._compositeRenderStates); + } + /** @internal */ + private getAccumFramebuffer(ctx: DrawContext, device: AbstractDevice) { + const vp = device.getViewport(); + const width = device.screenToDevice(vp.width); + const height = device.screenToDevice(vp.height); + if (this._accumBuffer) { + if ( + this._accumBuffer.getWidth() !== width || + this._accumBuffer.getHeight() !== height || + this._accumBuffer.getDepthAttachment() !== ctx.depthTexture + ) { + this._accumBuffer.dispose(); + this._accumBuffer = null; + } + } + if (!this._accumBuffer) { + const accumColor = device.createTexture2D('rgba16f', width, height, { + samplerOptions: { mipFilter: 'none' } + }); + const accumAlpha = device.createTexture2D(device.type === 'webgl' ? 'rgba16f' : 'r16f', width, height, { + samplerOptions: { mipFilter: 'none' } + }); + this._accumBuffer = device.createFrameBuffer([accumColor, accumAlpha], ctx.depthTexture); + } + return this._accumBuffer; + } + /** @internal */ + private static getCompositeProgram(device: AbstractDevice) { + if (!this._compositeProgram) { + this._compositeProgram = device.buildRenderProgram({ + vertex(pb) { + this.$inputs.pos = pb.vec2().attrib('position'); + this.$outputs.uv = pb.vec2(); + pb.main(function () { + this.$builtins.position = pb.vec4(this.$inputs.pos, 1, 1); + this.$outputs.uv = pb.add(pb.mul(this.$inputs.pos.xy, 0.5), pb.vec2(0.5)); + if (device.type === 'webgpu') { + this.$builtins.position.y = pb.neg(this.$builtins.position.y); + } + }); + }, + fragment(pb) { + this.$outputs.outColor = pb.vec4(); + this.accumColorTex = pb.tex2D().uniform(0); + this.accumAlphaTex = pb.tex2D().uniform(0); + pb.main(function () { + this.$l.accumColor = pb.textureSample(this.accumColorTex, this.$inputs.uv); + this.$l.accumAlpha = pb.textureSample(this.accumAlphaTex, this.$inputs.uv); + this.$l.r = this.accumColor.a; + this.accumColor.a = this.accumAlpha.r; + this.$outputs.outColor = pb.vec4( + pb.div(this.accumColor.rgb, pb.clamp(this.accumColor.a, 0.0001, 50000)), + this.r + ); + }); + } + }); + this._compositeBindGroup = device.createBindGroup(this._compositeProgram.bindGroupLayouts[0]); + this._compositeRenderStates = device.createRenderStateSet(); + this._compositeRenderStates + .useBlendingState() + .enable(true) + .setBlendFuncRGB('inv-src-alpha', 'src-alpha') + .setBlendFuncAlpha('zero', 'one'); + this._compositeRenderStates.useDepthState().enableTest(false).enableWrite(false); + } + return this._compositeProgram; + } +} diff --git a/libs/scene/src/scene/batchgroup.ts b/libs/scene/src/scene/batchgroup.ts index e5b8d7b8..896366f7 100644 --- a/libs/scene/src/scene/batchgroup.ts +++ b/libs/scene/src/scene/batchgroup.ts @@ -1,9 +1,8 @@ import { GraphNode } from './graph_node'; import type { CullVisitor, RenderPass } from '../render'; -import { RenderQueue, InstanceBindGroupAllocator, SceneRenderer } from '../render'; +import { RenderQueue, InstanceBindGroupAllocator } from '../render'; import type { Scene } from './scene'; import type { BoundingVolume } from '../utility/bounding_volume'; -import type { SceneNode } from '.'; /** * Batch group node @@ -18,7 +17,6 @@ export class BatchGroup extends GraphNode { } >; private _bindGroupAllocator: InstanceBindGroupAllocator; - private _transformHandler: (node: SceneNode) => void; private _changeTag: number; /** * Creates an instance of mesh node @@ -29,38 +27,13 @@ export class BatchGroup extends GraphNode { this._renderQueueMap = new WeakMap(); this._changeTag = 0; this._bindGroupAllocator = new InstanceBindGroupAllocator(); - this._transformHandler = (node: SceneNode) => { - if (node.isGraphNode() && node.isBatchable()) { - const lightInstanceInfo = node.getInstanceDataBuffer(SceneRenderer.sceneRenderPass); - if (lightInstanceInfo?.bindGroup) { - lightInstanceInfo.bindGroup.buffer.set(node.worldMatrix, lightInstanceInfo.offset); - lightInstanceInfo.bindGroup.dirty = true; - } - const depthInstanceInfo = node.getInstanceDataBuffer(SceneRenderer.depthRenderPass); - if (depthInstanceInfo?.bindGroup) { - depthInstanceInfo.bindGroup.buffer.set(node.worldMatrix, depthInstanceInfo.offset); - depthInstanceInfo.bindGroup.dirty = true; - } - const shadowMapInstanceInfo = node.getInstanceDataBuffer(SceneRenderer.shadowMapRenderPass); - if (shadowMapInstanceInfo?.bindGroup.buffer) { - shadowMapInstanceInfo.bindGroup.buffer.set(node.worldMatrix, shadowMapInstanceInfo.offset); - shadowMapInstanceInfo.bindGroup.dirty = true; - } - } - }; this.on('nodeattached', (node) => { node.iterate((child) => { if (child.isGraphNode()) { if (!child.isMesh()) { console.error('Only mesh node can be added to batch group'); } - child.on('transformchanged', this._transformHandler); child.placeToOctree = false; - if (child.isBatchable()) { - child.setInstanceDataBuffer(SceneRenderer.sceneRenderPass, null, 0); - child.setInstanceDataBuffer(SceneRenderer.depthRenderPass, null, 0); - child.setInstanceDataBuffer(SceneRenderer.shadowMapRenderPass, null, 0); - } } }); this._changeTag++; @@ -68,7 +41,6 @@ export class BatchGroup extends GraphNode { this.on('noderemoved', (node) => { node.iterate((child) => { if (child.isGraphNode()) { - child.off('transformchanged', this._transformHandler); child.placeToOctree = true; } }); @@ -112,6 +84,7 @@ export class BatchGroup extends GraphNode { cullVisitor.visit(node); } }); + queueInfo.queue.end(cullVisitor.camera, true); cullVisitor.frustumCulling = frustumCulling; cullVisitor.renderQueue = renderQueue; } diff --git a/libs/scene/src/scene/graph_node.ts b/libs/scene/src/scene/graph_node.ts index dfe70387..90dacfb2 100644 --- a/libs/scene/src/scene/graph_node.ts +++ b/libs/scene/src/scene/graph_node.ts @@ -31,7 +31,6 @@ export class GraphNode extends SceneNode { get octreeNode(): OctreeNode { return this._octreeNode; } - /** @internal */ set octreeNode(node: OctreeNode) { this._octreeNode = node; } diff --git a/libs/scene/src/scene/mesh.ts b/libs/scene/src/scene/mesh.ts index a595a9fc..cab88371 100644 --- a/libs/scene/src/scene/mesh.ts +++ b/libs/scene/src/scene/mesh.ts @@ -1,22 +1,23 @@ import type { Matrix4x4 } from '@zephyr3d/base'; -import { Vector4 } from '@zephyr3d/base'; +import { Vector4, applyMixins } from '@zephyr3d/base'; import { GraphNode } from './graph_node'; import { BoxFrameShape } from '../shapes'; import type { Material } from '../material'; import { LambertMaterial } from '../material'; -import type { RenderPass, Primitive, BatchDrawable, DrawContext, CachedBindGroup } from '../render'; +import type { RenderPass, Primitive, BatchDrawable, DrawContext } from '../render'; import { Application } from '../app'; import type { Texture2D } from '@zephyr3d/device'; import type { XForm } from './xform'; import type { Scene } from './scene'; import type { BoundingBox, BoundingVolume } from '../utility/bounding_volume'; import { QUEUE_OPAQUE } from '../values'; +import { mixinDrawable } from '../render/drawable_mixin'; /** * Mesh node * @public */ -export class Mesh extends GraphNode implements BatchDrawable { +export class Mesh extends applyMixins(GraphNode, mixinDrawable) implements BatchDrawable { /** @internal */ private _primitive: Primitive; /** @internal */ @@ -39,8 +40,6 @@ export class Mesh extends GraphNode implements BatchDrawable { protected _boundingBoxNode: Mesh; /** @internal */ protected _instanceColor: Vector4; - /** @internal */ - protected _instanceBufferInfo: Map; /** * Creates an instance of mesh node * @param scene - The scene to which the mesh node belongs @@ -55,7 +54,6 @@ export class Mesh extends GraphNode implements BatchDrawable { this._invBindMatrix = null; this._instanceHash = null; this._boundingBoxNode = null; - this._instanceBufferInfo = new Map(); this._instanceColor = Vector4.zero(); this._batchable = Application.instance.deviceType !== 'webgl'; this._bboxChangeCallback = this._onBoundingboxChange.bind(this); @@ -208,8 +206,15 @@ export class Mesh extends GraphNode implements BatchDrawable { * {@inheritDoc Drawable.draw} */ draw(ctx: DrawContext) { + this.bind(ctx); this.material.draw(this.primitive, ctx); } + /** + * {@inheritDoc Drawable.getMaterial} + */ + getMaterial(): Material { + return this.material; + } /** * {@inheritDoc Drawable.getBoneMatrices} */ @@ -229,33 +234,6 @@ export class Mesh extends GraphNode implements BatchDrawable { // mesh transform should be ignored when skinned return this; } - /** - * {@inheritDoc BatchDrawable.setInstanceDataBuffer} - */ - setInstanceDataBuffer(renderPass: RenderPass, bindGroup: CachedBindGroup, offset: number) { - const info = this._instanceBufferInfo.get(renderPass); - if (!info) { - if (bindGroup) { - this._instanceBufferInfo.set(renderPass, { - bindGroup, - offset - }); - } - } else { - if (bindGroup) { - info.bindGroup = bindGroup; - info.offset = offset; - } else { - this._instanceBufferInfo.delete(renderPass); - } - } - } - /** - * {@inheritDoc BatchDrawable.getInstanceDataBuffer} - */ - getInstanceDataBuffer(renderPass: RenderPass): { bindGroup: CachedBindGroup; offset: number } { - return this._instanceBufferInfo.get(renderPass) ?? null; - } /** @internal */ computeBoundingVolume(bv: BoundingVolume): BoundingVolume { let bbox: BoundingVolume; diff --git a/libs/scene/src/scene/scene.ts b/libs/scene/src/scene/scene.ts index 537e451f..3e5fa6c0 100644 --- a/libs/scene/src/scene/scene.ts +++ b/libs/scene/src/scene/scene.ts @@ -1,9 +1,7 @@ import type { Matrix4x4, AABB } from '@zephyr3d/base'; -import { Vector3, Vector4, Ray, makeEventTarget, nextPowerOf2 } from '@zephyr3d/base'; +import { Vector3, Vector4, Ray, makeEventTarget } from '@zephyr3d/base'; import { SceneNode } from './scene_node'; import { Octree } from './octree'; -import { OctreeUpdateVisitor } from './octree_update_visitor'; -import { Material } from '../material'; import { RaycastVisitor } from './raycast_visitor'; import { Application } from '../app'; import { Environment } from './environment'; @@ -170,10 +168,6 @@ export class Scene extends makeEventTarget(Object)<{ sceneupdate: SceneUpdateEve const frameInfo = Application.instance.device.frameInfo; if (frameInfo.frameCounter !== this._updateFrame) { this._updateFrame = frameInfo.frameCounter; - // uniform buffer garbage collect - if (!Material.getGCOptions().disabled) { - Material.garbageCollect(frameInfo.frameTimestamp); - } for (const an of this._animationSet) { an.update(); } @@ -224,25 +218,6 @@ export class Scene extends makeEventTarget(Object)<{ sceneupdate: SceneUpdateEve list.delete(node); } } - if (octree) { - const worldBox = octree.getRootNode().getBox(); - if (worldBox) { - const radius = Math.max( - Math.abs(worldBox.minPoint.x), - Math.abs(worldBox.minPoint.y), - Math.abs(worldBox.minPoint.z), - Math.abs(worldBox.maxPoint.x), - Math.abs(worldBox.maxPoint.y), - Math.abs(worldBox.maxPoint.z) - ); - const rootSize = nextPowerOf2(radius * 2); - if (rootSize > octree.getRootSize()) { - octree.initialize(rootSize, octree.getLeafSize()); - const v = new OctreeUpdateVisitor(octree); - this._rootNode.traverse(v); - } - } - } } } } diff --git a/libs/scene/src/scene/scene_node.ts b/libs/scene/src/scene/scene_node.ts index 78e8fc5f..956f0d82 100644 --- a/libs/scene/src/scene/scene_node.ts +++ b/libs/scene/src/scene/scene_node.ts @@ -90,7 +90,6 @@ export class SceneNode extends XForm { get placeToOctree(): boolean { return this._placeToOctree; } - /** @internal */ set placeToOctree(val: boolean) { if (!!val !== this._placeToOctree) { this._placeToOctree = !!val; @@ -171,6 +170,10 @@ export class SceneNode extends XForm { } /** * Iterate self and all of the children + * + * @remarks + * DO NOT remove child duration iteration! + * * @param callback - callback function that will be called on each node */ iterate(callback: (node: SceneNode) => void) { @@ -367,6 +370,7 @@ export class SceneNode extends XForm { /** @internal */ notifyHiddenChanged() { this._visibleChanged(); + this.dispatchEvent(this, 'visiblechanged'); for (const child of this._children) { if (child.showState === 'inherit') { child.notifyHiddenChanged(); diff --git a/libs/scene/src/scene/terrain/grass.ts b/libs/scene/src/scene/terrain/grass.ts index 9940adae..8490314b 100644 --- a/libs/scene/src/scene/terrain/grass.ts +++ b/libs/scene/src/scene/terrain/grass.ts @@ -1,19 +1,29 @@ import type { Matrix4x4, Vector4 } from '@zephyr3d/base'; -import { Vector2, nextPowerOf2 } from '@zephyr3d/base'; +import { Vector2, applyMixins, nextPowerOf2 } from '@zephyr3d/base'; import { Primitive } from '../../render/primitive'; import type { BatchDrawable, Drawable, DrawContext } from '../../render/drawable'; import type { XForm } from '../xform'; import type { QuadtreeNode } from './quadtree'; -import { Application } from '../../app'; import type { Camera } from '../../camera/camera'; import type { AbstractDevice, IndexBuffer, StructuredBuffer, Texture2D } from '@zephyr3d/device'; import type { Terrain } from './terrain'; import type { GraphNode } from '../graph_node'; import { GrassMaterial } from '../../material/grassmaterial'; +import { mixinDrawable } from '../../render/drawable_mixin'; +import type { Material } from '../../material'; -export class GrassCluster implements Drawable { +export class GrassClusterBase { + protected _terrain; + constructor(terrain: Terrain) { + this._terrain = terrain; + } + getXForm(): XForm> { + return this._terrain; + } +} + +export class GrassCluster extends applyMixins(GrassClusterBase, mixinDrawable) implements Drawable { private _primitive: Primitive; - private _terrain: Terrain; private _numInstances: number; private _material: GrassMaterial; constructor( @@ -24,22 +34,21 @@ export class GrassCluster implements Drawable { material: GrassMaterial, grassData: Float32Array ) { + super(terrain); this._primitive = new Primitive(); const instanceVertexBuffer = device.createVertexBuffer('tex1_f32x4', grassData); this._primitive.setVertexBuffer(baseVertexBuffer, 'vertex'); this._primitive.setVertexBuffer(instanceVertexBuffer, 'instance'); this._primitive.setIndexBuffer(indexBuffer); this._primitive.primitiveType = 'triangle-list'; - this._terrain = terrain; this._numInstances = grassData.length >> 2; this._material = material; - this._material.stateSet.useRasterizerState().setCullMode('none'); } getName() { return 'GrassCluster'; } - getXForm(): XForm> { - return this._terrain; + getMaterial(): Material { + return this._material; } getInstanceColor(): Vector4 { return this._terrain.getInstanceColor(); @@ -66,8 +75,7 @@ export class GrassCluster implements Drawable { return false; } draw(ctx: DrawContext) { - this._material.alphaToCoverage = Application.instance.device.getFrameBufferSampleCount() > 1; - this._material.alphaCutoff = this._material.alphaToCoverage ? 1 : 0.8; + this.bind(ctx); this._material.draw(this._primitive, ctx, this._numInstances); } } diff --git a/libs/scene/src/scene/terrain/patch.ts b/libs/scene/src/scene/terrain/patch.ts index d3931a2f..56c36e4a 100644 --- a/libs/scene/src/scene/terrain/patch.ts +++ b/libs/scene/src/scene/terrain/patch.ts @@ -1,5 +1,5 @@ import type { Matrix4x4 } from '@zephyr3d/base'; -import { Vector3, Vector4 } from '@zephyr3d/base'; +import { Vector3, Vector4, applyMixins } from '@zephyr3d/base'; import { BoundingBox } from '../../utility/bounding_volume'; import { Primitive } from '../../render/primitive'; import { Application } from '../../app'; @@ -11,10 +11,21 @@ import type { Quadtree } from './quadtree'; import type { Terrain } from './terrain'; import type { GraphNode } from '../graph_node'; import { QUEUE_OPAQUE, RENDER_PASS_TYPE_SHADOWMAP } from '../../values'; +import { mixinDrawable } from '../../render/drawable_mixin'; +import type { Material } from '../../material'; /** @internal */ -export class TerrainPatch implements Drawable { - private _terrain: Terrain; +export class TerrainPatchBase { + protected _terrain: Terrain; + constructor(terrain: Terrain) { + this._terrain = terrain; + } + getXForm(): XForm> { + return this._terrain; + } +} +/** @internal */ +export class TerrainPatch extends applyMixins(TerrainPatchBase, mixinDrawable) implements Drawable { private _geometry: Primitive; private _geometryLines: Primitive; private _quadtree: Quadtree; @@ -28,7 +39,7 @@ export class TerrainPatch implements Drawable { private _parent: TerrainPatch; private _offsetScale: Vector4; constructor(terrain: Terrain) { - this._terrain = terrain; + super(terrain); this._geometry = null; this._geometryLines = null; this._mipLevel = 0; @@ -102,20 +113,17 @@ export class TerrainPatch implements Drawable { getPickTarget(): GraphNode { return this._terrain; } + getMaterial(): Material { + return this._terrain.material; + } draw(ctx: DrawContext) { const isShadowMapPass = ctx.renderPass.type === RENDER_PASS_TYPE_SHADOWMAP; const primitive = this._quadtree.getTerrain().wireframe && !isShadowMapPass ? this.getGeometryWireframe() : this.getGeometry(); - const material = this._quadtree.getTerrain().material; - if (isShadowMapPass) { - material.stateSet.useRasterizerState().setCullMode('front'); - } - this._quadtree.getTerrain().material.draw(primitive, ctx); - if (isShadowMapPass) { - material.stateSet.defaultRasterizerState(); - } + this.bind(ctx); + this._terrain.material.draw(primitive, ctx); } setupCamera(viewportH: number, tanHalfFovy: number, maxPixelError: number): void { if (maxPixelError > 0 && tanHalfFovy > 0) { @@ -127,9 +135,6 @@ export class TerrainPatch implements Drawable { getName(): string { return 'TerrainPatch'; } - getXForm(): XForm> { - return this._quadtree.getTerrain(); - } getBoneMatrices(): Texture2D { return null; } diff --git a/libs/scene/src/scene/terrain/quadtree.ts b/libs/scene/src/scene/terrain/quadtree.ts index 3abee6f0..bdf0ecf7 100644 --- a/libs/scene/src/scene/terrain/quadtree.ts +++ b/libs/scene/src/scene/terrain/quadtree.ts @@ -372,7 +372,15 @@ export class Quadtree { cull(visitor: CullVisitor, viewPoint: Vector3, worldMatrix: Matrix4x4): number { if (this._rootNode && this._terrain) { const frustum = new Frustum(Matrix4x4.multiply(visitor.camera.viewProjectionMatrix, worldMatrix)); - return this.cull_r(visitor, this._rootNode, viewPoint, worldMatrix, frustum, true, false); + return this.cull_r( + visitor, + this._rootNode, + viewPoint, + worldMatrix, + frustum, + visitor.frustumCulling, + false + ); } return 0; } diff --git a/libs/scene/src/scene/terrain/terrain.ts b/libs/scene/src/scene/terrain/terrain.ts index de05b9cc..467eced2 100644 --- a/libs/scene/src/scene/terrain/terrain.ts +++ b/libs/scene/src/scene/terrain/terrain.ts @@ -1,6 +1,6 @@ import type { Ray } from '@zephyr3d/base'; import { Vector2, Vector3, Vector4 } from '@zephyr3d/base'; -import type { RenderStateSet, Texture2D } from '@zephyr3d/device'; +import type { Texture2D } from '@zephyr3d/device'; import { Quadtree } from './quadtree'; import { GraphNode } from '../graph_node'; import { Application } from '../../app'; @@ -50,8 +50,6 @@ export class Terrain extends GraphNode { private _castShadow: boolean; /** @internal */ private _instanceColor: Vector4; - /** @internal */ - private _overridenStateSet: RenderStateSet; /** * Creates an instance of Terrain * @param scene - The scene to which the terrain belongs @@ -74,7 +72,6 @@ export class Terrain extends GraphNode { this._viewPoint = null; this._castShadow = true; this._instanceColor = Vector4.zero(); - this._overridenStateSet = null; } /** @internal */ get quadtree(): Quadtree { @@ -159,10 +156,6 @@ export class Terrain extends GraphNode { get normalMap(): Texture2D { return this._quadtree.normalMap; } - /** @internal */ - get overridenStateSet(): RenderStateSet { - return this._overridenStateSet; - } /** * Creates the terrain * @@ -197,8 +190,6 @@ export class Terrain extends GraphNode { this._material.normalTexture = this._quadtree.normalMap; this._material.normalTexCoordIndex = -1; this._material.terrainInfo = new Vector4(this.scaledWidth, this.scaledHeight, 0, 0); - this._overridenStateSet = Application.instance.device.createRenderStateSet(); - this._overridenStateSet.useRasterizerState().setCullMode('front'); this.invalidateBoundingVolume(); // create grass layers if (options?.splatMap && options?.detailMaps?.grass) { @@ -256,7 +247,6 @@ export class Terrain extends GraphNode { grassTexture ); } - this._grassMaterial.stateSet.useRasterizerState().setCullMode('none'); this._grassManager.addGrassLayer( Application.instance.device, this, diff --git a/libs/scene/src/scene/xform.ts b/libs/scene/src/scene/xform.ts index 687cc269..f0022364 100644 --- a/libs/scene/src/scene/xform.ts +++ b/libs/scene/src/scene/xform.ts @@ -16,6 +16,7 @@ import type { SceneNode } from './scene_node'; export class XForm = XForm> extends makeEventTarget(Object)<{ nodeattached: SceneNode; noderemoved: SceneNode; + visiblechanged: SceneNode; transformchanged: SceneNode; }>() { /** @internal */ diff --git a/libs/scene/src/shaders/water.ts b/libs/scene/src/shaders/water.ts index f3b65d57..b33a539e 100644 --- a/libs/scene/src/shaders/water.ts +++ b/libs/scene/src/shaders/water.ts @@ -26,13 +26,13 @@ export function createProgramOcean(impl?: WaterShaderImpl) { this.$outputs.uv2 = pb.vec2(); this.flip = pb.int().uniform(0); this.viewProjMatrix = pb.mat4().uniform(0); - this.modelMatrix = pb.mat4().uniform(0); - this.gridScale = pb.float().uniform(0); + this.modelMatrix = pb.mat4().uniform(1); + this.gridScale = pb.float().uniform(1); this.level = pb.float().uniform(0); this.sizes = pb.vec4().uniform(0); this.croppinesses = pb.vec4().uniform(0); - this.offset = pb.vec2().uniform(0); - this.scale = pb.float().uniform(0); + this.offset = pb.vec2().uniform(1); + this.scale = pb.float().uniform(1); this.dx_hy_dz_dxdz0 = pb.tex2D().uniform(0); this.sx_sz_dxdx_dzdz0 = pb.tex2D().uniform(0); this.dx_hy_dz_dxdz1 = pb.tex2D().uniform(0); diff --git a/site/package.json b/site/package.json index 189262a8..f107626a 100644 --- a/site/package.json +++ b/site/package.json @@ -54,7 +54,7 @@ "dependencies": { "@types/colors": "^1.2.1", "@types/diff": "^5.0.2", - "@webgpu/types": "^0.1.31", + "@webgpu/types": "^0.1.40", "colors": "^1.4.0", "diff": "^5.0.0", "highlight.js": "^11.8.0", diff --git a/site/report/base.api.md b/site/report/base.api.md index 769abae3..3b54b228 100644 --- a/site/report/base.api.md +++ b/site/report/base.api.md @@ -43,7 +43,7 @@ export class AABB { } // @public -export function applyMixins(derivedCtor: any, baseCtors: any[]): void; +export function applyMixins any)[], T>(target: T, ...mixins: M): T & ExtractMixinType; // @public export enum BoxSide { @@ -101,6 +101,12 @@ export type EventMap = Record; // @public export type EventType = T[keyof T]; +// @public +export type ExtractMixinReturnType = M extends (target: infer A) => infer R ? R : never; + +// @public +export type ExtractMixinType = M extends [infer First] ? ExtractMixinReturnType : M extends [infer First, ...infer Rest] ? ExtractMixinReturnType & ExtractMixinType<[...Rest]> : never; + // @public export function floatToHalf(val: number): number; @@ -713,6 +719,9 @@ export type TypedArrayConstructor = { BYTES_PER_ELEMENT: number; }; +// @public +export type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; + // @public export function unpackFloat3(pk: number, result: T): void; diff --git a/site/report/device.api.md b/site/report/device.api.md index 9c4ae099..22d1ef11 100644 --- a/site/report/device.api.md +++ b/site/report/device.api.md @@ -12,13 +12,14 @@ import { VectorBase } from '@zephyr3d/base'; // @public export interface AbstractDevice extends IEventTarget { + beginCapture(): void; beginFrame(): boolean; buildComputeProgram(options: PBComputeOptions): GPUProgram; buildRenderProgram(options: PBRenderOptions): GPUProgram; - cancelNextFrameCall(f: () => void): void; canvas: HTMLCanvasElement; clearFrameBuffer(clearColor: Vector4, clearDepth: number, clearStencil: number): any; compute(workgroupCountX: number, workgroupCountY: number, workgroupCountZ: number): void; + copyBuffer(sourceBuffer: GPUDataBuffer, destBuffer: GPUDataBuffer, srcOffset: number, dstOffset: number, bytes: number): any; createBindGroup(layout: BindGroupLayout): BindGroup; createBuffer(sizeInBytes: number, options: BufferCreationOptions): GPUDataBuffer; createCubeTexture(format: TextureFormat, size: number, options?: TextureCreationOptions): TextureCube; @@ -45,7 +46,9 @@ export interface AbstractDevice extends IEventTarget { draw(primitiveType: PrimitiveType, first: number, count: number): void; drawInstanced(primitiveType: PrimitiveType, first: number, count: number, numInstances: number): void; drawText(text: string, x: number, y: number, color: string): any; + endCapture(): RenderBundle; endFrame(): void; + executeRenderBundle(renderBundle: RenderBundle): any; exitLoop(): void; flush(): void; frameInfo: FrameInfo; @@ -94,7 +97,7 @@ export interface AbstractDevice extends IEventTarget { // @public export interface ArrayTypeDetail { dimension: number; - elementType: PBPrimitiveTypeInfo | PBArrayTypeInfo | PBStructTypeInfo | PBAnyTypeInfo; + elementType: PBPrimitiveTypeInfo | PBArrayTypeInfo | PBStructTypeInfo | PBAnyTypeInfo | PBAtomicI32TypeInfo | PBAtomicU32TypeInfo; } // @public @@ -135,6 +138,8 @@ export abstract class BaseDevice { // (undocumented) protected _backend: DeviceBackend; // (undocumented) + abstract beginCapture(): void; + // (undocumented) beginFrame(): boolean; // (undocumented) protected _beginFrameCounter: number; @@ -145,8 +150,6 @@ export abstract class BaseDevice { // (undocumented) buildRenderProgram(options: PBRenderOptions): GPUProgram; // (undocumented) - cancelNextFrameCall(f: () => void): void; - // (undocumented) get canvas(): HTMLCanvasElement; // (undocumented) protected _canvas: HTMLCanvasElement; @@ -161,6 +164,8 @@ export abstract class BaseDevice { // (undocumented) protected abstract _compute(workgroupCountX: number, workgroupCountY: number, workgroupCountZ: number): void; // (undocumented) + abstract copyBuffer(sourceBuffer: GPUDataBuffer, destBuffer: GPUDataBuffer, srcOffset: number, dstOffset: number, bytes: number): any; + // (undocumented) protected _cpuTimer: CPUTimer; // (undocumented) abstract createBindGroup(layout: BindGroupLayout): BindGroup; @@ -227,10 +232,14 @@ export abstract class BaseDevice { // (undocumented) enableGPUTimeRecording(enable: boolean): void; // (undocumented) + abstract endCapture(): RenderBundle; + // (undocumented) endFrame(): void; // (undocumented) protected _endFrameTime: number; // (undocumented) + abstract executeRenderBundle(renderBundle: RenderBundle): any; + // (undocumented) exitLoop(): void; // (undocumented) abstract flush(): void; @@ -412,11 +421,15 @@ export interface BindGroup extends GPUObject { // (undocumented) getBuffer(name: string): GPUDataBuffer; // (undocumented) + getDynamicOffsets(): number[]; + // (undocumented) + getGPUId(): string; + // (undocumented) getLayout(): BindGroupLayout; // (undocumented) getTexture(name: string): BaseTexture; // (undocumented) - setBuffer(name: string, buffer: GPUDataBuffer): void; + setBuffer(name: string, buffer: GPUDataBuffer, offset?: number, bindOffset?: number, bindSize?: number): void; // (undocumented) setRawData(name: string, byteOffset: number, data: TypedArray, srcPos?: number, srcLength?: number): any; // (undocumented) @@ -486,6 +499,7 @@ export interface BlendingState { // @public export interface BufferBindingLayout { + dynamicOffsetIndex: number; hasDynamicOffset: boolean; minBindingSize?: number; type?: 'uniform' | 'storage' | 'read-only-storage'; @@ -701,6 +715,8 @@ export interface FrameBuffer extends GPUObject { // (undocumented) getDepthAttachment(): BaseTexture; // (undocumented) + getHash(): string; + // (undocumented) getHeight(): number; // (undocumented) getSampleCount(): number; @@ -1025,9 +1041,10 @@ export class PBAnyTypeInfo extends PBTypeInfo { // @public export class PBArrayTypeInfo extends PBTypeInfo { - constructor(elementType: PBPrimitiveTypeInfo | PBArrayTypeInfo | PBStructTypeInfo | PBAnyTypeInfo, dimension?: number); + constructor(elementType: PBPrimitiveTypeInfo | PBArrayTypeInfo | PBStructTypeInfo | PBAnyTypeInfo | PBAtomicI32TypeInfo | PBAtomicU32TypeInfo, dimension?: number); get dimension(): number; - get elementType(): PBPrimitiveTypeInfo | PBArrayTypeInfo | PBStructTypeInfo | PBAnyTypeInfo; + get elementType(): PBPrimitiveTypeInfo | PBArrayTypeInfo | PBStructTypeInfo | PBAnyTypeInfo | PBAtomicI32TypeInfo | PBAtomicU32TypeInfo; + haveAtomicMembers(): boolean; isArrayType(): this is PBArrayTypeInfo; // (undocumented) isCompatibleType(other: PBTypeInfo): boolean; @@ -1037,12 +1054,14 @@ export class PBArrayTypeInfo extends PBTypeInfo { // @public export class PBAtomicI32TypeInfo extends PBTypeInfo { constructor(); + haveAtomicMembers(): boolean; toBufferLayout(offset: number): UniformBufferLayout; } // @public export class PBAtomicU32TypeInfo extends PBTypeInfo { constructor(); + haveAtomicMembers(): boolean; toBufferLayout(offset: number): UniformBufferLayout; } @@ -1170,6 +1189,7 @@ export class PBPointerTypeInfo extends PBTypeInfo { constructor(pointerType: PBTypeInfo, addressSpace: PBAddressSpace); get addressSpace(): PBAddressSpace; set addressSpace(val: PBAddressSpace); + haveAtomicMembers(): boolean; isPointerType(): this is PBPointerTypeInfo; get pointerType(): PBTypeInfo; toBufferLayout(offset: number): UniformBufferLayout; @@ -1413,10 +1433,12 @@ export class PBShaderExp extends Proxiable { sampleType(type: 'float' | 'unfilterable-float' | 'sint' | 'uint' | 'depth'): PBShaderExp; setAt(index: number | PBShaderExp, val: number | boolean | PBShaderExp): void; storage(group: number): PBShaderExp; - storageBuffer(group: number): PBShaderExp; + storageBuffer(group: number, bindingSize?: number): PBShaderExp; + storageBufferReadonly(group: number, bindingSize?: number): PBShaderExp; + storageReadonly(group: number): PBShaderExp; tag(...args: ShaderExpTagValue[]): PBShaderExp; uniform(group: number): PBShaderExp; - uniformBuffer(group: number): PBShaderExp; + uniformBuffer(group: number, bindingSize?: number): PBShaderExp; workgroup(): PBShaderExp; } @@ -1433,6 +1455,7 @@ export class PBStructTypeInfo extends PBTypeInfo { name: string; type: PBPrimitiveTypeInfo | PBArrayTypeInfo | PBStructTypeInfo; }[]): PBStructTypeInfo; + haveAtomicMembers(): boolean; isStructType(): this is PBStructTypeInfo; get layout(): PBStructLayout; get structMembers(): { @@ -1528,14 +1551,17 @@ export class PBTextureTypeInfo extends PBTypeInfo { isStorageTexture(): boolean; isUIntTexture(): boolean; get readable(): boolean; + set readable(val: boolean); get storageTexelFormat(): TextureFormat; get textureType(): PBTextureType; toBufferLayout(offset: number): UniformBufferLayout; get writable(): boolean; + set writable(val: boolean); } // @public export abstract class PBTypeInfo { + haveAtomicMembers(): boolean; isAnyType(): this is PBAnyTypeInfo; isArrayType(): this is PBArrayTypeInfo; isAtomicI32(): this is PBAtomicI32TypeInfo; @@ -1596,8 +1622,27 @@ export interface ProgramBuilder { atan(val: number | PBShaderExp): PBShaderExp; atan2(y: number | PBShaderExp, x: number | PBShaderExp): PBShaderExp; atanh(val: number | PBShaderExp): PBShaderExp; + atomic_int: { + (): PBShaderExp; + (rhs: number): PBShaderExp; + (rhs: boolean): PBShaderExp; + (rhs: PBShaderExp): PBShaderExp; + (name: string): PBShaderExp; + ptr: ShaderTypeFunc; + [dim: number]: ShaderTypeFunc; + }; + atomic_uint: { + (): PBShaderExp; + (rhs: number): PBShaderExp; + (rhs: boolean): PBShaderExp; + (rhs: PBShaderExp): PBShaderExp; + (name: string): PBShaderExp; + ptr: ShaderTypeFunc; + [dim: number]: ShaderTypeFunc; + }; atomicAdd(ptr: PBShaderExp, value: number | PBShaderExp): PBShaderExp; atomicAnd(ptr: PBShaderExp, value: number | PBShaderExp): PBShaderExp; + atomicExchange(ptr: PBShaderExp, value: number | PBShaderExp): PBShaderExp; atomicLoad(ptr: PBShaderExp): any; atomicMax(ptr: PBShaderExp, value: number | PBShaderExp): PBShaderExp; atomicMin(ptr: PBShaderExp, value: number | PBShaderExp): PBShaderExp; @@ -2016,6 +2061,9 @@ export interface RasterizerState { setCullMode(mode: FaceMode): this; } +// @public +export type RenderBundle = unknown; + // @public export interface RenderProgramConstructParams { bindGroupLayouts: BindGroupLayout[]; @@ -2028,6 +2076,7 @@ export interface RenderProgramConstructParams { export interface RenderStateSet { apply(force?: boolean): void; readonly blendingState: BlendingState; + clone(): RenderStateSet; readonly colorState: ColorState; copyFrom(stateSet: RenderStateSet): void; defaultBlendingState(): void; @@ -2084,7 +2133,9 @@ export const semanticList: string[]; // @public export interface ShaderCaps { + maxStorageBufferSize: number; maxUniformBufferSize: number; + storageBufferOffsetAlignment: number; supportFragmentDepth: boolean; supportHighPrecisionFloat: boolean; supportHighPrecisionInt: boolean; diff --git a/site/report/scene.api.md b/site/report/scene.api.md index c6de2f75..9c8026b6 100644 --- a/site/report/scene.api.md +++ b/site/report/scene.api.md @@ -75,7 +75,6 @@ export class AABBTree { // @public export abstract class AbstractPostEffect { constructor(); - protected addIntermediateFramebuffer(name: string, depth: 'none' | 'current' | 'temporal'): void; abstract apply(ctx: DrawContext, inputColorTexture: Texture2D, sceneDepthTexture: Texture2D, srgbOutput: boolean): void; // @internal (undocumented) protected createRenderStates(device: AbstractDevice): RenderStateSet; @@ -87,14 +86,6 @@ export abstract class AbstractPostEffect { set enabled(val: boolean); // (undocumented) protected _enabled: boolean; - protected getIntermediateFramebuffer(name: string, format: TextureFormat, width: number, height: number): FrameBuffer; - // (undocumented) - protected _intermediateFramebuffers: { - [name: string]: { - framebuffer: FrameBuffer; - depth: 'none' | 'current' | 'temporal'; - }; - }; needFlip(device: AbstractDevice): boolean; get opaque(): boolean; // (undocumented) @@ -109,6 +100,24 @@ export abstract class AbstractPostEffect { abstract requireLinearDepthTexture(): boolean; } +// @public +export class ABufferOIT extends OIT { + constructor(numLayers?: number); + applyUniforms(ctx: DrawContext, bindGroup: BindGroup): void; + begin(ctx: DrawContext): number; + beginPass(ctx: DrawContext, pass: number): boolean; + calculateHash(): string; + dispose(): void; + end(ctx: DrawContext): void; + endPass(ctx: DrawContext, pass: number): void; + getType(): string; + outputFragmentColor(scope: PBInsideFunctionScope, color: PBShaderExp): boolean; + setRenderStates(rs: RenderStateSet): void; + setupFragmentOutput(scope: PBGlobalScope): void; + supportDevice(deviceType: string): boolean; + static readonly type = "ab"; +} + // @public export class AnimationClip { constructor(name: string, model?: SceneNode); @@ -588,14 +597,14 @@ export abstract class BaseLight extends GraphNode { // @public export interface BatchDrawable extends Drawable { - getInstanceDataBuffer(renderPass: RenderPass): { - bindGroup: CachedBindGroup; - offset: number; - }; + // (undocumented) + applyInstanceOffsetAndStride(renderQueue: RenderQueue, stride: number, offset: number): void; + // Warning: (ae-incompatible-release-tags) The symbol "applyMaterialUniforms" is marked as @public, but its signature references "DrawableInstanceInfo" which is marked as @internal + // + // (undocumented) + applyMaterialUniforms(instanceInfo: DrawableInstanceInfo): any; getInstanceId(renderPass: RenderPass): string; getInstanceUniforms(): Float32Array; - // Warning: (ae-incompatible-release-tags) The symbol "setInstanceDataBuffer" is marked as @public, but its signature references "CachedBindGroup" which is marked as @internal - setInstanceDataBuffer(renderPass: RenderPass, bindGroup: CachedBindGroup, offset: number): any; } // @public @@ -814,6 +823,7 @@ export const BUILTIN_ASSET_TEXTURE_SHEEN_LUT = "LUT_Sheen"; export type CachedBindGroup = { bindGroup: BindGroup; buffer: Float32Array; + offset: number; dirty: boolean; }; @@ -832,6 +842,10 @@ export class Camera extends SceneNode { set clipPlane(plane: Plane); // @internal (undocumented) protected _clipPlane: Plane; + get commandBufferReuse(): boolean; + set commandBufferReuse(val: boolean); + // @internal (undocumented) + protected _commandBufferReuse: boolean; // @internal (undocumented) protected _compute(): void; // @internal (undocumented) @@ -840,6 +854,10 @@ export class Camera extends SceneNode { set controller(controller: BaseCameraController); // @internal (undocumented) protected _controller: BaseCameraController; + get depthPrePass(): boolean; + set depthPrePass(val: boolean); + // @internal (undocumented) + protected _depthPrePass: boolean; // @internal (undocumented) protected _dirty: boolean; dispose(): void; @@ -871,6 +889,10 @@ export class Camera extends SceneNode { isCamera(): this is Camera; lookAt(eye: Vector3, target: Vector3, up: Vector3): this; lookAtCubeFace(face: CubeFace, position?: Vector3): this; + get oit(): OIT; + set oit(val: OIT); + // @internal (undocumented) + protected _oit: OIT; // @internal (undocumented) protected _onTransformChanged(invalidateLocal: boolean): void; // @internal (undocumented) @@ -1139,10 +1161,13 @@ export class DirectionalLight extends PunctualLight { // @public export interface Drawable { + // (undocumented) + applyTransformUniforms(renderQueue: RenderQueue): void; draw(ctx: DrawContext): any; getBoneMatrices(): Texture2D; getInstanceColor(): Vector4; getInvBindMatrix(): Matrix4x4; + getMaterial(): Material; getName(): string; getPickTarget(): GraphNode; getQueueType(): number; @@ -1150,11 +1175,23 @@ export interface Drawable { getXForm(): XForm; isBatchable(): this is BatchDrawable; isUnlit(): boolean; + // Warning: (ae-incompatible-release-tags) The symbol "pushRenderQueueRef" is marked as @public, but its signature references "RenderQueueRef" which is marked as @internal + pushRenderQueueRef(ref: RenderQueueRef): any; +} + +// Warning: (ae-internal-missing-underscore) The name "DrawableInstanceInfo" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal +export interface DrawableInstanceInfo { + // (undocumented) + bindGroup: CachedBindGroup; + // (undocumented) + offset: number; } // @public export interface DrawContext { - applyFog: boolean; + applyFog: FogType; camera: Camera; // Warning: (ae-forgotten-export) The symbol "ClusteredLight" needs to be exported by the entry point index.d.ts clusteredLight?: ClusteredLight; @@ -1165,22 +1202,28 @@ export interface DrawContext { defaultViewport?: boolean; depthFormat?: TextureFormat; depthTexture?: Texture2D; + device: AbstractDevice; drawEnvLight: boolean; env: Environment; flip: boolean; + // Warning: (ae-forgotten-export) The symbol "GlobalBindGroupAllocator" needs to be exported by the entry point index.d.ts + globalBindGroupAllocator: GlobalBindGroupAllocator; instanceData?: InstanceData; + instancing?: boolean; lightBlending: boolean; linearDepthTexture?: Texture2D; logger?: RenderLogger; + oit: OIT; primaryCamera: Camera; queue: number; renderPass: RenderPass; renderPassHash: string; + renderQueue?: RenderQueue; scene: Scene; // @internal (undocumented) shadowMapInfo?: Map; + skinAnimation?: boolean; sunLight?: DirectionalLight; - target: Drawable; timestamp: number; viewportHeight?: number; viewportWidth?: number; @@ -1542,14 +1585,20 @@ export class GraphNode extends SceneNode { export class GrassMaterial extends GrassMaterial_base { constructor(terrainSize: Vector2, normalMap: Texture2D, grassTexture?: Texture2D); // (undocumented) + apply(ctx: DrawContext): boolean; + // (undocumented) applyUniformValues(bindGroup: BindGroup, ctx: DrawContext, pass: number): void; // (undocumented) fragmentShader(scope: PBFunctionScope): void; // @override isTransparentPass(pass: number): boolean; // @override + supportInstancing(): boolean; + // @override supportLighting(): boolean; // (undocumented) + protected updateRenderStates(pass: number, stateSet: RenderStateSet, ctx: DrawContext): void; + // (undocumented) vertexShader(scope: PBFunctionScope): void; } @@ -1765,11 +1814,9 @@ export interface IMixinVertexColor { export class InstanceBindGroupAllocator { constructor(); // (undocumented) - allocateInstanceBindGroup(framestamp: number): CachedBindGroup; - // (undocumented) - _freeBindGroupList: CachedBindGroup[]; + allocateInstanceBindGroup(framestamp: number, sizeInFloats: number): CachedBindGroup; // (undocumented) - _usedBindGroupList: CachedBindGroup[]; + _bindGroupList: CachedBindGroup[]; } // @public @@ -1779,11 +1826,9 @@ export interface InstanceData { // (undocumented) bindGroup: CachedBindGroup; // (undocumented) - currentSize: number; + numInstances: number; // (undocumented) - hash: string; - // (undocumented) - maxSize: number; + offset: number; // (undocumented) stride: number; } @@ -1830,7 +1875,7 @@ export class LightPass extends RenderPass { // (undocumented) protected renderItems(ctx: DrawContext, renderQueue: RenderQueue): void; // (undocumented) - protected renderLightPass(ctx: DrawContext, items: RenderQueueItem[], lights: PunctualLight[]): void; + protected renderLightPass(ctx: DrawContext, itemList: RenderItemListBundle, lights: PunctualLight[], flags: any): void; // (undocumented) protected _shadowMapHash: string; } @@ -1854,39 +1899,28 @@ export class Material { // @internal get $isInstance(): boolean; constructor(); + apply(ctx: DrawContext): boolean; applyUniforms(bindGroup: BindGroup, ctx: DrawContext, needUpdate: boolean, pass: number): void; protected _applyUniforms(bindGroup: BindGroup, ctx: DrawContext, pass: number): void; - beginDraw(pass: number, ctx: DrawContext): boolean; // @internal (undocumented) - static bindGroupGarbageCollect(bindGroup: BindGroup): void; + bind(device: AbstractDevice, pass: number): boolean; // @internal (undocumented) - clearBindGroupCache(): number; + get coreMaterial(): this; // @internal (undocumented) - createHash(renderPassType: number, pass: number): string; - protected _createHash(renderPassType: number): string; + createHash(pass: number): string; + protected _createHash(): string; // @internal (undocumented) protected createProgram(ctx: DrawContext, pass: number): GPUProgram; protected _createProgram(pb: ProgramBuilder, ctx: DrawContext, pass: number): GPUProgram; - // @internal (undocumented) - protected createRenderStateSet(): RenderStateSet; - // (undocumented) - dispose(): void; + // @internal draw(primitive: Primitive, ctx: DrawContext, numInstances?: number): void; drawPrimitive(pass: number, primitive: Primitive, ctx: DrawContext, numInstances: number): void; - endDraw(pass: number): void; - static garbageCollect(ts: number): number; - static getGCOptions(): MaterialGCOptions; - // @internal (undocumented) - protected getHash(renderPassType: number, pass: number): string; - getMaterialBindGroup(): BindGroup; - // Warning: (ae-forgotten-export) The symbol "ProgramInfo" needs to be exported by the entry point index.d.ts - getOrCreateProgram(ctx: DrawContext, pass: number): ProgramInfo; // @internal (undocumented) - static getProgramByHashIndex(hash: string, index: number): GPUProgram; + protected getHash(pass: number): string; // (undocumented) getQueueType(): number; // @internal (undocumented) - protected _hash: string[][]; + protected _hash: string[]; get instanceId(): number; isBatchable(): boolean; isTransparentPass(pass: number): boolean; @@ -1898,23 +1932,11 @@ export class Material { // @internal (undocumented) optionChanged(changeHash: boolean): void; passToHash(pass: number): string; - // @internal (undocumented) - protected _renderStateSet: RenderStateSet; - static setGCOptions(opt: MaterialGCOptions): void; - get stateSet(): RenderStateSet; - set stateSet(stateset: RenderStateSet); + supportInstancing(): boolean; supportLighting(): boolean; + protected updateRenderStates(pass: number, renderStates: RenderStateSet, ctx: DrawContext): void; } -// @public -export type MaterialGCOptions = { - disabled?: boolean; - drawableCountThreshold?: number; - materialCountThreshold?: number; - inactiveTimeDuration?: number; - verbose?: boolean; -}; - // @public export interface MaterialTextureInfo { // (undocumented) @@ -1930,8 +1952,10 @@ export interface MaterialTextureInfo { // @public export const MAX_CLUSTERED_LIGHTS = 255; +// Warning: (ae-forgotten-export) The symbol "Mesh_base" needs to be exported by the entry point index.d.ts +// // @public -export class Mesh extends GraphNode implements BatchDrawable { +export class Mesh extends Mesh_base implements BatchDrawable { constructor(scene: Scene, primitive?: Primitive, material?: Material); // @internal (undocumented) protected _animatedBoundingBox: BoundingBox; @@ -1955,23 +1979,15 @@ export class Mesh extends GraphNode implements BatchDrawable { set drawBoundingBox(val: boolean); getBoneMatrices(): Texture2D; getInstanceColor(): Vector4; - getInstanceDataBuffer(renderPass: RenderPass): { - bindGroup: CachedBindGroup; - offset: number; - }; getInstanceId(renderPass: RenderPass): string; getInstanceUniforms(): Float32Array; getInvBindMatrix(): Matrix4x4; + getMaterial(): Material; getName(): string; getPickTarget(): GraphNode; getQueueType(): number; getXForm(): XForm; // @internal (undocumented) - protected _instanceBufferInfo: Map; - // @internal (undocumented) protected _instanceColor: Vector4; // @internal (undocumented) protected _instanceHash: string; @@ -1986,8 +2002,6 @@ export class Mesh extends GraphNode implements BatchDrawable { set primitive(prim: Primitive); setAnimatedBoundingBox(bbox: BoundingBox): void; setBoneMatrices(matrices: Texture2D): void; - // Warning: (ae-incompatible-release-tags) The symbol "setInstanceDataBuffer" is marked as @public, but its signature references "CachedBindGroup" which is marked as @internal - setInstanceDataBuffer(renderPass: RenderPass, bindGroup: CachedBindGroup, offset: number): void; setInvBindMatrix(matrix: Matrix4x4): void; } @@ -2002,11 +2016,10 @@ export class MeshMaterial extends Material { // @internal @override protected _applyUniforms(bindGroup: BindGroup, ctx: DrawContext, pass: number): void; applyUniformValues(bindGroup: BindGroup, ctx: DrawContext, pass: number): void; - beginDraw(pass: number, ctx: DrawContext): boolean; get blendMode(): BlendMode; set blendMode(val: BlendMode); // @internal @override - protected _createHash(renderPassType: number): string; + protected _createHash(): string; createInstance(): this; // @internal (undocumented) protected createProgram(ctx: DrawContext, pass: number): GPUProgram; @@ -2017,6 +2030,8 @@ export class MeshMaterial extends Material { static defineFeature(): number; // Warning: (ae-incompatible-release-tags) The symbol "defineInstanceUniform" is marked as @public, but its signature references "InstanceUniformType" which is marked as @internal static defineInstanceUniform(prop: string, type: InstanceUniformType): number; + // (undocumented) + doAlphaTest(scope: PBInsideFunctionScope, color: PBShaderExp): void; get drawContext(): DrawContext; featureUsed(feature: number): T; fragmentShader(scope: PBFunctionScope): void; @@ -2035,7 +2050,7 @@ export class MeshMaterial extends Material { get pass(): number; supportLighting(): boolean; uniformChanged(): void; - protected updateRenderStates(pass: number, ctx: DrawContext): void; + protected updateRenderStates(pass: number, stateSet: RenderStateSet, ctx: DrawContext): void; useFeature(feature: number, use: unknown): void; vertexShader(scope: PBFunctionScope): void; } @@ -2224,6 +2239,22 @@ export enum OctreePlacement { PPP = 0 } +// @public +export abstract class OIT { + abstract applyUniforms(ctx: DrawContext, bindGroup: BindGroup): any; + abstract begin(ctx: DrawContext): number; + abstract beginPass(ctx: DrawContext, pass: number): boolean; + abstract calculateHash(): string; + abstract dispose(): void; + abstract end(ctx: DrawContext): any; + abstract endPass(ctx: DrawContext, pass: number): any; + abstract getType(): string; + abstract outputFragmentColor(scope: PBInsideFunctionScope, color: PBShaderExp): boolean; + abstract setRenderStates(rs: RenderStateSet): any; + abstract setupFragmentOutput(scope: PBGlobalScope): any; + abstract supportDevice(deviceType: string): boolean; +} + // @public export class OrbitCameraController extends BaseCameraController { constructor(options?: OrbitCameraControllerOptions); @@ -2716,11 +2747,55 @@ export const RENDER_PASS_TYPE_LIGHT = 0; // @public export const RENDER_PASS_TYPE_SHADOWMAP = 1; +// Warning: (ae-internal-missing-underscore) The name "RenderItemList" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal +export interface RenderItemList { + // (undocumented) + opaque: RenderItemListBundle; + // (undocumented) + transparent: RenderItemListBundle; +} + +// Warning: (ae-internal-missing-underscore) The name "RenderItemListBundle" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export interface RenderItemListBundle { + // (undocumented) + lit: RenderItemListInfo[]; + // (undocumented) + unlit: RenderItemListInfo[]; +} + +// Warning: (ae-internal-missing-underscore) The name "RenderItemListInfo" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export interface RenderItemListInfo { + // (undocumented) + instanceItemList: RenderQueueItem[]; + // (undocumented) + instanceList: Record; + // (undocumented) + instanceRenderBundle?: RenderBundleWrapper; + // (undocumented) + itemList: RenderQueueItem[]; + // (undocumented) + materialList: Set; + // Warning: (ae-forgotten-export) The symbol "RenderBundleWrapper" needs to be exported by the entry point index.d.ts + // + // (undocumented) + renderBundle?: RenderBundleWrapper; + // (undocumented) + renderQueue: RenderQueue; + // (undocumented) + skinItemList: RenderQueueItem[]; + // (undocumented) + skinRenderBundle?: RenderBundleWrapper; +} + // @public export abstract class RenderPass { constructor(type: number); - // @internal (undocumented) - applyRenderStates(device: AbstractDevice, stateSet: RenderStateSet, ctx: DrawContext): void; get clearColor(): Vector4; set clearColor(color: Vector4); // @internal (undocumented) @@ -2738,23 +2813,19 @@ export abstract class RenderPass { // @internal (undocumented) protected drawItem(device: AbstractDevice, item: RenderQueueItem, ctx: DrawContext, reverseWinding: boolean): void; // @internal (undocumented) + protected drawItemList(itemList: RenderItemListInfo, ctx: DrawContext, reverseWinding: boolean): void; + // @internal (undocumented) protected drawScene(ctx: DrawContext, cullCamera: Camera, renderQueue?: RenderQueue): void; // @internal (undocumented) + protected getGlobalBindGroup(ctx: DrawContext): BindGroup; + // @internal (undocumented) getGlobalBindGroupHash(ctx: DrawContext): string; // @internal (undocumented) protected abstract _getGlobalBindGroupHash(ctx: DrawContext): any; // @internal (undocumented) - protected getGlobalBindGroupInfo(ctx: DrawContext): { - bindGroup: BindGroup; - layout: BindGroupLayout; - }; - // @internal (undocumented) - protected _globalBindGroups: Record; + protected _globalBindGroups: Record; // @internal (undocumented) - isAutoFlip(): boolean; + isAutoFlip(ctx: DrawContext): boolean; render(ctx: DrawContext, cullCamera?: Camera, renderQueue?: RenderQueue): void; // @internal (undocumented) protected abstract renderItems(ctx: DrawContext, renderQueue: RenderQueue): any; @@ -2767,23 +2838,37 @@ export abstract class RenderPass { export class RenderQueue { // Warning: (ae-incompatible-release-tags) The symbol "__constructor" is marked as @public, but its signature references "InstanceBindGroupAllocator" which is marked as @internal constructor(renderPass: RenderPass, bindGroupAllocator?: InstanceBindGroupAllocator); + // Warning: (ae-incompatible-release-tags) The symbol "binaryInsert" is marked as @public, but its signature references "RenderQueueItem" which is marked as @internal + // + // (undocumented) + binaryInsert(itemList: RenderQueueItem[], item: RenderQueueItem): void; + // @internal (undocumented) + dispose(): void; + // @internal (undocumented) + end(camera: Camera, createRenderBundles?: boolean): this; + // Warning: (ae-incompatible-release-tags) The symbol "getInstanceInfo" is marked as @public, but its signature references "DrawableInstanceInfo" which is marked as @internal + getInstanceInfo(drawable: Drawable): DrawableInstanceInfo; // @internal getMaxBatchSize(): number; - // Warning: (ae-forgotten-export) The symbol "RenderItemList" needs to be exported by the entry point index.d.ts + // Warning: (ae-incompatible-release-tags) The symbol "items" is marked as @public, but its signature references "RenderItemList" which is marked as @internal get items(): Record; push(camera: Camera, drawable: Drawable, renderOrder: number): void; pushLight(light: PunctualLight): void; pushRenderQueue(queue: RenderQueue): void; + // Warning: (ae-incompatible-release-tags) The symbol "ref" is marked as @public, but its signature references "RenderQueueRef" which is marked as @internal + get ref(): RenderQueueRef; get renderPass(): RenderPass; reset(): void; get shadowedLights(): PunctualLight[]; - sortItems(): void; + sortTransparentItems(cameraPos: Vector3): void; get sunLight(): DirectionalLight; set sunLight(light: DirectionalLight); get unshadowedLights(): PunctualLight[]; } -// @public +// Warning: (ae-internal-missing-underscore) The name "RenderQueueItem" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal export interface RenderQueueItem { // (undocumented) drawable: Drawable; @@ -2795,27 +2880,12 @@ export interface RenderQueueItem { sortDistance: number; } -// @public -export abstract class RenderScheme { - constructor(); - get currentScene(): Scene; - // @internal (undocumented) - protected _currentScene: Scene; - dispose(): void; - // @internal (undocumented) - protected abstract _dispose(): void; - // @internal (undocumented) - protected _enableDepthPass: boolean; - getShadowMapFormat(): TextureFormat; - renderScene(scene: Scene, camera: Camera, compositor?: Compositor): void; - // @internal (undocumented) - protected abstract _renderScene(ctx: DrawContext): void; - get requireDepthPass(): boolean; - set requireDepthPass(val: boolean); - // @internal (undocumented) - abstract setClearParams(color: Vector4, depth: number, stencil: number): void; - // @internal (undocumented) - protected _shadowMapFormat: TextureFormat; +// Warning: (ae-internal-missing-underscore) The name "RenderQueueRef" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal +export interface RenderQueueRef { + // (undocumented) + ref: RenderQueue; } // @public @@ -3089,7 +3159,11 @@ export class ShaderHelper { static getFogType(scope: PBInsideFunctionScope): PBShaderExp; static getGlobalUniforms(scope: PBInsideFunctionScope): PBShaderExp; // (undocumented) - static getInstanceBufferStrideUniformName(): string; + static getInstanceDataOffsetUniformName(): string; + // (undocumented) + static getInstanceDataStrideUniformName(): string; + // (undocumented) + static getInstanceDataUniformName(): string; static getInstancedUniform(scope: PBInsideFunctionScope, uniformIndex: number): PBShaderExp; // (undocumented) static getLightBufferUniformName(): string; @@ -3113,13 +3187,9 @@ export class ShaderHelper { static getShadowCameraParams(scope: PBInsideFunctionScope): PBShaderExp; static getShadowMap(scope: PBInsideFunctionScope): PBShaderExp; // @internal (undocumented) - static getSkinMatrix(scope: PBInsideFunctionScope): PBShaderExp; - // @internal (undocumented) static getSunLightDir(scope: PBInsideFunctionScope): PBShaderExp; static getViewMatrix(scope: PBInsideFunctionScope): PBShaderExp; static getViewProjectionMatrix(scope: PBInsideFunctionScope): PBShaderExp; - // (undocumented) - static getWorldMatricesUniformName(): string; static getWorldMatrix(scope: PBInsideFunctionScope): PBShaderExp; // (undocumented) static getWorldMatrixUniformName(): string; @@ -3135,7 +3205,7 @@ export class ShaderHelper { static resolveVertexPosition(scope: PBInsideFunctionScope, pos?: PBShaderExp): PBShaderExp; static resolveVertexTangent(scope: PBInsideFunctionScope, tangent?: PBShaderExp): PBShaderExp; // @internal (undocumented) - static setCameraUniforms(bindGroup: BindGroup, ctx: DrawContext, linear: boolean): void; + static setCameraUniforms(bindGroup: BindGroup, camera: Camera, flip: boolean, linear: boolean): void; static setClipSpacePosition(scope: PBInsideFunctionScope, pos: PBShaderExp): void; // @internal (undocumented) static setFogUniforms(bindGroup: BindGroup, fogType: number, fogColor: Vector4, fogParams: Vector4, apDensity: number, aerialPerspectiveLUT?: Texture2D): void; @@ -3145,6 +3215,7 @@ export class ShaderHelper { static setLightUniformsShadow(bindGroup: BindGroup, ctx: DrawContext, light: PunctualLight): void; // @internal (undocumented) static setLightUniformsShadowMap(bindGroup: BindGroup, ctx: DrawContext, light: PunctualLight): void; + static vertexShaderDrawableStuff(scope: PBGlobalScope, skinning: boolean, instanced: boolean): void; } // Warning: (ae-internal-missing-underscore) The name "ShadowConfig" should be prefixed with an underscore because the declaration is marked as @internal @@ -3189,8 +3260,6 @@ export type ShadowMapParams = { export class ShadowMapPass extends RenderPass { constructor(); // @internal (undocumented) - applyRenderStates(device: AbstractDevice, stateSet: RenderStateSet, ctx: DrawContext): void; - // @internal (undocumented) protected _currentLight: PunctualLight; // @internal (undocumented) protected _getGlobalBindGroupHash(ctx: DrawContext): string; @@ -3198,8 +3267,6 @@ export class ShadowMapPass extends RenderPass { set light(light: PunctualLight); // @internal (undocumented) protected renderItems(ctx: DrawContext, renderQueue: RenderQueue): void; - // @internal (undocumented) - protected _stateOverriden: RenderStateSet; } // @public @@ -3557,8 +3624,6 @@ export class Terrain extends GraphNode { set maxPixelError(val: number); get normalMap(): Texture2D; // @internal (undocumented) - get overridenStateSet(): RenderStateSet; - // @internal (undocumented) get patchSize(): number; // @internal (undocumented) get quadtree(): Quadtree; @@ -3614,11 +3679,15 @@ export class TerrainMaterial extends TerrainMaterial_base { // (undocumented) sampleDetailNormalMap(scope: PBInsideFunctionScope, tex: PBShaderExp, texCoord: PBShaderExp, normalScale: PBShaderExp, TBN: PBShaderExp): PBShaderExp; // @override + supportInstancing(): boolean; + // @override supportLighting(): boolean; // (undocumented) get terrainInfo(): Vector4; set terrainInfo(val: Vector4); // (undocumented) + protected updateRenderStates(pass: number, stateSet: RenderStateSet, ctx: DrawContext): void; + // (undocumented) vertexShader(scope: PBFunctionScope): void; } @@ -3629,10 +3698,11 @@ export type TerrainMaterialOptions = { detailMaps?: TerrainDetailMapInfo; }; +// Warning: (ae-forgotten-export) The symbol "TerrainPatch_base" needs to be exported by the entry point index.d.ts // Warning: (ae-internal-missing-underscore) The name "TerrainPatch" should be prefixed with an underscore because the declaration is marked as @internal // // @internal (undocumented) -export class TerrainPatch implements Drawable { +export class TerrainPatch extends TerrainPatch_base implements Drawable { constructor(terrain: Terrain); // (undocumented) computeBoundingBox(box: BoundingBox): void; @@ -3665,6 +3735,8 @@ export class TerrainPatch implements Drawable { // (undocumented) getLODDistance(): number; // (undocumented) + getMaterial(): Material; + // (undocumented) getMipLevel(): number; // (undocumented) getName(): string; @@ -3683,8 +3755,6 @@ export class TerrainPatch implements Drawable { // (undocumented) getStep(): number; // (undocumented) - getXForm(): XForm>; - // (undocumented) initialize(quadtree: Quadtree, parent: TerrainPatch, rowIndex: number, colIndex: number, baseVertices: Float32Array, normals: Vector3[], heightScale: number, elevations: Float32Array): boolean; // (undocumented) isBatchable(): this is BatchDrawable; @@ -3704,6 +3774,17 @@ export class TerrainPatch implements Drawable { sqrDistanceToPoint(point: Vector3): number; } +// Warning: (ae-internal-missing-underscore) The name "TerrainPatchBase" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export class TerrainPatchBase { + constructor(terrain: Terrain); + // (undocumented) + getXForm(): XForm>; + // (undocumented) + protected _terrain: Terrain; +} + // @public export type TextureFetchOptions = { mimeType?: string; @@ -3776,9 +3857,6 @@ export class TranslationTrack extends AnimationTrack { apply(node: SceneNode, currentTime: number, duration: number): boolean; } -// @public -export type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; - // Warning: (ae-forgotten-export) The symbol "UnlitMaterial_base" needs to be exported by the entry point index.d.ts // // @public @@ -3893,6 +3971,8 @@ export class WaterMesh { get foamWidth(): number; set foamWidth(val: number); // (undocumented) + getClipmapBindGroup(device: AbstractDevice): BindGroup; + // (undocumented) getWaveCroppiness(cascade: number): number; // (undocumented) getWaveLength(cascade: number): number; @@ -3943,6 +4023,24 @@ export interface WaterShaderImpl { shading(scope: PBInsideFunctionScope, worldPos: PBShaderExp, worldNormal: PBShaderExp, foamFactor: PBShaderExp): PBShaderExp; } +// @public +export class WeightedBlendedOIT extends OIT { + constructor(); + applyUniforms(ctx: DrawContext, bindGroup: BindGroup): void; + begin(ctx: DrawContext): number; + beginPass(ctx: DrawContext, pass: number): boolean; + calculateHash(): string; + dispose(): void; + end(ctx: DrawContext): void; + endPass(ctx: DrawContext, pass: number): void; + getType(): string; + outputFragmentColor(scope: PBInsideFunctionScope, color: PBShaderExp): boolean; + setRenderStates(rs: RenderStateSet): void; + setupFragmentOutput(scope: PBGlobalScope): void; + supportDevice(deviceType: string): boolean; + static readonly type = "wb"; +} + // @public export function worleyFBM(scope: PBInsideFunctionScope, p: PBShaderExp, freq: PBShaderExp | number): PBShaderExp; @@ -4020,11 +4118,6 @@ export class XForm = XForm> extends XForm_base { worldToThis(v: Vector4, result?: Vector4): Vector4; } -// Warnings were encountered during analysis: -// -// dist/index.d.ts:3525:9 - (ae-incompatible-release-tags) The symbol "bindGroup" is marked as @public, but its signature references "CachedBindGroup" which is marked as @internal -// dist/index.d.ts:4837:9 - (ae-incompatible-release-tags) The symbol "bindGroup" is marked as @public, but its signature references "CachedBindGroup" which is marked as @internal - // (No @packageDocumentation comment for this package) ``` diff --git a/site/rollup.config.mjs b/site/rollup.config.mjs index 3a3ffc65..ce5fd67a 100644 --- a/site/rollup.config.mjs +++ b/site/rollup.config.mjs @@ -81,7 +81,11 @@ if (!deepEqual(cachedZephr3d, dict)){ buildCache['@zephyr3d'] = dict; invalidAll = true; } +const pattern = process.env.SITE_TUT ? process.env.SITE_TUT.split(';') : null; fs.readdirSync(srcdir).filter((dir) => { + if (pattern && pattern.indexOf(dir) < 0) { + return; + } const fullpath = path.join(srcdir, dir); if (fs.statSync(fullpath).isDirectory()) { let main = path.join(fullpath, 'main.js'); @@ -155,6 +159,7 @@ function getTutTarget(input, output) { plugins: [ nodeResolve(), swc(), + /* terser({ module: true, toplevel: true, @@ -162,6 +167,7 @@ function getTutTarget(input, output) { comments: false } }), + */ copy({ targets: [ { @@ -181,6 +187,7 @@ function getTutTarget(input, output) { } export default (args) => { + console.log(JSON.stringify(srcfiles)); const tutTargets = srcfiles.map((f) => getTutTarget(f[0], f[1])); return [...tutTargets, getCacheTarget()]; }; diff --git a/site/src/demo-0/gltfviewer.ts b/site/src/demo-0/gltfviewer.ts index ed5529fd..f0d1bf2c 100644 --- a/site/src/demo-0/gltfviewer.ts +++ b/site/src/demo-0/gltfviewer.ts @@ -1,11 +1,9 @@ import * as zip from '@zip.js/zip.js'; import { Vector4, Vector3 } from '@zephyr3d/base'; -import type { SceneNode, Scene, AnimationSet } from '@zephyr3d/scene'; +import { SceneNode, Scene, AnimationSet, BatchGroup, PostWater, WeightedBlendedOIT, OIT, ABufferOIT } from '@zephyr3d/scene'; import type { AABB } from '@zephyr3d/base'; import { BoundingBox, - GraphNode, - Material, AssetManager, DirectionalLight, OrbitCameraController, @@ -26,36 +24,46 @@ export class GLTFViewer { private _assetManager: AssetManager; private _scene: Scene; private _tonemap: Tonemap; + private _water: PostWater; private _bloom: Bloom; private _fxaa: FXAA; + private _water: PostWater; + private _oit: OIT; private _doTonemap: boolean; + private _doWater: boolean; private _doBloom: boolean; private _doFXAA: boolean; + private _doWater: boolean; private _camera: PerspectiveCamera; private _light0: DirectionalLight; private _light1: DirectionalLight; private _fov: number; private _nearPlane: number; private _envMaps: EnvMaps; - //private _ui: UI; + private _batchGroup: BatchGroup; private _ui: Panel; private _compositor: Compositor; constructor(scene: Scene) { + const device = Application.instance.device; this._currentAnimation = null; this._modelNode = null; this._animationSet = null; this._scene = scene; this._envMaps = new EnvMaps(); - //this._ui = new UI(this); + this._batchGroup = new BatchGroup(scene); this._assetManager = new AssetManager(); this._tonemap = new Tonemap(); + this._water = new PostWater(0); this._bloom = new Bloom(); this._bloom.threshold = 0.85; this._bloom.intensity = 1.5; this._fxaa = new FXAA(); this._doTonemap = true; + this._doWater = false; this._doBloom = true; this._doFXAA = true; + this._doWater = false; + this._oit = new WeightedBlendedOIT(); this._fov = Math.PI / 3; this._nearPlane = 1; this._compositor = new Compositor(); @@ -65,11 +73,12 @@ export class GLTFViewer { this._camera = new PerspectiveCamera( scene, Math.PI / 3, - Application.instance.device.getDrawingBufferWidth() / - Application.instance.device.getDrawingBufferHeight(), + device.getDrawingBufferWidth() / + device.getDrawingBufferHeight(), 1, 160 ); + this._camera.oit = this._oit; this._camera.position.setXYZ(0, 0, 15); this._camera.controller = new OrbitCameraController(); this._light0 = new DirectionalLight(this._scene).setColor(new Vector4(1, 1, 1, 1)).setCastShadow(false); @@ -79,12 +88,6 @@ export class GLTFViewer { this._light1.shadow.shadowMapSize = 1024; this._light1.lookAt(new Vector3(0, 0, 0), new Vector3(-0.5, 0.707, 0.5), Vector3.axisPY()); this._envMaps.selectById(this._envMaps.getIdList()[0], this.scene); - Material.setGCOptions({ - drawableCountThreshold: 0, - materialCountThreshold: 0, - inactiveTimeDuration: 10000, - verbose: true - }); this._ui = new Panel(this); } get envMaps(): EnvMaps { @@ -146,9 +149,10 @@ export class GLTFViewer { async loadModel(url: string) { this._modelNode?.remove(); this._assetManager.purgeCache(); - this._assetManager.fetchModel(this._scene, url).then((info) => { + this._assetManager.fetchModel(this._scene, url, { enableInstancing: true }).then((info) => { this._modelNode?.dispose(); this._modelNode = info.group; + this._modelNode.parent = this._batchGroup; this._animationSet?.dispose(); this._animationSet = info.animationSet; this._modelNode.pickable = true; @@ -206,11 +210,33 @@ export class GLTFViewer { tonemapEnabled(): boolean { return this._doTonemap; } + waterEnabled(): boolean { + return this._doWater; + } FXAAEnabled(): boolean { return this._doFXAA; } + getOITType(): string { + return this._oit?.getType() ?? ''; + } + setOITType(val: string) { + if (this._oit?.getType() !== val) { + this._oit?.dispose(); + if (val === WeightedBlendedOIT.type) { + this._oit = new WeightedBlendedOIT(); + } else if (val === ABufferOIT.type) { + this._oit = new ABufferOIT(); + } else { + this._oit = null; + } + this._camera.oit = this._oit; + } + } syncPostEffects() { this._compositor.clear(); + if (this._doWater) { + this._compositor.appendPostEffect(this._water); + } if (this._doTonemap) { this._compositor.appendPostEffect(this._tonemap); } @@ -246,6 +272,12 @@ export class GLTFViewer { this.syncPostEffects(); } } + enableWater(enable: boolean) { + if (!!enable !== this._doWater) { + this._doWater = !!enable; + this.syncPostEffects(); + } + } enableFXAA(enable: boolean) { if (!!enable !== this._doFXAA) { this._doFXAA = !!enable; diff --git a/site/src/demo-0/ui.ts b/site/src/demo-0/ui.ts index 425bf683..bf9ad092 100644 --- a/site/src/demo-0/ui.ts +++ b/site/src/demo-0/ui.ts @@ -1,6 +1,6 @@ import { GUI } from 'lil-gui'; import { GLTFViewer } from './gltfviewer'; -import { Application } from '@zephyr3d/scene'; +import { ABufferOIT, Application, WeightedBlendedOIT } from '@zephyr3d/scene'; interface GUIParams { deviceType: string; @@ -8,21 +8,27 @@ interface GUIParams { iblLighting: boolean; punctualLighting: boolean; tonemap: boolean; + water: boolean; bloom: boolean; fxaa: boolean; - FPS: number; + FPS: string; + oitType: string; animation?: string; } export class Panel { private _viewer: GLTFViewer; private _deviceList: string[]; + private _oitTypes: string[]; + private _oitNames: string[]; private _params: GUIParams; private _gui: GUI; private _animationController: GUI; constructor(viewer: GLTFViewer){ this._viewer = viewer; this._deviceList = ['WebGL', 'WebGL2', 'WebGPU']; + this._oitTypes = ['', WeightedBlendedOIT.type, ABufferOIT.type]; + this._oitNames = ['None', 'weighted-blended', 'per-pixel linked list']; this._gui = new GUI({ container: document.body }); this._params = { deviceType: this._deviceList[this._deviceList.findIndex(val => val.toLowerCase() === Application.instance.device.type)], @@ -30,9 +36,11 @@ export class Panel { iblLighting: this._viewer.environmentLightEnabled, punctualLighting: this._viewer.punctualLightEnabled, tonemap: this._viewer.tonemapEnabled(), + water: this._viewer.waterEnabled(), bloom: this._viewer.bloomEnabled(), fxaa: this._viewer.FXAAEnabled(), - FPS: 0 + FPS: '', + oitType: this._oitNames[this._oitTypes.indexOf(this._viewer.getOITType())] }; this._animationController = null; this.create(); @@ -67,6 +75,7 @@ export class Panel { desc2.style.color = '#ffff00'; desc2.innerText = 'Drag HDR to change environment'; this._gui.domElement.append(desc1, desc2); + const systemSettings = this._gui.addFolder('System'); systemSettings.add(this._params, 'deviceType', this._deviceList) .name('Select device') @@ -93,6 +102,14 @@ export class Panel { this._viewer.punctualLightEnabled = value; }); + const oitSettings = this._gui.addFolder('OIT'); + oitSettings.add(this._params, 'oitType', this._oitNames) + .name('Select OIT type') + .onChange(value=>{ + const index = this._oitNames.indexOf(value); + this._viewer.setOITType(this._oitTypes[index]); + }); + const ppSettings = this._gui.addFolder('PostProcess'); ppSettings.add(this._params, 'tonemap') .name('Tonemap') @@ -109,42 +126,16 @@ export class Panel { .onChange(value=>{ this._viewer.enableFXAA(value); }); - } - /* - renderSettings(){ - ImGui.SetNextWindowPos(new ImGui.ImVec2(0, 0), ImGui.Cond.Always); - ImGui.SetNextWindowSize(new ImGui.ImVec2(300, Math.min(500, Application.instance.device.getViewport().height)), ImGui.Cond.Always); - if (ImGui.Begin('Settings')){ - ImGui.EndSection(1); + ppSettings.add(this._params, 'water') + .name('Water') + .onChange(value=>{ + this._viewer.enableWater(value); + }); - const animations = this._viewer.animationSet?.getAnimationNames() ?? []; - if (animations.length > 0) { - ImGui.BeginSection('Animation'); - const index = [animations.findIndex(ani => this._viewer.animationSet.isPlayingAnimation(ani))] as [number]; - ImGui.SetNextItemWidth(150); - if (ImGui.Combo('Animation', index, animations)){ - this._viewer.animationSet.playAnimation(animations[index[0]]); - } - ImGui.EndSection(1); - } - if (ImGui.BeginChild('')) { - ImGui.TextWrapped('Usage:'); - ImGui.TextWrapped('Drag GLTF/GLB/ZIP to view model.'); - ImGui.TextWrapped('Drag HDR to change environment.'); - ImGui.TextWrapped('Note:') - ImGui.TextWrapped('Morph target animation currently not supported.'); - } - ImGui.EndChild(); - } - ImGui.End(); - } - private renderStatusBar() { - if (ImGui.BeginStatusBar()) { - ImGui.Text(`Device: ${Application.instance.device.type}`); - ImGui.Text(`FPS: ${Application.instance.device.frameInfo.FPS.toFixed(2)}`); - ImGui.Text(`DrawCall: ${Application.instance.device.frameInfo.drawCalls}`); - ImGui.EndStatusBar(); - } + const perfSettings = this._gui.addFolder('Performance'); + perfSettings.add(this._params, 'FPS').name('FPS').disable(true).listen(); + setInterval(() => { + this._params.FPS = Application.instance.device.frameInfo.FPS.toFixed(2); + }, 1000); } - */ } diff --git a/site/src/demo-1/materials/fur.ts b/site/src/demo-1/materials/fur.ts index f69e7232..59f7f6c7 100644 --- a/site/src/demo-1/materials/fur.ts +++ b/site/src/demo-1/materials/fur.ts @@ -1,7 +1,7 @@ import { Vector4 } from '@zephyr3d/base'; -import type { BindGroup, PBFunctionScope, Texture2D } from '@zephyr3d/device'; +import type { BindGroup, PBFunctionScope, RenderStateSet, Texture2D } from '@zephyr3d/device'; import type { DrawContext, Primitive } from '@zephyr3d/scene'; -import { Application, MeshMaterial, QUEUE_TRANSPARENT, RENDER_PASS_TYPE_LIGHT, ShaderHelper, applyMaterialMixins, mixinLambert } from '@zephyr3d/scene'; +import { Application, MeshMaterial, QUEUE_TRANSPARENT, ShaderHelper, applyMaterialMixins, mixinLambert } from '@zephyr3d/scene'; export class FurMaterial extends applyMaterialMixins(MeshMaterial, mixinLambert) { private _thickness: number; @@ -84,19 +84,15 @@ export class FurMaterial extends applyMaterialMixins(MeshMaterial, mixinLambert) isBatchable(): boolean { return false; } - beginDraw(pass: number, ctx: DrawContext): boolean { + protected updateRenderStates(pass: number, stateSet: RenderStateSet, ctx: DrawContext): void { if (pass > 0) { - if (ctx.renderPass.type !== RENDER_PASS_TYPE_LIGHT) { - return false; - } else { - this.blendMode = 'blend'; - this.cullMode = 'none'; - } + this.blendMode = 'blend'; + this.cullMode = 'none'; } else { - this.blendMode = 'none'; + this.blendMode ='none'; this.cullMode = 'back'; } - return super.beginDraw(pass, ctx); + super.updateRenderStates(pass, stateSet, ctx); } drawPrimitive(pass: number, primitive: Primitive, ctx: DrawContext, numInstances: number): void { if (pass === 0 || !this._instancing) { diff --git a/site/src/demo-1/materials/toon.ts b/site/src/demo-1/materials/toon.ts index 7ac8b43f..91ca59b8 100644 --- a/site/src/demo-1/materials/toon.ts +++ b/site/src/demo-1/materials/toon.ts @@ -1,4 +1,4 @@ -import type { BindGroup, PBFunctionScope } from '@zephyr3d/device'; +import type { BindGroup, PBFunctionScope, RenderStateSet } from '@zephyr3d/device'; import { DrawContext, MeshMaterial, ShaderHelper, applyMaterialMixins, mixinAlbedoColor, mixinLambert } from '@zephyr3d/scene'; export class ToonMaterial extends applyMaterialMixins(MeshMaterial, mixinAlbedoColor, mixinLambert) { @@ -28,18 +28,18 @@ export class ToonMaterial extends applyMaterialMixins(MeshMaterial, mixinAlbedoC this.uniformChanged(); } } - protected updateRenderStates(pass: number, ctx: DrawContext): void { - super.updateRenderStates(pass, ctx); - this.stateSet.useRasterizerState().cullMode = pass === 0 ? 'front' : 'back'; + protected updateRenderStates(pass: number, stateSet: RenderStateSet, ctx: DrawContext): void { + super.updateRenderStates(pass, stateSet, ctx); + stateSet.useRasterizerState().cullMode = pass === 0 ? 'front' : 'back'; } applyUniformValues(bindGroup: BindGroup, ctx: DrawContext, pass: number): void { super.applyUniformValues(bindGroup, ctx, pass); - if (this.needFragmentColor(ctx)){ - if (pass > 0) { + if (pass > 0) { + if (this.needFragmentColor(ctx)) { bindGroup.setValue('bands', this._bands); - } else { - bindGroup.setValue('edge', this._edgeThickness); } + } else { + bindGroup.setValue('edge', this._edgeThickness); } } vertexShader(scope: PBFunctionScope): void { @@ -59,8 +59,8 @@ export class ToonMaterial extends applyMaterialMixins(MeshMaterial, mixinAlbedoC super.fragmentShader(scope); const that = this; const pb = scope.$builder; - scope.$l.albedo = that.calculateAlbedoColor(scope, scope.texCoords); if (this.needFragmentColor()){ + scope.$l.albedo = that.calculateAlbedoColor(scope, scope.texCoords); if (this.pass === 0) { this.outputFragmentColor(scope, scope.$inputs.worldPos, pb.vec4(0, 0, 0, scope.albedo.a)); } else { diff --git a/site/src/demo-3/demo.ts b/site/src/demo-3/demo.ts index bb5f2795..cd8a452e 100644 --- a/site/src/demo-3/demo.ts +++ b/site/src/demo-3/demo.ts @@ -1,6 +1,6 @@ import { PRNG, Quaternion, Vector3, Vector4 } from '@zephyr3d/base'; import { Texture2D } from '@zephyr3d/device'; -import { Application, AssetHierarchyNode, AssetManager, Bloom, Compositor, DirectionalLight, FXAA, GraphNode, MeshMaterial, ModelInfo, OrbitCameraController, PBRMetallicRoughnessMaterial, PBRSpecularGlossinessMaterial, PerspectiveCamera, Scene, SceneNode, SharedModel, Terrain, Tonemap } from '@zephyr3d/scene'; +import { Application, AssetHierarchyNode, AssetManager, Bloom, Compositor, DirectionalLight, FXAA, MeshMaterial, ModelInfo, OrbitCameraController, PBRMetallicRoughnessMaterial, PBRSpecularGlossinessMaterial, PerspectiveCamera, Scene, SceneNode, SharedModel, Terrain, Tonemap } from '@zephyr3d/scene'; import * as zip from '@zip.js/zip.js'; import { TreeMaterialMetallicRoughness } from './treematerial'; import { Panel } from './ui'; @@ -274,7 +274,7 @@ export class Demo { newMaterial.textureHeight = material.albedoTexture.height; newMaterial.blendMode = 'none'; newMaterial.alphaCutoff = 0.8; - newMaterial.stateSet.useRasterizerState().setCullMode('none'); + newMaterial.cullMode = 'none'; newMaterial.ior = material.ior; newMaterial.specularFactor = material.specularFactor; newMaterial.albedoColor = material.albedoColor; @@ -312,7 +312,7 @@ export class Demo { newMaterial.textureHeight = material.albedoTexture.height; newMaterial.blendMode = 'none'; newMaterial.alphaCutoff = 0.8; - newMaterial.stateSet.useRasterizerState().setCullMode('none'); + newMaterial.cullMode = 'none'; newMaterial.ior = material.ior; newMaterial.specularFactor = material.specularFactor; newMaterial.albedoColor = material.albedoColor; diff --git a/site/src/demo-4/main.ts b/site/src/demo-4/main.ts index ad221e7b..da828cd3 100644 --- a/site/src/demo-4/main.ts +++ b/site/src/demo-4/main.ts @@ -20,7 +20,7 @@ import { backendWebGL1, backendWebGL2 } from '@zephyr3d/backend-webgl'; import { PhysicsWorld } from './physics'; import { Panel } from './ui'; -const objectCount = 800; +const objectCount = 400; function getQueryString(name: string) { return new URL(window.location.toString()).searchParams.get(name) || null; @@ -56,11 +56,8 @@ PhysicsApp.ready().then(async () => { device.setFont('24px arial'); const scene = new Scene(); - const light = new DirectionalLight(scene) - .setColor(new Vector4(1, 1, 1, 1)) - .setCastShadow(false); + const light = new DirectionalLight(scene).setColor(new Vector4(1, 1, 1, 1)).setCastShadow(true); light.lookAt(new Vector3(0, 0, 0), new Vector3(0.5, -0.707, -0.5), Vector3.axisPY()); - light.castShadow = true; light.shadow.mode = 'pcf-opt'; light.shadow.pcfKernelSize = 3; light.shadow.numShadowCascades = 4; @@ -83,6 +80,11 @@ PhysicsApp.ready().then(async () => { PhysicsApp.on('resize', (ev) => { camera.aspect = ev.width / ev.height; }); + PhysicsApp.on('keydown', (ev) => { + if (ev.code === 'Space') { + light.castShadow = !light.castShadow; + } + }) PhysicsApp.on('tick', () => { camera.updateController(); camera.render(scene, compositor); @@ -103,27 +105,33 @@ PhysicsApp.ready().then(async () => { const objMaterial = new LambertMaterial(); const boxShape = new BoxShape({ size: 2 }); const sphereShape = new SphereShape(); - for (let i = 0; i < objectCount >> 1; i++) { - let instanceMaterial = objMaterial.createInstance(); - instanceMaterial.albedoColor = new Vector4(Math.random(), Math.random(), Math.random(), 1); - const box = new Mesh(scene, boxShape, instanceMaterial); - box.position.setXYZ(0, 50, 0); - box.parent = batchGroup; - queue.push(box); - instanceMaterial = objMaterial.createInstance(); - instanceMaterial.albedoColor = new Vector4(Math.random(), Math.random(), Math.random(), 1); - const sphere = new Mesh(scene, sphereShape, instanceMaterial); - sphere.position.setXYZ(0, 50, 0); - sphere.parent = batchGroup; - queue.push(sphere); + for (let i = 0; i < objectCount; i++) { + { + let instanceMaterial = objMaterial.createInstance(); + instanceMaterial.albedoColor = new Vector4(Math.random(), Math.random(), Math.random(), 1); + const sphere = new Mesh(scene, sphereShape, instanceMaterial); + sphere.position.setXYZ(0, 50, 0); + sphere.parent = batchGroup; + queue.push(sphere); + } + { + let instanceMaterial = objMaterial.createInstance(); + instanceMaterial.albedoColor = new Vector4(Math.random(), Math.random(), Math.random(), 1); + const box = new Mesh(scene, boxShape, instanceMaterial); + box.position.setXYZ(0, 50, 0); + box.parent = batchGroup; + queue.push(box); + } } physicsWorld.start(); setInterval(() => { const mesh = queue.shift(); - queue.push(mesh); - physicsWorld.positionMesh(mesh, 1, Math.random() * 8 - 4, 50, Math.random() * 8 - 4); - }, 50); + if (mesh) { + queue.push(mesh); + physicsWorld.positionMesh(mesh, 1, Math.random() * 8 - 4, 50, Math.random() * 8 - 4); + } + }, 300); new Panel(); PhysicsApp.run(); diff --git a/site/src/demo-5/main.ts b/site/src/demo-5/main.ts index 9e9eb1b1..3d1126a6 100644 --- a/site/src/demo-5/main.ts +++ b/site/src/demo-5/main.ts @@ -61,7 +61,7 @@ instancingApp.ready().then(async () => { const batchGroup = new BatchGroup(scene); const assetManager = new AssetManager(); - (async function(){ + await (async function(){ for (let i = 0; i < 2000; i++) { const stone1 = await assetManager.fetchModel(scene, 'assets/models/stone1.glb', { enableInstancing: true }); stone1.group.parent = batchGroup; diff --git a/site/src/demo-6/index.html b/site/src/demo-6/index.html new file mode 100644 index 00000000..425d6324 --- /dev/null +++ b/site/src/demo-6/index.html @@ -0,0 +1,32 @@ + + + + + + + zephyr3d demo + + + + +
+ +
+ + + diff --git a/site/src/demo-6/main.ts b/site/src/demo-6/main.ts new file mode 100644 index 00000000..53c942b2 --- /dev/null +++ b/site/src/demo-6/main.ts @@ -0,0 +1,114 @@ +import { backendWebGL1, backendWebGL2 } from '@zephyr3d/backend-webgl'; +import { backendWebGPU } from '@zephyr3d/backend-webgpu'; +import { Quaternion, Vector3, Vector4 } from '@zephyr3d/base'; +import { DeviceBackend } from '@zephyr3d/device'; +import { + Scene, + OrbitCameraController, + DirectionalLight, + Application, + PerspectiveCamera, + Compositor, + Tonemap, + BatchGroup, + LambertMaterial, + BoxShape, + Mesh +} from '@zephyr3d/scene'; +import { Panel } from './ui'; + +let instanceCount = 0; + +function getQueryString(name: string) { + return new URL(window.location.toString()).searchParams.get(name) || null; +} + +function getBackend(): DeviceBackend { + const type = getQueryString('dev'); + if (type === 'webgpu') { + if (backendWebGPU.supported()) { + return backendWebGPU; + } else { + console.warn('No WebGPU support, fall back to WebGL2'); + } + } + return backendWebGL2.supported() ? backendWebGL2 : backendWebGL1; +} + +const showUI = !!getQueryString('ui'); +const app = new Application({ + backend: getBackend(), + canvas: document.querySelector('#canvas') +}); + +app.ready().then(async () => { + const device = app.device; + const scene = new Scene(); + scene.env.light.strength = 0.1; + const camera = new PerspectiveCamera( + scene, + Math.PI / 3, + device.getDrawingBufferWidth() / device.getDrawingBufferHeight(), + 1, + 1000 + ); + camera.position.setXYZ(0, 0, 100); + camera.controller = new OrbitCameraController(); + app.inputManager.use(camera.handleEvent.bind(camera)); + + const compositor = new Compositor(); + compositor.appendPostEffect(new Tonemap()); + + const numMaterials = 8; + const batchGroup = new BatchGroup(scene); + const materials = Array.from({length: numMaterials}).map(val => { + const mat = new LambertMaterial(); + mat.albedoColor = new Vector4(Math.random(), Math.random(), Math.random(), 1); + return mat; + }); + const boxShape = new BoxShape(); + + const createNBoxes = function(n: number) { + for (let i = 0; i < n; i++) { + const mesh = new Mesh(scene, boxShape, materials[Math.floor(Math.random() * numMaterials)]); + const scale = 2 + Math.random() * 2 - 1. + mesh.position = new Vector3(Math.random() * 200 - 100, Math.random() * 200 - 100, Math.random() * 200 - 100); + mesh.scale = new Vector3(scale, scale, scale); + mesh.rotation = Quaternion.fromEulerAngle(Math.random(), Math.random(), Math.random(), 'ZYX'); + mesh.parent = batchGroup; + } + instanceCount += n; + return instanceCount; + }; + const removeNBoxes = function(n: number) { + for (let i = 0; i < Math.min(batchGroup.children.length, n); i++) { + batchGroup.children[0].remove(); + instanceCount--; + } + return instanceCount; + } + + createNBoxes(5000); + + const light = new DirectionalLight(scene).setCastShadow(false).setColor(new Vector4(1, 1, 1, 1)); + light.lookAt(Vector3.one(), Vector3.zero(), Vector3.axisPY()); + + app.on('resize', (ev) => { + camera.setPerspective(camera.getFOV(), ev.width / ev.height, camera.getNearPlane(), camera.getFarPlane()); + }); + + app.on('tick', (ev) => { + camera.updateController(); + camera.render(scene, compositor); + }); + + if (showUI) { + new Panel(instanceCount, camera, () => { + return createNBoxes(500); + }, () => { + return removeNBoxes(500); + }) + } + + app.run(); +}); diff --git a/site/src/demo-6/ui.ts b/site/src/demo-6/ui.ts new file mode 100644 index 00000000..b636bcc5 --- /dev/null +++ b/site/src/demo-6/ui.ts @@ -0,0 +1,60 @@ +import { GUI } from 'lil-gui'; +import { Application, Camera } from "@zephyr3d/scene"; + +interface GUIParams { + numInstances: number; + deviceType: string; + commandBufferReuse: boolean; + FPS: string; + add: () => void; + remove: () => void; +} + +export class Panel { + private _params: GUIParams; + private _camera: Camera; + private _gui: GUI; + constructor(instanceCount: number, camera: Camera, addFunc: () => number, removeFunc: () => number){ + this._camera = camera; + this._params = { + numInstances: instanceCount, + deviceType: Application.instance.device.type, + commandBufferReuse: this._camera.commandBufferReuse, + FPS: '', + add() { + this.numInstances = addFunc(); + }, + remove() { + this.numInstances = removeFunc(); + } + }; + this._gui = new GUI({ container: document.body }); + this.create(); + } + create(){ + const desc1 = document.createElement('p'); + desc1.style.marginTop = '1.5rem'; + desc1.style.padding = '0.5rem'; + desc1.style.color = '#ffff00'; + desc1.innerText = 'The models are rendered without the use of geometry instancing.'; + const desc2 = document.createElement('p'); + desc2.style.marginBottom = '1rem'; + desc2.style.padding = '0.5rem'; + desc2.style.color = '#ffff00'; + desc2.innerText = 'The command buffer reuse optimization is only valid when used with a WebGPU device.'; + this._gui.domElement.append(desc1, desc2); + + const stats = this._gui.addFolder('Stats'); + stats.add(this._params, 'commandBufferReuse').name('Reuse commandbuffer').onChange(value=>{ + this._camera.commandBufferReuse = value; + }); + stats.add(this._params, 'deviceType').name('Device').disable(true); + stats.add(this._params, 'numInstances').name('Instance count').disable(true).listen(); + stats.add(this._params, 'add').name('Add 500 instances'); + stats.add(this._params, 'remove').name('Remove 500 instances'); + stats.add(this._params, 'FPS').name('FPS').disable(true).listen(); + setInterval(() => { + this._params.FPS = Application.instance.device.frameInfo.FPS.toFixed(2); + }, 1000); + } +} diff --git a/site/src/demo-7/index.html b/site/src/demo-7/index.html new file mode 100644 index 00000000..2da3439d --- /dev/null +++ b/site/src/demo-7/index.html @@ -0,0 +1,32 @@ + + + + + + + zephyr3d demo + + + + +
+ +
+ + + diff --git a/site/src/demo-7/main.ts b/site/src/demo-7/main.ts new file mode 100644 index 00000000..5405ccf2 --- /dev/null +++ b/site/src/demo-7/main.ts @@ -0,0 +1,101 @@ +import { backendWebGL1, backendWebGL2 } from '@zephyr3d/backend-webgl'; +import { backendWebGPU } from '@zephyr3d/backend-webgpu'; +import { Vector3, Vector4 } from '@zephyr3d/base'; +import { DeviceBackend } from '@zephyr3d/device'; +import { + Scene, + OrbitCameraController, + DirectionalLight, + Application, + PerspectiveCamera, + Compositor, + Tonemap, + BatchGroup, + LambertMaterial, + BoxShape, + Mesh, + ABufferOIT, + WeightedBlendedOIT, +} from '@zephyr3d/scene'; +import { Panel } from './ui'; + +function getQueryString(name: string) { + return new URL(window.location.toString()).searchParams.get(name) || null; +} + +function getBackend(): DeviceBackend { + const type = getQueryString('dev'); + if (type === 'webgpu') { + if (backendWebGPU.supported()) { + return backendWebGPU; + } else { + console.warn('No WebGPU support, fall back to WebGL2'); + } + } + return backendWebGL2.supported() ? backendWebGL2 : backendWebGL1; +} + +const showUI = !!getQueryString('ui'); +const app = new Application({ + backend: getBackend(), + canvas: document.querySelector('#canvas') +}); + +app.ready().then(async () => { + const device = app.device; + const scene = new Scene(); + scene.env.sky.fogType = 'none'; + const camera = new PerspectiveCamera( + scene, + Math.PI / 3, + device.getDrawingBufferWidth() / device.getDrawingBufferHeight(), + 1, + 1000 + ); + camera.position.setXYZ(0, 0, 12); + camera.controller = new OrbitCameraController(); + camera.oit = device.type === 'webgpu' ? new ABufferOIT() : new WeightedBlendedOIT(); + camera.depthPrePass = true; + + app.inputManager.use(camera.handleEvent.bind(camera)); + + const compositor = new Compositor(); + compositor.appendPostEffect(new Tonemap()); + + const batchGroup = new BatchGroup(scene); + const boxShape = new BoxShape(); + + const opaqueMat = new LambertMaterial(); + for (let i = 0; i < 100; i++) { + const instanceMat = new LambertMaterial(); + instanceMat.blendMode = 'blend'; + instanceMat.albedoColor = new Vector4(Math.random(), Math.random(), Math.random(), Math.random()); + const transMesh = new Mesh(scene, boxShape, instanceMat); + transMesh.position.setXYZ(Math.random() * 5 - 2.5, Math.random() * 5 - 2.5, Math.random() * 5 - 2.5); + transMesh.parent = batchGroup; + } + for (let i = 0; i < 8; i++) { + const instanceMat = opaqueMat.createInstance(); + instanceMat.albedoColor = new Vector4(1, 0, 0, 1); + const opaqueMesh = new Mesh(scene, boxShape, instanceMat); + opaqueMesh.position.setXYZ(Math.random() * 5 - 2.5, Math.random() * 5 - 2.5, Math.random() * 5 - 2.5); + opaqueMesh.parent = batchGroup; + } + const light = new DirectionalLight(scene).setCastShadow(false).setColor(new Vector4(1, 1, 1, 1)); + light.lookAt(Vector3.one(), Vector3.zero(), Vector3.axisPY()); + + app.on('resize', (ev) => { + camera.setPerspective(camera.getFOV(), ev.width / ev.height, camera.getNearPlane(), camera.getFarPlane()); + }); + + app.on('tick', (ev) => { + camera.updateController(); + camera.render(scene, compositor); + }); + + if (showUI) { + new Panel(camera); + } + + app.run(); +}); diff --git a/site/src/demo-7/ui.ts b/site/src/demo-7/ui.ts new file mode 100644 index 00000000..6d54c028 --- /dev/null +++ b/site/src/demo-7/ui.ts @@ -0,0 +1,66 @@ +import { GUI } from 'lil-gui'; +import { ABufferOIT, Application, Camera, WeightedBlendedOIT } from '@zephyr3d/scene'; + +interface GUIParams { + deviceType: string; + FPS: string; + oitType: string; +} + +export class Panel { + private _deviceList: string[]; + private _oitTypes: string[]; + private _oitNames: string[]; + private _params: GUIParams; + private _camera: Camera; + private _gui: GUI; + constructor(camera: Camera){ + this._camera = camera; + this._deviceList = ['WebGL', 'WebGL2', 'WebGPU']; + this._gui = new GUI({ container: document.body }); + this._oitTypes = ['', WeightedBlendedOIT.type]; + this._oitNames = ['Sort back to front', 'weighted-blended']; + if (Application.instance.device.type === 'webgpu') { + this._oitTypes.push(ABufferOIT.type); + this._oitNames.push('per-pixel linked list'); + } + this._params = { + deviceType: this._deviceList[this._deviceList.findIndex(val => val.toLowerCase() === Application.instance.device.type)], + FPS: '', + oitType: this._oitNames[this._oitTypes.indexOf(this._camera.oit ? this._camera.oit.getType() : '')] + }; + this.create(); + } + create(){ + const systemSettings = this._gui.addFolder('System'); + systemSettings.add(this._params, 'deviceType', this._deviceList) + .name('Select device') + .onChange(value=>{ + const url = new URL(window.location.href); + url.searchParams.set('dev', value.toLowerCase()); + window.location.href = url.href; + }); + + const oitSettings = this._gui.addFolder('OIT'); + oitSettings.add(this._params, 'oitType', this._oitNames) + .name('Select OIT type') + .onChange(value=>{ + this._camera.oit?.dispose(); + this._camera.oit = null; + const index = this._oitNames.indexOf(value); + switch(this._oitTypes[index]) { + case ABufferOIT.type: + this._camera.oit = new ABufferOIT(); + break; + case WeightedBlendedOIT.type: + this._camera.oit = new WeightedBlendedOIT(); + break; + } + }); + const perfSettings = this._gui.addFolder('Performance'); + perfSettings.add(this._params, 'FPS').name('FPS').disable(true).listen(); + setInterval(() => { + this._params.FPS = Application.instance.device.frameInfo.FPS.toFixed(2); + }, 1000); + } +} diff --git a/site/src/tut-42/main.js b/site/src/tut-42/main.js index f382ead6..a1e4b836 100644 --- a/site/src/tut-42/main.js +++ b/site/src/tut-42/main.js @@ -18,11 +18,11 @@ class CartoonMaterial extends applyMaterialMixins(MeshMaterial, mixinLambert) { this.numPasses = 2; } // 重写此方法自定义每个Pass的渲染状态 - updateRenderStates(pass, ctx) { + updateRenderStates(pass, stateSet, ctx) { // 必须调用默认实现 - super.updateRenderStates(pass, ctx); + super.updateRenderStates(pass, stateSet, ctx); // 第一遍剔除正面,第二遍剔除背面 - this.stateSet.useRasterizerState().cullMode = pass === 0 ? 'front' : 'back'; + stateSet.useRasterizerState().cullMode = pass === 0 ? 'front' : 'back'; } // 提交Uniform常量 applyUniformValues(bindGroup, ctx, pass) { diff --git a/site/src/tut-43/main.js b/site/src/tut-43/main.js index e95376a6..97c68bfc 100644 --- a/site/src/tut-43/main.js +++ b/site/src/tut-43/main.js @@ -13,12 +13,12 @@ class MyLambertMaterial extends applyMaterialMixins(MeshMaterial, mixinLambert) // 漫反射贴图,该帖图是否存在会生成两个shader变体 this.diffuseTexture = null; } - // 每次渲染之前更新变体值 - beginDraw(pass, ctx) { + // 每次应用之前更新变体值 + apply(ctx) { // 有无diffuseTexture形成两个变体 this.useFeature(MyLambertMaterial.featureDiffuseTexture, !!this.diffuseTexture); // 默认实现必须调用 - return super.beginDraw(pass, ctx); + return super.apply(ctx); } // 受光照影响 supportLighting() { diff --git a/site/src/tut-9/main.js b/site/src/tut-9/main.js index 1ae1192c..2f663020 100644 --- a/site/src/tut-9/main.js +++ b/site/src/tut-9/main.js @@ -13,7 +13,7 @@ myApp.ready().then(function () { // Create an unlit material const material = new UnlitMaterial(); // Disable backface culling - material.stateSet.useRasterizerState().setCullMode('none'); + material.cullMode = 'none'; // Use vertex color material.vertexColor = true; // Fill the triangle data diff --git a/site/web/demo.html b/site/web/demo.html index 8048d966..a4592696 100644 --- a/site/web/demo.html +++ b/site/web/demo.html @@ -95,6 +95,8 @@

zephyr3d demos

  • TerrainTerrain
  • InstancingInstancing
  • PhysicsPhysics
  • +
  • CommandBuffer ReuseCommand buffer reuse
  • +
  • OITOrder-independent-transparency
  • @@ -103,7 +105,8 @@

    zephyr3d demos

    const iframe = document.querySelector('iframe'); const linkCallback = function () { const id = this.id; - iframe.src = 'tut/' + id + '.html' + '?dev=webgl2&ui=1'; + const dev = this.getAttribute('dev'); + iframe.src = 'tut/' + id + '.html' + '?dev=' + (dev || 'webgl2') + '&ui=1'; const links = document.querySelectorAll('li'); for (const link of links) { if (link === this) { diff --git a/site/web/en/_sidebar.md b/site/web/en/_sidebar.md index 50f63a36..2c9e730f 100644 --- a/site/web/en/_sidebar.md +++ b/site/web/en/_sidebar.md @@ -48,6 +48,8 @@ - [Water](en/water.md) - Geometry instancing - [Introduction](en/instancing-intro.md) + - Order-Independent Transparency + - [Introduction](en/oit.md) - Multi-viewport Rendering - [Introduction](en/multi-views.md) - Custom material diff --git a/site/web/en/instancing-intro.md b/site/web/en/instancing-intro.md index 4e4b7e82..3d42ca3e 100644 --- a/site/web/en/instancing-intro.md +++ b/site/web/en/instancing-intro.md @@ -87,7 +87,13 @@ this attribute is added, the material will automatically call the createInstance ``` -## Important Notice: +## Transparent Objects -**Do not invoke the createInstance method of a material if instance rendering is not required. This is because createInstance creates a Proxy of the original material, and the Proxy's get() method is very slow, significantly impacting performance.** +In most cases, transparent objects need to be rendered from far to near, but if geometry instancing is used, +distance sorting is not possible. When using geometry instancing for transparent objects, it is recommended +to use Order-Independent Transparency (OIT) rendering techniques. We currently support two OIT rendering methods: +1. Weighted Blended, suitable for WebGL, WebGL2, and WebGPU devices +2. Per-Pixel Linked List, only applicable to WebGPU devices + +For more details, refer to: [OIT Rendering](en/oit.md) diff --git a/site/web/en/mesh-material.md b/site/web/en/mesh-material.md index 684e9ea8..5b8194a6 100644 --- a/site/web/en/mesh-material.md +++ b/site/web/en/mesh-material.md @@ -88,7 +88,7 @@ Below, we'll create an unlit triangle mesh by manually populating the vertex dat // Creates an unlit material const material = new UnlitMaterial(); // Disable backface culling - material.stateSet.useRasterizerState().setCullMode('none'); + material.getRenderStateSet(0).useRasterizerState().setCullMode('none'); // Use vertex color material.vertexColor = true; diff --git a/site/web/en/oit.md b/site/web/en/oit.md new file mode 100644 index 00000000..18c7ef96 --- /dev/null +++ b/site/web/en/oit.md @@ -0,0 +1,59 @@ +# Order-Independent Transparency + +Zephyr3d supports two types of Order-Independent Transparency (OIT) techniques: +Weighted Blended OIT and Per-Pixel Linked List OIT. + +## Weighted Blended OIT + +Weighted Blended OIT is a weight-based transparency blending technique. It calculates +the color and opacity weights for each fragment in the fragment shader and then blends +these fragments with weighted blending in the final composition stage. + +This technique is relatively simple to implement, performs well, and can handle complex +transparent scenes effectively. However, it may not completely solve all transparency +sorting issues and could result in visual artifacts in certain cases. + +Weighted Blended OIT is supported on WebGL, WebGL2, and WebGPU devices. + +The following code enables rendering of transparent objects using Weighted Blended OIT: + +```javascript + +// Specify Weighted Blended rendering of transparent objects for the camera +camera.oit = new WeightedBlendedOIT(); + +``` + +## Per-Pixel Linked List OIT + +Per-Pixel Linked List OIT is a per-pixel linked list transparency rendering technique. +It constructs a linked list for each fragment in the fragment shader, storing color +and depth information for that fragment. + +This technique excels in accurately handling the rendering order of transparent objects, +even in complex scenes. However, it requires more memory and computational resources. + +Per-Pixel Linked List OIT is only available for WebGPU devices. + +The following code enables rendering of transparent objects using Per-Pixel Linked List: + +```javascript + +// Specify Per-Pixel Linked List rendering of transparent objects for the camera +// The constructor parameter specifies the number of supported transparency levels, default is 16. +camera.oit = new ABufferOIT(20); + +``` + +## Note + +After use, the OIT object must be released to prevent resource leakage. + +```javascript + +// Release the OIT object +camera.oit.dispose(); +camera.oit = null; + +``` + diff --git a/site/web/en/user-material-multipass.md b/site/web/en/user-material-multipass.md index ff40bb8c..1f18fbba 100644 --- a/site/web/en/user-material-multipass.md +++ b/site/web/en/user-material-multipass.md @@ -30,11 +30,11 @@ class CartoonMaterial extends applyMaterialMixins(MeshMaterial, mixinLambert) { this.numPasses = 2; } // Customize the rendering state for each Pass by overriding this method. - updateRenderStates(pass, ctx) { + updateRenderStates(pass, stateSet, ctx) { // The default implementation must be called. - super.updateRenderStates(pass, ctx); + super.updateRenderStates(pass, stateSet, ctx); // Cull front face in the first pass, and the back face in the second pass. - this.stateSet.useRasterizerState().cullMode = pass === 0 ? 'front' : 'back'; + stateSet.useRasterizerState().cullMode = pass === 0 ? 'front' : 'back'; } applyUniformValues(bindGroup, ctx, pass) { super.applyUniformValues(bindGroup, ctx, pass); diff --git a/site/web/en/user-material-var.md b/site/web/en/user-material-var.md index 2b4e036a..6c0ecd20 100644 --- a/site/web/en/user-material-var.md +++ b/site/web/en/user-material-var.md @@ -46,11 +46,11 @@ class MyLambertMaterial extends applyMaterialMixins(MeshMaterial, mixinLambert) // Diffuse texture,default to null this.diffuseTexture = null; } - // Update variant values before each render - beginDraw(pass, ctx) { + // Update variant values before applying material + apply(ctx) { this.useFeature(MyLambertMaterial.featureDiffuseTexture, !!this.diffuseTexture); // Default implementation must be invoked. - return super.beginDraw(pass, ctx); + return super.apply(ctx); } supportLighting() { return true; diff --git a/site/web/en/user-material.md b/site/web/en/user-material.md index be8b803e..b36f4dd3 100644 --- a/site/web/en/user-material.md +++ b/site/web/en/user-material.md @@ -61,19 +61,16 @@ After inheriting from MeshMaterial,custom materials may need to override some cl This is where the fragment shader implementation for the material goes, which also requires overriding. Make sure to call its default implementation when overriding. - - [MeshMaterial.updateRenderStates(pass, ctx)](/doc/markdown/./scene.meshmaterial.updaterenderstates) + - [MeshMaterial.updateRenderStates(pass, stateSet, ctx)](/doc/markdown/./scene.meshmaterial.updaterenderstates) - Called within the beginDraw() method to set the material's rendering states. When overriding, you must call its default implementation. + Will be called before the objects that uses this material being rendered. When overriding, you must call its default implementation. - - [MeshMaterial.beginDraw(pass, ctx)](/doc/markdown/./scene.material.begindraw) + - [MeshMaterial.apply(ctx)](/doc/markdown/./scene.material.apply) - Called before rendering a pass of the material, it sets up Shaders and uniform constants as needed. Returning true allows the pass to be rendered, while false skips it. When overriding, you must call its default implementation. + Will be called while applying the material. it sets up Shaders and uniform constants as needed. When overriding, you must call its default implementation. - - [MeshMaterial.endDraw(pass, ctx)](/doc/markdown/./scene.material.begindraw) - Called after a Pass of the material has been rendered. When overriding, you must call its default implementation. - -类[ShaderHelper](/doc/markdown/./scene.shaderhelper)提供了编写材质需要的诸多工具函数。 +The [ShaderHelper](/doc/markdown/./scene.shaderhelper) class provides numerous utility functions necessary for writing materials. ## System desgin diff --git a/site/web/media/commandbufferreuse.jpg b/site/web/media/commandbufferreuse.jpg new file mode 100644 index 00000000..a326218b Binary files /dev/null and b/site/web/media/commandbufferreuse.jpg differ diff --git a/site/web/media/oit.jpg b/site/web/media/oit.jpg new file mode 100644 index 00000000..933202bf Binary files /dev/null and b/site/web/media/oit.jpg differ diff --git a/site/web/zh-cn/_sidebar.md b/site/web/zh-cn/_sidebar.md index 096ca510..1caa7ba7 100644 --- a/site/web/zh-cn/_sidebar.md +++ b/site/web/zh-cn/_sidebar.md @@ -44,6 +44,8 @@ - [水面渲染](zh-cn/water.md) - 几何体实例化 - [概述](zh-cn/instancing-intro.md) + - 顺序无关的透明度渲染 + - [概述](zh-cn/oit.md) - 多视口渲染 - [概述](zh-cn/multi-views.md) - 自定义材质 diff --git a/site/web/zh-cn/instancing-intro.md b/site/web/zh-cn/instancing-intro.md index b501b016..32e32c46 100644 --- a/site/web/zh-cn/instancing-intro.md +++ b/site/web/zh-cn/instancing-intro.md @@ -63,7 +63,7 @@ for (let i = 0; i < 10; i++) { const instancedModels = []; const nonInstancedModels = []; // 模型地址 - const modelUrl = 'http://model/path'; + const modelUrl = 'http://foo/bar.glb'; for (let i = 0; i < 100; i++) { // 加载相同的模型并设置enableInstancing属性为true,这些模型自动使用实例化渲染 instancedModels.push(await assetManager.fetchModel(scene, url, { @@ -77,6 +77,16 @@ for (let i = 0; i < 10; i++) { ``` -## 重要说明 +## 透明物体 + +通常情况下,透明物体需要由远及近渲染,但是如果使用了几何体实例化,则无法通过距离排序。 +如果你对透明物体使用几何体实例化,推荐使用顺序无关的透明物体渲染技术(OIT)。我们目前 +支持两种OIT渲染方式. + +1. Weighted Blended,适用于WebGL,WebGL2和WebGPU设备 +2. Per-Pixel Linked List,仅适用于WebGPU设备 + +具体参见:[OIT渲染](zh-cn/oit.md) + + -**如果不需要实例化渲染,请勿调用材质的createInstance方法,因为createInstance会创建原材质的一个Proxy,而Proxy的get()方法非常慢,将会严重影响运行效率。** diff --git a/site/web/zh-cn/mesh-material.md b/site/web/zh-cn/mesh-material.md index 80561a6b..85fbb949 100644 --- a/site/web/zh-cn/mesh-material.md +++ b/site/web/zh-cn/mesh-material.md @@ -89,7 +89,7 @@ assetManager.fetchTexture('assets/images/earthnormal.png', { // 创建一个无光照材质 const material = new UnlitMaterial(); // 禁止背面剔除 - material.stateSet.useRasterizerState().setCullMode('none'); + material.getRenderStateSet(0).useRasterizerState().setCullMode('none'); // 使用顶点色 material.vertexColor = true; diff --git a/site/web/zh-cn/oit.md b/site/web/zh-cn/oit.md new file mode 100644 index 00000000..0f6c29c6 --- /dev/null +++ b/site/web/zh-cn/oit.md @@ -0,0 +1,56 @@ +# 顺序无关的透明度渲染 + +我们的引擎支持两种顺序无关的透明度渲染(Order-Independent Transparency, OIT)技术: +Weighted Blended OIT和Per-Pixel Linked List OIT。 + +## Weighted Blended OIT + +Weighted Blended OIT是一种基于权重的透明度混合技术。它通过在片元着色器中计算每个片元 +的颜色和透明度权重,然后在后期合成阶段对这些片元进行加权混合。 + +该技术的优点是实现相对简单,性能较好,且可以很好地处理复杂的透明场景。缺点是无法完全解决 +所有的透明度排序问题,在某些情况下可能会产生视觉瑕疵。 + +WebGL,WebGL2和WebGPU设备均支持Weighted Blended OIT。 + +以下代码允许使用Weigted Blended OIT进行透明物体渲染 + +```javascript + +// 为相机指定Weighted Blended渲染透明物体 +camera.oit = new WeightedBlendedOIT(); + +``` + +## Per-Pixel Linked List OIT + +Per-Pixel Linked List OIT是一种基于每像素链表的透明度渲染技术。它在片元着色器中为每个 +片元构建一个链表,链表中存储了该片元的颜色和深度信息。 + +该技术的优点是能够准确地处理透明物体的渲染顺序,即使在复杂场景下也能够正确渲染。缺点是需 +要更多的显存和计算资源。 + +Per-Pixel Linked List OIT仅可用于WebGPU设备。 + +以下代码允许使用Per-Pixel Linked List进行透明物体渲染 + +```javascript + +// 为相机指定Per-Pixel Linked List渲染透明物体 +// 构造函数的参数是支持的透明层级数量,默认是16. +camera.oit = new ABufferOIT(20); + +``` + +## 注意 + +OIT对象在使用完毕以后必须释放以免资源泄露。 + +```javascript + +// 释放OIT对象 +camera.oit.dispose(); +camera.oit = null; + +``` + diff --git a/site/web/zh-cn/user-material-multipass.md b/site/web/zh-cn/user-material-multipass.md index 657de332..758e7653 100644 --- a/site/web/zh-cn/user-material-multipass.md +++ b/site/web/zh-cn/user-material-multipass.md @@ -26,11 +26,11 @@ class CartoonMaterial extends applyMaterialMixins(MeshMaterial, mixinLambert) { this.numPasses = 2; } // 重写此方法自定义每个Pass的渲染状态 - updateRenderStates(pass, ctx) { + updateRenderStates(pass, stateSet, ctx) { // 必须调用默认实现 - super.updateRenderStates(pass, ctx); + super.updateRenderStates(pass, stateSet, ctx); // 第一遍剔除正面,第二遍剔除背面 - this.stateSet.useRasterizerState().cullMode = pass === 0 ? 'front' : 'back'; + stateSet.useRasterizerState().cullMode = pass === 0 ? 'front' : 'back'; } // 提交Uniform常量 applyUniformValues(bindGroup, ctx, pass) { diff --git a/site/web/zh-cn/user-material-var.md b/site/web/zh-cn/user-material-var.md index f6865974..456aa4ac 100644 --- a/site/web/zh-cn/user-material-var.md +++ b/site/web/zh-cn/user-material-var.md @@ -46,12 +46,12 @@ class MyLambertMaterial extends applyMaterialMixins(MeshMaterial, mixinLambert) // 漫反射贴图,默认为空 this.diffuseTexture = null; } - // 每次渲染之前更新变体值 - beginDraw(pass, ctx) { + // 在材质被应用之前更新变体值 + apply(ctx) { // 有无diffuseTexture形成两个变体 this.useFeature(MyLambertMaterial.featureDiffuseTexture, !!this.diffuseTexture); // 默认实现必须调用 - return super.beginDraw(pass, ctx); + return super.apply(ctx); } // 受光照影响 supportLighting() { diff --git a/site/web/zh-cn/user-material.md b/site/web/zh-cn/user-material.md index 2d06e8a0..377e34e6 100644 --- a/site/web/zh-cn/user-material.md +++ b/site/web/zh-cn/user-material.md @@ -66,19 +66,14 @@ 这里是该材质的fragmentShader实现,必需重写。 重写此方法必须调用其默认实现。 - - [MeshMaterial.updateRenderStates(pass, ctx)](/doc/markdown/./scene.meshmaterial.updaterenderstates) + - [MeshMaterial.updateRenderStates(pass, stateSet, ctx)](/doc/markdown/./scene.meshmaterial.updaterenderstates) - 在beginDraw()方法中调用,用于设置材质的渲染状态。 + 每次当使用该材质的渲染对象被渲染之前会调用,用于设置材质的渲染状态。 重写此方法必须调用其默认实现。 - - [MeshMaterial.beginDraw(pass, ctx)](/doc/markdown/./scene.material.begindraw) + - [MeshMaterial.apply(ctx)](/doc/markdown/./scene.material.apply) - 当开始渲染材质的某一Pass之前调用,按需创建Shader设置uniform常量。返回true正常渲染,返回false则不渲染此Pass。 - 重写此方法必须调用其默认实现。 - - - [MeshMaterial.endDraw(pass, ctx)](/doc/markdown/./scene.material.begindraw) - - 材质的某一Pass渲染完成之后调用 + 当应用材质时调用,按需创建Shader设置uniform常量。 重写此方法必须调用其默认实现。 类[ShaderHelper](/doc/markdown/./scene.shaderhelper)提供了编写材质需要的诸多工具函数。 diff --git a/test/package.json b/test/package.json index a846fb01..4335e87f 100644 --- a/test/package.json +++ b/test/package.json @@ -47,7 +47,7 @@ "dependencies": { "@types/colors": "^1.2.1", "@types/diff": "^5.0.2", - "@webgpu/types": "^0.1.31", + "@webgpu/types": "^0.1.40", "colors": "^1.4.0", "diff": "^5.0.0", "lodash-es": "^4.17.21", diff --git a/test/src/instancing/main.ts b/test/src/instancing/main.ts index 0b0db960..b5d225f9 100644 --- a/test/src/instancing/main.ts +++ b/test/src/instancing/main.ts @@ -1,26 +1,34 @@ -import { Vector3, Vector4 } from '@zephyr3d/base'; -import type { PBRMetallicRoughnessMaterial } from '@zephyr3d/scene'; import { Scene, OrbitCameraController, - AssetManager, - DirectionalLight, Application, PerspectiveCamera, Compositor, Tonemap, - BatchGroup + BatchGroup, + DirectionalLight, + BoxShape, + LambertMaterial, + Mesh, + WeightedBlendedOIT, + ABufferOIT, } from '@zephyr3d/scene'; import * as common from '../common'; +import { imGuiEndFrame, imGuiInit, imGuiInjectEvent, imGuiNewFrame } from '@zephyr3d/imgui'; +import { Vector3, Vector4 } from '@zephyr3d/base'; const instancingApp = new Application({ backend: common.getBackend(), - canvas: document.querySelector('#canvas') + canvas: document.querySelector('#canvas'), + pixelRatio: 1 }); instancingApp.ready().then(async () => { const device = instancingApp.device; + await imGuiInit(device); const scene = new Scene(); + scene.env.sky.fogType = 'none'; + //scene.env.sky.skyType = 'none'; const camera = new PerspectiveCamera( scene, Math.PI / 3, @@ -28,16 +36,39 @@ instancingApp.ready().then(async () => { 1, 1000 ); - camera.position.setXYZ(0, 0, 60); + camera.position.setXYZ(0, 0, 6); camera.controller = new OrbitCameraController(); + camera.oit = device.type === 'webgpu' ? new ABufferOIT() : new WeightedBlendedOIT(); + camera.depthPrePass = true; + + instancingApp.inputManager.use(imGuiInjectEvent); instancingApp.inputManager.use(camera.handleEvent.bind(camera)); + const inspector = new common.Inspector(scene, null, camera); const compositor = new Compositor(); compositor.appendPostEffect(new Tonemap()); const batchGroup = new BatchGroup(scene); + const boxShape = new BoxShape(); + + const mat = new LambertMaterial(); + mat.blendMode = 'blend'; + for (let i = 0; i < 100; i++) { + const instanceMat = mat.createInstance(); + instanceMat.albedoColor = new Vector4(Math.random(), Math.random(), Math.random(), Math.random()); + const boxMesh = new Mesh(scene, boxShape, instanceMat); + boxMesh.position.setXYZ(Math.random() * 5 - 2.5, Math.random() * 5 - 2.5, Math.random() * 5 - 2.5); + boxMesh.parent = batchGroup; + } + const mat2 = new LambertMaterial(); + mat2.albedoColor = new Vector4(1, 0, 0, 1); + const mesh2 = new Mesh(scene, boxShape, mat2); + mesh2.scale = new Vector3(4, 4, 4); + mesh2.parent = batchGroup; + + /* const assetManager = new AssetManager(); - for (let i = 0; i < 2000; i++) { + for (let i = 0; i < 2; i++) { assetManager.fetchModel(scene, 'assets/stone1.glb', { enableInstancing: true }).then((info) => { info.group.parent = batchGroup; info.group.position.setXYZ( @@ -75,16 +106,20 @@ instancingApp.ready().then(async () => { }); }); } - +*/ const light = new DirectionalLight(scene).setCastShadow(false).setColor(new Vector4(1, 1, 1, 1)); light.lookAt(Vector3.one(), Vector3.zero(), Vector3.axisPY()); instancingApp.on('resize', (ev) => { camera.setPerspective(camera.getFOV(), ev.width / ev.height, camera.getNearPlane(), camera.getFarPlane()); }); + instancingApp.on('tick', (ev) => { camera.updateController(); - camera.render(scene, compositor); + camera.render(scene); + imGuiNewFrame(); + inspector.render(); + imGuiEndFrame(); }); instancingApp.run(); }); diff --git a/test/src/instancing/materal.ts b/test/src/instancing/materal.ts new file mode 100644 index 00000000..27688af1 --- /dev/null +++ b/test/src/instancing/materal.ts @@ -0,0 +1,41 @@ +import type { BindGroup, PBFunctionScope } from '@zephyr3d/device'; +import { Application, DrawContext, MeshMaterial, ShaderHelper } from '@zephyr3d/scene'; + +export class LinearDepthMaterial extends MeshMaterial { + private _screenSize: Int32Array; + constructor() { + super(); + this._screenSize = new Int32Array(2); + } + applyUniformValues(bindGroup: BindGroup, ctx: DrawContext, pass: number): void { + super.applyUniformValues(bindGroup, ctx, pass); + if (this.needFragmentColor(ctx)){ + const device = Application.instance.device; + this._screenSize[0] = device.getDrawingBufferWidth(); + this._screenSize[1] = device.getDrawingBufferHeight(); + bindGroup.setValue('screenSize', this._screenSize); + bindGroup.setTexture('linearDepthTex', ctx.linearDepthTexture); + } + } + vertexShader(scope: PBFunctionScope): void { + super.vertexShader(scope); + const pb = scope.$builder; + scope.$l.oPos = ShaderHelper.resolveVertexPosition(scope); + scope.$outputs.worldPos = pb.mul(ShaderHelper.getWorldMatrix(scope), pb.vec4(scope.oPos, 1)).xyz; + ShaderHelper.setClipSpacePosition(scope, pb.mul(ShaderHelper.getViewProjectionMatrix(scope), pb.vec4(scope.$outputs.worldPos, 1))); + } + fragmentShader(scope: PBFunctionScope): void { + super.fragmentShader(scope); + const pb = scope.$builder; + if (this.needFragmentColor()){ + scope.linearDepthTex = pb.tex2D().uniform(2); + scope.screenSize = pb.ivec2().uniform(2); + // scope.$l.uv = pb.div(scope.$builtins.fragCoord.xy, pb.vec2(scope.screenSize)); + // scope.$l.texelPos = pb.ivec2(pb.floor(pb.mul(scope.uv, pb.vec2(scope.screenSize)))); + scope.$l.linearDepth = pb.textureLoad(scope.linearDepthTex, pb.ivec2(scope.$builtins.fragCoord.xy), 0).r; + this.outputFragmentColor(scope, scope.$inputs.worldPos, pb.vec4(pb.pow(pb.sub(1, scope.linearDepth),8), 0, 0, 1)); + } else { + this.outputFragmentColor(scope, scope.$inputs.worldPos, null); + } + } +}