Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(webgl): Respect the renderOrder property on material configs #942

Merged
merged 1 commit into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
192 changes: 93 additions & 99 deletions src/rendering/renderers/webGl/WebGlRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,18 +189,17 @@ export class WebGlRenderer extends Renderer {

/**
* @typedef MaterialRenderData
* @property {MeshRenderData[]} meshes
* @property {import("./WebGlMaterialConfig.js").WebGlMaterialConfig} materialConfig
* @property {Map<import("../../Material.js").Material, MeshRenderData[]>} materials
*/

/**
* @typedef ProgramRenderData
* @property {Map<import("../../Material.js").Material, MaterialRenderData>} materialRenderDatas
* @typedef MaterialConfigRenderData
* @property {Map<WebGLProgram, MaterialRenderData>} materialRenderDatas
*/

// Group all meshes by program and material
/** @type {Map<WebGLProgram, ProgramRenderData>} */
const programRenderDatas = new Map();
// Group all meshes by material config
/** @type {Map<import("./WebGlMaterialConfig.js").WebGlMaterialConfig, MaterialConfigRenderData>} */
const materialConfigRenderDatas = new Map();
for (const meshRenderData of meshRenderDatas) {
if (!meshRenderData.component.mesh || !meshRenderData.component.mesh.vertexState) continue;
for (const material of meshRenderData.component.materials) {
Expand All @@ -212,126 +211,121 @@ export class WebGlRenderer extends Renderer {

const program = this.#getProgram(materialConfig.vertexShader, materialConfig.fragmentShader);

let programRenderData = programRenderDatas.get(program);
let programRenderData = materialConfigRenderDatas.get(materialConfig);
if (!programRenderData) {
programRenderData = {
materialRenderDatas: new Map(),
};
programRenderDatas.set(program, programRenderData);
materialConfigRenderDatas.set(materialConfig, programRenderData);
}

let materialRenderData = programRenderData.materialRenderDatas.get(material);
let materialRenderData = programRenderData.materialRenderDatas.get(program);
if (!materialRenderData) {
materialRenderData = {
meshes: [],
materialConfig,
materials: new Map(),
};
programRenderData.materialRenderDatas.set(material, materialRenderData);
programRenderData.materialRenderDatas.set(program, materialRenderData);
}

materialRenderData.meshes.push(meshRenderData);
let meshes = materialRenderData.materials.get(material);
if (!meshes) {
meshes = [];
materialRenderData.materials.set(material, meshes);
}

meshes.push(meshRenderData);
}
}

// Sort meshes by program render order
const sortedProgramRenderDatas = Array.from(programRenderDatas.entries());
// TODO: Users can set the render order on the material config, but we are storing a list of programs.
// Essentially what we will want to do is store by program AND render order.
// This will make things less performant, for example, a user could have:
// - a material with program A, render order 10
// - a material with program B, render order 20
// - a material with program A again, render order 30
// Essentially sandwitching program B between two draw calls with program A.
// But if the user has specifically requested this render order, we should respect that request.

// sortedProgramRenderDatas.sort((a, b) => {
// const aConfig = a[1].materialConfig;
// const bConfig = b[1].materialConfig;
// return aConfig.renderOrder - bConfig.renderOrder;
// });

for (const [program, programRenderData] of sortedProgramRenderDatas) {
gl.useProgram(program);

const programData = this.#getCachedProgramData(program);
const viewUniformLocations = programData.getViewUniformLocations(gl);
const modelUniformLocations = programData.getModelUniformLocations(gl);

if (viewUniformLocations.viewProjectionMatrix) {
gl.uniformMatrix4fv(viewUniformLocations.viewProjectionMatrix, false, new Float32Array(viewProjectionMatrix.getFlatArrayBuffer("f32").buffer));
}
// Sort material configs by render order
const sortedProgramRenderDatas = Array.from(materialConfigRenderDatas.entries());
sortedProgramRenderDatas.sort((a, b) => {
return a[0].renderOrder - b[0].renderOrder;
});

for (const [materialConfig, programRenderData] of sortedProgramRenderDatas) {
for (const [program, materialRenderData] of programRenderData.materialRenderDatas) {
gl.useProgram(program);
const programData = this.#getCachedProgramData(program);
const viewUniformLocations = programData.getViewUniformLocations(gl);
const modelUniformLocations = programData.getModelUniformLocations(gl);

for (const [material, materialRenderData] of programRenderData.materialRenderDatas) {
const cullModeData = material.getMappedPropertyForMapType(WebGlMaterialMapType, "cullMode");
const cullMode = cullModeData?.value ?? "back";
if (cullMode == "front") {
this.#setCullMode(gl.FRONT);
} else if (cullMode == "back") {
this.#setCullMode(gl.BACK);
} else if (cullMode == "none") {
this.#setCullMode(null);
if (viewUniformLocations.viewProjectionMatrix) {
gl.uniformMatrix4fv(viewUniformLocations.viewProjectionMatrix, false, new Float32Array(viewProjectionMatrix.getFlatArrayBuffer("f32").buffer));
}

this.#setBlendMode(materialRenderData.materialConfig.blend);
for (const [material, meshRenderDatas] of materialRenderData.materials) {
const cullModeData = material.getMappedPropertyForMapType(WebGlMaterialMapType, "cullMode");
const cullMode = cullModeData?.value ?? "back";
if (cullMode == "front") {
this.#setCullMode(gl.FRONT);
} else if (cullMode == "back") {
this.#setCullMode(gl.BACK);
} else if (cullMode == "none") {
this.#setCullMode(null);
}

for (const { mappedData, value } of material.getMappedPropertiesForMapType(WebGlMaterialMapType)) {
if (mappedData.mappedName == "cullMode") continue;
if (mappedData.mappedType == "custom") {
const errorExample = `const customData = new MaterialCustomData();
this.#setBlendMode(materialConfig.blend);

for (const { mappedData, value } of material.getMappedPropertiesForMapType(WebGlMaterialMapType)) {
if (mappedData.mappedName == "cullMode") continue;
if (mappedData.mappedType == "custom") {
const errorExample = `const customData = new MaterialCustomData();
Material.setProperty("${mappedData.mappedName}", customData)`;
if (!value) {
throw new Error(`Assertion failed, material property "${mappedData.mappedName}" expected custom data but no property was set on the material. Set one with:\n${errorExample}`);
}
if (!(value instanceof CustomMaterialData)) {
throw new Error(`Assertion failed, material property "${mappedData.mappedName}" expected custom data but the property was a MaterialCustomData instance. Set custom data with:\n${errorExample}`);
}
const location = programData.getMaterialUniformLocation(gl, mappedData.mappedName);
if (location) {
value.fireCallback(/** @type {WebGlRenderer} */ (this), gl, location);
if (!value) {
throw new Error(`Assertion failed, material property "${mappedData.mappedName}" expected custom data but no property was set on the material. Set one with:\n${errorExample}`);
}
if (!(value instanceof CustomMaterialData)) {
throw new Error(`Assertion failed, material property "${mappedData.mappedName}" expected custom data but the property was a MaterialCustomData instance. Set custom data with:\n${errorExample}`);
}
const location = programData.getMaterialUniformLocation(gl, mappedData.mappedName);
if (location) {
value.fireCallback(/** @type {WebGlRenderer} */ (this), gl, location);
}
} else {
throw new Error("Not yet implemented");
}
} else {
throw new Error("Not yet implemented");
}
}

for (const { component: meshComponent, worldMatrix } of materialRenderData.meshes) {
const mesh = meshComponent.mesh;
if (!mesh) continue;
for (const { component: meshComponent, worldMatrix } of meshRenderDatas) {
const mesh = meshComponent.mesh;
if (!mesh) continue;

if (modelUniformLocations.mvpMatrix) {
const mvpMatrix = Mat4.multiplyMatrices(worldMatrix, viewProjectionMatrix);
gl.uniformMatrix4fv(modelUniformLocations.mvpMatrix, false, new Float32Array(mvpMatrix.getFlatArrayBuffer("f32").buffer));
}
if (modelUniformLocations.mvpMatrix) {
const mvpMatrix = Mat4.multiplyMatrices(worldMatrix, viewProjectionMatrix);
gl.uniformMatrix4fv(modelUniformLocations.mvpMatrix, false, new Float32Array(mvpMatrix.getFlatArrayBuffer("f32").buffer));
}

const meshData = this.#getCachedMeshData(mesh);
const indexBufferData = meshData.getIndexBufferData();
if (indexBufferData) {
let indexFormat;
if (mesh.indexFormat == Mesh.IndexFormat.UINT_16) {
indexFormat = gl.UNSIGNED_SHORT;
} else if (mesh.indexFormat == Mesh.IndexFormat.UINT_32) {
if (!this.#uint32IndexFormatExtension) {
this.#uint32IndexFormatExtension = gl.getExtension("OES_element_index_uint");
const meshData = this.#getCachedMeshData(mesh);
const indexBufferData = meshData.getIndexBufferData();
if (indexBufferData) {
let indexFormat;
if (mesh.indexFormat == Mesh.IndexFormat.UINT_16) {
indexFormat = gl.UNSIGNED_SHORT;
} else if (mesh.indexFormat == Mesh.IndexFormat.UINT_32) {
if (!this.#uint32IndexFormatExtension) {
this.#uint32IndexFormatExtension = gl.getExtension("OES_element_index_uint");
}
indexFormat = gl.UNSIGNED_INT;
} else {
throw new Error(`Mesh has an invalid index format: ${mesh.indexFormat}`);
}
indexFormat = gl.UNSIGNED_INT;
} else {
throw new Error(`Mesh has an invalid index format: ${mesh.indexFormat}`);
}
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBufferData.buffer);

let i = 0;
for (const { buffer, attributes, stride } of meshData.getAttributeBufferData()) {
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
for (const { componentCount, type, normalized, offset } of attributes) {
gl.vertexAttribPointer(i, componentCount, type, normalized, stride, offset);
gl.enableVertexAttribArray(i);
i++;
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBufferData.buffer);

let i = 0;
for (const { buffer, attributes, stride } of meshData.getAttributeBufferData()) {
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
for (const { componentCount, type, normalized, offset } of attributes) {
gl.vertexAttribPointer(i, componentCount, type, normalized, stride, offset);
gl.enableVertexAttribArray(i);
i++;
}
}
}

gl.drawElements(gl.TRIANGLES, indexBufferData.count, indexFormat, 0);
} else {
gl.drawElements(gl.TRIANGLES, indexBufferData.count, indexFormat, 0);
} else {
// TODO
}
}
}
}
Expand Down
44 changes: 44 additions & 0 deletions test/unit/src/rendering/renderers/webGl/WebGlRenderer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -532,3 +532,47 @@ Deno.test({
});
},
});

Deno.test({
name: "Materials are rendered by render order",
async fn() {
await runWithWebGlMocksAsync(async () => {
const { scene, domTarget, camComponent, commandLog } = await basicRendererSetup();

const vertexState = createVertexState();

const { material: materialA, materialConfig: configA } = createMaterial();
configA.renderOrder = 0;
configA.blend = {
srcFactor: 0,
dstFactor: 0,
};
createCubeEntity({ scene, material: materialA, vertexState });

const { material: materialB, materialConfig: configB } = createMaterial();
configB.renderOrder = 1;
configB.blend = {
srcFactor: 1,
dstFactor: 1,
};
createCubeEntity({ scene, material: materialB, vertexState });

domTarget.render(camComponent);
assertLogEquals(commandLog.getFilteredCommands("blendFuncSeparate"), [
{ name: "blendFuncSeparate", args: [0, 0, 0, 0] },
{ name: "blendFuncSeparate", args: [1, 1, 1, 1] },
]);

commandLog.clear();
// We flip the render order of the two materials to check if the two blend states get flipped.
configA.renderOrder = 1;
configB.renderOrder = 0;

domTarget.render(camComponent);
assertLogEquals(commandLog.getFilteredCommands("blendFuncSeparate"), [
// We don't expect a 1,1,1,1 command because that's already the current blend state
{ name: "blendFuncSeparate", args: [0, 0, 0, 0] },
]);
});
},
});
Loading