Skip to content

Commit

Permalink
Move functions to EffectShaderData
Browse files Browse the repository at this point in the history
  • Loading branch information
vanruesc committed Dec 29, 2023
1 parent 5e67097 commit 7bf0e7f
Show file tree
Hide file tree
Showing 4 changed files with 270 additions and 271 deletions.
270 changes: 2 additions & 268 deletions src/passes/EffectPass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,275 +13,11 @@ import {
import { Pass } from "../core/Pass.js";
import { Effect } from "../effects/Effect.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";

/**
* Prefixes substrings within the given strings.
*
* @param prefix - A prefix.
* @param substrings - The substrings.
* @param shadersOrMacros - A collection of shaders or macros.
*/

function prefixSubstrings(prefix: string, substrings: Iterable<string>,
shadersOrMacros: Map<string, string | number | boolean | null>): void {

for(const substring of substrings) {

// Prefix the substring and build a RegExp that searches for the unprefixed version.
const prefixed = "$1" + prefix + substring.charAt(0).toUpperCase() + substring.slice(1);
const regExp = new RegExp("([^\\.])(\\b" + substring + "\\b)", "g");

for(const entry of shadersOrMacros.entries()) {

if(typeof entry[1] === "string") {

// Replace all occurances of the substring with the prefixed version.
shadersOrMacros.set(entry[0], entry[1].replace(regExp, prefixed));

}

}

}

}

/**
* A collection of fragment shader information.
*/

interface FragmentShaderInfo {

mainImageExists: boolean;
mainUvExists: boolean;

}

/**
* Validates the given effect.
*
* @param effect - The effect.
* @param data - Effect shader data.
* @throws {@link Error} if the effect is invalid or cannot be merged.
* @return Fragment shader information.
*/

function validateEffect(effect: Effect, data: EffectShaderData): FragmentShaderInfo {

const fragmentShader = effect.fragmentShader;

if(fragmentShader === null) {

throw new Error(`Missing fragment shader (${effect.name})`);

}

const mainImageExists = (fragmentShader !== undefined && /mainImage/.test(fragmentShader));
const mainUvExists = (fragmentShader !== undefined && /mainUv/.test(fragmentShader));

if(isConvolutionPass(effect, true)) {

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 a valid mainImage or mainUv function (${effect.name})`);

} else if(mainImageExists && !/GData\s+\w+/.test(fragmentShader)) {

throw new Error(`Invalid mainImage signature (${effect.name})`);

}

return { mainImageExists, 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.
*/

function integrateEffect(prefix: string, effect: Effect, data: EffectShaderData): void {

const { mainImageExists, mainUvExists } = validateEffect(effect, data);
let fragmentShader = effect.fragmentShader!;
let vertexShader = effect.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) ?? "";

const varyings = new Set<string>();
const names = new Set<string>();

if(mainUvExists) {

fragmentMainUv += `\t${prefix}MainUv(UV);\n`;
data.uvTransformation = true;

}

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";

// Collect names of varyings and functions.
for(const m of vertexShader.matchAll(/(?:out\s+\w+\s+([\S\s]*?);)/g)) {

// 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 vertexShader.matchAll(functionRegExp)) {

names.add(m[1]);

}

}

for(const m of fragmentShader.matchAll(functionRegExp)) {

names.add(m[1]);

}

for(const d of effect.input.defines.keys()) {

// Ignore parameters of function-like macros.
names.add(d.replace(/\([\w\s,]*\)/g, ""));

}

for(const u of effect.input.uniforms.keys()) {

names.add(u);

}

// 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));

// 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);

if(mainImageExists) {

// 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 regExpGData = new RegExp(`${gDataParamName}.${value}`);

if(regExpGData.test(fragmentShader)) {

data.gData.add(value);

}

}

if(effect.inputColorSpace !== null && effect.inputColorSpace !== data.colorSpace) {

fragmentMainImage += (effect.inputColorSpace === SRGBColorSpace) ?
"color0 = LinearTosRGB(color0);\n\t" :
"color0 = sRGBToLinear(color0);\n\t";

}

// Remember the color space at this stage.
if(effect.outputColorSpace !== null) {

data.colorSpace = effect.outputColorSpace;

} else if(effect.inputColorSpace !== null) {

data.colorSpace = effect.inputColorSpace;

}

fragmentMainImage += `color1 = ${prefix}MainImage(color0, UV, gData);\n\t`;

// Include the blend opacity uniform of this effect.
const blendOpacity = prefix + "BlendOpacity";
data.uniforms.set(blendOpacity, blendMode.opacityUniform);

// 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`;

}

// 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);

}

/**
* An effect pass.
*
Expand Down Expand Up @@ -419,14 +155,12 @@ export class EffectPass extends Pass<EffectMaterial> implements EventListenerObj

for(const effect of this.effects) {

if(effect.blendMode.blendFunction.shader === null) {
if(effect.blendMode.blendFunction.shader !== null) {

continue;
data.integrateEffect(`e${id++}`, effect);

}

integrateEffect(`e${id++}`, effect, data);

}

data.shaderParts.set(Section.FRAGMENT_HEAD_GBUFFER, data.createGBufferStruct());
Expand Down
Loading

0 comments on commit 7bf0e7f

Please sign in to comment.