gpu-curtains
Version:
gpu-curtains is a 3D WebGPU rendering engine. It can be used as a standalone 3D engine, but also includes extra classes focused on mapping 3d objects to DOM elements; It allows users to synchronize values such as position, sizing, or scale between them.
213 lines (212 loc) • 9.76 kB
JavaScript
import { throwWarning } from "../../utils/utils.mjs";
import { isRenderer } from "../renderers/utils.mjs";
import { Material } from "./Material.mjs";
import { RenderPipelineEntry } from "../pipelines/RenderPipelineEntry.mjs";
import { compareRenderingOptions } from "./utils.mjs";
import { getDefaultProjectedVertexShaderCode } from "../shaders/full/vertex/get-default-projected-vertex-shader-code.mjs";
import { getDefaultVertexShaderCode } from "../shaders/full/vertex/get-default-vertex-shader-code.mjs";
import { getDefaultFragmentCode } from "../shaders/full/fragment/get-default-fragment-code.mjs";
//#region src/core/materials/RenderMaterial.ts
/**
* Create a {@link Material} specifically built to draw the vertices of a {@link core/geometries/Geometry.Geometry | Geometry}. Internally used by all kind of Meshes.
*
* ## Render pipeline
*
* A {@link RenderMaterial} automatically creates a {@link RenderPipelineEntry}. Once all the {@link core/bindGroups/BindGroup.BindGroup | BindGroup} have been created, they are sent with the shaders code and the {@link RenderMaterialOptions#rendering | rendering options} to the {@link RenderPipelineEntry}, which is in turns responsible for creating the {@link GPURenderPipeline}.
*
* After the {@link GPURenderPipeline} has been successfully compiled, the {@link RenderMaterial} is considered to be ready.
*/
var RenderMaterial = class extends Material {
/**
* RenderMaterial constructor
* @param renderer - {@link Renderer} class object or {@link GPUCurtains} class object used to create this {@link RenderMaterial}.
* @param parameters - {@link RenderMaterialParams} used to create our {@link RenderMaterial}.
*/
constructor(renderer, parameters) {
const type = "RenderMaterial";
renderer = isRenderer(renderer, type);
if (!parameters.shaders) parameters.shaders = {};
if (!parameters.shaders?.vertex) parameters.shaders.vertex = {
code: parameters.useProjection ? getDefaultProjectedVertexShaderCode : getDefaultVertexShaderCode,
entryPoint: "main"
};
if (!parameters.shaders.vertex.entryPoint) parameters.shaders.vertex.entryPoint = "main";
if (parameters.shaders.fragment === void 0) parameters.shaders.fragment = {
entryPoint: "main",
code: getDefaultFragmentCode
};
super(renderer, parameters);
this.type = type;
this.renderer = renderer;
const { shaders } = parameters;
const { useProjection, transparent, depth, depthWriteEnabled, depthCompare, depthFormat, depthBias, depthBiasClamp, depthBiasSlopeScale, stencil, sampleCount, alphaToCoverageEnabled, mask, cullMode, verticesOrder, topology, unclippedDepth } = parameters;
let { targets } = parameters;
if (targets === void 0) targets = [{ format: this.renderer.options.context.format }];
if (targets && targets.length && !targets[0].format) targets[0].format = this.renderer.options.context.format;
if (stencil) {
if (!stencil.front) stencil.front = {};
if (stencil.front && !stencil.back) stencil.back = stencil.front;
if (!stencil.stencilReference) stencil.stencilReference = 0;
if (!stencil.stencilReadMask) stencil.stencilReadMask = 16777215;
if (!stencil.stencilWriteMask) stencil.stencilWriteMask = 16777215;
}
this.options = {
...this.options,
shaders,
rendering: {
useProjection,
transparent,
depth,
depthWriteEnabled,
depthCompare,
depthFormat,
depthBias: depthBias !== void 0 ? depthBias : 0,
depthBiasClamp: depthBiasClamp !== void 0 ? depthBiasClamp : 0,
depthBiasSlopeScale: depthBiasSlopeScale !== void 0 ? depthBiasSlopeScale : 0,
...stencil && { stencil },
sampleCount,
alphaToCoverageEnabled: !!alphaToCoverageEnabled,
mask: mask !== void 0 ? mask : 16777215,
targets,
cullMode,
verticesOrder,
topology,
unclippedDepth: !!unclippedDepth
}
};
this.attributes = null;
this.pipelineEntry = null;
}
/**
* Set or reset this {@link RenderMaterial} {@link RenderMaterial.renderer | renderer}. Will also update the renderer camera bind group if needed.
* @param renderer - New {@link Renderer} or {@link GPUCurtains} instance to use.
*/
setRenderer(renderer) {
if (this.useCameraBindGroup && this.renderer) this.renderer.cameraLightsBindGroup.consumers.delete(this.uuid);
super.setRenderer(renderer);
if (this.useCameraBindGroup) {
this.bindGroups[0] = this.renderer.cameraLightsBindGroup;
this.renderer.cameraLightsBindGroup.consumers.add(this.uuid);
}
}
/**
* Set (or reset) the current {@link pipelineEntry}. Use the {@link Renderer#pipelineManager | renderer pipelineManager} to check whether we can get an already created {@link RenderPipelineEntry} from cache or if we should create a new one.
*/
setPipelineEntry() {
this.pipelineEntry = this.renderer.pipelineManager.createRenderPipeline(this);
}
/**
* Compile the {@link RenderPipelineEntry}.
*/
async compilePipelineEntry() {
await this.pipelineEntry.compilePipelineEntry();
}
/**
* Check if attributes and all bind groups are ready, create them if needed, set {@link RenderPipelineEntry} bind group buffers and compile the pipeline.
*/
async compileMaterial() {
if (this.ready) return;
await super.compileMaterial();
if (this.attributes && !this.pipelineEntry) this.setPipelineEntry();
if (this.pipelineEntry && this.pipelineEntry.canCompile) await this.compilePipelineEntry();
}
/**
* Set or reset one of the {@link RenderMaterialRenderingOptions | rendering options}. Should be use with great caution, because if the {@link RenderPipelineEntry#pipeline | render pipeline} has already been compiled, it can cause a pipeline flush.
* @param renderingOptions - New {@link RenderMaterialRenderingOptions | rendering options} properties to be set.
*/
setRenderingOptions(renderingOptions = {}) {
if (renderingOptions.transparent && renderingOptions.targets.length && !renderingOptions.targets[0].blend) renderingOptions.targets[0].blend = RenderPipelineEntry.getDefaultTransparentBlending();
const newProperties = compareRenderingOptions(renderingOptions, this.options.rendering);
const oldRenderingOptions = { ...this.options.rendering };
this.options.rendering = {
...this.options.rendering,
...renderingOptions
};
if (this.pipelineEntry) if (this.pipelineEntry.ready && newProperties.length) {
if (!this.renderer.production) {
const oldProps = newProperties.map((key) => {
return { [key]: Array.isArray(oldRenderingOptions[key]) ? oldRenderingOptions[key].map((optKey) => optKey) : oldRenderingOptions[key] };
});
const newProps = newProperties.map((key) => {
return { [key]: Array.isArray(renderingOptions[key]) ? renderingOptions[key].map((optKey) => optKey) : renderingOptions[key] };
});
throwWarning(`${this.options.label}: the change of rendering options is causing this RenderMaterial pipeline to be recompiled. This should be avoided.\n\nOld rendering options: ${JSON.stringify(oldProps.reduce((acc, v) => {
return {
...acc,
...v
};
}, {}), null, 4)}\n\n--------\n\nNew rendering options: ${JSON.stringify(newProps.reduce((acc, v) => {
return {
...acc,
...v
};
}, {}), null, 4)}`);
}
this.setPipelineEntry();
} else this.pipelineEntry.options.rendering = {
...this.pipelineEntry.options.rendering,
...this.options.rendering
};
}
/**
* Get all useful {@link core/geometries/Geometry.Geometry | Geometry} properties needed to create attributes buffers.
* @param geometry - The geometry to draw.
*/
setAttributesFromGeometry(geometry) {
this.attributes = {
wgslStructFragment: geometry.wgslStructFragment,
vertexBuffers: geometry.vertexBuffers,
layoutCacheKey: geometry.layoutCacheKey
};
if ("indexBuffer" in geometry && geometry.indexBuffer && geometry.topology.includes("strip")) this.setRenderingOptions({
...this.options.rendering,
stripIndexFormat: geometry.indexBuffer.bufferFormat
});
}
/**
* Get the {@link RenderMaterial} pipeline buffers cache key based on its {@link core/bindGroups/BindGroup.BindGroup | BindGroup} cache keys and eventually {@link attributes} cache keys.
* @returns - Current cache key.
* @readonly
*/
get cacheKey() {
return (this.attributes?.layoutCacheKey || "") + super.cacheKey;
}
/**
* Get whether this {@link RenderMaterial} uses the renderer camera and lights bind group.
* @readonly
* */
get useCameraBindGroup() {
return "cameraLightsBindGroup" in this.renderer && this.options.rendering.useProjection;
}
/**
* Create the bind groups if they need to be created, but first add camera and lights bind group if needed.
*/
createBindGroups() {
if (this.useCameraBindGroup) {
this.bindGroups.push(this.renderer.cameraLightsBindGroup);
this.renderer.cameraLightsBindGroup.consumers.add(this.uuid);
}
super.createBindGroups();
}
/**
* Update all bind groups, except for the camera and light bind groups if present, as it is already updated by the renderer itself.
*/
updateBindGroups() {
const startBindGroupIndex = this.useCameraBindGroup ? 1 : 0;
if (this.useCameraBindGroup && this.bindGroups.length) {
if (this.bindGroups[0].needsPipelineFlush && this.pipelineEntry.ready) this.pipelineEntry.flushPipelineEntry(this.bindGroups);
}
for (let i = startBindGroupIndex; i < this.bindGroups.length; i++) this.updateBindGroup(this.bindGroups[i]);
}
/**
* Render the material if it is ready. Call super, and the set the pass encoder stencil reference if needed.
* @param pass - Current pass encoder.
*/
render(pass) {
if (!this.ready) return;
super.render(pass);
if (this.options.rendering.stencil) pass.setStencilReference(this.options.rendering.stencil.stencilReference ?? 0);
}
};
//#endregion
export { RenderMaterial };