diff --git a/examples/assets/models/low-poly-tree.glb b/examples/assets/models/low-poly-tree.glb new file mode 100644 index 00000000000..1a98b4eac11 Binary files /dev/null and b/examples/assets/models/low-poly-tree.glb differ diff --git a/examples/assets/models/low-poly-tree.txt b/examples/assets/models/low-poly-tree.txt new file mode 100644 index 00000000000..07cc2c66afe --- /dev/null +++ b/examples/assets/models/low-poly-tree.txt @@ -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/ diff --git a/examples/assets/scripts/misc/gooch-material.mjs b/examples/assets/scripts/misc/gooch-material.mjs new file mode 100644 index 00000000000..9211123b6ea --- /dev/null +++ b/examples/assets/scripts/misc/gooch-material.mjs @@ -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 }; diff --git a/examples/src/examples/graphics/hardware-instancing.example.mjs b/examples/src/examples/graphics/instancing-basic.example.mjs similarity index 97% rename from examples/src/examples/graphics/hardware-instancing.example.mjs rename to examples/src/examples/graphics/instancing-basic.example.mjs index 67e1e502871..c691ad214e1 100644 --- a/examples/src/examples/graphics/hardware-instancing.example.mjs +++ b/examples/src/examples/graphics/instancing-basic.example.mjs @@ -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'; diff --git a/examples/src/examples/graphics/instancing-custom.example.mjs b/examples/src/examples/graphics/instancing-custom.example.mjs new file mode 100644 index 00000000000..f55268838f1 --- /dev/null +++ b/examples/src/examples/graphics/instancing-custom.example.mjs @@ -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 }; diff --git a/examples/src/examples/graphics/instancing-gooch.example.mjs b/examples/src/examples/graphics/instancing-gooch.example.mjs new file mode 100644 index 00000000000..b19d33f5af1 --- /dev/null +++ b/examples/src/examples/graphics/instancing-gooch.example.mjs @@ -0,0 +1,183 @@ +// @config DESCRIPTION This example demonstrates how a custom shader can be used to render instanced geometry, but also skinned, morphed and static geometry. A simple Gooch shading shader is used. +import * as pc from 'playcanvas'; +import { deviceType, rootPath, fileImport } from 'examples/utils'; + +// import the createGoochMaterial function from the gooch-material.mjs file +const { createGoochMaterial } = await fileImport(rootPath + '/static/assets/scripts/misc/gooch-material.mjs'); + +const canvas = /** @type {HTMLCanvasElement} */ (document.getElementById('application-canvas')); +window.focus(); + +const assets = { + tree: new pc.Asset('cube', 'container', { url: rootPath + '/static/assets/models/low-poly-tree.glb' }), + + bitmoji: new pc.Asset('model', 'container', { url: rootPath + '/static/assets/models/bitmoji.glb' }), + danceAnim: new pc.Asset('walkAnim', 'container', { url: rootPath + '/static/assets/animations/bitmoji/win-dance.glb' }), + + helipad: new pc.Asset( + 'helipad-env-atlas', + 'texture', + { url: rootPath + '/static/assets/cubemaps/helipad-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, + pc.AnimComponentSystem +]; +createOptions.resourceHandlers = [ + pc.TextureHandler, + pc.ContainerHandler, + pc.AnimClipHandler +]; + +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(); + + // a helper function to apply a material to all mesh instances of an entity + const applyMaterial = (entity, materials) => { + entity.findComponents('render').forEach((render) => { + render.meshInstances.forEach((meshInstance) => { + const goochMaterial = createGoochMaterial(meshInstance.material.diffuseMap); + meshInstance.material = goochMaterial; + materials.push(goochMaterial); + }); + }); + }; + + // setup skydome + app.scene.skyboxMip = 2; + app.scene.envAtlas = assets.helipad.resource; + + // set up some general scene rendering properties + app.scene.rendering.toneMapping = pc.TONEMAP_ACES; + + // Create an Entity with a camera component + const camera = new pc.Entity(); + camera.addComponent('camera', {}); + app.root.addChild(camera); + + // number of instanced trees to render + const instanceCount = 500; + + // 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 data = new Float32Array(instanceCount * 4); + + for (let i = 0; i < instanceCount; i++) { + + // random points in the ring + const radius0 = 2; + const radius1 = 10; + const angle = Math.random() * 2 * Math.PI; + const radius = Math.sqrt(Math.random() * (radius1 ** 2 - radius0 ** 2) + radius0 ** 2); + const x = radius * Math.cos(angle); + const z = radius * Math.sin(angle); + + const offset = i * 4; + data[offset + 0] = x; // x + data[offset + 1] = 1; // y + data[offset + 2] = z; // z + data[offset + 3] = 0.03 + Math.random() * 0.25; // scale + } + + const vertexBuffer = new pc.VertexBuffer(app.graphicsDevice, vbFormat, instanceCount, { + data: data + }); + + // create a forest by intantiating a tree model and setting it up for instancing + const forest = assets.tree.resource.instantiateRenderEntity(); + app.root.addChild(forest); + + // find the mesh instance we want to instantiate, and swap its material for the custom gooch material, + // while preserving its texture + const meshInstance = forest.findComponent('render').meshInstances[0]; + const material = createGoochMaterial(meshInstance.material.diffuseMap); + meshInstance.material = material; + + // initialize instancing using the vertex buffer on meshInstance + meshInstance.setInstancing(vertexBuffer); + + // Create an Entity for the ground - this is a static geometry. Create a new instance of the gooch material, + // without a texture. + const ground = new pc.Entity('Ground'); + const groundMaterial = createGoochMaterial(null, [0.13, 0.55, 0.13]); // no texture + ground.addComponent('render', { + type: 'box', + material: groundMaterial + }); + ground.setLocalScale(30, 1, 30); + ground.setLocalPosition(0, -0.5, 0); + app.root.addChild(ground); + + // store al materials to allow for easy modification + const materials = [material, groundMaterial]; + + // animated / morphed bitmoji model + const bitmojiEntity = assets.bitmoji.resource.instantiateRenderEntity({ castShadows: false }); + bitmojiEntity.setLocalScale(2.5, 2.5, 2.5); + bitmojiEntity.setLocalPosition(0, 0, 0); + app.root.addChild(bitmojiEntity); + applyMaterial(bitmojiEntity, materials); + + // play the animation + bitmojiEntity.addComponent('anim', { activate: true }); + const walkTrack = assets.danceAnim.resource.animations[0].resource; + bitmojiEntity.anim.assignAnimation('Walk', walkTrack, undefined, 0.62); + + + // Set an update function on the app's update event + let time = 0; + app.on('update', function (dt) { + time += dt; + + // generate a light direction that rotates around the scene, and set it on the materials + const lightDir = new pc.Vec3(Math.sin(time), -0.5, Math.cos(time)).normalize(); + const lightDirArray = [-lightDir.x, -lightDir.y, -lightDir.z]; + + materials.forEach((mat) => { + mat.setParameter('uLightDir', lightDirArray); + mat.update(); + }); + + // orbit the camera + camera.setLocalPosition(8 * Math.sin(time * 0.01), 3, 8 * Math.cos(time * 0.01)); + camera.lookAt(new pc.Vec3(0, 1, 0)); + }); +}); + +export { app }; diff --git a/examples/thumbnails/graphics_instancing-basic_large.webp b/examples/thumbnails/graphics_instancing-basic_large.webp new file mode 100644 index 00000000000..c21ec7ee2dd Binary files /dev/null and b/examples/thumbnails/graphics_instancing-basic_large.webp differ diff --git a/examples/thumbnails/graphics_instancing-basic_small.webp b/examples/thumbnails/graphics_instancing-basic_small.webp new file mode 100644 index 00000000000..adcc1120fc2 Binary files /dev/null and b/examples/thumbnails/graphics_instancing-basic_small.webp differ diff --git a/examples/thumbnails/graphics_instancing-custom_large.webp b/examples/thumbnails/graphics_instancing-custom_large.webp new file mode 100644 index 00000000000..cfabb30a3c2 Binary files /dev/null and b/examples/thumbnails/graphics_instancing-custom_large.webp differ diff --git a/examples/thumbnails/graphics_instancing-custom_small.webp b/examples/thumbnails/graphics_instancing-custom_small.webp new file mode 100644 index 00000000000..08a9738931c Binary files /dev/null and b/examples/thumbnails/graphics_instancing-custom_small.webp differ diff --git a/examples/thumbnails/graphics_instancing-gooch_large.webp b/examples/thumbnails/graphics_instancing-gooch_large.webp new file mode 100644 index 00000000000..e0f926d6245 Binary files /dev/null and b/examples/thumbnails/graphics_instancing-gooch_large.webp differ diff --git a/examples/thumbnails/graphics_instancing-gooch_small.webp b/examples/thumbnails/graphics_instancing-gooch_small.webp new file mode 100644 index 00000000000..0bb6b04f29a Binary files /dev/null and b/examples/thumbnails/graphics_instancing-gooch_small.webp differ diff --git a/examples/thumbnails/graphics_hardware-instancing_large.webp b/examples/thumbnails/graphics_instancing-simple_large.webp similarity index 100% rename from examples/thumbnails/graphics_hardware-instancing_large.webp rename to examples/thumbnails/graphics_instancing-simple_large.webp diff --git a/examples/thumbnails/graphics_hardware-instancing_small.webp b/examples/thumbnails/graphics_instancing-simple_small.webp similarity index 100% rename from examples/thumbnails/graphics_hardware-instancing_small.webp rename to examples/thumbnails/graphics_instancing-simple_small.webp diff --git a/src/core/preprocessor.js b/src/core/preprocessor.js index 273f1320ed2..7e1d9768c60 100644 --- a/src/core/preprocessor.js +++ b/src/core/preprocessor.js @@ -329,7 +329,7 @@ class Preprocessor { // cut out the include line and replace it with the included string const includeSource = includes?.get(identifier); - if (includeSource) { + if (includeSource !== undefined) { source = source.substring(0, include.index - 1) + includeSource + source.substring(INCLUDE.lastIndex); // process the just included test diff --git a/src/scene/materials/standard-material.js b/src/scene/materials/standard-material.js index d99379eae16..9e8404714fb 100644 --- a/src/scene/materials/standard-material.js +++ b/src/scene/materials/standard-material.js @@ -630,6 +630,9 @@ class StandardMaterial extends Material { this._chunks[p] = source._chunks[p]; } + // clone user attributes + this.userAttributes = new Map(source.userAttributes); + return this; } diff --git a/src/scene/shader-lib/chunks/chunks.js b/src/scene/shader-lib/chunks/chunks.js index ad5fb463ad4..00a81c575bc 100644 --- a/src/scene/shader-lib/chunks/chunks.js +++ b/src/scene/shader-lib/chunks/chunks.js @@ -184,6 +184,7 @@ import tonemappingNeutralPS from './common/frag/tonemappingNeutral.js'; import tonemappingNonePS from './common/frag/tonemappingNone.js'; import transformVS from './common/vert/transform.js'; import transformCoreVS from './common/vert/transformCore.js'; +import transformInstancingVS from './common/vert/transformInstancing.js'; import transmissionPS from './standard/frag/transmission.js'; import twoSidedLightingPS from './lit/frag/twoSidedLighting.js'; import uv0VS from './lit/vert/uv0.js'; @@ -386,6 +387,7 @@ const shaderChunks = { tonemappingNonePS, transformVS, transformCoreVS, + transformInstancingVS, transmissionPS, twoSidedLightingPS, uv0VS, diff --git a/src/scene/shader-lib/chunks/common/vert/normalCore.js b/src/scene/shader-lib/chunks/common/vert/normalCore.js index 6e27f223ed4..cbbcd1c82bf 100644 --- a/src/scene/shader-lib/chunks/common/vert/normalCore.js +++ b/src/scene/shader-lib/chunks/common/vert/normalCore.js @@ -37,7 +37,7 @@ vec3 getLocalNormal(vec3 vertexNormal) { } #elif defined(INSTANCING) mat3 getNormalMatrix(mat4 modelMatrix) { - return mat3(instance_line1.xyz, instance_line2.xyz, instance_line3.xyz); + return mat3(modelMatrix[0].xyz, modelMatrix[1].xyz, modelMatrix[2].xyz); } #else mat3 getNormalMatrix(mat4 modelMatrix) { diff --git a/src/scene/shader-lib/chunks/common/vert/transformCore.js b/src/scene/shader-lib/chunks/common/vert/transformCore.js index 6c8382fd120..b0d2bdaa0d7 100644 --- a/src/scene/shader-lib/chunks/common/vert/transformCore.js +++ b/src/scene/shader-lib/chunks/common/vert/transformCore.js @@ -50,14 +50,7 @@ uniform mat3 matrix_normal; #elif defined(INSTANCING) - attribute vec4 instance_line1; - attribute vec4 instance_line2; - attribute vec4 instance_line3; - attribute vec4 instance_line4; - - mat4 getModelMatrix() { - return mat4(instance_line1, instance_line2, instance_line3, instance_line4); - } + #include "transformInstancing" #else diff --git a/src/scene/shader-lib/chunks/common/vert/transformInstancing.js b/src/scene/shader-lib/chunks/common/vert/transformInstancing.js new file mode 100644 index 00000000000..a425bb01157 --- /dev/null +++ b/src/scene/shader-lib/chunks/common/vert/transformInstancing.js @@ -0,0 +1,11 @@ +export default /* glsl */` + +attribute vec4 instance_line1; +attribute vec4 instance_line2; +attribute vec4 instance_line3; +attribute vec4 instance_line4; + +mat4 getModelMatrix() { + return mat4(instance_line1, instance_line2, instance_line3, instance_line4); +} +`; diff --git a/src/scene/shader-lib/programs/lit-shader.js b/src/scene/shader-lib/programs/lit-shader.js index 0706690f455..dadb1a3be75 100644 --- a/src/scene/shader-lib/programs/lit-shader.js +++ b/src/scene/shader-lib/programs/lit-shader.js @@ -223,10 +223,15 @@ class LitShader { } if (this.options.useInstancing) { - this.attributes.instance_line1 = SEMANTIC_ATTR12; - this.attributes.instance_line2 = SEMANTIC_ATTR13; - this.attributes.instance_line3 = SEMANTIC_ATTR14; - this.attributes.instance_line4 = SEMANTIC_ATTR15; + + // only attach these if the default instancing chunk is used, otherwise it is expected + // for the user to provide required attributes using material.setAttribute + if (this.chunks.transformInstancingVS === shaderChunks.transformInstancingVS) { + this.attributes.instance_line1 = SEMANTIC_ATTR12; + this.attributes.instance_line2 = SEMANTIC_ATTR13; + this.attributes.instance_line3 = SEMANTIC_ATTR14; + this.attributes.instance_line4 = SEMANTIC_ATTR15; + } } code += chunks.transformVS; @@ -1513,6 +1518,7 @@ class LitShader { const vIncludes = new Map(); vIncludes.set('transformCore', this.chunks.transformCoreVS); + vIncludes.set('transformInstancing', this.chunks.transformInstancingVS); vIncludes.set('skinTexVS', this.chunks.skinTexVS); vIncludes.set('skinBatchTexVS', this.chunks.skinBatchTexVS); diff --git a/src/scene/shader-lib/programs/shader-generator-shader.js b/src/scene/shader-lib/programs/shader-generator-shader.js index e62ec496f0b..ef82294ecf1 100644 --- a/src/scene/shader-lib/programs/shader-generator-shader.js +++ b/src/scene/shader-lib/programs/shader-generator-shader.js @@ -78,11 +78,13 @@ class ShaderGeneratorShader extends ShaderGenerator { includes.set('shaderPassDefines', shaderPassInfo.shaderDefines); includes.set('userCode', desc.vertexCode); includes.set('transformCore', shaderChunks.transformCoreVS); + includes.set('transformInstancing', ''); // no default instancing, needs to be implemented in the user shader includes.set('normalCore', shaderChunks.normalCoreVS); includes.set('skinCode', shaderChunks.skinTexVS); includes.set('skinTexVS', shaderChunks.skinTexVS); if (options.skin) defines.set('SKIN', true); + if (options.useInstancing) defines.set('INSTANCING', true); if (options.useMorphPosition || options.useMorphNormal) { defines.set('MORPHING', true); if (options.useMorphTextureBasedInt) defines.set('MORPHING_INT', true);