Skip to content

Commit

Permalink
[BREAKING] More general instancing (#6867)
Browse files Browse the repository at this point in the history
* Material can specify defines for the shader

* Hardware Instancing improvements

* examples

* screenshots

* comment

* comment

---------

Co-authored-by: Martin Valigursky <mvaligursky@snapchat.com>
  • Loading branch information
mvaligursky and Martin Valigursky authored Aug 2, 2024
1 parent 07668c7 commit 6ab7df0
Show file tree
Hide file tree
Showing 22 changed files with 540 additions and 14 deletions.
Binary file added examples/assets/models/low-poly-tree.glb
Binary file not shown.
4 changes: 4 additions & 0 deletions examples/assets/models/low-poly-tree.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
The low-poly-tree model has been obtained from this address:
https://sketchfab.com/3d-models/low-poly-tree-with-twisting-branches-4e2589134f2442bcbdab51c1f306cd58
It's distributed under CC license:
https://creativecommons.org/licenses/by/4.0/
149 changes: 149 additions & 0 deletions examples/assets/scripts/misc/gooch-material.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import {
Vec3,
ShaderMaterial,
SEMANTIC_POSITION,
SEMANTIC_NORMAL,
SEMANTIC_ATTR12,
SEMANTIC_ATTR13,
SEMANTIC_TEXCOORD0
} from 'playcanvas';

const createGoochMaterial = (texture, color) => {

// create a new material with a custom shader
const material = new ShaderMaterial({
uniqueName: 'GoochShader',
vertexCode: /* glsl */ `
// include code transform shader functionality provided by the engine. It automatically
// declares vertex_position attribute, and handles skinning and morphing if necessary.
// It also adds uniforms: matrix_viewProjection, matrix_model, matrix_normal.
// Functions added: getModelMatrix, getLocalPosition
#include "transformCore"
// include code for normal shader functionality provided by the engine. It automatically
// declares vertex_normal attribute, and handles skinning and morphing if necessary.
// Functions added: getNormalMatrix, getLocalNormal
#include "normalCore"
// add additional attributes we need
attribute vec2 aUv0;
// engine supplied uniforms
uniform vec3 view_position;
// out custom uniforms
uniform vec3 uLightDir;
uniform float uMetalness;
// variables we pass to the fragment shader
varying vec2 uv0;
varying float brightness;
// use instancing if required
#if INSTANCING
// add instancing attributes we need for our case - here we have position and scale
attribute vec3 aInstPosition;
attribute float aInstScale;
// instancing needs to provide a model matrix, the rest is handled by the engine when using transformCore
mat4 getModelMatrix() {
return mat4(
vec4(aInstScale, 0.0, 0.0, 0.0),
vec4(0.0, aInstScale, 0.0, 0.0),
vec4(0.0, 0.0, aInstScale, 0.0),
vec4(aInstPosition, 1.0)
);
}
#endif
void main(void)
{
// use functionality from transformCore to get a world position, which includes skinning, morphing or instancing as needed
mat4 modelMatrix = getModelMatrix();
vec3 localPos = getLocalPosition(vertex_position.xyz);
vec4 worldPos = modelMatrix * vec4(localPos, 1.0);
// use functionality from normalCore to get the world normal, which includes skinning, morphing or instancing as needed
mat3 normalMatrix = getNormalMatrix(modelMatrix);
vec3 localNormal = getLocalNormal(vertex_normal);
vec3 worldNormal = normalize(normalMatrix * localNormal);
// wrap-around diffuse lighting
brightness = (dot(worldNormal, uLightDir) + 1.0) * 0.5;
// Pass the texture coordinates
uv0 = aUv0;
// Transform the geometry
gl_Position = matrix_viewProjection * worldPos;
}
`,
fragmentCode: /* glsl */ `
varying float brightness;
varying vec2 uv0;
uniform vec3 uColor;
#if DIFFUSE_MAP
uniform sampler2D uDiffuseMap;
#endif
// Good shading constants - could be exposed as uniforms instead
float diffuseCool = 0.4;
float diffuseWarm = 0.4;
vec3 cool = vec3(0, 0, 0.6);
vec3 warm = vec3(0.6, 0, 0);
void main(void)
{
float alpha = 1.0f;
vec3 colorLinear = uColor;
// shader variant using a diffuse texture
#if DIFFUSE_MAP
vec4 diffuseLinear = texture2D(uDiffuseMap, uv0);
colorLinear *= diffuseLinear.rgb;
alpha = diffuseLinear.a;
#endif
// simple Gooch shading that highlights structural and contextual data
vec3 kCool = min(cool + diffuseCool * colorLinear, 1.0);
vec3 kWarm = min(warm + diffuseWarm * colorLinear, 1.0);
colorLinear = mix(kCool, kWarm, brightness);
// handle standard color processing - the called functions are automatically attached to the
// shader based on the current fog / tone-mapping / gamma settings
vec3 fogged = addFog(colorLinear);
vec3 toneMapped = toneMap(fogged);
gl_FragColor.rgb = gammaCorrectOutput(toneMapped);
gl_FragColor.a = alpha;
}
`,
attributes: {
vertex_position: SEMANTIC_POSITION,
vertex_normal: SEMANTIC_NORMAL,
aUv0: SEMANTIC_TEXCOORD0,

// instancing attributes
aInstPosition: SEMANTIC_ATTR12,
aInstScale: SEMANTIC_ATTR13
}
});

// default parameters
material.setParameter('uColor', color ?? [1, 1, 1]);

if (texture) {
material.setParameter('uDiffuseMap', texture);
material.setDefine('DIFFUSE_MAP', true);
}

const lightDir = new Vec3(0.5, -0.5, 0.5).normalize();
material.setParameter('uLightDir', [-lightDir.x, -lightDir.y, -lightDir.z]);

return material;
};

export { createGoochMaterial };
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @config DESCRIPTION This example shows how to use the instancing feature of a StandardMaterial to render multiple copies of a mesh.
import * as pc from 'playcanvas';
import { deviceType, rootPath } from 'examples/utils';

Expand Down
172 changes: 172 additions & 0 deletions examples/src/examples/graphics/instancing-custom.example.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// @config DESCRIPTION This example demonstrates how to customize the shader handling the instancing of a StandardMaterial.
import * as pc from 'playcanvas';
import { deviceType, rootPath } from 'examples/utils';

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

const assets = {
helipad: new pc.Asset(
'helipad-env-atlas',
'texture',
{ url: rootPath + '/static/assets/cubemaps/table-mountain-env-atlas.png' },
{ type: pc.TEXTURETYPE_RGBP, mipmaps: false }
)
};

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.componentSystems = [pc.RenderComponentSystem, pc.CameraComponentSystem];
createOptions.resourceHandlers = [pc.TextureHandler];

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();

// setup skydome
app.scene.skyboxMip = 2;
app.scene.exposure = 0.8;
app.scene.envAtlas = assets.helipad.resource;

// set up some general scene rendering properties
app.scene.rendering.toneMapping = pc.TONEMAP_ACES;
app.scene.ambientLight = new pc.Color(0.1, 0.1, 0.1);

// Create an Entity with a camera component
const camera = new pc.Entity();
camera.addComponent('camera', {});
app.root.addChild(camera);

// create static vertex buffer containing the instancing data
const vbFormat = new pc.VertexFormat(app.graphicsDevice, [
{ semantic: pc.SEMANTIC_ATTR12, components: 3, type: pc.TYPE_FLOAT32 }, // position
{ semantic: pc.SEMANTIC_ATTR13, components: 1, type: pc.TYPE_FLOAT32 } // scale
]);

// store data for individual instances into array, 4 floats each
const instanceCount = 3000;
const data = new Float32Array(instanceCount * 4);

const range = 10;
for (let i = 0; i < instanceCount; i++) {
const offset = i * 4;
data[offset + 0] = Math.random() * range - range * 0.5; // x
data[offset + 1] = Math.random() * range - range * 0.5; // y
data[offset + 2] = Math.random() * range - range * 0.5; // z
data[offset + 3] = 0.1 + Math.random() * 0.1; // scale
}

const vertexBuffer = new pc.VertexBuffer(app.graphicsDevice, vbFormat, instanceCount, {
data: data
});

// create standard material - this will be used for instanced, but also non-instanced rendering
const material = new pc.StandardMaterial();
material.gloss = 0.5;
material.metalness = 1;
material.diffuse = new pc.Color(0.7, 0.5, 0.7);
material.useMetalness = true;

// set up additional attributes needed for instancing
material.setAttribute('aInstPosition', pc.SEMANTIC_ATTR12);
material.setAttribute('aInstScale', pc.SEMANTIC_ATTR13);

// and a custom instancing shader chunk, which will be used in case the mesh instance has instancing enabled
material.chunks.transformInstancingVS = `
// instancing attributes
attribute vec3 aInstPosition;
attribute float aInstScale;
// uniforms
uniform float uTime;
uniform vec3 uCenter;
// all instancing chunk needs to do is to implement getModelMatrix function, which returns a world matrix for the instance
mat4 getModelMatrix() {
// we have world position in aInstPosition, but modify it based on distance from uCenter for some displacement effect
vec3 direction = aInstPosition - uCenter;
float distanceFromCenter = length(direction);
float displacementIntensity = exp(-distanceFromCenter * 0.2) ; //* (1.9 + abs(sin(uTime * 1.5)));
vec3 worldPos = aInstPosition - direction * displacementIntensity;
// create matrix based on the modified poition, and scale
return mat4(
vec4(aInstScale, 0.0, 0.0, 0.0),
vec4(0.0, aInstScale, 0.0, 0.0),
vec4(0.0, 0.0, aInstScale, 0.0),
vec4(worldPos, 1.0)
);
}
`;

material.update();

// Create an Entity with a sphere and the instancing material
const instancingEntity = new pc.Entity('InstancingEntity');
instancingEntity.addComponent('render', {
material: material,
type: 'sphere'
});
app.root.addChild(instancingEntity);

// initialize instancing using the vertex buffer on meshInstance of the created mesh instance
const meshInst = instancingEntity.render.meshInstances[0];
meshInst.setInstancing(vertexBuffer);

// add a non-instanced sphere, using the same material. A non-instanced version of the shader
// is automatically created by the engine
const sphere = new pc.Entity('sphere');
sphere.addComponent('render', {
material: material,
type: 'sphere'
});
sphere.setLocalScale(2, 2, 2);
app.root.addChild(sphere);

// An update function executes once per frame
let time = 0;
const spherePos = new pc.Vec3();
app.on('update', function (dt) {
time += dt;

// move the large sphere up and down
spherePos.set(0, Math.sin(time) * 2, 0);
sphere.setLocalPosition(spherePos);

// update uniforms of the instancing material
material.setParameter('uTime', time);
material.setParameter('uCenter', [spherePos.x, spherePos.y, spherePos.z]);

// orbit camera around
camera.setLocalPosition(8 * Math.sin(time * 0.1), 0, 8 * Math.cos(time * 0.1));
camera.lookAt(pc.Vec3.ZERO);
});
});

export { app };
Loading

0 comments on commit 6ab7df0

Please sign in to comment.