pex-renderer
Version:
Physically Based Renderer (PBR) and scene graph designed as ECS for PEX: define entities to be rendered as collections of components with their update orchestrated by systems.
285 lines (250 loc) • 8.24 kB
JavaScript
import { mat3, mat2x3 } from "pex-math";
import { pipeline as SHADERS, parser as ShaderParser } from "pex-shaders";
import { NAMESPACE, TEMP_MAT2X3 } from "./utils.js";
import ProgramCache from "./program-cache.js";
export default () => ({
cache: {
// Cache based on: vertex source (material.vert or default), fragment source (material.frag or default), list of flags and material hooks
programs: new ProgramCache(),
// Cache based on: program.id, material.blend and material.id (if present)
pipelines: {},
},
getProgramFlagsAndUniforms(ctx, entity, options = {}) {
const { flagDefinitions } = options; //FIXME MARCIN: incoherent name
const flags = [
ctx.capabilities.maxColorAttachments > 1 && "USE_DRAW_BUFFERS",
];
const uniforms = {};
for (let i = 0; i < flagDefinitions.length; i++) {
const [path, defineName, opts = {}] = flagDefinitions[i];
// Assumes defs are ordered and "requires/excludes" item was before
if (
(opts.requires && !flags.includes(opts.requires)) ||
(opts.excludes && flags.includes(opts.excludes))
) {
continue;
}
//TODO: GC
const obj = { ...entity, options };
let value = obj;
// Parse the object path
for (let j = 0; j < path.length; j++) {
value = value[path[j]];
}
// Compute flags and uniforms
if (opts.type === "texture") {
if (!value) continue;
flags.push(`USE_${defineName}`);
flags.push(`${defineName}_TEX_COORD ${value.texCoord || "0"}`);
uniforms[opts.uniform] = value.texture || value;
// Compute texture transform
if (value.texture && (value.offset || value.rotation || value.scale)) {
mat2x3.identity(TEMP_MAT2X3);
mat2x3.translate(TEMP_MAT2X3, value.offset || [0, 0]);
mat2x3.rotate(TEMP_MAT2X3, -value.rotation || 0);
mat2x3.scale(TEMP_MAT2X3, value.scale || [1, 1]);
value.matrix = mat3.fromMat2x3(
value.matrix ? mat3.identity(value.matrix) : mat3.create(),
TEMP_MAT2X3,
);
}
if (value.matrix) {
flags.push(`USE_${defineName}_MATRIX`);
uniforms[opts.uniform + "Matrix"] = value.matrix;
}
// If not nullish or has default
} else if (
!(value === undefined || value === null) ||
opts.default !== undefined
) {
// Pass the compare test
if (opts.compare !== undefined) {
if (opts.compare === value) flags.push(defineName);
} else {
// Set flag as name + value
if (opts.type === "value") {
// Set value flag if not empty string and use default if nullish
if (value !== "") {
flags.push(`${defineName} ${value ?? opts.default}`);
}
// Set flag only if truthy
// TODO: specular 0 is allowed so this check will not pass
} else if (value) {
flags.push(defineName);
// Set fallback flag if falsy (false, 0, "", NaN)
} else if (opts.fallback) {
flags.push(opts.fallback);
}
}
// Set uniform with default if value is nullish
if (opts.uniform) uniforms[opts.uniform] = value ?? opts.default;
} else if (opts.fallback) {
// No value and no default
flags.push(opts.fallback);
}
}
return { flags: flags.filter(Boolean), uniforms };
},
shadersPostReplace(descriptor, entity, uniforms, debugRender) {
if (debugRender) {
const mode = debugRender.toLowerCase();
const scale = mode.includes("normal") ? " * 0.5 + 0.5" : "";
const pow = ["ao", "normal", "metallic", "roughness"].some((type) =>
mode.includes(type),
)
? "2.2"
: "1";
if (mode.includes("texcoord")) debugRender = `vec3(${debugRender}, 0.0)`;
descriptor.frag = descriptor.frag.replace(
"#define HOOK_FRAG_END",
/* glsl */ `#define HOOK_FRAG_END
vec4 debugColor = vec4(pow(vec3(${debugRender}${scale}), vec3(${pow})), 1.0);
#if (__VERSION__ >= 300)
outColor = debugColor;
#else
gl_FragData[0] = debugColor;
#endif
`,
);
}
const hooks = entity.material?.hooks;
if (hooks) {
if (hooks.vert) {
for (let [hookName, hookCode] of Object.entries(hooks.vert)) {
descriptor.vert = descriptor.vert.replace(
`#define HOOK_VERT_${hookName}`,
hookCode,
);
}
}
if (hooks.frag) {
for (let [hookName, hookCode] of Object.entries(hooks.frag)) {
descriptor.frag = descriptor.frag.replace(
`#define HOOK_FRAG_${hookName}`,
hookCode,
);
}
}
if (hooks.uniforms) {
const hookUniforms = hooks.uniforms(entity, []);
Object.assign(uniforms, hookUniforms);
}
}
},
buildProgram(ctx, vert, frag, defines) {
let program = null;
try {
program = ctx.program({ vert, frag });
} catch (error) {
program = ctx.program({
vert: ShaderParser.build(ctx, SHADERS.error.vert, defines),
frag: ShaderParser.build(ctx, SHADERS.error.frag, defines),
});
console.error(error);
console.warn(
NAMESPACE,
`glsl error\n`,
ShaderParser.getFormattedError(error, { vert, frag }),
);
}
return program;
},
getProgram(ctx, entity, options) {
const { flags, uniforms } = this.getProgramFlagsAndUniforms(
ctx,
entity,
options,
);
const descriptor = {
vert: entity.material?.vert || options.vert,
frag: entity.material?.frag || options.frag,
};
this.shadersPostReplace(
//TODO MARCIN: Code smell, inline mutation of descriptors
descriptor,
entity,
uniforms,
options.debugRender,
);
const { vert, frag } = descriptor;
let program = this.cache.programs.get(flags, vert, frag);
if (!program) {
const defines = options.debugRender
? flags.filter((flag) => flag !== options.debugRender)
: flags;
const vertSrc = ShaderParser.build(
ctx,
vert,
defines,
entity.material?.extensions,
);
const fragSrc = ShaderParser.build(
ctx,
frag,
defines,
entity.material?.extensions,
);
if (options.debug) {
console.debug(
NAMESPACE,
"pipeline-cache",
"new program",
flags,
entity,
);
}
program = this.buildProgram(
ctx,
ShaderParser.replaceStrings(vertSrc, options),
ShaderParser.replaceStrings(fragSrc, options),
defines,
);
this.cache.programs.set(flags, vert, frag, program);
}
return { program, uniforms };
},
// Helper to compute a hash made of numbers, boolean coerced to a bit and nullish values to U
getHashFromProps(obj, props, debug) {
return props
.map((key) => {
const value = obj[key];
let bit = 1;
if (Number.isFinite(value)) {
bit = value;
} else if (value === true || value === false) {
bit = Number(value);
} else {
bit = value ?? "U";
}
return debug ? `${key}_${bit}` : bit;
})
.join(debug ? "_" : "");
},
getPipeline(ctx, entity, options = {}, pipelineOptions = {}) {
const { program, uniforms } = this.getProgram(ctx, entity, options);
const pipelineHash = `${program.id}_${options.hash}`;
if (
!this.cache.pipelines[pipelineHash] ||
entity?.material?.needsPipelineUpdate
) {
if (entity.material) entity.material.needsPipelineUpdate = false;
if (options.debug) {
console.debug(
NAMESPACE,
"pipeline-cache",
"new pipeline",
pipelineHash,
entity,
);
}
this.cache.pipelines[pipelineHash] = ctx.pipeline({
program,
...pipelineOptions,
});
}
const pipeline = this.cache.pipelines[pipelineHash];
return { pipeline, uniforms };
},
// TODO:
dispose() {},
});