_'use strict';
function getWGSLCoordinate(value, side, invert = false) {
const direction = invert ? -1 : 1;
const p = value / side;
return (p * 2 - 1) * direction;
};
/**
* To tell the {@link RenderPass} how to display the triangles.
* Default `TRIANGLE_LIST`
* @example
*
* renderPass.topology = PrimitiveTopology.POINT_LIST;
*/
export class PrimitiveTopology {
/** @type {GPUPrimitiveTopology} */
static POINT_LIST = 'point-list';
/** @type {GPUPrimitiveTopology} */
static LINE_LIST = 'line-list';
/** @type {GPUPrimitiveTopology} */
static LINE_STRIP = 'line-strip';
/** @type {GPUPrimitiveTopology} */
static TRIANGLE_LIST = 'triangle-list';
/** @type {GPUPrimitiveTopology} */
static TRIANGLE_STRIP = 'triangle-strip';
};
/**
* To tell the {@link RenderPass} how the data from the previous RenderPass
* is preserved on screen or cleared.
* Default `CLEAR`
* @example
*
* renderPass.loadOp = LoadOp.LOAD;
*/
export class LoadOp {
/** @type {GPULoadOp} */
static CLEAR = 'clear';
/** @type {GPULoadOp} */
static LOAD = 'load';
}
/**
* To tell the {@link RenderPass} what polygons are Front Facing
* Default `CCW`
* @example
*
* renderPass.frontFace = FrontFace.CCW;
*/
export class FrontFace {
/** @type {GPUFrontFace} */
static CCW = 'ccw';
/** @type {GPUFrontFace} */
static CC = 'cc';
}
/**
* To tell the {@link RenderPass} what polygons should be discarded
* Default `BACK`
* @example
*
* renderPass.cullMode = CullMode.BACK;
*/
export class CullMode {
/** @type {GPUCullMode} */
static NONE = 'none';
/** @type {GPUCullMode} */
static FRONT = 'front';
/** @type {GPUCullMode} */
static BACK = 'back';
}
/**
* A RenderPass is a way to have a block of shaders to pass to your application pipeline and
* these render passes will be executed in the order you pass them in the {@link Points#init} method.
*
* @example
* import Points, { RenderPass } from 'points';
* // vert, frag and compute are strings with the wgsl shaders.
* let renderPasses = [
* new RenderPass(vert1, frag1, compute1),
* new RenderPass(vert2, frag2, compute2)
* ];
* // we pass the array of renderPasses
* await points.init(renderPasses);
*
* @example
* // init param example
* const waves = new RenderPass(vertexShader, fragmentShader, null, 8, 8, 1, (points, params) => {
* points.setSampler('renderpass_feedbackSampler', null);
* points.setTexture2d('renderpass_feedbackTexture', true);
* points.setUniform('waves_scale', params.scale || .45);
* points.setUniform('waves_intensity', params.intensity || .03);
* });
* waves.required = ['scale', 'intensity'];
*/
class RenderPass {
#index = null;
#vertexShader;
#computeShader;
#fragmentShader;
#compiledShaders
#computePipeline = null;
#renderPipeline = null;
#name = null;
/**
* @type {GPUBindGroup}
*/
#computeBindGroup = null;
/**
* @type {GPUBindGroup}
*/
#fragmentBindGroup = null;
/**
* @type {GPUBindGroup}
*/
#vertexBindGroup = null;
/**
* @type {GPUBindGroupLayout}
*/
#bindGroupLayoutFragment = null;
/**
* @type {GPUBindGroupLayout}
*/
#bindGroupLayoutVertex = null;
/**
* @type {GPUBindGroupLayout}
*/
#bindGroupLayoutCompute = null;
#hasComputeShader;
#hasVertexShader;
#hasFragmentShader;
#hasVertexAndFragmentShader;
#workgroupCountX;
#workgroupCountY;
#workgroupCountZ;
#callback = null;
#required = null;
#instanceCount = 1;
#internal = false;
#params = null;
#vertexArray = [];
#vertexBuffer = null;
#vertexBufferInfo = null;
#depthWriteEnabled = false;
#loadOp = LoadOp.CLEAR;
#clearValue = { r: 0.0, g: 0.0, b: 0.0, a: 1.0 };
#meshCounter = 0;
#meshes = [];
#topology = PrimitiveTopology.TRIANGLE_LIST;
#cullMode = CullMode.BACK;
#frontFace = FrontFace.CCW;
#descriptor = {
colorAttachments: [
{
//view: textureView,
clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
loadOp: 'clear',
storeOp: 'store',
}
],
// depthStencilAttachment: {
// //view: this.#depthTexture.createView(),
// depthClearValue: 1.0,
// depthLoadOp: 'clear',
// depthStoreOp: 'store'
// }
};
#depthStencilAttachment = {
//view: this.#depthTexture.createView(),
depthClearValue: 1.0,
depthLoadOp: 'clear',
depthStoreOp: 'store'
}
#bundle = null;
#device = null;
/**
* A collection of Vertex, Compute and Fragment shaders that represent a RenderPass.
* This is useful for PostProcessing.
* @param {String} vertexShader WGSL Vertex Shader in a String.
* @param {String} fragmentShader WGSL Fragment Shader in a String.
* @param {String} computeShader WGSL Compute Shader in a String.
* @param {String} workgroupCountX Workgroup amount in X.
* @param {String} workgroupCountY Workgroup amount in Y.
* @param {String} workgroupCountZ Workgroup amount in Z.
* @param {function(points:Points, params:Object):void} init Method to add custom
* uniforms or storage (points.set* methods).
* This is made for post processing multiple `RenderPass`.
* The method `init` will be called to initialize the buffer parameters.
*
*/
constructor(vertexShader, fragmentShader, computeShader, workgroupCountX, workgroupCountY, workgroupCountZ, init) {
this.#vertexShader = vertexShader;
this.#computeShader = computeShader;
this.#fragmentShader = fragmentShader;
this.#callback = init;
this.#internal = !!init; // if it has the init then is a external Render Pass (Post Process)
this.#compiledShaders = {
vertex: '',
compute: '',
fragment: '',
};
this.#hasComputeShader = !!this.#computeShader;
this.#hasVertexShader = !!this.#vertexShader;
this.#hasFragmentShader = !!this.#fragmentShader;
this.#hasVertexAndFragmentShader = this.#hasVertexShader && this.#hasFragmentShader;
this.#workgroupCountX = workgroupCountX || 8;
this.#workgroupCountY = workgroupCountY || 8;
this.#workgroupCountZ = workgroupCountZ || 1;
Object.seal(this);
}
/**
* Get the current RenderPass index order in the pipeline.
* When you add a RenderPass to the constructor or via
* {@link Points#addRenderPass}, this is the order it receives.
*/
get index() {
return this.#index;
}
set index(value) {
this.#index = value;
}
/**
* get the vertex shader content
*/
get vertexShader() {
return this.#vertexShader;
}
/**
* get the compute shader content
*/
get computeShader() {
return this.#computeShader;
}
/**
* get the fragment shader content
*/
get fragmentShader() {
return this.#fragmentShader;
}
set computePipeline(value) {
this.#computePipeline = value;
}
get computePipeline() {
return this.#computePipeline;
}
set renderPipeline(value) {
this.#renderPipeline = value;
}
get renderPipeline() {
return this.#renderPipeline;
}
set computeBindGroup(value) {
this.#computeBindGroup = value;
}
get computeBindGroup() {
return this.#computeBindGroup;
}
set fragmentBindGroup(value) {
this.#fragmentBindGroup = value;
}
get fragmentBindGroup() {
return this.#fragmentBindGroup;
}
set vertexBindGroup(value) {
this.#vertexBindGroup = value;
}
get vertexBindGroup() {
return this.#vertexBindGroup;
}
set bindGroupLayoutFragment(value) {
this.#bindGroupLayoutFragment = value;
}
get bindGroupLayoutFragment() {
return this.#bindGroupLayoutFragment;
}
set bindGroupLayoutVertex(value) {
this.#bindGroupLayoutVertex = value;
}
get bindGroupLayoutVertex() {
return this.#bindGroupLayoutVertex;
}
set bindGroupLayoutCompute(value) {
this.#bindGroupLayoutCompute = value;
}
get bindGroupLayoutCompute() {
return this.#bindGroupLayoutCompute;
}
get compiledShaders() {
return this.#compiledShaders;
}
get hasComputeShader() {
return this.#hasComputeShader;
}
get hasVertexShader() {
return this.#hasVertexShader;
}
get hasFragmentShader() {
return this.#hasFragmentShader;
}
get hasVertexAndFragmentShader() {
return this.#hasVertexAndFragmentShader;
}
/**
* How many workgroups are in the X dimension.
*/
get workgroupCountX() {
return this.#workgroupCountX;
}
/**
* @param {Number} val
*/
set workgroupCountX(val) {
this.#workgroupCountX = val;
}
/**
* How many workgroups are in the Y dimension.
*/
get workgroupCountY() {
return this.#workgroupCountY;
}
/**
* @param {Number} val
*/
set workgroupCountY(val) {
this.#workgroupCountY = val;
}
/**
* How many workgroups are in the Z dimension.
*/
get workgroupCountZ() {
return this.#workgroupCountZ;
}
/**
* @param {Number} val
*/
set workgroupCountZ(val) {
this.#workgroupCountZ = val;
}
/**
* Function where the `init` parameter (set in the constructor) is executed
* and this call will pass the parameters that the RenderPass
* requires to run.
* @param {Points} points instance of {@link Points} to call set* functions
* like {@link Points#setUniform} and others.
*/
init(points) {
this.#params ||= {};
this.#callback?.(points, this.#params);
}
get required() {
return this.#required;
}
/**
* List of buffer names that are required for this RenderPass so if it shows
* them in the console.
* @param {Array<String>} val names of the parameters `params` in
* {@link RenderPass#setInit} that are required.
* This is only used for a post processing RenderPass.
*/
set required(val) {
this.#required = val;
}
/**
* Number of instances that will be created of the current mesh (Vertex Buffer)
* in this RenderPass. This means if you have a quad, it will create
* `instanceCount` number of independent quads on the screen.
* Useful for instanced particles driven by a Storage buffer.
*/
get instanceCount() {
// TODO: lock the value with a flag
this.#instanceCount = this.#meshes.reduce((sum, mesh) => sum + mesh.instanceCount, 0);
return this.#instanceCount;
}
get name() {
return this.#name;
}
set name(val) {
this.#name = val;
}
get internal() {
return this.#internal;
}
/**
* Parameters specifically for Post RenderPass
*/
get params() {
return this.#params;
}
/**
* @param {Object} val data that can be assigned to the RenderPass when
* the {@link Points#addRenderPass} method is called.
*/
set params(val) {
this.#params = val;
}
get vertexArray() {
return new Float32Array(this.#vertexArray);
}
set vertexArray(val) {
this.#vertexArray = val;
}
get vertexBufferInfo() {
return this.#vertexBufferInfo;
}
set vertexBufferInfo(val) {
this.#vertexBufferInfo = val;
}
get vertexBuffer() {
return this.#vertexBuffer;
}
set vertexBuffer(val) {
this.#vertexBuffer = val;
}
get depthWriteEnabled() {
return this.#depthWriteEnabled;
}
/**
* Controls whether your fragment shader can write to the depth buffer.
* By default `true`.
* To allow transparency and a custom type of sort, set this as false;
* @param {Boolean} val
*/
set depthWriteEnabled(val) {
if (val) {
this.#descriptor.depthStencilAttachment = this.#depthStencilAttachment;
}
this.#depthWriteEnabled = val;
}
get loadOp() {
return this.#loadOp;
}
/**
* Controls if the last RenderPass data is preserved on screen or cleared.
* Default {@link LoadOp#CLEAR}
* @param {LoadOp | GPULoadOp} val
*/
set loadOp(val) {
this.#loadOp = val;
this.#descriptor.colorAttachments[0].loadOp = this.#loadOp;
}
get clearValue() {
return this.#clearValue;
}
/**
* Sets the color used to clear the RenderPass before drawing.
* (only if {@link RenderPass#loadOp | loadOp} is set to `clear`)
* default: black
* @param {{ r: Number, g: Number, b: Number, a: Number }} val
*/
set clearValue(val) {
this.#clearValue = val;
this.#descriptor.colorAttachments[0].clearValue = this.#clearValue;
}
/**
* @type {GPURenderPassDescriptor}
*/
get descriptor() {
return this.#descriptor;
}
get topology() {
return this.#topology;
}
/**
* To render as Triangles, lines or points.
* Use class {@link PrimitiveTopology}
* @param {GPUPrimitiveTopology} val
*/
set topology(val) {
this.#topology = val;
}
get cullMode() {
return this.#cullMode;
}
/**
* Triangles to discard.
* Default `BACK`.
* Use class {@link CullMode}
* @param {CullMode | GPUCullMode} val
*/
set cullMode(val) {
this.#cullMode = val;
}
get frontFace() {
return this.#frontFace;
}
/**
* Direction of the triangles.
* Counter Clockwise (CCW) or Clockwise (CW)
* Default `CCW`.
* Use class {@link frontFace}
* @param {FrontFace | GPUFrontFace} val
*/
set frontFace(val) {
this.#frontFace = val;
}
get bundle() {
return this.#bundle;
}
/**
* Render Bundle for performance
* @param {GPURenderBundle} val
*/
set bundle(val) {
this.#bundle = val;
}
get device() {
return this.#device;
}
/**
* Device reference to check if RenderBundle needs to be rebuilt
* @param {GPUDevice} val
*/
set device(val) {
this.#device = val;
}
/**
* - **currently for internal use**<br>
* - **might be private in the future**<br>
* Adds two triangles as a quad called Point
* @param {Coordinate} coordinate `x` from 0 to canvas.width, `y` from 0 to canvas.height, `z` it goes from 0.0 to 1.0 and forward
* @param {Number} width point width
* @param {Number} height point height
* @param {Array<RGBAColor>} colors one color per corner
* @param {HTMLCanvasElement} canvas canvas element
* @param {Boolean} useTexture
* @ignore
*/
addPoint(coordinate, width, height, colors, canvas, useTexture = false) {
const { x, y, z } = coordinate;
const nx = getWGSLCoordinate(x, canvas.width); // left
const ny = getWGSLCoordinate(y, canvas.height, true); // top
const nw = getWGSLCoordinate(x + width, canvas.width); // right
const nh = getWGSLCoordinate(y + height, canvas.height); // bottom
const nz = z;
const normals = [0, 0, 1];
const id = this.#meshCounter;
const { r: r0, g: g0, b: b0, a: a0 } = colors[0]; // top-left
const { r: r1, g: g1, b: b1, a: a1 } = colors[1]; // bottom-left
const { r: r2, g: g2, b: b2, a: a2 } = colors[2]; // top-right
const { r: r3, g: g3, b: b3, a: a3 } = colors[3]; // bottom-right
this.#vertexArray.push(
+nx, +ny, nz, 1, r0, g0, b0, a0, (+nx + 1) * 0.5, (+ny + 1) * 0.5, ...normals, id, // top-left
+nx, -nh, nz, 1, r1, g1, b1, a1, (+nx + 1) * 0.5, (-nh + 1) * 0.5, ...normals, id, // bottom-left
+nw, +ny, nz, 1, r2, g2, b2, a2, (+nw + 1) * 0.5, (+ny + 1) * 0.5, ...normals, id, // top-right
);
this.#vertexArray.push(
+nx, -nh, nz, 1, r1, g1, b1, a1, (+nx + 1) * 0.5, (-nh + 1) * 0.5, ...normals, id, // bottom-left
+nw, -nh, nz, 1, r3, g3, b3, a3, (+nw + 1) * 0.5, (-nh + 1) * 0.5, ...normals, id, // bottom-right
+nw, +ny, nz, 1, r2, g2, b2, a2, (+nw + 1) * 0.5, (+ny + 1) * 0.5, ...normals, id, // top-right
);
const mesh = {
name: '_plane_',
id,
instanceCount: 1,
verticesCount: 6
};
this.#meshes.push(mesh);
++this.#meshCounter;
return mesh;
}
/**
* Adds a mesh quad
* @param {String} name The name will show up in the `mesh` Uniform.
* @param {{x:Number, y:Number, z:Number}} coordinate
* @param {{width:Number, height:Number}} dimensions
* @param {{r:Number, g:Number, b:Number, a:Number}} color
* @param {{x:Number, y:Number }} segments mesh subdivisions
*
* @example
*
* renderPass.addPlane('plane', { x: 0, y: 0, z: 0 }, { width: 2, height: 2 }).instanceCount = NUMPARTICLES;
*/
addPlane(
name,
coordinate = { x: 0, y: 0, z: 0 },
dimensions = { width: 1, height: 1 },
color = { r: 1, g: 0, b: 1, a: 0 },
segments = { x: 1, y: 1 }
) {
const { x, y, z } = coordinate;
const { width, height } = dimensions;
const { x: sx, y: sy } = segments;
const hw = width / 2;
const hh = height / 2;
const { r, g, b, a } = color;
const normal = [0, 0, 1];
const id = this.#meshCounter;
const grid = [];
for (let iy = 0; iy <= sy; iy++) {
const v = iy / sy;
const posY = y - hh + v * height;
for (let ix = 0; ix <= sx; ix++) {
const u = ix / sx;
const posX = x - hw + u * width;
grid.push({
position: [posX, posY, z],
uv: [u, v]
});
}
}
for (let iy = 0; iy < sy; iy++) {
for (let ix = 0; ix < sx; ix++) {
const rowSize = sx + 1;
const i0 = iy * rowSize + ix;
const i1 = i0 + 1;
const i2 = i0 + rowSize;
const i3 = i2 + 1;
const quad = [
grid[i0], grid[i1], grid[i3],
grid[i0], grid[i3], grid[i2]
];
for (const { position: [vx, vy, vz], uv: [u, v] } of quad) {
this.#vertexArray.push(+vx, +vy, +vz, 1, r, g, b, a, u, v, ...normal, id);
}
}
}
const mesh = {
name,
id,
instanceCount: 1,
verticesCount: sx * sy * 6
};
this.#meshes.push(mesh);
++this.#meshCounter;
return mesh;
}
/**
* Adds a mesh cube
* @param {String} name The name will show up in the `mesh` Uniform.
* @param {{x:Number, y:Number, z:Number}} coordinate
* @param {{width:Number, height:Number, depth:Number}} dimensions
* @param {{r:Number, g:Number, b:Number, a:Number}} color
*
* @example
*
* renderPass.addCube('base_cube').instanceCount = NUMPARTICLES;
*/
addCube(
name,
coordinate = { x: 0, y: 0, z: 0 },
dimensions = { width: 1, height: 1, depth: 1 },
color = { r: 1, g: 0, b: 1, a: 0 }
) {
const { x, y, z } = coordinate;
const { width, height, depth } = dimensions;
const hw = width / 2;
const hh = height / 2;
const hd = depth / 2;
const corners = [
[x - hw, y - hh, z - hd], // 0: left-bottom-back
[x + hw, y - hh, z - hd], // 1: right-bottom-back
[x + hw, y + hh, z - hd], // 2: right-top-back
[x - hw, y + hh, z - hd], // 3: left-top-back
[x - hw, y - hh, z + hd], // 4: left-bottom-front
[x + hw, y - hh, z + hd], // 5: right-bottom-front
[x + hw, y + hh, z + hd], // 6: right-top-front
[x - hw, y + hh, z + hd], // 7: left-top-front
];
const faceUVs = [
[[0, 0], [1, 0], [1, 1], [0, 1]], // back
[[0, 0], [1, 0], [1, 1], [0, 1]], // front
[[0, 0], [1, 0], [1, 1], [0, 1]], // left
[[0, 0], [1, 0], [1, 1], [0, 1]], // right
[[0, 0], [1, 0], [1, 1], [0, 1]], // top
[[0, 0], [1, 0], [1, 1], [0, 1]], // bottom
];
const faceNormals = [
[0, 0, -1], // back
[0, 0, 1], // front
[-1, 0, 0], // left
[1, 0, 0], // right
[0, 1, 0], // top
[0, -1, 0], // bottom
];
const faces = [
[0, 3, 2, 1], // back
[4, 5, 6, 7], // front
[0, 4, 7, 3], // left
[5, 1, 2, 6], // right
[3, 7, 6, 2], // top
[0, 1, 5, 4], // bottom
];
for (let i = 0; i < 6; i++) {
const [i0, i1, i2, i3] = faces[i];
// const color = faceColors[i];
const { r, g, b, a } = color;
const normals = faceNormals[i];
const v = [corners[i0], corners[i1], corners[i2], corners[i3]];
const uv = faceUVs[i];
const verts = [
[v[0], uv[0]],
[v[1], uv[1]],
[v[2], uv[2]],
[v[0], uv[0]],
[v[2], uv[2]],
[v[3], uv[3]],
];
for (const [[vx, vy, vz], [u, v]] of verts) {
this.#vertexArray.push(+vx, +vy, +vz, 1, r, g, b, a, u, v, ...normals, this.#meshCounter);
}
}
const mesh = {
name,
id: this.#meshCounter,
instanceCount: 1,
verticesCount: 36
}
this.#meshes.push(mesh);
++this.#meshCounter;
return mesh;
}
/**
* Adds a mesh sphere
* @param {String} name The name will show up in the `mesh` Uniform.
* @param {{x:Number, y:Number, z:Number}} coordinate
* @param {{r:Number, g:Number, b:Number, a:Number}} color
* @param {Number} radius
* @param {Number} segments
* @param {Number} rings
*
* @example
*
* renderPass.addSphere('sphere').instanceCount = 100;
*/
addSphere(
name,
coordinate = { x: 0, y: 0, z: 0 },
color = { r: 1, g: 0, b: 1, a: 0 },
radius = 1,
segments = 16,
rings = 12
) {
const { x, y, z } = coordinate;
const { r, g, b, a } = color;
const vertexGrid = [];
// generate vertices
for (let lat = 0; lat <= rings; lat++) {
const theta = (lat * Math.PI) / rings;
const sinTheta = Math.sin(theta);
const cosTheta = Math.cos(theta);
vertexGrid[lat] = [];
for (let lon = 0; lon <= segments; lon++) {
const phi = (lon * 2 * Math.PI) / segments;
const sinPhi = Math.sin(phi);
const cosPhi = Math.cos(phi);
const nx = cosPhi * sinTheta;
const ny = cosTheta;
const nz = sinPhi * sinTheta;
const vx = x + radius * nx;
const vy = y + radius * ny;
const vz = z + radius * nz;
const u = lon / segments;
const v = lat / rings;
vertexGrid[lat][lon] = [vx, vy, vz, 1, r, g, b, a, u, v, nx, ny, nz, this.#meshCounter];
}
}
// generate triangles
for (let lat = 0; lat < rings; lat++) {
for (let lon = 0; lon < segments; lon++) {
const v1 = vertexGrid[lat][lon];
const v2 = vertexGrid[lat + 1][lon];
const v3 = vertexGrid[lat + 1][lon + 1];
const v4 = vertexGrid[lat][lon + 1];
// triangle 1
this.#vertexArray.push(...v1, ...v3, ...v2);
// triangle 2
this.#vertexArray.push(...v1, ...v4, ...v3);
}
}
const mesh = {
name,
id: this.#meshCounter,
instanceCount: 1,
verticesCount: rings * segments * 6
}
this.#meshes.push(mesh);
++this.#meshCounter;
return mesh;
}
/**
* Adds a Torus mesh
* @param {String} name The name will show up in the `mesh` Uniform.
* @param {{x:Number, y:Number, z:Number}} coordinate
* @param {Number} radius
* @param {Number} tube
* @param {Number} radialSegments
* @param {Number} tubularSegments
* @param {{r:Number, g:Number, b:Number, a:Number}} color
* @returns {Object}
*
* @example
*
* renderPass.addTorus('myTorus');
*/
addTorus(
name,
coordinate = { x: 0, y: 0, z: 0 },
radius = 1,
tube = .4,
radialSegments = 32,
tubularSegments = 24,
color = { r: 1, g: 0, b: 1, a: 1 }
) {
const { x, y, z } = coordinate;
const { r, g, b, a } = color;
const vertices = [];
const normals = [];
const uvs = [];
const indices = [];
for (let k = 0; k <= radialSegments; k++) {
const v = k / radialSegments * Math.PI * 2;
const cosV = Math.cos(v);
const sinV = Math.sin(v);
for (let i = 0; i <= tubularSegments; i++) {
const u = i / tubularSegments * Math.PI * 2;
const cosU = Math.cos(u);
const sinU = Math.sin(u);
const tx = (radius + tube * cosV) * cosU + x;
const ty = (radius + tube * cosV) * sinU + y;
const tz = tube * sinV + z;
const nx = cosV * cosU;
const ny = cosV * sinU;
const nz = sinV;
vertices.push([tx, ty, tz]);
normals.push([nx, ny, nz]);
uvs.push([i / tubularSegments, k / radialSegments]);
}
}
for (let k = 1; k <= radialSegments; k++) {
for (let i = 1; i <= tubularSegments; i++) {
const a = (tubularSegments + 1) * k + i - 1;
const b = (tubularSegments + 1) * (k - 1) + i - 1;
const c = (tubularSegments + 1) * (k - 1) + i;
const d = (tubularSegments + 1) * k + i;
indices.push([a, b, d]);
indices.push([b, c, d]);
}
}
for (const [i0, i1, i2] of indices) {
for (const i of [i0, i1, i2]) {
const [vx, vy, vz] = vertices[i];
const [nx, ny, nz] = normals[i];
const [u, v] = uvs[i];
this.#vertexArray.push(vx, vy, vz, 1, r, g, b, a, u, v, nx, ny, nz, this.#meshCounter);
}
}
const mesh = {
name,
id: this.#meshCounter,
instanceCount: 1,
verticesCount: indices.length * 3
};
this.#meshes.push(mesh);
++this.#meshCounter;
return mesh;
}
/**
* Adds a Cylinder mesh
* @param {String} name The name will show up in the `mesh` Uniform.
* @param {{x:Number, y:Number, z:Number}} coordinate
* @param {Number} radius
* @param {Number} height
* @param {Number} radialSegments
* @param {Boolean} cap
* @param {{r:Number, g:Number, b:Number, a:Number}} color
* @returns {Object}
*
* @example
* renderPass.addCylinder('myCylinder');
*/
addCylinder(
name,
coordinate = { x: 0, y: 0, z: 0 },
radius = .5,
height = 1,
radialSegments = 32,
cap = true,
color = { r: 1, g: 0, b: 1, a: 1 }
) {
const { x: cx, y: cy, z: cz } = coordinate;
const { r, g, b, a } = color;
const halfHeight = height / 2;
const vertices = [];
const normals = [];
const uvs = [];
const indices = [];
// sides
for (let i = 0; i <= radialSegments; i++) {
const theta = (i / radialSegments) * Math.PI * 2;
const cosTheta = Math.cos(theta);
const sinTheta = Math.sin(theta);
const px = cx + radius * cosTheta;
const pz = cz + radius * sinTheta;
vertices.push([px, cy - halfHeight, pz]); // bottom
normals.push([cosTheta, 0, sinTheta]);
uvs.push([i / radialSegments, 0]);
vertices.push([px, cy + halfHeight, pz]); // top
normals.push([cosTheta, 0, sinTheta]);
uvs.push([i / radialSegments, 1]);
}
for (let i = 0; i < radialSegments; i++) {
const base = i * 2;
indices.push([base, base + 1, base + 3]);
indices.push([base, base + 3, base + 2]);
}
// caps
if (cap) {
const bottomCenterIndex = vertices.length;
vertices.push([cx, cy - halfHeight, cz]);
normals.push([0, -1, 0]);
uvs.push([.5, .5]);
const topCenterIndex = vertices.length;
vertices.push([cx, cy + halfHeight, cz]);
normals.push([0, 1, 0]);
uvs.push([.5, .5]);
for (let i = 0; i < radialSegments; i++) {
const theta = (i / radialSegments) * Math.PI * 2;
const nextTheta = ((i + 1) / radialSegments) * Math.PI * 2;
const x0 = cx + radius * Math.cos(theta);
const z0 = cz + radius * Math.sin(theta);
const x1 = cx + radius * Math.cos(nextTheta);
const z1 = cz + radius * Math.sin(nextTheta);
const bottomIdx0 = vertices.length;
vertices.push([x0, cy - halfHeight, z0]);
normals.push([0, -1, 0]);
uvs.push([.5 + .5 * Math.cos(theta), .5 + .5 * Math.sin(theta)]);
const bottomIdx1 = vertices.length;
vertices.push([x1, cy - halfHeight, z1]);
normals.push([0, -1, 0]);
uvs.push([.5 + .5 * Math.cos(nextTheta), .5 + .5 * Math.sin(nextTheta)]);
indices.push([bottomCenterIndex, bottomIdx0, bottomIdx1]);
const topIdx0 = vertices.length;
vertices.push([x0, cy + halfHeight, z0]);
normals.push([0, 1, 0]);
uvs.push([.5 + .5 * Math.cos(theta), .5 + .5 * Math.sin(theta)]);
const topIdx1 = vertices.length;
vertices.push([x1, cy + halfHeight, z1]);
normals.push([0, 1, 0]);
uvs.push([.5 + .5 * Math.cos(nextTheta), .5 + .5 * Math.sin(nextTheta)]);
indices.push([topCenterIndex, topIdx1, topIdx0]);
}
}
for (const [i0, i1, i2] of indices) {
for (const i of [i0, i1, i2]) {
const [vx, vy, vz] = vertices[i];
const [nx, ny, nz] = normals[i];
const [u, v] = uvs[i];
this.#vertexArray.push(vx, vy, vz, 1, r, g, b, a, u, v, nx, ny, nz, this.#meshCounter);
}
}
const mesh = {
name,
id: this.#meshCounter,
instanceCount: 1,
verticesCount: indices.length * 3
};
this.#meshes.push(mesh);
++this.#meshCounter;
return mesh;
}
/**
* Add a external mesh with the provided required data.
* @param {String} name The name will show up in the `mesh` Uniform.
* @param {Array<{x:Number, y:Number, z:Number}>} vertices
* @param {Array<{r:Number, g:Number, b:Number, a:Number}>} colors
* @param {Array<{u:Number, v:Number}>} uvs
* @param {Array<Number>} normals
*
* @example
*
* const url = '../models/monkey.glb';
* const data = await loadAndExtract(url);
* const { positions, colors, uvs, normals, indices, colorSize, texture } = data[0]
* renderPass.addMesh('monkey', positions, colors, colorSize, uvs, normals, indices)
* renderPass.depthWriteEnabled = true;
*
*/
addMesh(name, vertices, colors, colorSize, uvs, normals, indices) {
const verticesCount = indices.length;
for (let i = 0; i < verticesCount; i++) {
const index = indices[i];
const vertex = vertices.slice(index * 3, index * 3 + 3);
const color = colors?.slice(index * colorSize, index * colorSize + colorSize);
const uv = uvs.slice(index * 2, index * 2 + 2);
const normal = normals.slice(index * 3, index * 3 + 3);
const [x, y, z] = vertex;
const [r, g, b] = color || [1, 0, 1];
const [u, v] = uv;
this.#vertexArray.push(+x, +y, +z, 1, r, g, b, 1, u, v, ...normal, this.#meshCounter);
}
const mesh = {
name,
id: this.#meshCounter,
instanceCount: 1,
verticesCount
}
this.#meshes.push(mesh);
++this.#meshCounter;
return mesh;
}
/**
* For internal purposes
* ids and names of the meshes
*/
get meshes() {
return this.#meshes;
}
}
export default RenderPass;