Skip to content

Commit

Permalink
Support for EXT_mesh_gpu_instancing extension (playcanvas#6869)
Browse files Browse the repository at this point in the history
Co-authored-by: Martin Valigursky <mvaligursky@snapchat.com>
  • Loading branch information
2 people authored and MAG-AdrianMeredith committed Aug 5, 2024
1 parent 0a14ba1 commit 8b90a2b
Show file tree
Hide file tree
Showing 6 changed files with 209 additions and 6 deletions.
96 changes: 96 additions & 0 deletions examples/src/examples/graphics/instancing-glb.example.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// @config DESCRIPTION This example demonstrates the functionality of the EXT_mesh_gpu_instancing extension, which enables GPU instancing of meshes stored in a glTF file.
import * as pc from 'playcanvas';
import { deviceType, rootPath } from 'examples/utils';

const canvas = /** @type {HTMLCanvasElement} */ (document.getElementById('application-canvas'));
window.focus();

const assets = {
script: new pc.Asset('script', 'script', { url: rootPath + '/static/scripts/camera/orbit-camera.js' }),
helipad: new pc.Asset(
'helipad-env-atlas',
'texture',
{ url: rootPath + '/static/assets/cubemaps/table-mountain-env-atlas.png' },
{ type: pc.TEXTURETYPE_RGBP, mipmaps: false }
),
glb: new pc.Asset('glb', 'container', { url: rootPath + '/static/assets/models/simple-instancing.glb' })
};

const gfxOptions = {
deviceTypes: [deviceType],
glslangUrl: rootPath + '/static/lib/glslang/glslang.js',
twgslUrl: rootPath + '/static/lib/twgsl/twgsl.js'
};

const device = await pc.createGraphicsDevice(canvas, gfxOptions);
device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);

const createOptions = new pc.AppOptions();
createOptions.graphicsDevice = device;
createOptions.mouse = new pc.Mouse(document.body);
createOptions.touch = new pc.TouchDevice(document.body);

createOptions.componentSystems = [
pc.RenderComponentSystem,
pc.CameraComponentSystem,
pc.LightComponentSystem,
pc.ScriptComponentSystem
];
createOptions.resourceHandlers = [
pc.TextureHandler,
pc.ContainerHandler,
pc.ScriptHandler
];

const app = new pc.AppBase(canvas);
app.init(createOptions);

// Set the canvas to fill the window and automatically change resolution to be the same as the canvas size
app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);
app.setCanvasResolution(pc.RESOLUTION_AUTO);

// Ensure canvas is resized when window changes size
const resize = () => app.resizeCanvas();
window.addEventListener('resize', resize);
app.on('destroy', () => {
window.removeEventListener('resize', resize);
});

const assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets);
assetListLoader.load(() => {
app.start();

// get the instance of the cube it set up with render component and add it to scene
const entity = assets.glb.resource.instantiateRenderEntity();
app.root.addChild(entity);

// Create an Entity with a camera component
const camera = new pc.Entity();
camera.addComponent('camera', {
clearColor: new pc.Color(0.2, 0.1, 0.1),
farClip: 100
});
camera.translate(25, 15, 25);

// add orbit camera script with a mouse and a touch support
camera.addComponent('script');
camera.script.create('orbitCamera', {
attributes: {
inertiaFactor: 0.2,
focusEntity: entity,
distanceMax: 60,
frameOnStart: false
}
});
camera.script.create('orbitCameraInputMouse');
camera.script.create('orbitCameraInputTouch');

app.root.addChild(camera);

// set skybox
app.scene.envAtlas = assets.helipad.resource;
app.scene.rendering.toneMapping = pc.TONEMAP_ACES;
app.scene.skyboxMip = 1;
});

export { app };
Binary file not shown.
Binary file not shown.
21 changes: 19 additions & 2 deletions src/framework/parsers/glb-container-resource.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { SkinInstanceCache } from '../../scene/skin-instance-cache.js';

import { Entity } from '../entity.js';
import { Asset } from '../asset/asset.js';
import { VertexFormat } from '../../platform/graphics/vertex-format.js';
import { VertexBuffer } from '../../platform/graphics/vertex-buffer.js';

// Container resource returned by the GlbParser. Implements the ContainerResource interface.
class GlbContainerResource {
Expand Down Expand Up @@ -79,7 +81,7 @@ class GlbContainerResource {
const defaultMaterial = this._defaultMaterial;
const skinnedMeshInstances = [];

const createMeshInstance = function (root, entity, mesh, materials, meshDefaultMaterials, skins, gltfNode) {
const createMeshInstance = function (root, entity, mesh, materials, meshDefaultMaterials, skins, gltfNode, nodeInstancingMap) {

// clone mesh instance
const materialIndex = meshDefaultMaterials[mesh.id];
Expand All @@ -100,6 +102,21 @@ class GlbContainerResource {
});
}

// if the node is instanced, hook up instancing
const instData = nodeInstancingMap.get(gltfNode);
if (instData) {

const matrices = instData.matrices;
const vbFormat = VertexFormat.getDefaultInstancingFormat(mesh.device);
const vb = new VertexBuffer(mesh.device, vbFormat, matrices.length / 16, {
data: matrices
});
meshInstance.setInstancing(vb);

// mark the vertex buffer for destruction when the mesh instance is destroyed
meshInstance.instancingData._destroyVertexBuffer = true;
}

return meshInstance;
};

Expand Down Expand Up @@ -127,7 +144,7 @@ class GlbContainerResource {
for (let mi = 0; mi < meshGroup.length; mi++) {
const mesh = meshGroup[mi];
if (mesh) {
const cloneMi = createMeshInstance(root, entity, mesh, glb.materials, glb.meshDefaultMaterials, glb.skins, gltfNode);
const cloneMi = createMeshInstance(root, entity, mesh, glb.materials, glb.meshDefaultMaterials, glb.skins, gltfNode, glb.nodeInstancingMap);

// add it to list
if (!attachedMi) {
Expand Down
82 changes: 78 additions & 4 deletions src/framework/parsers/glb-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import { Asset } from '../asset/asset.js';
import { ABSOLUTE_URL } from '../asset/constants.js';

import { dracoDecode } from './draco-decoder.js';
import { Quat } from '../../core/math/quat.js';

// resources loaded from GLB file that the parser returns
class GlbResources {
Expand Down Expand Up @@ -78,6 +79,8 @@ class GlbResources {

cameras;

nodeInstancingMap;

destroy() {
// render needs to dec ref meshes
if (this.renders) {
Expand Down Expand Up @@ -1590,7 +1593,7 @@ const createAnimation = (gltfAnimation, animationIndex, gltfAccessors, bufferVie
const tempMat = new Mat4();
const tempVec = new Vec3();

const createNode = (gltfNode, nodeIndex) => {
const createNode = (gltfNode, nodeIndex, nodeInstancingMap) => {
const entity = new GraphNode();

if (gltfNode.hasOwnProperty('name') && gltfNode.name.length > 0) {
Expand Down Expand Up @@ -1625,6 +1628,12 @@ const createNode = (gltfNode, nodeIndex) => {
entity.setLocalScale(s[0], s[1], s[2]);
}

if (gltfNode.hasOwnProperty('extensions') && gltfNode.extensions.EXT_mesh_gpu_instancing) {
nodeInstancingMap.set(gltfNode, {
ext: gltfNode.extensions.EXT_mesh_gpu_instancing
});
}

return entity;
};

Expand Down Expand Up @@ -1791,7 +1800,69 @@ const createAnimations = (gltf, nodes, bufferViews, options) => {
});
};

const createNodes = (gltf, options) => {
const createInstancing = (device, gltf, nodeInstancingMap, bufferViews) => {

const accessors = gltf.accessors;
nodeInstancingMap.forEach((data, entity) => {
const attributes = data.ext.attributes;

let translations;
if (attributes.hasOwnProperty('TRANSLATION')) {
const accessor = accessors[attributes.TRANSLATION];
translations = getAccessorDataFloat32(accessor, bufferViews);
}

let rotations;
if (attributes.hasOwnProperty('ROTATION')) {
const accessor = accessors[attributes.ROTATION];
rotations = getAccessorDataFloat32(accessor, bufferViews);
}

let scales;
if (attributes.hasOwnProperty('SCALE')) {
const accessor = accessors[attributes.SCALE];
scales = getAccessorDataFloat32(accessor, bufferViews);
}

const instanceCount = (translations ? translations.length / 3 : 0) ||
(rotations ? rotations.length / 4 : 0) ||
(scales ? scales.length / 3 : 0);

if (instanceCount) {

const matrices = new Float32Array(instanceCount * 16);
const pos = new Vec3();
const rot = new Quat();
const scl = new Vec3(1, 1, 1);
const matrix = new Mat4();
let matrixIndex = 0;

for (let i = 0; i < instanceCount; i++) {
const i3 = i * 3;
if (translations) {
pos.set(translations[i3], translations[i3 + 1], translations[i3 + 2]);
}
if (rotations) {
const i4 = i * 4;
rot.set(rotations[i4], rotations[i4 + 1], rotations[i4 + 2], rotations[i4 + 3]);
}
if (scales) {
scl.set(scales[i3], scales[i3 + 1], scales[i3 + 2]);
}

matrix.setTRS(pos, rot, scl);

// copy matrix elements into array of floats
for (let m = 0; m < 16; m++)
matrices[matrixIndex++] = matrix.data[m];
}

data.matrices = matrices;
}
});
};

const createNodes = (gltf, options, nodeInstancingMap) => {
if (!gltf.hasOwnProperty('nodes') || gltf.nodes.length === 0) {
return [];
}
Expand All @@ -1804,7 +1875,7 @@ const createNodes = (gltf, options) => {
if (preprocess) {
preprocess(gltfNode);
}
const node = process(gltfNode, index);
const node = process(gltfNode, index, nodeInstancingMap);
if (postprocess) {
postprocess(gltfNode, node);
}
Expand Down Expand Up @@ -1972,7 +2043,8 @@ const createResources = async (device, gltf, bufferViews, textures, options) =>
Debug.warn('glTF model may have flipped UVs. Please reconvert.');
}

const nodes = createNodes(gltf, options);
const nodeInstancingMap = new Map();
const nodes = createNodes(gltf, options, nodeInstancingMap);
const scenes = createScenes(gltf, nodes);
const lights = createLights(gltf, nodes, options);
const cameras = createCameras(gltf, nodes, options);
Expand All @@ -1982,6 +2054,7 @@ const createResources = async (device, gltf, bufferViews, textures, options) =>
const bufferViewData = await Promise.all(bufferViews);
const { meshes, meshVariants, meshDefaultMaterials, promises } = createMeshes(device, gltf, bufferViewData, flipV, options);
const animations = createAnimations(gltf, nodes, bufferViewData, options);
createInstancing(device, gltf, nodeInstancingMap, bufferViewData);

// textures must have finished loading in order to create materials
const textureAssets = await Promise.all(textures);
Expand Down Expand Up @@ -2013,6 +2086,7 @@ const createResources = async (device, gltf, bufferViews, textures, options) =>
result.skins = skins;
result.lights = lights;
result.cameras = cameras;
result.nodeInstancingMap = nodeInstancingMap;

if (postprocess) {
postprocess(gltf, result);
Expand Down
16 changes: 16 additions & 0 deletions src/scene/mesh-instance.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,26 @@ class InstancingData {
/** @type {import('../platform/graphics/vertex-buffer.js').VertexBuffer|null} */
vertexBuffer = null;

/**
* True if the vertex buffer is destroyed when the mesh instance is destroyed.
*
* @type {boolean}
*/
_destroyVertexBuffer = false;

/**
* @param {number} numObjects - The number of objects instanced.
*/
constructor(numObjects) {
this.count = numObjects;
}

destroy() {
if (this._destroyVertexBuffer) {
this.vertexBuffer?.destroy();
}
this.vertexBuffer = null;
}
}

/**
Expand Down Expand Up @@ -785,6 +799,8 @@ class MeshInstance {

// make sure material clears references to this meshInstance
this.material = null;

this.instancingData?.destroy();
}

// shader uniform names for lightmaps
Expand Down

0 comments on commit 8b90a2b

Please sign in to comment.