diff --git a/src/passes/EffectPass.ts b/src/passes/EffectPass.ts index 4afffdf13..cc90f9103 100644 --- a/src/passes/EffectPass.ts +++ b/src/passes/EffectPass.ts @@ -1,19 +1,21 @@ import { Event, EventListener, - Scene, OrthographicCamera, PerspectiveCamera, BaseEvent, SRGBColorSpace, - WebGLRenderer + WebGLRenderer, + Material, + Texture } from "three"; import { Pass } from "../core/Pass.js"; import { Effect } from "../effects/Effect.js"; -import { EffectAttribute } from "../enums/EffectAttribute.js"; import { EffectShaderSection as Section } from "../enums/EffectShaderSection.js"; +import { GData } from "../enums/GData.js"; import { EffectMaterial } from "../materials/EffectMaterial.js"; +import { isConvolutionPass } from "../utils/functions/pass.js"; import { EffectShaderData } from "../utils/EffectShaderData.js"; import { Log } from "../utils/Log.js"; import { Resolution } from "../utils/Resolution.js"; @@ -51,200 +53,233 @@ function prefixSubstrings(prefix: string, substrings: Iterable, } /** - * Integrates the given effect by collecting relevant shader data. + * A collection of fragment shader information. + */ + +interface FragmentShaderInfo { + + mainImageExists: boolean; + mainUvExists: boolean; + +} + +/** + * Validates the given effect. * - * @param prefix - A prefix. * @param effect - The effect. - * @param data - Cumulative shader data. + * @param data - Effect shader data. * @throws {@link Error} if the effect is invalid or cannot be merged. + * @return Fragment shader information. */ -function integrateEffect(prefix: string, effect: Effect, data: EffectShaderData): void { +function validateEffect(effect: Effect, data: EffectShaderData): FragmentShaderInfo { - if(effect.fragmentShader === null) { + const fragmentShader = effect.fragmentShader; + + if(fragmentShader === null) { throw new Error(`Missing fragment shader (${effect.name})`); } - let fragmentShader = effect.fragmentShader; - let vertexShader = effect.vertexShader; - const mainImageExists = (fragmentShader !== undefined && /mainImage/.test(fragmentShader)); const mainUvExists = (fragmentShader !== undefined && /mainUv/.test(fragmentShader)); - data.attributes |= effect.attributes; + if(isConvolutionPass(effect, true)) { - if(mainUvExists && (data.attributes & EffectAttribute.CONVOLUTION) !== 0) { + data.convolutionEffects.add(effect); + + } + + if(data.convolutionEffects.size > 1) { + + const effectNames = Array.from(data.convolutionEffects).map(x => x.name).join(", "); + throw new Error(`Convolution effects cannot be merged (${effectNames})`); + + } else if(mainUvExists && data.convolutionEffects.size > 0) { throw new Error(`Effects that transform UVs are incompatible with convolution effects (${effect.name})`); } else if(!mainImageExists && !mainUvExists) { - throw new Error(`Could not find mainImage or mainUv function (${effect.name})`); + throw new Error(`Could not find a valid mainImage or mainUv function (${effect.name})`); + + } else if(mainImageExists && !/GData\s+\w+/.test(fragmentShader)) { - } else { + throw new Error(`Invalid mainImage signature (${effect.name})`); - const functionRegExp = /\w+\s+(\w+)\([\w\s,]*\)\s*{/g; + } - const shaderParts = data.shaderParts; - let fragmentHead = shaderParts.get(Section.FRAGMENT_HEAD) ?? ""; - let fragmentMainUv = shaderParts.get(Section.FRAGMENT_MAIN_UV) ?? ""; - let fragmentMainImage = shaderParts.get(Section.FRAGMENT_MAIN_IMAGE) ?? ""; - let vertexHead = shaderParts.get(Section.VERTEX_HEAD) ?? ""; - let vertexMainSupport = shaderParts.get(Section.VERTEX_MAIN_SUPPORT) ?? ""; + return { mainImageExists, mainUvExists }; - const varyings = new Set(); - const names = new Set(); +} - if(mainUvExists) { +/** + * Integrates the given effect by collecting relevant shader data. + * + * @param prefix - A prefix. + * @param effect - The effect. + * @param data - Cumulative effect shader data. + * @throws {@link Error} if the effect is invalid or cannot be merged. + */ - fragmentMainUv += `\t${prefix}MainUv(UV);\n`; - data.uvTransformation = true; +function integrateEffect(prefix: string, effect: Effect, data: EffectShaderData): void { - } + const { mainImageExists, mainUvExists } = validateEffect(effect, data); + let fragmentShader = effect.fragmentShader!; + let vertexShader = effect.vertexShader; - if(vertexShader !== null && /mainSupport/.test(vertexShader)) { + const shaderParts = data.shaderParts; + let fragmentHead = shaderParts.get(Section.FRAGMENT_HEAD) ?? ""; + let fragmentMainUv = shaderParts.get(Section.FRAGMENT_MAIN_UV) ?? ""; + let fragmentMainImage = shaderParts.get(Section.FRAGMENT_MAIN_IMAGE) ?? ""; + let vertexHead = shaderParts.get(Section.VERTEX_HEAD) ?? ""; + let vertexMainSupport = shaderParts.get(Section.VERTEX_MAIN_SUPPORT) ?? ""; - // Build the mainSupport call (with optional uv parameter). - const needsUv = /mainSupport *\([\w\s]*?uv\s*?\)/.test(vertexShader); - vertexMainSupport += `\t${prefix}MainSupport(`; - vertexMainSupport += needsUv ? "vUv);\n" : ");\n"; + const varyings = new Set(); + const names = new Set(); - // Collect names of varyings and functions. - for(const m of vertexShader.matchAll(/(?:varying\s+\w+\s+([\S\s]*?);)/g)) { + if(mainUvExists) { - // Handle unusual formatting and commas. - for(const n of m[1].split(/\s*,\s*/)) { + fragmentMainUv += `\t${prefix}MainUv(UV);\n`; + data.uvTransformation = true; - data.varyings.add(n); - varyings.add(n); - names.add(n); + } - } + const functionRegExp = /\w+\s+(\w+)\([\w\s,]*\)\s*{/g; - } + if(vertexShader !== null && /mainSupport/.test(vertexShader)) { + + // Build the mainSupport call (with optional uv parameter). + const needsUv = /mainSupport\s*\([\w\s]*?uv\s*?\)/.test(vertexShader); + vertexMainSupport += `\t${prefix}MainSupport(`; + vertexMainSupport += needsUv ? "vUv);\n" : ");\n"; - for(const m of vertexShader.matchAll(functionRegExp)) { + // Collect names of varyings and functions. + for(const m of vertexShader.matchAll(/(?:out\s+\w+\s+([\S\s]*?);)/g)) { - names.add(m[1]); + // Handle unusual formatting and commas. + for(const n of m[1].split(/\s*,\s*/)) { + + data.varyings.add(n); + varyings.add(n); + names.add(n); } } - for(const m of fragmentShader.matchAll(functionRegExp)) { + for(const m of vertexShader.matchAll(functionRegExp)) { names.add(m[1]); } - for(const d of effect.defines.keys()) { - - // Ignore parameters of function-like macros. - names.add(d.replace(/\([\w\s,]*\)/g, "")); - - } + } - for(const u of effect.uniforms.keys()) { + for(const m of fragmentShader.matchAll(functionRegExp)) { - names.add(u); + names.add(m[1]); - } + } - // Remove potential false positives. - names.delete("while"); - names.delete("for"); - names.delete("if"); + for(const d of effect.input.defines.keys()) { - // Store prefixed uniforms and macros. - effect.uniforms.forEach((v, k) => data.uniforms.set(prefix + k.charAt(0).toUpperCase() + k.slice(1), v)); - effect.defines.forEach((v, k) => data.defines.set(prefix + k.charAt(0).toUpperCase() + k.slice(1), v)); + // Ignore parameters of function-like macros. + names.add(d.replace(/\([\w\s,]*\)/g, "")); - // Prefix varyings, functions and uniforms in shaders and macros. - const shaders = new Map([["fragment", fragmentShader], ["vertex", vertexShader]]); - prefixSubstrings(prefix, names, data.defines); - prefixSubstrings(prefix, names, shaders); - fragmentShader = shaders.get("fragment") as string; - vertexShader = shaders.get("vertex") as string; + } - // Collect unique blend modes. - const blendMode = effect.blendMode; - data.blendModes.set(blendMode.blendFunction.id, blendMode); + for(const u of effect.input.uniforms.keys()) { - if(mainImageExists) { + names.add(u); - if(effect.inputColorSpace !== null && effect.inputColorSpace !== data.colorSpace) { + } - fragmentMainImage += (effect.inputColorSpace === SRGBColorSpace) ? - "color0 = LinearTosRGB(color0);\n\t" : - "color0 = sRGBToLinear(color0);\n\t"; + // Remove known false positives. + names.delete("while"); + names.delete("for"); + names.delete("if"); - } + // Store prefixed uniforms and macros. + effect.input.uniforms.forEach((v, k) => data.uniforms.set(prefix + k.charAt(0).toUpperCase() + k.slice(1), v)); + effect.input.defines.forEach((v, k) => data.defines.set(prefix + k.charAt(0).toUpperCase() + k.slice(1), v)); - if(effect.outputColorSpace !== null) { + // Prefix varyings, functions and uniforms in shaders and macros. + const shaders = new Map([["fragment", fragmentShader], ["vertex", vertexShader]]); + prefixSubstrings(prefix, names, data.defines); + prefixSubstrings(prefix, names, shaders); + fragmentShader = shaders.get("fragment") as string; + vertexShader = shaders.get("vertex") as string; - data.colorSpace = effect.outputColorSpace; + // Collect unique blend modes. + const blendMode = effect.blendMode; + data.blendModes.set(blendMode.blendFunction.id, blendMode); - } else if(effect.inputColorSpace !== null) { + if(mainImageExists) { - data.colorSpace = effect.inputColorSpace; + // Already checked param existence during effect validation. + const gDataParamName = fragmentShader.match(/GData\s+(\w+)/)![0]; - } + // Detect GData usage. + for(const value of Object.values(GData)) { - // const depthParamRegExp = /MainImage *\([\w\s,]*?depth[\w\s,]*?\)/; - fragmentMainImage += `${prefix}MainImage(color0, UV, `; + const regExpGData = new RegExp(`${gDataParamName}.${value}`); - // Check if the effect reads depth in the fragment shader. - /* - if((data.attributes & EffectAttribute.DEPTH) !== 0 && depthParamRegExp.test(fragmentShader)) { + if(regExpGData.test(fragmentShader)) { - fragmentMainImage += "depth, "; - data.readDepth = true; + data.gData.add(value); } - */ - fragmentMainImage += "color1);\n\t"; + } - // Include the blend opacity uniform of this effect. - const blendOpacity = prefix + "BlendOpacity"; - data.uniforms.set(blendOpacity, blendMode.opacityUniform); + if(effect.inputColorSpace !== null && effect.inputColorSpace !== data.colorSpace) { - // Blend the result of this effect with the input color (color0 = dst, color1 = src). - fragmentMainImage += `color0 = ${blendMode.blendFunction.name}(color0, color1, ${blendOpacity});\n\n\t`; - fragmentHead += `uniform float ${blendOpacity};\n\n`; + fragmentMainImage += (effect.inputColorSpace === SRGBColorSpace) ? + "color0 = LinearTosRGB(color0);\n\t" : + "color0 = sRGBToLinear(color0);\n\t"; } - // Include the modified code in the final shader. - fragmentHead += fragmentShader + "\n"; + // Remember the color space at this stage. + if(effect.outputColorSpace !== null) { - if(vertexShader !== null) { + data.colorSpace = effect.outputColorSpace; - vertexHead += vertexShader + "\n"; + } else if(effect.inputColorSpace !== null) { + + data.colorSpace = effect.inputColorSpace; } - shaderParts.set(Section.FRAGMENT_HEAD, fragmentHead); - shaderParts.set(Section.FRAGMENT_MAIN_UV, fragmentMainUv); - shaderParts.set(Section.FRAGMENT_MAIN_IMAGE, fragmentMainImage); - shaderParts.set(Section.VERTEX_HEAD, vertexHead); - shaderParts.set(Section.VERTEX_MAIN_SUPPORT, vertexMainSupport); + fragmentMainImage += `color1 = ${prefix}MainImage(color0, UV, gData);\n\t`; - if(effect.extensions !== null) { + // Include the blend opacity uniform of this effect. + const blendOpacity = prefix + "BlendOpacity"; + data.uniforms.set(blendOpacity, blendMode.opacityUniform); - // Collect required WebGL extensions. - for(const extension of effect.extensions) { + // Blend the result of this effect with the input color (color0 = dst, color1 = src). + fragmentMainImage += `color0 = ${blendMode.blendFunction.name}(color0, color1, ${blendOpacity});\n\n\t`; + fragmentHead += `uniform float ${blendOpacity};\n\n`; - data.extensions.add(extension); + } - } + // Include the modified code in the final shader. + fragmentHead += fragmentShader + "\n"; - } + if(vertexShader !== null) { + + vertexHead += vertexShader + "\n"; } + shaderParts.set(Section.FRAGMENT_HEAD, fragmentHead); + shaderParts.set(Section.FRAGMENT_MAIN_UV, fragmentMainUv); + shaderParts.set(Section.FRAGMENT_MAIN_IMAGE, fragmentMainImage); + shaderParts.set(Section.VERTEX_HEAD, vertexHead); + shaderParts.set(Section.VERTEX_MAIN_SUPPORT, vertexMainSupport); + } /** @@ -261,13 +296,7 @@ export class EffectPass extends Pass implements EventListenerObj * An event listener that forwards events to {@link handleEvent}. */ - private listener: EventListener; - - /** - * The effects. - */ - - private _effects: Effect[]; + private listener: EventListener>; /** * An animation time scale. @@ -287,119 +316,107 @@ export class EffectPass extends Pass implements EventListenerObj this.fullscreenMaterial = new EffectMaterial(); this.listener = (e: Event) => this.handleEvent(e); - this._effects = effects; + this.effects = effects; this.timeScale = 1.0; } - override get scene(): Scene | null { + override get camera(): OrthographicCamera | PerspectiveCamera | null { - return super.scene; + return super.camera; } - override set scene(value: Scene | null) { + override set camera(value: OrthographicCamera | PerspectiveCamera | null) { - super.scene = value; + super.camera = value; - for(const effect of this.effects) { + if(value !== null) { - effect.scene = value; + this.fullscreenMaterial.copyCameraSettings(value); } } - override get camera(): OrthographicCamera | PerspectiveCamera | null { + override get subpasses(): ReadonlyArray> { - return super.camera; + return super.subpasses; } - override set camera(value: OrthographicCamera | PerspectiveCamera | null) { + private override set subpasses(value: Pass[]) { - super.camera = value; - - for(const effect of this.effects) { + for(const effect of super.subpasses) { - effect.camera = value; + effect.removeEventListener(Pass.EVENT_CHANGE, this.listener); } - if(value !== null) { + super.subpasses = value; + Object.freeze(super.subpasses); - this.fullscreenMaterial.copyCameraSettings(value); + for(const effect of super.subpasses) { + + effect.addEventListener(Pass.EVENT_CHANGE, this.listener); } + this.rebuild(); + } /** - * Indicates whether dithering is enabled. + * The effects. */ - get dithering(): boolean { + get effects(): ReadonlyArray { - return this.fullscreenMaterial.dithering; + return this.subpasses as ReadonlyArray; } - set dithering(value: boolean) { + protected set effects(value: Effect[]) { - const material = this.fullscreenMaterial; - material.dithering = value; - material.needsUpdate = true; + this.subpasses = value; } /** - * The effects. + * Indicates whether dithering is enabled. */ - get effects(): Effect[] { + get dithering(): boolean { - return this._effects; + return this.fullscreenMaterial.dithering; } - protected set effects(value: Effect[]) { - - for(const effect of this._effects) { - - effect.removeEventListener(Pass.EVENT_CHANGE, this.listener); - - } - - this._effects = value.sort((a, b) => (b.attributes - a.attributes)); - Object.freeze(this._effects); - - for(const effect of this._effects) { - - effect.addEventListener(Pass.EVENT_CHANGE, this.listener); - - } + set dithering(value: boolean) { - this.rebuild(); + const material = this.fullscreenMaterial; + material.dithering = value; + material.needsUpdate = true; } /** - * Updates the compound shader material. + * Updates the composite shader material. * * @throws {@link Error} if the current effects cannot be merged. */ private updateMaterial(): void { - const data = new EffectShaderData(); - let id = 0; - if(this.effects.length === 0) { throw new Error("There are no effects to merge"); } + const data = new EffectShaderData(); + let id = 0; + for(const effect of this.effects) { if(effect.blendMode.blendFunction.shader === null) { @@ -408,68 +425,30 @@ export class EffectPass extends Pass implements EventListenerObj } - if((data.attributes & effect.attributes & EffectAttribute.CONVOLUTION) !== 0) { - - throw new Error(`Convolution effects cannot be merged (${effect.name})`); - - } else { - - integrateEffect(`e${id++}`, effect, data); - - } - - } - - if(id === 0) { - - throw new Error("Invalid effect combination"); + integrateEffect(`e${id++}`, effect, data); } - let fragmentHead = data.shaderParts.get(Section.FRAGMENT_HEAD) as string; - let fragmentMainImage = data.shaderParts.get(Section.FRAGMENT_MAIN_IMAGE) as string; - let fragmentMainUv = data.shaderParts.get(Section.FRAGMENT_MAIN_UV) as string; - - // Integrate the relevant blend functions. - const blendRegExp = /\bblend\b/g; - - for(const blendMode of data.blendModes.values()) { - - const blendFunctionShader = blendMode.blendFunction.shader!; - fragmentHead += blendFunctionShader.replace(blendRegExp, blendMode.blendFunction.name) + "\n"; - - } - - // Check if any effect relies on depth. - /* if((data.attributes & EffectAttribute.DEPTH) !== 0) { - - // Check if depth should be read. - if(data.readDepth) { - - fragmentMainImage = "float depth = readDepth(UV);\n\n\t" + fragmentMainImage; - - } - - // Only request a depth texture if none has been provided yet. - this.needsDepthTexture = (this.getDepthTexture() === null); + data.shaderParts.set(Section.FRAGMENT_HEAD_GBUFFER, data.createGBufferStruct()); + data.shaderParts.set(Section.FRAGMENT_HEAD_GDATA, data.createGDataStruct()); + data.shaderParts.set(Section.FRAGMENT_MAIN_GDATA, data.createGDataSetup()); - } else { - - this.needsDepthTexture = false; - - } */ + const fragmentHead = data.shaderParts.get(Section.FRAGMENT_HEAD) as string; + data.shaderParts.set(Section.FRAGMENT_HEAD, fragmentHead + data.createBlendFunctions()); - if(data.colorSpace === "srgb") { + if(data.colorSpace === SRGBColorSpace) { // Convert back to linear. - fragmentMainImage += "color0 = sRGBToLinear(color0);\n\t"; + const fragmentMainImage = data.shaderParts.get(Section.FRAGMENT_MAIN_IMAGE) as string; + data.shaderParts.set(Section.FRAGMENT_MAIN_IMAGE, fragmentMainImage + "color0 = sRGBToLinear(color0);\n\t"); } // Check if any effect transforms UVs in the fragment shader. if(data.uvTransformation) { - fragmentMainUv = "vec2 transformedUv = vUv;\n" + fragmentMainUv; + const fragmentMainUv = data.shaderParts.get(Section.FRAGMENT_MAIN_UV) as string; + data.shaderParts.set(Section.FRAGMENT_MAIN_UV, "vec2 transformedUv = vUv;\n" + fragmentMainUv); data.defines.set("UV", "transformedUv"); } else { @@ -478,25 +457,18 @@ export class EffectPass extends Pass implements EventListenerObj } - data.shaderParts.set(Section.FRAGMENT_HEAD, fragmentHead); - data.shaderParts.set(Section.FRAGMENT_MAIN_IMAGE, fragmentMainImage); - data.shaderParts.set(Section.FRAGMENT_MAIN_UV, fragmentMainUv); - // Ensure that leading preprocessor directives start on a new line. - data.shaderParts.forEach( - (value, key, map) => map.set(key, value !== null ? value.trim().replace(/^#/, "\n#") : null) - ); + data.shaderParts.forEach((v, k, map) => map.set(k, v !== null ? v.trim().replace(/^#/, "\n#") : null)); this.fullscreenMaterial .setShaderParts(data.shaderParts) .setDefines(data.defines) - .setUniforms(data.uniforms) - .setExtensions(data.extensions); + .setUniforms(data.uniforms); } /** - * Rebuilds the compound shader material. + * Rebuilds the composite shader material. */ protected rebuild(): void { @@ -527,23 +499,39 @@ export class EffectPass extends Pass implements EventListenerObj protected override onInputChange(): void { + const entries: [string, Texture | null][] = []; + + for(const component of this.input.gBuffer) { + + entries.push([component, this.input.buffers.get(component) || null]); + + } + + this.fullscreenMaterial.gBuffer = Object.fromEntries(entries); + console.log(this.fullscreenMaterial.gBuffer); + } override checkRequirements(renderer: WebGLRenderer): void { + for(const effect of this.effects) { + + effect.checkRequirements(renderer); + + } + } override dispose(): void { - super.dispose(); - for(const effect of this.effects) { effect.removeEventListener("change", this.listener); - effect.dispose(); } + super.dispose(); + } render(): void { @@ -561,7 +549,7 @@ export class EffectPass extends Pass implements EventListenerObj } const material = this.fullscreenMaterial; - material.time += this.timer.delta * this.timeScale; + material.time += this.timer.getDelta() * this.timeScale; this.renderer.setRenderTarget(this.output.defaultBuffer); this.renderFullscreen(); diff --git a/src/utils/EffectShaderData.ts b/src/utils/EffectShaderData.ts index f52dbefdf..79faf357a 100644 --- a/src/utils/EffectShaderData.ts +++ b/src/utils/EffectShaderData.ts @@ -1,8 +1,8 @@ -import { ColorSpace, Uniform } from "three"; +import { ColorSpace, LinearSRGBColorSpace, Uniform } from "three"; import { ShaderData } from "../core/ShaderData.js"; -import { EffectAttribute } from "../enums/EffectAttribute.js"; +import { GData } from "../enums/GData.js"; +import { Effect } from "../effects/Effect.js"; import { EffectShaderSection } from "../enums/EffectShaderSection.js"; -import { WebGLExtension } from "../enums/WebGLExtension.js"; import { BlendMode } from "../effects/blending/BlendMode.js"; /** @@ -29,12 +29,6 @@ export class EffectShaderData implements ShaderData { readonly blendModes: Map; - /** - * Required extensions. - */ - - readonly extensions: Set; - /** * A set of varyings. */ @@ -42,22 +36,22 @@ export class EffectShaderData implements ShaderData { readonly varyings: Set; /** - * Collective effect attributes. + * A collection of required GBuffer data. */ - attributes: EffectAttribute; + readonly gData: Set; /** - * Indicates whether the shader transforms UV coordinates in the fragment shader. + * A list of effects that use convolution operations. */ - uvTransformation: boolean; + readonly convolutionEffects: Set; /** - * Indicates whether the shader reads depth in the fragment shader. + * Indicates whether the shader transforms UV coordinates in the fragment shader. */ - readDepth: boolean; + uvTransformation: boolean; /** * Keeps track of the current color space. @@ -72,8 +66,10 @@ export class EffectShaderData implements ShaderData { constructor() { this.shaderParts = new Map([ + [EffectShaderSection.FRAGMENT_HEAD_GBUFFER, null], [EffectShaderSection.FRAGMENT_HEAD, null], [EffectShaderSection.FRAGMENT_MAIN_UV, null], + [EffectShaderSection.FRAGMENT_MAIN_GDATA, null], [EffectShaderSection.FRAGMENT_MAIN_IMAGE, null], [EffectShaderSection.VERTEX_HEAD, null], [EffectShaderSection.VERTEX_MAIN_SUPPORT, null] @@ -82,12 +78,186 @@ export class EffectShaderData implements ShaderData { this.defines = new Map(); this.uniforms = new Map(); this.blendModes = new Map(); - this.extensions = new Set(); this.varyings = new Set(); - this.attributes = EffectAttribute.NONE; + this.gData = new Set([GData.COLOR]); + this.convolutionEffects = new Set(); this.uvTransformation = false; - this.readDepth = false; - this.colorSpace = "srgb-linear"; + this.colorSpace = LinearSRGBColorSpace; + + } + + /** + * Creates a struct that defines the required GBuffer components. + * + * @return The shader code. + */ + + createGBufferStruct(): string { + + const gData = this.gData; + let s = "struct GBuffer {\n"; + + if(gData.has(GData.COLOR)) { + + // Precision depends on the configured frame buffer type. + s += "\tFRAME_BUFFER_PRECISION sampler2D color;\n"; + + } + + if(gData.has(GData.DEPTH)) { + + // Precision depends on the hardware. + s += "\tDEPTH_BUFFER_PRECISION sampler2D depth;\n"; + + } + + if(gData.has(GData.NORMAL)) { + + s += "\tmediump sampler2D normal;\n"; + + } + + if(gData.has(GData.ROUGHNESS) || gData.has(GData.METALNESS)) { + + s += "\tlowp sampler2D roughnessMetalness;\n"; + + } + + s += "}\n"; + + return s; + + } + + /** + * Creates a struct that defines the required GBuffer data. + * + * @return The shader code. + */ + + createGDataStruct(): string { + + const gData = this.gData; + let s = "struct GData {\n"; + + if(gData.has(GData.COLOR)) { + + s += "\tvec4 color;\n"; + + } + + if(gData.has(GData.DEPTH)) { + + s += "\tfloat depth;\n"; + + } + + if(gData.has(GData.NORMAL)) { + + s += "\tvec3 normal;\n"; + + } + + if(gData.has(GData.ROUGHNESS)) { + + s += "\tfloat roughness;\n"; + + } + + if(gData.has(GData.METALNESS)) { + + s += "\tfloat metalness;\n"; + + } + + if(gData.has(GData.LUMINANCE)) { + + s += "\tfloat luminance;\n"; + + } + + s += "}\n"; + + return s; + + } + + /** + * Creates the GData setup code. + * + * @return The shader code. + */ + + createGDataSetup(): string { + + const gData = this.gData; + let s = "GData gData;"; + + if(gData.has(GData.COLOR)) { + + s += "gData.color = texture(gBuffer.color, UV);\n"; + + } + + if(gData.has(GData.DEPTH)) { + + s += "gData.depth = texture(gBuffer.depth, UV).r;\n"; + + } + + if(gData.has(GData.NORMAL)) { + + s += "gData.normal = texture(gBuffer.normal, UV).xyz;\n"; + + } + + if(gData.has(GData.ROUGHNESS) || gData.has(GData.METALNESS)) { + + s += "vec2 roughnessMetalness = texture(gBuffer.roughnessMetalness, UV).rg;\n"; + + } + + if(gData.has(GData.ROUGHNESS)) { + + s += "gData.roughness = roughnessMetalness.r;\n"; + + } + + if(gData.has(GData.METALNESS)) { + + s += "gData.metalness = roughnessMetalness.g;\n"; + + } + + if(gData.has(GData.LUMINANCE)) { + + s += "gData.luminance = luminance(gData.color.rgb);\n"; + + } + + return s; + + } + + /** + * Creates the relevant blend function declarations. + * + * @return The shader code. + */ + + createBlendFunctions(): string { + + const blendRegExp = /\bblend\b/g; + let s = ""; + + for(const blendMode of this.blendModes.values()) { + + const blendFunctionShader = blendMode.blendFunction.shader!; + s += blendFunctionShader.replace(blendRegExp, blendMode.blendFunction.name) + "\n"; + + } + + return s; }