-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[BREAKING] More general instancing (#6867)
* 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
1 parent
07668c7
commit 6ab7df0
Showing
22 changed files
with
540 additions
and
14 deletions.
There are no files selected for viewing
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
1 change: 1 addition & 0 deletions
1
.../graphics/hardware-instancing.example.mjs → ...les/graphics/instancing-basic.example.mjs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
172 changes: 172 additions & 0 deletions
172
examples/src/examples/graphics/instancing-custom.example.mjs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
Oops, something went wrong.