diff --git a/examples/assets/models/NormalTangentTest.glb b/examples/assets/models/NormalTangentTest.glb new file mode 100644 index 00000000000..2aa17fd8f75 Binary files /dev/null and b/examples/assets/models/NormalTangentTest.glb differ diff --git a/examples/assets/models/NormalTangentTest.txt b/examples/assets/models/NormalTangentTest.txt new file mode 100644 index 00000000000..1727382ec56 --- /dev/null +++ b/examples/assets/models/NormalTangentTest.txt @@ -0,0 +1,8 @@ +Model Information: +* title: MorphStressTest +* source: https://github.com/KhronosGroup/glTF-Sample-Models/blob/main/2.0/NormalTangentTest/README.md +* author: Ed Mackey + +Model License: +* license type: CC-BY-4.0 (http://creativecommons.org/licenses/by/4.0/) +* requirements: Author must be credited. Commercial use is allowed. diff --git a/examples/src/examples/graphics/normals-and-tangents/config.mjs b/examples/src/examples/graphics/normals-and-tangents/config.mjs new file mode 100644 index 00000000000..234308d6477 --- /dev/null +++ b/examples/src/examples/graphics/normals-and-tangents/config.mjs @@ -0,0 +1,7 @@ +/** + * @type {import('../../../../types.mjs').ExampleConfig} + */ +export default { + HIDDEN: true, + WEBGPU_ENABLED: true +}; diff --git a/examples/src/examples/graphics/normals-and-tangents/example.mjs b/examples/src/examples/graphics/normals-and-tangents/example.mjs new file mode 100644 index 00000000000..ac979fba247 --- /dev/null +++ b/examples/src/examples/graphics/normals-and-tangents/example.mjs @@ -0,0 +1,111 @@ +import * as pc from 'playcanvas'; +import { deviceType, rootPath } from '@examples/utils'; + +const canvas = document.getElementById('application-canvas'); +if (!(canvas instanceof HTMLCanvasElement)) { + throw new Error('No canvas found'); +} + +const assets = { + orbitCamera: 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/helipad-env-atlas.png' }, + { type: pc.TEXTURETYPE_RGBP, mipmaps: false } + ), + model: new pc.Asset('model', 'container', { url: rootPath + '/static/assets/models/NormalTangentTest.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); +const createOptions = new pc.AppOptions(); +createOptions.graphicsDevice = device; +createOptions.mouse = new pc.Mouse(document.body); +createOptions.touch = new pc.TouchDevice(document.body); +createOptions.keyboard = new pc.Keyboard(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(); + + // Setup skydome + app.scene.envAtlas = assets.helipad.resource; + app.scene.toneMapping = pc.TONEMAP_ACES; + app.scene.skyboxRotation = new pc.Quat().setFromEulerAngles(0, 70, 0); + app.scene.skyboxIntensity = 1.5; + + const leftEntity = assets.model.resource.instantiateRenderEntity(); + leftEntity.setLocalEulerAngles(0, 90, 0); + leftEntity.setPosition(0, 0, 1); + leftEntity.setLocalScale(0.8, 0.8, 0.8); + app.root.addChild(leftEntity); + + const rightEntity = assets.model.resource.instantiateRenderEntity(); + rightEntity.setLocalEulerAngles(0, 90, 0); + rightEntity.setPosition(0, 0, -1); + rightEntity.setLocalScale(-0.8, -0.8, -0.8); + app.root.addChild(rightEntity); + + // Create a camera with an orbit camera script + const camera = new pc.Entity(); + camera.addComponent("camera"); + camera.addComponent("script"); + camera.script.create("orbitCamera", { + attributes: { + inertiaFactor: 0 // Override default of 0 (no inertia) + } + }); + camera.script.create("orbitCameraInputMouse"); + camera.script.create("orbitCameraInputTouch"); + app.root.addChild(camera); + camera.script.orbitCamera.pitch = 0; + camera.script.orbitCamera.yaw = 90; + camera.script.orbitCamera.distance = 4; + + const directionalLight = new pc.Entity(); + directionalLight.addComponent("light", { + type: "directional", + color: pc.Color.WHITE, + castShadows: true, + intensity: 1, + shadowBias: 0.2, + normalOffsetBias: 0.05, + shadowResolution: 2048 + }); + directionalLight.setEulerAngles(45, 180, 0); + app.root.addChild(directionalLight); +}); + +export { app }; diff --git a/src/scene/shader-lib/chunks/chunks.js b/src/scene/shader-lib/chunks/chunks.js index 95f4929e92e..e29204db240 100644 --- a/src/scene/shader-lib/chunks/chunks.js +++ b/src/scene/shader-lib/chunks/chunks.js @@ -196,6 +196,7 @@ import tonemappingNonePS from './common/frag/tonemappingNone.js'; import transformVS from './common/vert/transform.js'; import transformDeclVS from './common/vert/transformDecl.js'; import transmissionPS from './standard/frag/transmission.js'; +import twoSidedLightingPS from './lit/frag/twoSidedLighting.js'; import uv0VS from './lit/vert/uv0.js'; import uv1VS from './lit/vert/uv1.js'; import viewDirPS from './lit/frag/viewDir.js'; @@ -408,6 +409,7 @@ const shaderChunks = { transformVS, transformDeclVS, transmissionPS, + twoSidedLightingPS, uv0VS, uv1VS, viewDirPS, diff --git a/src/scene/shader-lib/chunks/lit/frag/twoSidedLighting.js b/src/scene/shader-lib/chunks/lit/frag/twoSidedLighting.js new file mode 100644 index 00000000000..15562ce8bcd --- /dev/null +++ b/src/scene/shader-lib/chunks/lit/frag/twoSidedLighting.js @@ -0,0 +1,6 @@ +export default /* glsl */` +uniform float twoSidedLightingNegScaleFactor; +void handleTwoSidedLighting() { + dTBN[2] *= gl_FrontFacing ? twoSidedLightingNegScaleFactor : -twoSidedLightingNegScaleFactor; +} +`; diff --git a/src/scene/shader-lib/programs/lit-shader.js b/src/scene/shader-lib/programs/lit-shader.js index 4b11e61039c..f8d8628af6a 100644 --- a/src/scene/shader-lib/programs/lit-shader.js +++ b/src/scene/shader-lib/programs/lit-shader.js @@ -748,6 +748,9 @@ class LitShader { func.append(chunks.TBNObjectSpacePS); } } + if (options.twoSidedLighting) { + func.append(chunks.twoSidedLightingPS); + } } // FIXME: only add these when needed @@ -990,34 +993,24 @@ class LitShader { func.append(chunks.clusteredLightPS); } - if (options.twoSidedLighting) { - decl.append("uniform float twoSidedLightingNegScaleFactor;"); - } - // FRAGMENT SHADER BODY code.append(this._fsGetStartCode(code, device, chunks, options)); if (this.needsNormal) { - if (options.twoSidedLighting) { - code.append(" dVertexNormalW = normalize(gl_FrontFacing ? vNormalW * twoSidedLightingNegScaleFactor : -vNormalW * twoSidedLightingNegScaleFactor);"); - } else { - code.append(" dVertexNormalW = normalize(vNormalW);"); - } + code.append(" dVertexNormalW = normalize(vNormalW);"); if ((options.useHeights || options.useNormals) && options.hasTangents) { - if (options.twoSidedLighting) { - code.append(" dTangentW = gl_FrontFacing ? vTangentW * twoSidedLightingNegScaleFactor : -vTangentW * twoSidedLightingNegScaleFactor;"); - code.append(" dBinormalW = gl_FrontFacing ? vBinormalW * twoSidedLightingNegScaleFactor : -vBinormalW * twoSidedLightingNegScaleFactor;"); - } else { - code.append(" dTangentW = vTangentW;"); - code.append(" dBinormalW = vBinormalW;"); - } + code.append(" dTangentW = vTangentW;"); + code.append(" dBinormalW = vBinormalW;"); } code.append(" getViewDir();"); if (hasTBN) { code.append(" getTBN(dTangentW, dBinormalW, dVertexNormalW);"); + if (options.twoSidedLighting) { + code.append(" handleTwoSidedLighting();"); + } } }