diff --git a/examples/src/examples/graphics/index.mjs b/examples/src/examples/graphics/index.mjs index 3aa8af4c44d..21c5449b439 100644 --- a/examples/src/examples/graphics/index.mjs +++ b/examples/src/examples/graphics/index.mjs @@ -14,6 +14,7 @@ export * from "./grab-pass.mjs"; export * from "./ground-fog.mjs"; export * from "./hardware-instancing.mjs"; export * from "./hierarchy.mjs"; +export * from "./integer-textures.mjs"; export * from "./layers.mjs"; export * from "./lights-baked-a-o.mjs"; export * from "./lights-baked.mjs"; diff --git a/examples/src/examples/graphics/integer-textures.mjs b/examples/src/examples/graphics/integer-textures.mjs new file mode 100644 index 00000000000..01e92d13ae1 --- /dev/null +++ b/examples/src/examples/graphics/integer-textures.mjs @@ -0,0 +1,591 @@ +import * as pc from 'playcanvas'; + +/** + * @param {import('../../app/example.mjs').ControlOptions} options - The options. + * @returns {JSX.Element} The returned JSX Element. + */ +function controls({ observer, ReactPCUI, jsx, fragment }) { + const { BindingTwoWay, Container, Button, InfoBox, LabelGroup, Panel, SliderInput, SelectInput } = ReactPCUI; + + return fragment( + jsx(InfoBox, { + icon: 'E218', + title: 'WebGL 1.0', + text: 'Integer textures are not supported on WebGL 1.0 devices', + hidden: !(pc.app?.graphicsDevice.isWebGL1 ?? false) + }), + jsx(Panel, { headerText: 'Sand simulation' }, + jsx(LabelGroup, { text: 'Brush' }, + jsx(SelectInput, { + binding: new BindingTwoWay(), + link: { observer, path: 'options.brush' }, + type: "string", + value: 1, + options: [ + { v: 1, t: 'Sand' }, + { v: 2, t: 'Orange Sand' }, + { v: 3, t: 'Gray Sand' }, + { v: 4, t: 'Stone' } + ] + }) + ), + jsx(LabelGroup, { text: 'Brush size' }, + jsx(SliderInput, { + binding: new BindingTwoWay(), + link: { observer, path: 'options.brushSize' }, + value: 8, + min: 1, + max: 16, + precision: 0 + }) + ), + jsx(Container, { flex: true, flexGrow: 1 }, + jsx(Button, { + text: 'Reset', + onClick: () => observer.emit('reset') + }) + ) + ) + ); +} + +/** + * @typedef {{ 'sandSimulation.frag': string, 'renderOutput.frag': string }} Files + * @typedef {import('../../options.mjs').ExampleOptions} Options + * @param {Options} options - The example options. + * @returns {Promise} The example application. + */ +async function example({ canvas, data, deviceType, files, glslangPath, twgslPath, dracoPath }) { + // + // In this example, integer textures are used to store the state of each pixel in a simulation. + // The simulation is run in a shader, and the results are rendered to a texture. + // + // Integer textures can be useful for "compute-like" use cases, where you want to store + // arbitrary data in each pixel, and then use a shader to process the data. + // + // This example uses integer textures instead of floats in order to store + // multiple properties (element, shade, movedThisFrame) in the bits of each pixel. + // + + const STEPS_PER_FRAME = 4; + const PLANE_WIDTH = 10; + const PLANE_HEIGHT = 10; + + const TEXTURE_RATIO = PLANE_WIDTH / PLANE_HEIGHT; + const TEXTURE_HEIGHT = 512; + const TEXTURE_WIDTH = TEXTURE_HEIGHT * TEXTURE_RATIO; + + // set up and load draco module, as the glb we load is draco compressed + pc.WasmModule.setConfig('DracoDecoderModule', { + glueUrl: dracoPath + 'draco.wasm.js', + wasmUrl: dracoPath + 'draco.wasm.wasm', + fallbackUrl: dracoPath + 'draco.js' + }); + + const assets = {}; + + const gfxOptions = { + deviceTypes: [deviceType], + glslangUrl: glslangPath + 'glslang.js', + twgslUrl: twgslPath + 'twgsl.js' + }; + + const device = await pc.createGraphicsDevice(canvas, gfxOptions); + const createOptions = new pc.AppOptions(); + createOptions.graphicsDevice = device; + createOptions.keyboard = new pc.Keyboard(document.body); + + createOptions.componentSystems = [ + pc.RenderComponentSystem, + pc.CameraComponentSystem, + pc.LightComponentSystem + ]; + createOptions.resourceHandlers = []; + + 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); + }); + + // Helpers to create integer pixel buffers and render targets which we will ping-pong between + const createPixelColorBuffer = (i) => { + return new pc.Texture(device, { + name: `PixelBuffer_${i}`, + width: TEXTURE_WIDTH, + height: TEXTURE_HEIGHT, + // Note that we are using an unsigned integer format here. + // This can be helpful for storing bitfields in each pixel. + // In this example, we are storing 3 different properties + // in a single Uint8 value. + format: pc.PIXELFORMAT_R8U, + addressU: pc.ADDRESS_CLAMP_TO_EDGE, + addressV: pc.ADDRESS_CLAMP_TO_EDGE + }); + }; + const createPixelRenderTarget = (i, colorBuffer) => { + return new pc.RenderTarget({ + name: `PixelRenderTarget_${i}`, + colorBuffer: colorBuffer + }); + }; + + // Create our integer pixel buffers and render targets + const pixelColorBuffers = []; + const pixelRenderTargets = []; + if (!device.isWebGL1) { + pixelColorBuffers.push(createPixelColorBuffer(0), createPixelColorBuffer(1)); + pixelRenderTargets.push(createPixelRenderTarget(0, pixelColorBuffers[0])); + pixelRenderTargets.push(createPixelRenderTarget(1, pixelColorBuffers[1])); + } + + const sourceTexture = pixelColorBuffers[0]; + const sourceRenderTarget = pixelRenderTargets[0]; + const sandRenderTarget = pixelRenderTargets[1]; + + // Create an output texture and render target to render + // a visual representation of the simulation + const outputTexture = new pc.Texture(device, { + name: 'OutputTexture', + width: TEXTURE_WIDTH, + height: TEXTURE_HEIGHT, + format: pc.PIXELFORMAT_RGBA8, + minFilter: pc.FILTER_LINEAR_MIPMAP_LINEAR, + magFilter: pc.FILTER_LINEAR, + addressU: pc.ADDRESS_REPEAT, + addressV: pc.ADDRESS_REPEAT + }); + const outputRenderTarget = createPixelRenderTarget(2, outputTexture); + + // This is shader runs the sand simulation + // It uses integer textures to store the state of each pixel + const sandShader = pc.createShaderFromCode( + device, + pc.RenderPassShaderQuad.quadVertexShader, + files['sandSimulation.frag'], + 'SandShader', + { aPosition: pc.SEMANTIC_POSITION }, + // Note that we are changing the shader output type to 'uint' + // This means we only have to return a single integer value from the shader, + // whereas the default is to return a vec4. This option allows you to pass + // an array of types to specify the output type for each color attachment. + // Unspecified types are assumed to be 'vec4'. + { fragmentOutputTypes: ['uint'] } + ); + + // This shader reads the integer textures + // and renders a visual representation of the simulation + const outputShader = pc.createShaderFromCode( + device, + pc.RenderPassShaderQuad.quadVertexShader, + files['renderOutput.frag'], + 'RenderOutputShader', + { aPosition: pc.SEMANTIC_POSITION } + // For the output shader, we don't need to specify the output type, + // as we are returning a vec4 by default. + ); + + // Write the initial simulation state to the integer texture + const resetData = () => { + if (device.isWebGL1) return; + // Loop through the pixels in the texture + // and initialize them to either AIR, SAND or WALL + const sourceTextureData = sourceTexture.lock(); + for (let x = 0; x < sourceTexture.width; x++) { + for (let y = 0; y < sourceTexture.height; y++) { + const i = (y * sourceTexture.width + x); + + const isDefaultWall = x > sourceTexture.width * 0.3 && x < sourceTexture.width * 0.7 && y > sourceTexture.height * 0.7 && y < sourceTexture.height * 0.8; + + if (isDefaultWall) { // Create the default wall in the middle of the screen + // The WALL element is used to mark pixels that should not be moved + // It uses the integer '4' (see sandCommon.frag) + sourceTextureData[i] = 4; + } else if (Math.random() > 0.94) { // Sprinkle some sand randomly around the scene + // The SAND element is used to mark pixels that fall like sand + // It uses the integer '1' (see sandCommon.frag) + sourceTextureData[i] = 1; + // The shade of each pixel is stored in the upper 4 bits of the integer + // Here we write a random value to the shade bits + sourceTextureData[i] |= (Math.floor(Math.random() * 15) << 4); + } else { + // The AIR element is used to mark pixels that are empty + // Other than the wall and sand, all pixels are initialized to AIR + sourceTextureData[i] = 0; + } + } + } + sourceTexture.unlock(); + }; + + resetData(); + data.on('reset', resetData); + + const assetListLoader = new pc.AssetListLoader(Object.values(assets), app.assets); + assetListLoader.load(() => { + data.set('options', { + brush: 1, + brushSize: 8 + }); + + app.start(); + + // Create an Entity with a camera component + const cameraEntity = new pc.Entity(); + cameraEntity.addComponent("camera", { + clearColor: new pc.Color(0.4, 0.45, 0.5), + farClip: 500 + }); + + // add camera to the world + cameraEntity.setPosition(0, 5, 15); + cameraEntity.lookAt(0, 5, 0); + app.root.addChild(cameraEntity); + + // create material used on the ground plane + const groundMaterial = new pc.StandardMaterial(); + groundMaterial.gloss = 0.6; + groundMaterial.metalness = 0.4; + groundMaterial.diffuse = new pc.Color(0.95, 0.85, 0.85); + groundMaterial.useMetalness = true; + groundMaterial.useLighting = true; + groundMaterial.update(); + + // Create the ground plane + const ground = new pc.Entity(); + ground.addComponent('render', { + castShadows: false, + castShadowsLightmap: false, + lightmapped: false, + type: "plane", + material: groundMaterial + }); + app.root.addChild(ground); + ground.setLocalPosition(0, 0, 0); + ground.setLocalScale(40, 40, 40); + + // Create a directional light + const lightEntity = new pc.Entity(); + lightEntity.addComponent("light", { + type: "directional", + color: pc.Color.WHITE, + range: 100, + intensity: 1, + shadowDistance: 256, + castShadows: true, + shadowBias: 0.1 + // normalOffsetBias: 0.2 + }); + lightEntity.setLocalEulerAngles(60, 40, 0); + lightEntity.setLocalPosition(0, 10, 0); + app.root.addChild(lightEntity); + + // create a plane called gameScreen to display the sand + // simulation visualization texture + const gameScreen = new pc.Entity(); + gameScreen.addComponent('render', { + castShadows: true, + receiveShadows: false, + castShadowsLightmap: false, + lightmapped: false, + type: "plane" + }); + gameScreen.setLocalPosition(0, 5, 0); + gameScreen.setLocalScale(PLANE_WIDTH, 1, PLANE_HEIGHT); + gameScreen.setEulerAngles(90, 0, 0); + + /** @type {pc.StandardMaterial} */ + const gameScreenMaterial = gameScreen.render.material; + gameScreenMaterial.emissiveMap = outputTexture; + gameScreenMaterial.useLighting = false; + gameScreenMaterial.update(); + app.root.addChild(gameScreen); + + // Create a matching plane for mouse picking + const gamePlane = new pc.Plane(new pc.Vec3(0, 0, 1), 0); + + // Setup mouse controls + const mouse = new pc.Mouse(document.body); + const keyboard = new pc.Keyboard(document.body); + + mouse.disableContextMenu(); + + // Reset on space bar, select brush on 1-4 + keyboard.on(pc.EVENT_KEYUP, (event) => { + switch (event.key) { + case pc.KEY_SPACE: + resetData(); + break; + case pc.KEY_1: + data.set('options.brush', 1); + break; + case pc.KEY_2: + data.set('options.brush', 2); + break; + case pc.KEY_3: + data.set('options.brush', 3); + break; + case pc.KEY_4: + data.set('options.brush', 4); + break; + } + }, this); + + let mouseState = 0; + mouse.on(pc.EVENT_MOUSEDOWN, function (event) { + if (event.button === pc.MOUSEBUTTON_LEFT) { + if (keyboard.isPressed(pc.KEY_SHIFT)) { + mouseState = 2; + } else { + mouseState = 1; + } + } else if (event.button === pc.MOUSEBUTTON_RIGHT) { + mouseState = 2; + } + }); + mouse.on(pc.EVENT_MOUSEUP, function () { + mouseState = 0; + }); + + const lookRange = 1.5; + const mouseRay = new pc.Ray(); + const planePoint = new pc.Vec3(); + const mousePos = new pc.Vec2(); + const mouseUniform = new Float32Array(2); + mouse.on(pc.EVENT_MOUSEMOVE, function (event) { + const x = event.x; + const y = event.y; + + mousePos.x = x; + mousePos.y = y; + + const centerX = app.graphicsDevice.width / 2; + const centerY = app.graphicsDevice.height / 2; + + const xOffset = (x - centerX) / app.graphicsDevice.width; + const yOffset = (y - centerY) / app.graphicsDevice.height; + + cameraEntity.lookAt(xOffset * lookRange, 5 - yOffset * lookRange, 0); + if (cameraEntity.camera) { + cameraEntity.camera.screenToWorld(event.x, event.y, cameraEntity.camera.farClip, mouseRay.direction); + mouseRay.origin.copy(cameraEntity.getPosition()); + mouseRay.direction.sub(mouseRay.origin).normalize(); + gamePlane.intersectsRay(mouseRay, planePoint); + planePoint.x = (PLANE_WIDTH / 2) + planePoint.x; + planePoint.y = PLANE_HEIGHT - planePoint.y; + mousePos.set(planePoint.x / PLANE_WIDTH, planePoint.y / PLANE_HEIGHT); + } + }); + + let passNum = 0; + app.on("update", function (/** @type {number} */) { + if (device.isWebGL1) { + // WebGL1 does not support integer textures + return; + } + + mouseUniform[0] = mousePos.x; + mouseUniform[1] = mousePos.y; + + const brushRadius = data.get('options.brushSize') / Math.max(TEXTURE_WIDTH, TEXTURE_HEIGHT); + const brush = data.get('options.brush') ?? 1; + + // Run the sand simulation shader + for (let i = 0; i < STEPS_PER_FRAME; i++) { + device.scope.resolve('sourceTexture').setValue(sourceTexture); + device.scope.resolve('mousePosition').setValue(mouseUniform); + device.scope.resolve('mouseButton').setValue(mouseState); + device.scope.resolve('brush').setValue(brush); + device.scope.resolve('brushRadius').setValue(brushRadius); + device.scope.resolve('passNum').setValue(passNum); + device.scope.resolve('randomVal').setValue(Math.random()); + pc.drawQuadWithShader(device, sandRenderTarget, sandShader); + device.copyRenderTarget(sandRenderTarget, sourceRenderTarget, true, false); + passNum = (passNum + 1) % 16; + } + + // Render a visual representation of the simulation + device.scope.resolve('sourceTexture').setValue(sandRenderTarget.colorBuffer); + device.scope.resolve('mousePosition').setValue(mouseUniform); + device.scope.resolve('brushRadius').setValue(brushRadius); + pc.drawQuadWithShader(device, outputRenderTarget, outputShader); + + }); + }); + return app; +} + +export class IntegerTextureExample { + static CATEGORY = 'Graphics'; + static WEBGPU_ENABLED = true; + static DESCRIPTION = ``; + static example = example; + static controls = controls; + static sharedShaderChunks = { + 'sandCommon.frag': /* glsl */` + const uint AIR = 0u; + const uint SAND = 1u; + const uint ORANGESAND = 2u; + const uint GRAYSAND = 3u; + const uint WALL = 4u; + + bool isInBounds(ivec2 c, ivec2 size) { + return c.x > 0 && c.x < size.x - 1 && c.y > 0 && c.y < size.y - 1; + } + + struct Particle { + uint element; // 3 bits + bool movedThisFrame; // 1 bit + uint shade; // 4 bits + uint waterMass; // 8 bits + }; + + float rand(vec2 pos, float val) { + return fract(pos.x * pos.y * val * 1000.0); + } + + uint pack(Particle particle) { + uint packed = 0u; + packed |= (particle.element & 0x7u); // Store element in the lowest 3 bits + packed |= ((particle.movedThisFrame ? 1u : 0u) << 3); // Store movedThisFrame in the next bit + packed |= (particle.shade << 4); // Store shade in the next 4 bits + + return packed; // Second component is reserved/unused + } + + Particle unpack(uint packed) { + Particle particle; + particle.element = packed & 0x7u; // Extract lowest 3 bits + particle.movedThisFrame = ((packed >> 3) & 0x1u) != 0u; // Extract the next bit + particle.shade = (packed >> 4) & 0xFu; // Extract the next 4 bits + return particle; + } + + Particle getParticle(ivec2 c) { + uint val = texelFetch(sourceTexture, c, 0).r; + return unpack(val); + } + ` + }; + + static FILES = { + 'sandSimulation.frag': /* glsl */` + precision highp usampler2D; + + uniform usampler2D sourceTexture; + uniform vec2 mousePosition; + uniform uint mouseButton; + uniform uint passNum; + uniform uint brush; + uniform float randomVal; + uniform float brushRadius; + + varying vec2 uv0; + + ${IntegerTextureExample.sharedShaderChunks['sandCommon.frag']} + + void main() { + + ivec2 size = textureSize(sourceTexture, 0); + ivec2 coord = ivec2(uv0 * vec2(size)); + + if (!isInBounds(coord, size)) { + gl_FragColor = WALL; + return; + } + + float mouseDist = distance(mousePosition, uv0); + int dir = int(passNum % 3u) - 1; + + Particle currentParticle = getParticle(coord); + Particle nextState = currentParticle; + + if (mouseButton == 1u && mouseDist < brushRadius) { + nextState.element = brush; + nextState.movedThisFrame = true; + nextState.shade = uint(rand(uv0, randomVal * float(passNum)) * 15.0); + } else if (mouseButton == 2u && mouseDist < brushRadius) { + nextState.element = AIR; + nextState.movedThisFrame = false; + nextState.shade = uint(rand(uv0, randomVal * float(passNum)) * 15.0); + } + + currentParticle.movedThisFrame = false; + if (currentParticle.element == AIR) { + Particle above = getParticle(coord + ivec2(dir, -1)); + if (above.element != AIR && above.element != WALL) { + nextState = above; + nextState.movedThisFrame = true; + } + } else if (currentParticle.element != WALL) { + Particle below = getParticle(coord + ivec2(-dir, 1)); + if (below.element == AIR && !below.movedThisFrame) { + nextState = below; + nextState.movedThisFrame = false; + } + } + + gl_FragColor = pack(nextState); + } + `, + 'renderOutput.frag': /* glsl */` + precision highp usampler2D; + uniform usampler2D sourceTexture; + uniform vec2 mousePosition; + uniform float brushRadius; + varying vec2 uv0; + + vec3 whiteColor = vec3(1.0); + vec3 skyBlueColor = vec3(0.6, 0.7, 0.8); + vec3 yellowSandColor = vec3(0.73, 0.58, 0.26); + vec3 orangeSandColor = vec3(0.87, 0.43, 0.22); + vec3 graySandColor = vec3(0.13, 0.16, 0.17); + vec3 grayWallColor = vec3(0.5, 0.5, 0.5); + vec3 waterBlueColor = vec3(0.2, 0.3, 0.8); + + float circle( vec2 p, float r ) { + return length(p) - r; + } + + const float circleOutline = 0.0025; + + ${IntegerTextureExample.sharedShaderChunks['sandCommon.frag']} + + void main() { + ivec2 size = textureSize(sourceTexture, 0); + ivec2 coord = ivec2(uv0 * vec2(size)); + Particle particle = getParticle(coord); + + vec3 gameColor = skyBlueColor; + if (particle.element == SAND) { + gameColor = mix(yellowSandColor, whiteColor, (float(particle.shade) / 15.0) * 0.5); + } else if (particle.element == WALL) { + gameColor = grayWallColor; + } else if (particle.element == ORANGESAND) { + gameColor = mix(orangeSandColor, whiteColor, (float(particle.shade) / 15.0) * 0.5); + } else if (particle.element == GRAYSAND) { + gameColor = mix(graySandColor, whiteColor, (float(particle.shade) / 15.0) * 0.5); + } + + // Render a brush circle + float d = length(uv0 - mousePosition); + float wd = fwidth(d); + float circle = smoothstep(brushRadius + wd, brushRadius, d); + float circleInner = smoothstep(brushRadius - circleOutline + wd, brushRadius - circleOutline, d); + float brush = max(circle - circleInner, 0.0) * 0.5; + + vec3 outColor = mix(gameColor, vec3(1.0), brush); + + gl_FragColor = vec4(outColor, 1.0); + } + ` + }; +} diff --git a/examples/src/options.mjs b/examples/src/options.mjs index 942e8741c35..93c736c5e38 100644 --- a/examples/src/options.mjs +++ b/examples/src/options.mjs @@ -11,6 +11,6 @@ * @property {string} ammoPath - The ammo path. * @property {string} basisPath - The basis path. * @property {any} pcx - The pcx. - * @property {FILES} files - The files. * @template {Record} [FILES=Record] + * @property {FILES} files - The files. */ diff --git a/src/platform/graphics/bind-group-format.js b/src/platform/graphics/bind-group-format.js index 08abaa36449..b57b27cffae 100644 --- a/src/platform/graphics/bind-group-format.js +++ b/src/platform/graphics/bind-group-format.js @@ -3,7 +3,7 @@ import { Debug, DebugHelper } from '../../core/debug.js'; import { TEXTUREDIMENSION_2D, TEXTUREDIMENSION_CUBE, TEXTUREDIMENSION_3D, TEXTUREDIMENSION_2D_ARRAY, - SAMPLETYPE_FLOAT, PIXELFORMAT_RGBA8 + SAMPLETYPE_FLOAT, PIXELFORMAT_RGBA8, SAMPLETYPE_INT, SAMPLETYPE_UINT } from './constants.js'; let id = 0; @@ -180,7 +180,7 @@ class BindGroupFormat { let bindIndex = this.bufferFormats.length; this.textureFormats.forEach((format) => { - const textureType = textureDimensionInfo[format.textureDimension]; + let textureType = textureDimensionInfo[format.textureDimension]; Debug.assert(textureType, "Unsupported texture type", format.textureDimension); // handle texture2DArray by renaming the texture object and defining a replacement macro @@ -191,9 +191,16 @@ class BindGroupFormat { extraCode = `#define ${format.name} sampler2DArray(${format.name}${namePostfix}, ${format.name}_sampler)\n`; } + if (format.sampleType === SAMPLETYPE_INT) { + textureType = `i${textureType}`; + } else if (format.sampleType === SAMPLETYPE_UINT) { + textureType = `u${textureType}`; + } + code += `layout(set = ${bindGroup}, binding = ${bindIndex++}) uniform ${textureType} ${format.name}${namePostfix};\n` + `layout(set = ${bindGroup}, binding = ${bindIndex++}) uniform sampler ${format.name}_sampler;\n` + extraCode; + }); return code; diff --git a/src/platform/graphics/constants.js b/src/platform/graphics/constants.js index 70406b9fa5a..712b1c0102a 100644 --- a/src/platform/graphics/constants.js +++ b/src/platform/graphics/constants.js @@ -618,6 +618,133 @@ export const PIXELFORMAT_ATC_RGBA = 30; */ export const PIXELFORMAT_BGRA8 = 31; +/** + * 8-bit signed integer single-channel (R) format (Not supported by WebGL1). + * + * @type {number} + */ +export const PIXELFORMAT_R8I = 32; + +/** + * 8-bit unsigned integer single-channel (R) format (Not supported by WebGL1). + * + * @type {number} + */ +export const PIXELFORMAT_R8U = 33; + +/** + * 16-bit signed integer single-channel (R) format (Not supported by WebGL1). + * + * @type {number} + */ +export const PIXELFORMAT_R16I = 34; + +/** + * 16-bit unsigned integer single-channel (R) format (Not supported by WebGL1). + * + * @type {number} + */ +export const PIXELFORMAT_R16U = 35; + +/** + * 32-bit signed integer single-channel (R) format (Not supported by WebGL1). + * + * @type {number} + */ +export const PIXELFORMAT_R32I = 36; + +/** + * 32-bit unsigned integer single-channel (R) format (Not supported by WebGL1). + * + * @type {number} + */ +export const PIXELFORMAT_R32U = 37; + +/** + * 8-bit per-channel signed integer (RG) format (Not supported by WebGL1). + * + * @type {number} + */ +export const PIXELFORMAT_RG8I = 38; + +/** + * 8-bit per-channel unsigned integer (RG) format (Not supported by WebGL1). + * + * @type {number} + */ +export const PIXELFORMAT_RG8U = 39; + +/** + * 16-bit per-channel signed integer (RG) format (Not supported by WebGL1). + * + * @type {number} + */ +export const PIXELFORMAT_RG16I = 40; + +/** + * 16-bit per-channel unsigned integer (RG) format (Not supported by WebGL1). + * + * @type {number} + */ +export const PIXELFORMAT_RG16U = 41; + +/** + * 32-bit per-channel signed integer (RG) format (Not supported by WebGL1). + * + * @type {number} + */ +export const PIXELFORMAT_RG32I = 42; + +/** + * 32-bit per-channel unsigned integer (RG) format (Not supported by WebGL1). + * + * @type {number} + */ +export const PIXELFORMAT_RG32U = 43; + +/** + * 8-bit per-channel signed integer (RGBA) format (Not supported by WebGL1). + * + * @type {number} + */ +export const PIXELFORMAT_RGBA8I = 44; + +/** + * 8-bit per-channel unsigned integer (RGBA) format (Not supported by WebGL1). + * + * @type {number} + */ +export const PIXELFORMAT_RGBA8U = 45; + +/** + * 16-bit per-channel signed integer (RGBA) format (Not supported by WebGL1). + * + * @type {number} + */ +export const PIXELFORMAT_RGBA16I = 46; + +/** + * 16-bit per-channel unsigned integer (RGBA) format (Not supported by WebGL1). + * + * @type {number} + */ +export const PIXELFORMAT_RGBA16U = 47; + +/** + * 32-bit per-channel signed integer (RGBA) format (Not supported by WebGL1). + * + * @type {number} + */ +export const PIXELFORMAT_RGBA32I = 48; + +/** + * 32-bit per-channel unsigned integer (RGBA) format (Not supported by WebGL1). + * + * @type {number} + */ +export const PIXELFORMAT_RGBA32U = 49; + + // map of engine PIXELFORMAT_*** enums to information about the format export const pixelFormatInfo = new Map([ @@ -655,12 +782,36 @@ export const pixelFormatInfo = new Map([ [PIXELFORMAT_PVRTC_4BPP_RGBA_1, { name: 'PVRTC_4BPP_RGBA_1', blockSize: 8 }], [PIXELFORMAT_ASTC_4x4, { name: 'ASTC_4x4', blockSize: 16 }], [PIXELFORMAT_ATC_RGB, { name: 'ATC_RGB', blockSize: 8 }], - [PIXELFORMAT_ATC_RGBA, { name: 'ATC_RGBA', blockSize: 16 }] + [PIXELFORMAT_ATC_RGBA, { name: 'ATC_RGBA', blockSize: 16 }], + + // uncompressed integer formats (Not supported on WebGL1) + [PIXELFORMAT_R8I, { name: 'R8I', size: 1, isInt: true }], + [PIXELFORMAT_R8U, { name: 'R8U', size: 1, isInt: true }], + [PIXELFORMAT_R16I, { name: 'R16I', size: 2, isInt: true }], + [PIXELFORMAT_R16U, { name: 'R16U', size: 2, isInt: true }], + [PIXELFORMAT_R32I, { name: 'R32I', size: 4, isInt: true }], + [PIXELFORMAT_R32U, { name: 'R32U', size: 4, isInt: true }], + [PIXELFORMAT_RG8I, { name: 'RG8I', size: 2, isInt: true }], + [PIXELFORMAT_RG8U, { name: 'RG8U', size: 2, isInt: true }], + [PIXELFORMAT_RG16I, { name: 'RG16I', size: 4, isInt: true }], + [PIXELFORMAT_RG16U, { name: 'RG16U', size: 4, isInt: true }], + [PIXELFORMAT_RG32I, { name: 'RG32I', size: 8, isInt: true }], + [PIXELFORMAT_RG32U, { name: 'RG32U', size: 8, isInt: true }], + [PIXELFORMAT_RGBA8I, { name: 'RGBA8I', size: 4, isInt: true }], + [PIXELFORMAT_RGBA8U, { name: 'RGBA8U', size: 4, isInt: true }], + [PIXELFORMAT_RGBA16I, { name: 'RGBA16I', size: 8, isInt: true }], + [PIXELFORMAT_RGBA16U, { name: 'RGBA16U', size: 8, isInt: true }], + [PIXELFORMAT_RGBA32I, { name: 'RGBA32I', size: 16, isInt: true }], + [PIXELFORMAT_RGBA32U, { name: 'RGBA32U', size: 16, isInt: true }] ]); // update this function when exposing additional compressed pixel formats export const isCompressedPixelFormat = (format) => { - return pixelFormatInfo.get(format).blockSize !== undefined; + return pixelFormatInfo.get(format)?.blockSize !== undefined; +}; + +export const isIntegerPixelFormat = (format) => { + return pixelFormatInfo.get(format)?.isInt === true; }; // get the pixel format array type @@ -669,12 +820,31 @@ export const getPixelFormatArrayType = (format) => { case PIXELFORMAT_RGB32F: case PIXELFORMAT_RGBA32F: return Float32Array; + case PIXELFORMAT_R32I: + case PIXELFORMAT_RG32I: + case PIXELFORMAT_RGBA32I: + return Int32Array; + case PIXELFORMAT_R32U: + case PIXELFORMAT_RG32U: + case PIXELFORMAT_RGBA32U: + return Uint32Array; + case PIXELFORMAT_R16I: + case PIXELFORMAT_RG16I: + case PIXELFORMAT_RGBA16I: + return Int16Array; + case PIXELFORMAT_R16U: + case PIXELFORMAT_RG16U: + case PIXELFORMAT_RGBA16U: case PIXELFORMAT_RGB565: case PIXELFORMAT_RGBA5551: case PIXELFORMAT_RGBA4: case PIXELFORMAT_RGB16F: case PIXELFORMAT_RGBA16F: return Uint16Array; + case PIXELFORMAT_R8I: + case PIXELFORMAT_RG8I: + case PIXELFORMAT_RGBA8I: + return Int8Array; default: return Uint8Array; } @@ -1070,6 +1240,8 @@ export const TEXTUREDIMENSION_3D = '3d'; export const SAMPLETYPE_FLOAT = 0; export const SAMPLETYPE_UNFILTERABLE_FLOAT = 1; export const SAMPLETYPE_DEPTH = 2; +export const SAMPLETYPE_INT = 3; +export const SAMPLETYPE_UINT = 4; /** * Texture data is not stored a specific projection format. @@ -1169,6 +1341,7 @@ export const TYPE_FLOAT32 = 6; */ export const TYPE_FLOAT16 = 7; +// Uniform types export const UNIFORMTYPE_BOOL = 0; export const UNIFORMTYPE_INT = 1; export const UNIFORMTYPE_FLOAT = 2; @@ -1196,7 +1369,38 @@ export const UNIFORMTYPE_VEC4ARRAY = 23; export const UNIFORMTYPE_MAT4ARRAY = 24; export const UNIFORMTYPE_TEXTURE2D_ARRAY = 25; +// Unsigned uniform types +export const UNIFORMTYPE_UINT = 26; +export const UNIFORMTYPE_UVEC2 = 27; +export const UNIFORMTYPE_UVEC3 = 28; +export const UNIFORMTYPE_UVEC4 = 29; + +// Integer uniform array types +export const UNIFORMTYPE_INTARRAY = 30; +export const UNIFORMTYPE_UINTARRAY = 31; +export const UNIFORMTYPE_BOOLARRAY = 32; +export const UNIFORMTYPE_IVEC2ARRAY = 33; +export const UNIFORMTYPE_UVEC2ARRAY = 34; +export const UNIFORMTYPE_BVEC2ARRAY = 35; +export const UNIFORMTYPE_IVEC3ARRAY = 36; +export const UNIFORMTYPE_UVEC3ARRAY = 37; +export const UNIFORMTYPE_BVEC3ARRAY = 38; +export const UNIFORMTYPE_IVEC4ARRAY = 39; +export const UNIFORMTYPE_UVEC4ARRAY = 40; +export const UNIFORMTYPE_BVEC4ARRAY = 41; + +// Integer texture types +export const UNIFORMTYPE_ITEXTURE2D = 42; +export const UNIFORMTYPE_UTEXTURE2D = 43; +export const UNIFORMTYPE_ITEXTURECUBE = 44; +export const UNIFORMTYPE_UTEXTURECUBE = 45; +export const UNIFORMTYPE_ITEXTURE3D = 46; +export const UNIFORMTYPE_UTEXTURE3D = 47; +export const UNIFORMTYPE_ITEXTURE2D_ARRAY = 48; +export const UNIFORMTYPE_UTEXTURE2D_ARRAY = 49; + export const uniformTypeToName = [ + // Uniforms 'bool', 'int', 'float', @@ -1206,9 +1410,9 @@ export const uniformTypeToName = [ 'ivec2', 'ivec3', 'ivec4', - 'bec2', - 'bec3', - 'bec4', + 'bvec2', + 'bvec3', + 'bvec4', 'mat2', 'mat3', 'mat4', @@ -1220,9 +1424,89 @@ export const uniformTypeToName = [ 'sampler3D', '', // not directly handled: UNIFORMTYPE_VEC2ARRAY '', // not directly handled: UNIFORMTYPE_VEC3ARRAY - '' // not directly handled: UNIFORMTYPE_VEC4ARRAY + '', // not directly handled: UNIFORMTYPE_VEC4ARRAY + '', // not directly handled: UNIFORMTYPE_MAT4ARRAY + 'sampler2DArray', + 'uint', + 'uvec2', + 'uvec3', + 'uvec4', + '', // not directly handled: UNIFORMTYPE_INTARRAY + '', // not directly handled: UNIFORMTYPE_UINTARRAY + '', // not directly handled: UNIFORMTYPE_BOOLARRAY + '', // not directly handled: UNIFORMTYPE_IVEC2ARRAY + '', // not directly handled: UNIFORMTYPE_UVEC2ARRAY + '', // not directly handled: UNIFORMTYPE_BVEC2ARRAY + '', // not directly handled: UNIFORMTYPE_IVEC3ARRAY + '', // not directly handled: UNIFORMTYPE_UVEC3ARRAY + '', // not directly handled: UNIFORMTYPE_BVEC3ARRAY + '', // not directly handled: UNIFORMTYPE_IVEC4ARRAY + '', // not directly handled: UNIFORMTYPE_UVEC4ARRAY + '', // not directly handled: UNIFORMTYPE_BVEC4ARRAY + 'isampler2D', + 'usampler2D', + 'isamplerCube', + 'usamplerCube', + 'isampler3D', + 'usampler3D', + 'isampler2DArray', + 'usampler2DArray' ]; +// Map to convert uniform type to storage type, used in uniform-buffer.js +export const uniformTypeToStorage = new Uint8Array([ + TYPE_INT32, // UNIFORMTYPE_BOOL + TYPE_INT32, // UNIFORMTYPE_INT + TYPE_FLOAT32, // UNIFORMTYPE_FLOAT + TYPE_FLOAT32, // UNIFORMTYPE_VEC2 + TYPE_FLOAT32, // UNIFORMTYPE_VEC3 + TYPE_FLOAT32, // UNIFORMTYPE_VEC4 + TYPE_INT32, // UNIFORMTYPE_IVEC2 + TYPE_INT32, // UNIFORMTYPE_IVEC3 + TYPE_INT32, // UNIFORMTYPE_IVEC4 + TYPE_INT32, // UNIFORMTYPE_BVEC2 + TYPE_INT32, // UNIFORMTYPE_BVEC3 + TYPE_INT32, // UNIFORMTYPE_BVEC4 + TYPE_FLOAT32, // UNIFORMTYPE_MAT2 + TYPE_FLOAT32, // UNIFORMTYPE_MAT3 + TYPE_FLOAT32, // UNIFORMTYPE_MAT4 + TYPE_INT32, // UNIFORMTYPE_TEXTURE2D + TYPE_INT32, // UNIFORMTYPE_TEXTURECUBE + TYPE_FLOAT32, // UNIFORMTYPE_FLOATARRAY + TYPE_INT32, // UNIFORMTYPE_TEXTURE2D_SHADOW + TYPE_INT32, // UNIFORMTYPE_TEXTURECUBE_SHADOW + TYPE_INT32, // UNIFORMTYPE_TEXTURE3D + TYPE_FLOAT32, // UNIFORMTYPE_VEC2ARRAY + TYPE_FLOAT32, // UNIFORMTYPE_VEC3ARRAY + TYPE_FLOAT32, // UNIFORMTYPE_VEC4ARRAY + TYPE_FLOAT32, // UNIFORMTYPE_MAT4ARRAY + TYPE_INT32, // UNIFORMTYPE_TEXTURE2D_ARRAY + TYPE_UINT32, // UNIFORMTYPE_UINT + TYPE_UINT32, // UNIFORMTYPE_UVEC2 + TYPE_UINT32, // UNIFORMTYPE_UVEC3 + TYPE_UINT32, // UNIFORMTYPE_UVEC4 + TYPE_INT32, // UNIFORMTYPE_INTARRAY + TYPE_UINT32, // UNIFORMTYPE_UINTARRAY + TYPE_INT32, // UNIFORMTYPE_BOOLARRAY + TYPE_INT32, // UNIFORMTYPE_IVEC2ARRAY + TYPE_UINT32, // UNIFORMTYPE_UVEC2ARRAY + TYPE_INT32, // UNIFORMTYPE_BVEC2ARRAY + TYPE_INT32, // UNIFORMTYPE_IVEC3ARRAY + TYPE_UINT32, // UNIFORMTYPE_UVEC3ARRAY + TYPE_INT32, // UNIFORMTYPE_BVEC3ARRAY + TYPE_INT32, // UNIFORMTYPE_IVEC4ARRAY + TYPE_UINT32, // UNIFORMTYPE_UVEC4ARRAY + TYPE_INT32, // UNIFORMTYPE_BVEC4ARRAY + TYPE_INT32, // UNIFORMTYPE_ITEXTURE2D + TYPE_UINT32, // UNIFORMTYPE_UTEXTURE2D + TYPE_INT32, // UNIFORMTYPE_ITEXTURECUBE + TYPE_UINT32, // UNIFORMTYPE_UTEXTURECUBE + TYPE_INT32, // UNIFORMTYPE_ITEXTURE3D + TYPE_UINT32, // UNIFORMTYPE_UTEXTURE3D + TYPE_INT32, // UNIFORMTYPE_ITEXTURE2D_ARRAY + TYPE_UINT32 // UNIFORMTYPE_UTEXTURE2D_ARRAY +]); + /** * A WebGL 1 device type. * diff --git a/src/platform/graphics/shader-chunks/frag/gles2.js b/src/platform/graphics/shader-chunks/frag/gles2.js index 62b11aa3bb5..3f887dc84eb 100644 --- a/src/platform/graphics/shader-chunks/frag/gles2.js +++ b/src/platform/graphics/shader-chunks/frag/gles2.js @@ -31,6 +31,8 @@ export default /* glsl */` #endif #define texture2DBias texture2D +#define itexture2D texture2D +#define utexture2D texture2D // pass / accept shadow map or texture as a function parameter, on webgl this is simply passed as is // but this is needed for WebGPU diff --git a/src/platform/graphics/shader-chunks/frag/gles3.js b/src/platform/graphics/shader-chunks/frag/gles3.js index 4e3c2716c4d..a6a211af7dd 100644 --- a/src/platform/graphics/shader-chunks/frag/gles3.js +++ b/src/platform/graphics/shader-chunks/frag/gles3.js @@ -1,45 +1,50 @@ export default /* glsl */` -layout(location = 0) out highp vec4 pc_fragColor; + +#ifndef outType_0 +#define outType_0 vec4 +#endif + +layout(location = 0) out highp outType_0 pc_fragColor; #ifndef REMOVE_COLOR_ATTACHMENT_1 #if COLOR_ATTACHMENT_1 -layout(location = 1) out highp vec4 pc_fragColor1; +layout(location = 1) out highp outType_1 pc_fragColor1; #endif #endif #ifndef REMOVE_COLOR_ATTACHMENT_2 #if COLOR_ATTACHMENT_2 -layout(location = 2) out highp vec4 pc_fragColor2; +layout(location = 2) out highp outType_2 pc_fragColor2; #endif #endif #ifndef REMOVE_COLOR_ATTACHMENT_3 #if COLOR_ATTACHMENT_3 -layout(location = 3) out highp vec4 pc_fragColor3; +layout(location = 3) out highp outType_3 pc_fragColor3; #endif #endif #ifndef REMOVE_COLOR_ATTACHMENT_4 #if COLOR_ATTACHMENT_4 -layout(location = 4) out highp vec4 pc_fragColor4; +layout(location = 4) out highp outType_4 pc_fragColor4; #endif #endif #ifndef REMOVE_COLOR_ATTACHMENT_5 #if COLOR_ATTACHMENT_5 -layout(location = 5) out highp vec4 pc_fragColor5; +layout(location = 5) out highp outType_5 pc_fragColor5; #endif #endif #ifndef REMOVE_COLOR_ATTACHMENT_6 #if COLOR_ATTACHMENT_6 -layout(location = 6) out highp vec4 pc_fragColor6; +layout(location = 6) out highp outType_6 pc_fragColor6; #endif #endif #ifndef REMOVE_COLOR_ATTACHMENT_7 #if COLOR_ATTACHMENT_7 -layout(location = 7) out highp vec4 pc_fragColor7; +layout(location = 7) out highp outType_7 pc_fragColor7; #endif #endif @@ -66,6 +71,8 @@ layout(location = 7) out highp vec4 pc_fragColor7; #define texture2DGradEXT textureGrad #define texture2DProjGradEXT textureProjGrad #define textureCubeGradEXT textureGrad +#define utexture2D texture +#define itexture2D texture // sample shadows using textureGrad to remove derivatives in the dynamic loops (which are used by // clustered lighting) - as DirectX shader compiler tries to unroll the loops and takes long time diff --git a/src/platform/graphics/shader-chunks/frag/webgpu.js b/src/platform/graphics/shader-chunks/frag/webgpu.js index 35800ef77ff..45f0b780098 100644 --- a/src/platform/graphics/shader-chunks/frag/webgpu.js +++ b/src/platform/graphics/shader-chunks/frag/webgpu.js @@ -3,14 +3,39 @@ export default /* glsl */` // texelFetch support and others #extension GL_EXT_samplerless_texture_functions : require -layout(location = 0) out highp vec4 pc_fragColor; -layout(location = 1) out highp vec4 pc_fragColor1; -layout(location = 2) out highp vec4 pc_fragColor2; -layout(location = 3) out highp vec4 pc_fragColor3; -layout(location = 4) out highp vec4 pc_fragColor4; -layout(location = 5) out highp vec4 pc_fragColor5; -layout(location = 6) out highp vec4 pc_fragColor6; -layout(location = 7) out highp vec4 pc_fragColor7; +#ifndef outType_0 +#define outType_0 vec4 +#endif +#ifndef outType_1 +#define outType_1 vec4 +#endif +#ifndef outType_2 +#define outType_2 vec4 +#endif +#ifndef outType_3 +#define outType_3 vec4 +#endif +#ifndef outType_4 +#define outType_4 vec4 +#endif +#ifndef outType_5 +#define outType_5 vec4 +#endif +#ifndef outType_6 +#define outType_6 vec4 +#endif +#ifndef outType_7 +#define outType_7 vec4 +#endif + +layout(location = 0) out highp outType_0 pc_fragColor; +layout(location = 1) out highp outType_1 pc_fragColor1; +layout(location = 2) out highp outType_2 pc_fragColor2; +layout(location = 3) out highp outType_3 pc_fragColor3; +layout(location = 4) out highp outType_4 pc_fragColor4; +layout(location = 5) out highp outType_5 pc_fragColor5; +layout(location = 6) out highp outType_6 pc_fragColor6; +layout(location = 7) out highp outType_7 pc_fragColor7; #define gl_FragColor pc_fragColor @@ -29,6 +54,8 @@ layout(location = 7) out highp vec4 pc_fragColor7; #define textureCube(res, uv) texture(samplerCube(res, res ## _sampler), uv) #define textureCubeLodEXT(res, uv, lod) textureLod(samplerCube(res, res ## _sampler), uv, lod) #define textureShadow(res, uv) textureLod(sampler2DShadow(res, res ## _sampler), uv, 0.0) +#define itexture2D(res, uv) texture(isampler2D(res, res ## _sampler), uv) +#define utexture2D(res, uv) texture(usampler2D(res, res ## _sampler), uv) // TODO: implement other texture sampling macros // #define texture2DProj textureProj diff --git a/src/platform/graphics/shader-chunks/vert/gles3.js b/src/platform/graphics/shader-chunks/vert/gles3.js index d25c3def862..f5bc503d850 100644 --- a/src/platform/graphics/shader-chunks/vert/gles3.js +++ b/src/platform/graphics/shader-chunks/vert/gles3.js @@ -2,6 +2,8 @@ export default /* glsl */` #define attribute in #define varying out #define texture2D texture +#define utexture2D texture +#define itexture2D texture #define GL2 #define VERTEXSHADER `; diff --git a/src/platform/graphics/shader-chunks/vert/webgpu.js b/src/platform/graphics/shader-chunks/vert/webgpu.js index e9dd9c4c726..51106156247 100644 --- a/src/platform/graphics/shader-chunks/vert/webgpu.js +++ b/src/platform/graphics/shader-chunks/vert/webgpu.js @@ -4,6 +4,8 @@ export default /* glsl */` #extension GL_EXT_samplerless_texture_functions : require #define texture2D(res, uv) texture(sampler2D(res, res ## _sampler), uv) +#define itexture2D(res, uv) texture(isampler2D(res, res ## _sampler), uv) +#define utexture2D(res, uv) texture(usampler2D(res, res ## _sampler), uv) #define GL2 #define WEBGPU diff --git a/src/platform/graphics/shader-processor.js b/src/platform/graphics/shader-processor.js index 2044dc2efa9..0379044c827 100644 --- a/src/platform/graphics/shader-processor.js +++ b/src/platform/graphics/shader-processor.js @@ -5,7 +5,7 @@ import { UNIFORM_BUFFER_DEFAULT_SLOT_NAME, SAMPLETYPE_FLOAT, SAMPLETYPE_DEPTH, SAMPLETYPE_UNFILTERABLE_FLOAT, TEXTUREDIMENSION_2D, TEXTUREDIMENSION_2D_ARRAY, TEXTUREDIMENSION_CUBE, TEXTUREDIMENSION_3D, - TYPE_FLOAT32, TYPE_INT8, TYPE_INT16, TYPE_INT32, TYPE_FLOAT16 + TYPE_FLOAT32, TYPE_INT8, TYPE_INT16, TYPE_INT32, TYPE_FLOAT16, SAMPLETYPE_INT, SAMPLETYPE_UINT } from './constants.js'; import { UniformFormat, UniformBufferFormat } from './uniform-buffer-format.js'; import { BindGroupFormat, BindBufferFormat, BindTextureFormat } from './bind-group-format.js'; @@ -25,7 +25,7 @@ const MARKER = '@@@'; const ARRAY_IDENTIFIER = /([\w-]+)\[(.*?)\]/; const precisionQualifiers = new Set(['highp', 'mediump', 'lowp']); -const shadowSamplers = new Set(['sampler2DShadow', 'samplerCubeShadow']); +const shadowSamplers = new Set(['sampler2DShadow', 'samplerCubeShadow', 'sampler2DArrayShadow']); const textureDimensions = { sampler2D: TEXTUREDIMENSION_2D, sampler3D: TEXTUREDIMENSION_3D, @@ -33,7 +33,15 @@ const textureDimensions = { samplerCubeShadow: TEXTUREDIMENSION_CUBE, sampler2DShadow: TEXTUREDIMENSION_2D, sampler2DArray: TEXTUREDIMENSION_2D_ARRAY, - sampler2DArrayShadow: TEXTUREDIMENSION_2D_ARRAY + sampler2DArrayShadow: TEXTUREDIMENSION_2D_ARRAY, + isampler2D: TEXTUREDIMENSION_2D, + usampler2D: TEXTUREDIMENSION_2D, + isampler3D: TEXTUREDIMENSION_3D, + usampler3D: TEXTUREDIMENSION_3D, + isamplerCube: TEXTUREDIMENSION_CUBE, + usamplerCube: TEXTUREDIMENSION_CUBE, + isampler2DArray: TEXTUREDIMENSION_2D_ARRAY, + usampler2DArray: TEXTUREDIMENSION_2D_ARRAY }; class UniformLine { @@ -79,6 +87,8 @@ class UniformLine { } this.isSampler = this.type.indexOf('sampler') !== -1; + this.isSignedInt = this.type.indexOf('isampler') !== -1; + this.isUnsignedInt = this.type.indexOf('usampler') !== -1; } } @@ -276,10 +286,16 @@ class ShaderProcessor { // WebGpu does not currently support filtered float format textures, and so we map them to unfilterable type // as we sample them without filtering anyways let sampleType = SAMPLETYPE_FLOAT; - if (uniform.precision === 'highp') - sampleType = SAMPLETYPE_UNFILTERABLE_FLOAT; - if (shadowSamplers.has(uniform.type)) - sampleType = SAMPLETYPE_DEPTH; + if (uniform.isSignedInt) { + sampleType = SAMPLETYPE_INT; + } else if (uniform.isUnsignedInt) { + sampleType = SAMPLETYPE_UINT; + } else { + if (uniform.precision === 'highp') + sampleType = SAMPLETYPE_UNFILTERABLE_FLOAT; + if (shadowSamplers.has(uniform.type)) + sampleType = SAMPLETYPE_DEPTH; + } // dimension const dimension = textureDimensions[uniform.type]; diff --git a/src/platform/graphics/shader-utils.js b/src/platform/graphics/shader-utils.js index fd4a91eb950..0bdb0870236 100644 --- a/src/platform/graphics/shader-utils.js +++ b/src/platform/graphics/shader-utils.js @@ -50,22 +50,36 @@ class ShaderUtils { * @param {string} [options.fragmentDefines] - The fragment shader defines. * @param {string} [options.fragmentExtensions] - The fragment shader extensions code. * @param {string} [options.fragmentPreamble] - The preamble string for the fragment shader. - * @param {boolean} [options.useTransformFeedback] - Whether to use transform feedback. Defaults - * to false. + * @param {boolean} [options.useTransformFeedback] - Whether to use transform feedback. Defaults to false. + * @param {string | string[]} [options.fragmentOutputTypes] - Fragment shader output types, + * which default to vec4. Passing a string will set the output type for all color attachments. + * Passing an array will set the output type for each color attachment. * @returns {object} Returns the created shader definition. */ static createDefinition(device, options) { Debug.assert(options); - const getDefines = (gpu, gl2, gl1, isVertex) => { + const getDefines = (gpu, gl2, gl1, isVertex, options) => { const deviceIntro = device.isWebGPU ? gpu : (device.isWebGL2 ? gl2 : ShaderUtils.gl1Extensions(device, options) + gl1); // a define per supported color attachment, which strips out unsupported output definitions in the deviceIntro let attachmentsDefine = ''; - for (let i = 0; i < device.maxColorAttachments; i++) { - attachmentsDefine += `#define COLOR_ATTACHMENT_${i}\n`; + + // Define the fragment shader output type, vec4 by default + if (!isVertex) { + // Normalize fragmentOutputTypes to an array + let fragmentOutputTypes = options.fragmentOutputTypes ?? 'vec4'; + if (!Array.isArray(fragmentOutputTypes)) { + fragmentOutputTypes = [fragmentOutputTypes]; + } + + for (let i = 0; i < device.maxColorAttachments; i++) { + attachmentsDefine += `#define COLOR_ATTACHMENT_${i}\n`; + const outType = fragmentOutputTypes[i] ?? 'vec4'; + attachmentsDefine += `#define outType_${i} ${outType}\n`; + } } return attachmentsDefine + deviceIntro; @@ -74,7 +88,7 @@ class ShaderUtils { const name = options.name ?? 'Untitled'; // vertex code - const vertDefines = options.vertexDefines || getDefines(webgpuVS, gles3VS, '', true); + const vertDefines = options.vertexDefines || getDefines(webgpuVS, gles3VS, '', true, options); const vertCode = ShaderUtils.versionCode(device) + vertDefines + sharedFS + @@ -82,7 +96,7 @@ class ShaderUtils { options.vertexCode; // fragment code - const fragDefines = options.fragmentDefines || getDefines(webgpuFS, gles3FS, gles2FS, false); + const fragDefines = options.fragmentDefines || getDefines(webgpuFS, gles3FS, gles2FS, false, options); const fragCode = (options.fragmentPreamble || '') + ShaderUtils.versionCode(device) + fragDefines + diff --git a/src/platform/graphics/shader.js b/src/platform/graphics/shader.js index ef199855079..db21985af5a 100644 --- a/src/platform/graphics/shader.js +++ b/src/platform/graphics/shader.js @@ -54,6 +54,9 @@ class Shader { * WebGPU platform. * @param {boolean} [definition.useTransformFeedback] - Specifies that this shader outputs * post-VS data to a buffer. + * @param {string | string[]} [definition.fragmentOutputTypes] - Fragment shader output types, + * which default to vec4. Passing a string will set the output type for all color attachments. + * Passing an array will set the output type for each color attachment. * @param {string} [definition.shaderLanguage] - Specifies the shader language of vertex and * fragment shaders. Defaults to {@link SHADERLANGUAGE_GLSL}. * @example diff --git a/src/platform/graphics/texture.js b/src/platform/graphics/texture.js index 0aa21427af9..5d61c393700 100644 --- a/src/platform/graphics/texture.js +++ b/src/platform/graphics/texture.js @@ -15,7 +15,8 @@ import { TEXHINT_SHADOWMAP, TEXHINT_ASSET, TEXHINT_LIGHTMAP, TEXTURELOCK_WRITE, TEXTUREPROJECTION_NONE, TEXTUREPROJECTION_CUBE, - TEXTURETYPE_DEFAULT, TEXTURETYPE_RGBM, TEXTURETYPE_RGBE, TEXTURETYPE_RGBP, TEXTURETYPE_SWIZZLEGGGR + TEXTURETYPE_DEFAULT, TEXTURETYPE_RGBM, TEXTURETYPE_RGBE, TEXTURETYPE_RGBP, TEXTURETYPE_SWIZZLEGGGR, + isIntegerPixelFormat, FILTER_NEAREST } from './constants.js'; let id = 0; @@ -198,6 +199,12 @@ class Texture { this._format = options.format ?? PIXELFORMAT_RGBA8; this._compressed = isCompressedPixelFormat(this._format); + this._integerFormat = isIntegerPixelFormat(this._format); + if (this._integerFormat) { + options.mipmaps = false; + options.minFilter = FILTER_NEAREST; + options.magFilter = FILTER_NEAREST; + } if (graphicsDevice.supportsVolumeTextures) { this._volume = options.volume ?? false; @@ -384,8 +391,12 @@ class Texture { */ set minFilter(v) { if (this._minFilter !== v) { - this._minFilter = v; - this.propertyChanged(1); + if (isIntegerPixelFormat(this._format)) { + Debug.warn("Texture#minFilter: minFilter property cannot be changed on an integer texture, will remain FILTER_NEAREST", this); + } else { + this._minFilter = v; + this.propertyChanged(1); + } } } @@ -403,8 +414,12 @@ class Texture { */ set magFilter(v) { if (this._magFilter !== v) { - this._magFilter = v; - this.propertyChanged(2); + if (isIntegerPixelFormat(this._format)) { + Debug.warn("Texture#magFilter: magFilter property cannot be changed on an integer texture, will remain FILTER_NEAREST", this); + } else { + this._magFilter = v; + this.propertyChanged(2); + } } } @@ -542,10 +557,13 @@ class Texture { */ set mipmaps(v) { if (this._mipmaps !== v) { - this._mipmaps = v; if (this.device.isWebGPU) { Debug.warn("Texture#mipmaps: mipmap property is currently not allowed to be changed on WebGPU, create the texture appropriately.", this); + } else if (isIntegerPixelFormat(this._format)) { + Debug.warn("Texture#mipmaps: mipmap property cannot be changed on an integer texture, will remain false", this); + } else { + this._mipmaps = v; } if (v) this._needsMipmapsUpload = true; @@ -718,7 +736,8 @@ class Texture { return (this.format === PIXELFORMAT_RGB16F || this.format === PIXELFORMAT_RGB32F || this.format === PIXELFORMAT_RGBA16F || - this.format === PIXELFORMAT_RGBA32F) ? 'linear' : 'srgb'; + this.format === PIXELFORMAT_RGBA32F || + isIntegerPixelFormat(this.format)) ? 'linear' : 'srgb'; } } diff --git a/src/platform/graphics/uniform-buffer-format.js b/src/platform/graphics/uniform-buffer-format.js index 4c1bd0ba236..189e5fb4806 100644 --- a/src/platform/graphics/uniform-buffer-format.js +++ b/src/platform/graphics/uniform-buffer-format.js @@ -2,30 +2,38 @@ import { Debug } from '../../core/debug.js'; import { math } from '../../core/math/math.js'; import { uniformTypeToName, bindGroupNames, - UNIFORMTYPE_BOOL, UNIFORMTYPE_INT, UNIFORMTYPE_FLOAT, UNIFORMTYPE_VEC2, UNIFORMTYPE_VEC3, - UNIFORMTYPE_VEC4, UNIFORMTYPE_IVEC2, UNIFORMTYPE_IVEC3, UNIFORMTYPE_IVEC4, UNIFORMTYPE_BVEC2, - UNIFORMTYPE_BVEC3, UNIFORMTYPE_BVEC4, UNIFORMTYPE_MAT4, UNIFORMTYPE_MAT2, UNIFORMTYPE_MAT3, - UNIFORMTYPE_FLOATARRAY, UNIFORMTYPE_VEC2ARRAY, UNIFORMTYPE_VEC3ARRAY, UNIFORMTYPE_VEC4ARRAY, - UNIFORMTYPE_MAT4ARRAY + UNIFORMTYPE_BOOL, UNIFORMTYPE_INT, UNIFORMTYPE_FLOAT, UNIFORMTYPE_UINT, UNIFORMTYPE_VEC2, + UNIFORMTYPE_VEC3, UNIFORMTYPE_VEC4, UNIFORMTYPE_IVEC2, UNIFORMTYPE_UVEC2, UNIFORMTYPE_IVEC3, + UNIFORMTYPE_IVEC4, UNIFORMTYPE_BVEC2, UNIFORMTYPE_BVEC3, UNIFORMTYPE_UVEC3, UNIFORMTYPE_BVEC4, + UNIFORMTYPE_MAT4, UNIFORMTYPE_MAT2, UNIFORMTYPE_MAT3, UNIFORMTYPE_FLOATARRAY, UNIFORMTYPE_UVEC4, + UNIFORMTYPE_VEC2ARRAY, UNIFORMTYPE_VEC3ARRAY, UNIFORMTYPE_VEC4ARRAY, UNIFORMTYPE_MAT4ARRAY, UNIFORMTYPE_INTARRAY, + UNIFORMTYPE_UINTARRAY, UNIFORMTYPE_BOOLARRAY, UNIFORMTYPE_IVEC2ARRAY, UNIFORMTYPE_UVEC2ARRAY, + UNIFORMTYPE_BVEC2ARRAY, UNIFORMTYPE_IVEC3ARRAY, UNIFORMTYPE_UVEC3ARRAY, UNIFORMTYPE_BVEC3ARRAY, + UNIFORMTYPE_IVEC4ARRAY, UNIFORMTYPE_UVEC4ARRAY, UNIFORMTYPE_BVEC4ARRAY } from './constants.js'; -// map of UNIFORMTYPE_*** to number of 32bit elements -const uniformTypeToNumElements = []; -uniformTypeToNumElements[UNIFORMTYPE_FLOAT] = 1; -uniformTypeToNumElements[UNIFORMTYPE_VEC2] = 2; -uniformTypeToNumElements[UNIFORMTYPE_VEC3] = 3; -uniformTypeToNumElements[UNIFORMTYPE_VEC4] = 4; -uniformTypeToNumElements[UNIFORMTYPE_INT] = 1; -uniformTypeToNumElements[UNIFORMTYPE_IVEC2] = 2; -uniformTypeToNumElements[UNIFORMTYPE_IVEC3] = 3; -uniformTypeToNumElements[UNIFORMTYPE_IVEC4] = 4; -uniformTypeToNumElements[UNIFORMTYPE_BOOL] = 1; -uniformTypeToNumElements[UNIFORMTYPE_BVEC2] = 2; -uniformTypeToNumElements[UNIFORMTYPE_BVEC3] = 3; -uniformTypeToNumElements[UNIFORMTYPE_BVEC4] = 4; -uniformTypeToNumElements[UNIFORMTYPE_MAT2] = 8; // 2 x vec4 -uniformTypeToNumElements[UNIFORMTYPE_MAT3] = 12; // 3 x vec4 -uniformTypeToNumElements[UNIFORMTYPE_MAT4] = 16; // 4 x vec4 +// map of UNIFORMTYPE_*** to number of 32bit components +const uniformTypeToNumComponents = []; +uniformTypeToNumComponents[UNIFORMTYPE_FLOAT] = 1; +uniformTypeToNumComponents[UNIFORMTYPE_VEC2] = 2; +uniformTypeToNumComponents[UNIFORMTYPE_VEC3] = 3; +uniformTypeToNumComponents[UNIFORMTYPE_VEC4] = 4; +uniformTypeToNumComponents[UNIFORMTYPE_INT] = 1; +uniformTypeToNumComponents[UNIFORMTYPE_IVEC2] = 2; +uniformTypeToNumComponents[UNIFORMTYPE_IVEC3] = 3; +uniformTypeToNumComponents[UNIFORMTYPE_IVEC4] = 4; +uniformTypeToNumComponents[UNIFORMTYPE_BOOL] = 1; +uniformTypeToNumComponents[UNIFORMTYPE_BVEC2] = 2; +uniformTypeToNumComponents[UNIFORMTYPE_BVEC3] = 3; +uniformTypeToNumComponents[UNIFORMTYPE_BVEC4] = 4; +uniformTypeToNumComponents[UNIFORMTYPE_MAT2] = 8; // 2 x vec4 +uniformTypeToNumComponents[UNIFORMTYPE_MAT3] = 12; // 3 x vec4 +uniformTypeToNumComponents[UNIFORMTYPE_MAT4] = 16; // 4 x vec4 +uniformTypeToNumComponents[UNIFORMTYPE_UINT] = 1; +uniformTypeToNumComponents[UNIFORMTYPE_UVEC2] = 2; +uniformTypeToNumComponents[UNIFORMTYPE_UVEC3] = 3; +uniformTypeToNumComponents[UNIFORMTYPE_UVEC4] = 4; + /** * A class storing description of an individual uniform, stored inside a uniform buffer. @@ -60,6 +68,22 @@ class UniformFormat { */ count; + /** + * Number of components in each element (e.g. vec2 has 2 components, mat4 has 16 components) + * + * @type {number} + */ + numComponents; + + /** + * True if this is an array of elements (i.e. count > 0) + * + * @type {number} + */ + get isArrayType() { + return this.count > 0; + } + constructor(name, type, count = 0) { // just a name @@ -70,14 +94,33 @@ class UniformFormat { this.type = type; + this.numComponents = uniformTypeToNumComponents[type]; + Debug.assert(this.numComponents, `Unhandled uniform format ${type} used for ${name}`); + this.updateType = type; - if (count) { + if (count > 0) { switch (type) { case UNIFORMTYPE_FLOAT: this.updateType = UNIFORMTYPE_FLOATARRAY; break; + case UNIFORMTYPE_INT: this.updateType = UNIFORMTYPE_INTARRAY; break; + case UNIFORMTYPE_UINT: this.updateType = UNIFORMTYPE_UINTARRAY; break; + case UNIFORMTYPE_BOOL: this.updateType = UNIFORMTYPE_BOOLARRAY; break; + case UNIFORMTYPE_VEC2: this.updateType = UNIFORMTYPE_VEC2ARRAY; break; + case UNIFORMTYPE_IVEC2: this.updateType = UNIFORMTYPE_IVEC2ARRAY; break; + case UNIFORMTYPE_UVEC2: this.updateType = UNIFORMTYPE_UVEC2ARRAY; break; + case UNIFORMTYPE_BVEC2: this.updateType = UNIFORMTYPE_BVEC2ARRAY; break; + case UNIFORMTYPE_VEC3: this.updateType = UNIFORMTYPE_VEC3ARRAY; break; + case UNIFORMTYPE_IVEC3: this.updateType = UNIFORMTYPE_IVEC3ARRAY; break; + case UNIFORMTYPE_UVEC3: this.updateType = UNIFORMTYPE_UVEC3ARRAY; break; + case UNIFORMTYPE_BVEC3: this.updateType = UNIFORMTYPE_BVEC3ARRAY; break; + case UNIFORMTYPE_VEC4: this.updateType = UNIFORMTYPE_VEC4ARRAY; break; + case UNIFORMTYPE_IVEC4: this.updateType = UNIFORMTYPE_IVEC4ARRAY; break; + case UNIFORMTYPE_UVEC4: this.updateType = UNIFORMTYPE_UVEC4ARRAY; break; + case UNIFORMTYPE_BVEC4: this.updateType = UNIFORMTYPE_BVEC4ARRAY; break; + case UNIFORMTYPE_MAT4: this.updateType = UNIFORMTYPE_MAT4ARRAY; break; default: @@ -96,14 +139,14 @@ class UniformFormat { this.invalid = true; }); - let elementSize = uniformTypeToNumElements[type]; - Debug.assert(elementSize, `Unhandled uniform format ${type} used for ${name}`); + let componentSize = this.numComponents; - // element size for arrays is aligned up to vec4 - if (count) - elementSize = math.roundUp(elementSize, 4); + // component size for arrays is aligned up to vec4 + if (count) { + componentSize = math.roundUp(componentSize, 4); + } - this.byteSize = elementSize * 4; + this.byteSize = componentSize * 4; if (count) this.byteSize *= count; diff --git a/src/platform/graphics/uniform-buffer.js b/src/platform/graphics/uniform-buffer.js index 798e7fa075c..802702aa88f 100644 --- a/src/platform/graphics/uniform-buffer.js +++ b/src/platform/graphics/uniform-buffer.js @@ -4,7 +4,7 @@ import { UNIFORMTYPE_INT, UNIFORMTYPE_FLOAT, UNIFORMTYPE_VEC2, UNIFORMTYPE_VEC3, UNIFORMTYPE_VEC4, UNIFORMTYPE_IVEC2, UNIFORMTYPE_IVEC3, UNIFORMTYPE_IVEC4, UNIFORMTYPE_FLOATARRAY, UNIFORMTYPE_VEC2ARRAY, UNIFORMTYPE_VEC3ARRAY, - UNIFORMTYPE_MAT2, UNIFORMTYPE_MAT3 + UNIFORMTYPE_MAT2, UNIFORMTYPE_MAT3, UNIFORMTYPE_UINT, UNIFORMTYPE_UVEC2, UNIFORMTYPE_UVEC3, UNIFORMTYPE_UVEC4, UNIFORMTYPE_INTARRAY, UNIFORMTYPE_UINTARRAY, UNIFORMTYPE_BOOLARRAY, UNIFORMTYPE_IVEC2ARRAY, UNIFORMTYPE_IVEC3ARRAY, UNIFORMTYPE_UVEC2ARRAY, UNIFORMTYPE_UVEC3ARRAY, UNIFORMTYPE_BVEC2ARRAY, UNIFORMTYPE_BVEC3ARRAY } from './constants.js'; import { DynamicBufferAllocation } from './dynamic-buffers.js'; @@ -117,6 +117,83 @@ _updateFunctions[UNIFORMTYPE_VEC3ARRAY] = (uniformBuffer, value, offset, count) } }; +_updateFunctions[UNIFORMTYPE_UINT] = (uniformBuffer, value, offset, count) => { + const dst = uniformBuffer.storageUint32; + dst[offset] = value; +}; + +_updateFunctions[UNIFORMTYPE_UVEC2] = (uniformBuffer, value, offset, count) => { + const dst = uniformBuffer.storageUint32; + dst[offset] = value[0]; + dst[offset + 1] = value[1]; +}; + +_updateFunctions[UNIFORMTYPE_UVEC3] = (uniformBuffer, value, offset, count) => { + const dst = uniformBuffer.storageUint32; + dst[offset] = value[0]; + dst[offset + 1] = value[1]; + dst[offset + 2] = value[2]; +}; + +_updateFunctions[UNIFORMTYPE_UVEC4] = (uniformBuffer, value, offset, count) => { + const dst = uniformBuffer.storageUint32; + dst[offset] = value[0]; + dst[offset + 1] = value[1]; + dst[offset + 2] = value[2]; + dst[offset + 3] = value[3]; +}; + +_updateFunctions[UNIFORMTYPE_INTARRAY] = function (uniformBuffer, value, offset, count) { + const dst = uniformBuffer.storageInt32; + for (let i = 0; i < count; i++) { + dst[offset + i * 4] = value[i]; + } +}; +_updateFunctions[UNIFORMTYPE_BOOLARRAY] = _updateFunctions[UNIFORMTYPE_INTARRAY]; + +_updateFunctions[UNIFORMTYPE_UINTARRAY] = function (uniformBuffer, value, offset, count) { + const dst = uniformBuffer.storageUint32; + for (let i = 0; i < count; i++) { + dst[offset + i * 4] = value[i]; + } +}; + +_updateFunctions[UNIFORMTYPE_IVEC2ARRAY] = (uniformBuffer, value, offset, count) => { + const dst = uniformBuffer.storageInt32; + for (let i = 0; i < count; i++) { + dst[offset + i * 4] = value[i * 2]; + dst[offset + i * 4 + 1] = value[i * 2 + 1]; + } +}; +_updateFunctions[UNIFORMTYPE_BVEC2ARRAY] = _updateFunctions[UNIFORMTYPE_IVEC2ARRAY]; + +_updateFunctions[UNIFORMTYPE_UVEC2ARRAY] = (uniformBuffer, value, offset, count) => { + const dst = uniformBuffer.storageUint32; + for (let i = 0; i < count; i++) { + dst[offset + i * 4] = value[i * 2]; + dst[offset + i * 4 + 1] = value[i * 2 + 1]; + } +}; + +_updateFunctions[UNIFORMTYPE_IVEC3ARRAY] = (uniformBuffer, value, offset, count) => { + const dst = uniformBuffer.storageInt32; + for (let i = 0; i < count; i++) { + dst[offset + i * 4] = value[i * 3]; + dst[offset + i * 4 + 1] = value[i * 3 + 1]; + dst[offset + i * 4 + 2] = value[i * 3 + 2]; + } +}; +_updateFunctions[UNIFORMTYPE_BVEC3ARRAY] = _updateFunctions[UNIFORMTYPE_IVEC3ARRAY]; + +_updateFunctions[UNIFORMTYPE_UVEC3ARRAY] = (uniformBuffer, value, offset, count) => { + const dst = uniformBuffer.storageUint32; + for (let i = 0; i < count; i++) { + dst[offset + i * 4] = value[i * 3]; + dst[offset + i * 4 + 1] = value[i * 3 + 1]; + dst[offset + i * 4 + 2] = value[i * 3 + 2]; + } +}; + /** * A uniform buffer represents a GPU memory buffer storing the uniforms. * @@ -137,6 +214,9 @@ class UniformBuffer { /** @type {Int32Array} */ storageInt32; + /** @type {Uint32Array} */ + storageUint32; + /** * A render version used to track the last time the properties requiring bind group to be * updated were changed. @@ -204,6 +284,7 @@ class UniformBuffer { */ assignStorage(storage) { this.storageInt32 = storage; + this.storageUint32 = new Uint32Array(storage.buffer, storage.byteOffset, storage.byteLength / 4); this.storageFloat32 = new Float32Array(storage.buffer, storage.byteOffset, storage.byteLength / 4); } diff --git a/src/platform/graphics/webgl/webgl-graphics-device.js b/src/platform/graphics/webgl/webgl-graphics-device.js index 4848ea8d9fa..d759167a591 100644 --- a/src/platform/graphics/webgl/webgl-graphics-device.js +++ b/src/platform/graphics/webgl/webgl-graphics-device.js @@ -18,6 +18,11 @@ import { UNIFORMTYPE_BVEC3, UNIFORMTYPE_BVEC4, UNIFORMTYPE_MAT2, UNIFORMTYPE_MAT3, UNIFORMTYPE_MAT4, UNIFORMTYPE_TEXTURE2D, UNIFORMTYPE_TEXTURECUBE, UNIFORMTYPE_FLOATARRAY, UNIFORMTYPE_TEXTURE2D_SHADOW, UNIFORMTYPE_TEXTURECUBE_SHADOW, UNIFORMTYPE_TEXTURE3D, UNIFORMTYPE_VEC2ARRAY, UNIFORMTYPE_VEC3ARRAY, UNIFORMTYPE_VEC4ARRAY, + UNIFORMTYPE_UINT, UNIFORMTYPE_UVEC2, UNIFORMTYPE_UVEC3, UNIFORMTYPE_UVEC4, UNIFORMTYPE_ITEXTURE2D, UNIFORMTYPE_UTEXTURE2D, + UNIFORMTYPE_ITEXTURECUBE, UNIFORMTYPE_UTEXTURECUBE, UNIFORMTYPE_ITEXTURE3D, UNIFORMTYPE_UTEXTURE3D, UNIFORMTYPE_ITEXTURE2D_ARRAY, + UNIFORMTYPE_UTEXTURE2D_ARRAY, UNIFORMTYPE_INTARRAY, UNIFORMTYPE_UINTARRAY, UNIFORMTYPE_BOOLARRAY, UNIFORMTYPE_IVEC2ARRAY, + UNIFORMTYPE_BVEC2ARRAY, UNIFORMTYPE_UVEC2ARRAY, UNIFORMTYPE_IVEC3ARRAY, UNIFORMTYPE_BVEC3ARRAY, UNIFORMTYPE_UVEC3ARRAY, + UNIFORMTYPE_IVEC4ARRAY, UNIFORMTYPE_BVEC4ARRAY, UNIFORMTYPE_UVEC4ARRAY, UNIFORMTYPE_MAT4ARRAY, semanticToLocation, UNIFORMTYPE_TEXTURE2D_ARRAY, PRIMITIVE_TRISTRIP, @@ -562,11 +567,28 @@ class WebglGraphicsDevice extends GraphicsDevice { this.pcUniformType[gl.FLOAT_MAT4] = UNIFORMTYPE_MAT4; this.pcUniformType[gl.SAMPLER_2D] = UNIFORMTYPE_TEXTURE2D; this.pcUniformType[gl.SAMPLER_CUBE] = UNIFORMTYPE_TEXTURECUBE; + this.pcUniformType[gl.UNSIGNED_INT] = UNIFORMTYPE_UINT; + this.pcUniformType[gl.UNSIGNED_INT_VEC2] = UNIFORMTYPE_UVEC2; + this.pcUniformType[gl.UNSIGNED_INT_VEC3] = UNIFORMTYPE_UVEC3; + this.pcUniformType[gl.UNSIGNED_INT_VEC4] = UNIFORMTYPE_UVEC4; + if (this.isWebGL2) { this.pcUniformType[gl.SAMPLER_2D_SHADOW] = UNIFORMTYPE_TEXTURE2D_SHADOW; this.pcUniformType[gl.SAMPLER_CUBE_SHADOW] = UNIFORMTYPE_TEXTURECUBE_SHADOW; this.pcUniformType[gl.SAMPLER_2D_ARRAY] = UNIFORMTYPE_TEXTURE2D_ARRAY; this.pcUniformType[gl.SAMPLER_3D] = UNIFORMTYPE_TEXTURE3D; + + this.pcUniformType[gl.INT_SAMPLER_2D] = UNIFORMTYPE_ITEXTURE2D; + this.pcUniformType[gl.UNSIGNED_INT_SAMPLER_2D] = UNIFORMTYPE_UTEXTURE2D; + + this.pcUniformType[gl.INT_SAMPLER_CUBE] = UNIFORMTYPE_ITEXTURECUBE; + this.pcUniformType[gl.UNSIGNED_INT_SAMPLER_2D] = UNIFORMTYPE_UTEXTURECUBE; + + this.pcUniformType[gl.INT_SAMPLER_3D] = UNIFORMTYPE_ITEXTURE3D; + this.pcUniformType[gl.UNSIGNED_INT_SAMPLER_3D] = UNIFORMTYPE_UTEXTURE3D; + + this.pcUniformType[gl.INT_SAMPLER_2D_ARRAY] = UNIFORMTYPE_ITEXTURE2D_ARRAY; + this.pcUniformType[gl.UNSIGNED_INT_SAMPLER_2D_ARRAY] = UNIFORMTYPE_UTEXTURE2D_ARRAY; } this.targetToSlot = {}; @@ -688,6 +710,85 @@ class WebglGraphicsDevice extends GraphicsDevice { gl.uniform4fv(uniform.locationId, value); }; + this.commitFunction[UNIFORMTYPE_UINT] = function (uniform, value) { + if (uniform.value !== value) { + gl.uniform1ui(uniform.locationId, value); + uniform.value = value; + } + }; + this.commitFunction[UNIFORMTYPE_UVEC2] = function (uniform, value) { + uniformValue = uniform.value; + scopeX = value[0]; + scopeY = value[1]; + if (uniformValue[0] !== scopeX || uniformValue[1] !== scopeY) { + gl.uniform2uiv(uniform.locationId, value); + uniformValue[0] = scopeX; + uniformValue[1] = scopeY; + } + }; + this.commitFunction[UNIFORMTYPE_UVEC3] = function (uniform, value) { + uniformValue = uniform.value; + scopeX = value[0]; + scopeY = value[1]; + scopeZ = value[2]; + if (uniformValue[0] !== scopeX || uniformValue[1] !== scopeY || uniformValue[2] !== scopeZ) { + gl.uniform3uiv(uniform.locationId, value); + uniformValue[0] = scopeX; + uniformValue[1] = scopeY; + uniformValue[2] = scopeZ; + } + }; + this.commitFunction[UNIFORMTYPE_UVEC4] = function (uniform, value) { + uniformValue = uniform.value; + scopeX = value[0]; + scopeY = value[1]; + scopeZ = value[2]; + scopeW = value[3]; + if (uniformValue[0] !== scopeX || uniformValue[1] !== scopeY || uniformValue[2] !== scopeZ || uniformValue[3] !== scopeW) { + gl.uniform4uiv(uniform.locationId, value); + uniformValue[0] = scopeX; + uniformValue[1] = scopeY; + uniformValue[2] = scopeZ; + uniformValue[3] = scopeW; + } + }; + + this.commitFunction[UNIFORMTYPE_INTARRAY] = function (uniform, value) { + gl.uniform1iv(uniform.locationId, value); + }; + this.commitFunction[UNIFORMTYPE_UINTARRAY] = function (uniform, value) { + gl.uniform1uiv(uniform.locationId, value); + }; + this.commitFunction[UNIFORMTYPE_BOOLARRAY] = this.commitFunction[UNIFORMTYPE_INTARRAY]; + + this.commitFunction[UNIFORMTYPE_IVEC2ARRAY] = function (uniform, value) { + gl.uniform2iv(uniform.locationId, value); + }; + this.commitFunction[UNIFORMTYPE_UVEC2ARRAY] = function (uniform, value) { + gl.uniform2uiv(uniform.locationId, value); + }; + this.commitFunction[UNIFORMTYPE_BVEC2ARRAY] = this.commitFunction[UNIFORMTYPE_IVEC2ARRAY]; + + this.commitFunction[UNIFORMTYPE_IVEC3ARRAY] = function (uniform, value) { + gl.uniform3iv(uniform.locationId, value); + }; + this.commitFunction[UNIFORMTYPE_UVEC3ARRAY] = function (uniform, value) { + gl.uniform3uiv(uniform.locationId, value); + }; + this.commitFunction[UNIFORMTYPE_BVEC3ARRAY] = this.commitFunction[UNIFORMTYPE_IVEC3ARRAY]; + + this.commitFunction[UNIFORMTYPE_IVEC4ARRAY] = function (uniform, value) { + gl.uniform4iv(uniform.locationId, value); + }; + this.commitFunction[UNIFORMTYPE_UVEC4ARRAY] = function (uniform, value) { + gl.uniform4uiv(uniform.locationId, value); + }; + this.commitFunction[UNIFORMTYPE_BVEC4ARRAY] = this.commitFunction[UNIFORMTYPE_IVEC4ARRAY]; + + this.commitFunction[UNIFORMTYPE_MAT4ARRAY] = function (uniform, value) { + gl.uniformMatrix4fv(uniform.locationId, false, value); + }; + this.supportsBoneTextures = this.extTextureFloat && this.maxVertexTextures > 0; // Calculate an estimate of the maximum number of bones that can be uploaded to the GPU diff --git a/src/platform/graphics/webgl/webgl-shader-input.js b/src/platform/graphics/webgl/webgl-shader-input.js index 2dcbf4be691..f78673b5bc6 100644 --- a/src/platform/graphics/webgl/webgl-shader-input.js +++ b/src/platform/graphics/webgl/webgl-shader-input.js @@ -1,5 +1,5 @@ import { UNIFORMTYPE_FLOAT, UNIFORMTYPE_FLOATARRAY, UNIFORMTYPE_VEC2, UNIFORMTYPE_VEC3, UNIFORMTYPE_VEC4, - UNIFORMTYPE_VEC2ARRAY, UNIFORMTYPE_VEC3ARRAY, UNIFORMTYPE_VEC4ARRAY } from '../constants.js'; + UNIFORMTYPE_VEC2ARRAY, UNIFORMTYPE_VEC3ARRAY, UNIFORMTYPE_VEC4ARRAY, UNIFORMTYPE_INT, UNIFORMTYPE_INTARRAY, UNIFORMTYPE_UINT, UNIFORMTYPE_UINTARRAY, UNIFORMTYPE_BOOL, UNIFORMTYPE_BOOLARRAY, UNIFORMTYPE_IVEC2, UNIFORMTYPE_IVEC2ARRAY, UNIFORMTYPE_UVEC2, UNIFORMTYPE_UVEC2ARRAY, UNIFORMTYPE_BVEC2, UNIFORMTYPE_BVEC2ARRAY, UNIFORMTYPE_IVEC3, UNIFORMTYPE_UVEC3, UNIFORMTYPE_BVEC3, UNIFORMTYPE_IVEC4, UNIFORMTYPE_UVEC4, UNIFORMTYPE_BVEC4, UNIFORMTYPE_IVEC3ARRAY, UNIFORMTYPE_UVEC3ARRAY, UNIFORMTYPE_BVEC3ARRAY, UNIFORMTYPE_IVEC4ARRAY, UNIFORMTYPE_UVEC4ARRAY, UNIFORMTYPE_BVEC4ARRAY } from '../constants.js'; import { Version } from '../version.js'; /** @@ -31,9 +31,24 @@ class WebglShaderInput { if (name.substring(name.length - 3) === "[0]") { switch (type) { case UNIFORMTYPE_FLOAT: type = UNIFORMTYPE_FLOATARRAY; break; + case UNIFORMTYPE_INT: type = UNIFORMTYPE_INTARRAY; break; + case UNIFORMTYPE_UINT: type = UNIFORMTYPE_UINTARRAY; break; + case UNIFORMTYPE_BOOL: type = UNIFORMTYPE_BOOLARRAY; break; + case UNIFORMTYPE_VEC2: type = UNIFORMTYPE_VEC2ARRAY; break; + case UNIFORMTYPE_IVEC2: type = UNIFORMTYPE_IVEC2ARRAY; break; + case UNIFORMTYPE_UVEC2: type = UNIFORMTYPE_UVEC2ARRAY; break; + case UNIFORMTYPE_BVEC2: type = UNIFORMTYPE_BVEC2ARRAY; break; + case UNIFORMTYPE_VEC3: type = UNIFORMTYPE_VEC3ARRAY; break; + case UNIFORMTYPE_IVEC3: type = UNIFORMTYPE_IVEC3ARRAY; break; + case UNIFORMTYPE_UVEC3: type = UNIFORMTYPE_UVEC3ARRAY; break; + case UNIFORMTYPE_BVEC3: type = UNIFORMTYPE_BVEC3ARRAY; break; + case UNIFORMTYPE_VEC4: type = UNIFORMTYPE_VEC4ARRAY; break; + case UNIFORMTYPE_IVEC4: type = UNIFORMTYPE_IVEC4ARRAY; break; + case UNIFORMTYPE_UVEC4: type = UNIFORMTYPE_UVEC4ARRAY; break; + case UNIFORMTYPE_BVEC4: type = UNIFORMTYPE_BVEC4ARRAY; break; } } diff --git a/src/platform/graphics/webgl/webgl-shader.js b/src/platform/graphics/webgl/webgl-shader.js index c743c71496c..36bdd21d86f 100644 --- a/src/platform/graphics/webgl/webgl-shader.js +++ b/src/platform/graphics/webgl/webgl-shader.js @@ -361,9 +361,23 @@ class WebglShader { const shaderInput = new WebglShaderInput(device, info.name, device.pcUniformType[info.type], location); - if (info.type === gl.SAMPLER_2D || info.type === gl.SAMPLER_CUBE || - (device.isWebGL2 && (info.type === gl.SAMPLER_2D_SHADOW || info.type === gl.SAMPLER_CUBE_SHADOW || - info.type === gl.SAMPLER_3D || info.type === gl.SAMPLER_2D_ARRAY)) + if ( + info.type === gl.SAMPLER_2D || + info.type === gl.SAMPLER_CUBE || + ( + device.isWebGL2 && ( + info.type === gl.UNSIGNED_INT_SAMPLER_2D || + info.type === gl.INT_SAMPLER_2D || + info.type === gl.SAMPLER_2D_SHADOW || + info.type === gl.SAMPLER_CUBE_SHADOW || + info.type === gl.SAMPLER_3D || + info.type === gl.INT_SAMPLER_3D || + info.type === gl.UNSIGNED_INT_SAMPLER_3D || + info.type === gl.SAMPLER_2D_ARRAY || + info.type === gl.INT_SAMPLER_2D_ARRAY || + info.type === gl.UNSIGNED_INT_SAMPLER_2D_ARRAY + ) + ) ) { this.samplers.push(shaderInput); } else { diff --git a/src/platform/graphics/webgl/webgl-texture.js b/src/platform/graphics/webgl/webgl-texture.js index 366a3829004..c6599e0d419 100644 --- a/src/platform/graphics/webgl/webgl-texture.js +++ b/src/platform/graphics/webgl/webgl-texture.js @@ -7,7 +7,10 @@ import { PIXELFORMAT_DEPTHSTENCIL, PIXELFORMAT_111110F, PIXELFORMAT_SRGB, PIXELFORMAT_SRGBA, PIXELFORMAT_ETC1, PIXELFORMAT_ETC2_RGB, PIXELFORMAT_ETC2_RGBA, PIXELFORMAT_PVRTC_2BPP_RGB_1, PIXELFORMAT_PVRTC_2BPP_RGBA_1, PIXELFORMAT_PVRTC_4BPP_RGB_1, PIXELFORMAT_PVRTC_4BPP_RGBA_1, PIXELFORMAT_ASTC_4x4, PIXELFORMAT_ATC_RGB, - PIXELFORMAT_ATC_RGBA, PIXELFORMAT_BGRA8 + PIXELFORMAT_ATC_RGBA, PIXELFORMAT_BGRA8, PIXELFORMAT_R8I, PIXELFORMAT_R8U, PIXELFORMAT_R16I, PIXELFORMAT_R16U, + PIXELFORMAT_R32I, PIXELFORMAT_R32U, PIXELFORMAT_RG16I, PIXELFORMAT_RG16U, PIXELFORMAT_RG32I, PIXELFORMAT_RG32U, + PIXELFORMAT_RG8I, PIXELFORMAT_RG8U, PIXELFORMAT_RGBA16I, PIXELFORMAT_RGBA16U, PIXELFORMAT_RGBA32I, PIXELFORMAT_RGBA32U, + PIXELFORMAT_RGBA8I, PIXELFORMAT_RGBA8U } from '../constants.js'; /** @@ -279,6 +282,99 @@ class WebglTexture { this._glInternalFormat = gl.SRGB8_ALPHA8; this._glPixelType = gl.UNSIGNED_BYTE; break; + // Integer texture formats (R) (WebGL2 only) + case PIXELFORMAT_R8I: // WebGL2 only + this._glFormat = gl.RED_INTEGER; + this._glInternalFormat = gl.R8I; + this._glPixelType = gl.BYTE; + break; + case PIXELFORMAT_R8U: // WebGL2 only + this._glFormat = gl.RED_INTEGER; + this._glInternalFormat = gl.R8UI; + this._glPixelType = gl.UNSIGNED_BYTE; + break; + case PIXELFORMAT_R16I: // WebGL2 only + this._glFormat = gl.RED_INTEGER; + this._glInternalFormat = gl.R16I; + this._glPixelType = gl.SHORT; + break; + case PIXELFORMAT_R16U: // WebGL2 only + this._glFormat = gl.RED_INTEGER; + this._glInternalFormat = gl.R16UI; + this._glPixelType = gl.UNSIGNED_SHORT; + break; + case PIXELFORMAT_R32I: // WebGL2 only + this._glFormat = gl.RED_INTEGER; + this._glInternalFormat = gl.R32I; + this._glPixelType = gl.INT; + break; + case PIXELFORMAT_R32U: // WebGL2 only + this._glFormat = gl.RED_INTEGER; + this._glInternalFormat = gl.R32UI; + this._glPixelType = gl.UNSIGNED_INT; + break; + // Integer texture formats (RG) (WebGL2 only) + case PIXELFORMAT_RG8I: // WebGL2 only + this._glFormat = gl.RG_INTEGER; + this._glInternalFormat = gl.RG8I; + this._glPixelType = gl.BYTE; + break; + case PIXELFORMAT_RG8U: // WebGL2 only + this._glFormat = gl.RG_INTEGER; + this._glInternalFormat = gl.RG8UI; + this._glPixelType = gl.UNSIGNED_BYTE; + break; + case PIXELFORMAT_RG16I: // WebGL2 only + this._glFormat = gl.RG_INTEGER; + this._glInternalFormat = gl.RG16I; + this._glPixelType = gl.SHORT; + break; + case PIXELFORMAT_RG16U: // WebGL2 only + this._glFormat = gl.RG_INTEGER; + this._glInternalFormat = gl.RG16UI; + this._glPixelType = gl.UNSIGNED_SHORT; + break; + case PIXELFORMAT_RG32I: // WebGL2 only + this._glFormat = gl.RG_INTEGER; + this._glInternalFormat = gl.RG32I; + this._glPixelType = gl.INT; + break; + case PIXELFORMAT_RG32U: // WebGL2 only + this._glFormat = gl.RG_INTEGER; + this._glInternalFormat = gl.RG32UI; + this._glPixelType = gl.UNSIGNED_INT; + break; + // Integer texture formats (RGBA) (WebGL2 only) + case PIXELFORMAT_RGBA8I: // WebGL2 only + this._glFormat = gl.RGBA_INTEGER; + this._glInternalFormat = gl.RGBA8I; + this._glPixelType = gl.BYTE; + break; + case PIXELFORMAT_RGBA8U: // WebGL2 only + this._glFormat = gl.RGBA_INTEGER; + this._glInternalFormat = gl.RGBA8UI; + this._glPixelType = gl.UNSIGNED_BYTE; + break; + case PIXELFORMAT_RGBA16I: // WebGL2 only + this._glFormat = gl.RGBA_INTEGER; + this._glInternalFormat = gl.RGBA16I; + this._glPixelType = gl.SHORT; + break; + case PIXELFORMAT_RGBA16U: // WebGL2 only + this._glFormat = gl.RGBA_INTEGER; + this._glInternalFormat = gl.RGBA16UI; + this._glPixelType = gl.UNSIGNED_SHORT; + break; + case PIXELFORMAT_RGBA32I: // WebGL2 only + this._glFormat = gl.RGBA_INTEGER; + this._glInternalFormat = gl.RGBA32I; + this._glPixelType = gl.INT; + break; + case PIXELFORMAT_RGBA32U: // WebGL2 only + this._glFormat = gl.RGBA_INTEGER; + this._glInternalFormat = gl.RGBA32UI; + this._glPixelType = gl.UNSIGNED_INT; + break; case PIXELFORMAT_BGRA8: Debug.error("BGRA8 texture format is not supported by WebGL."); break; @@ -328,7 +424,7 @@ class WebglTexture { mipObject = texture._levels[mipLevel]; resMult = 1 / Math.pow(2, mipLevel); - if (mipLevel === 1 && !texture._compressed && texture._levels.length < requiredMipLevels) { + if (mipLevel === 1 && !texture._compressed && !texture._integerFormat && texture._levels.length < requiredMipLevels) { // We have more than one mip levels we want to assign, but we need all mips to make // the texture complete. Therefore first generate all mip chain from 0, then assign custom mips. // (this implies the call to _completePartialMipLevels above was unsuccessful) @@ -622,7 +718,7 @@ class WebglTexture { } } - if (!texture._compressed && texture._mipmaps && texture._needsMipmapsUpload && (texture.pot || device.isWebGL2) && texture._levels.length === 1) { + if (!texture._compressed && !texture._integerFormat && texture._mipmaps && texture._needsMipmapsUpload && (texture.pot || device.isWebGL2) && texture._levels.length === 1) { gl.generateMipmap(this._glTarget); texture._mipmapsUploaded = true; } diff --git a/src/platform/graphics/webgpu/constants.js b/src/platform/graphics/webgpu/constants.js index be49739ceec..da2299e51ec 100644 --- a/src/platform/graphics/webgpu/constants.js +++ b/src/platform/graphics/webgpu/constants.js @@ -5,7 +5,10 @@ import { PIXELFORMAT_DEPTHSTENCIL, PIXELFORMAT_111110F, PIXELFORMAT_SRGB, PIXELFORMAT_SRGBA, PIXELFORMAT_ETC1, PIXELFORMAT_ETC2_RGB, PIXELFORMAT_ETC2_RGBA, PIXELFORMAT_PVRTC_2BPP_RGB_1, PIXELFORMAT_PVRTC_2BPP_RGBA_1, PIXELFORMAT_PVRTC_4BPP_RGB_1, PIXELFORMAT_PVRTC_4BPP_RGBA_1, PIXELFORMAT_ASTC_4x4, PIXELFORMAT_ATC_RGB, - PIXELFORMAT_ATC_RGBA, PIXELFORMAT_BGRA8 + PIXELFORMAT_ATC_RGBA, PIXELFORMAT_BGRA8, PIXELFORMAT_R8I, PIXELFORMAT_R8U, PIXELFORMAT_R16I, PIXELFORMAT_R16U, + PIXELFORMAT_R32I, PIXELFORMAT_R32U, PIXELFORMAT_RG16I, PIXELFORMAT_RG16U, PIXELFORMAT_RG32I, PIXELFORMAT_RG32U, + PIXELFORMAT_RG8I, PIXELFORMAT_RG8U, PIXELFORMAT_RGBA16I, PIXELFORMAT_RGBA16U, PIXELFORMAT_RGBA32I, PIXELFORMAT_RGBA32U, + PIXELFORMAT_RGBA8I, PIXELFORMAT_RGBA8U } from '../constants.js'; // map of PIXELFORMAT_*** to GPUTextureFormat @@ -42,3 +45,21 @@ gpuTextureFormats[PIXELFORMAT_ASTC_4x4] = 'astc-4x4-unorm'; gpuTextureFormats[PIXELFORMAT_ATC_RGB] = ''; gpuTextureFormats[PIXELFORMAT_ATC_RGBA] = ''; gpuTextureFormats[PIXELFORMAT_BGRA8] = 'bgra8unorm'; +gpuTextureFormats[PIXELFORMAT_R8I] = 'r8sint'; +gpuTextureFormats[PIXELFORMAT_R8U] = 'r8uint'; +gpuTextureFormats[PIXELFORMAT_R16I] = 'r16sint'; +gpuTextureFormats[PIXELFORMAT_R16U] = 'r16uint'; +gpuTextureFormats[PIXELFORMAT_R32I] = 'r32sint'; +gpuTextureFormats[PIXELFORMAT_R32U] = 'r32uint'; +gpuTextureFormats[PIXELFORMAT_RG8I] = 'rg8sint'; +gpuTextureFormats[PIXELFORMAT_RG8U] = 'rg8uint'; +gpuTextureFormats[PIXELFORMAT_RG16I] = 'rg16sint'; +gpuTextureFormats[PIXELFORMAT_RG16U] = 'rg16uint'; +gpuTextureFormats[PIXELFORMAT_RG32I] = 'rg32sint'; +gpuTextureFormats[PIXELFORMAT_RG32U] = 'rg32uint'; +gpuTextureFormats[PIXELFORMAT_RGBA8I] = 'rgba8sint'; +gpuTextureFormats[PIXELFORMAT_RGBA8U] = 'rgba8uint'; +gpuTextureFormats[PIXELFORMAT_RGBA16I] = 'rgba16sint'; +gpuTextureFormats[PIXELFORMAT_RGBA16U] = 'rgba16uint'; +gpuTextureFormats[PIXELFORMAT_RGBA32I] = 'rgba32sint'; +gpuTextureFormats[PIXELFORMAT_RGBA32U] = 'rgba32uint'; diff --git a/src/platform/graphics/webgpu/webgpu-bind-group-format.js b/src/platform/graphics/webgpu/webgpu-bind-group-format.js index d741faab2c2..17446ceb53e 100644 --- a/src/platform/graphics/webgpu/webgpu-bind-group-format.js +++ b/src/platform/graphics/webgpu/webgpu-bind-group-format.js @@ -1,6 +1,6 @@ import { Debug, DebugHelper } from '../../../core/debug.js'; import { StringIds } from '../../../core/string-ids.js'; -import { SAMPLETYPE_FLOAT, SAMPLETYPE_UNFILTERABLE_FLOAT, SAMPLETYPE_DEPTH } from '../constants.js'; +import { SAMPLETYPE_FLOAT, SAMPLETYPE_UNFILTERABLE_FLOAT, SAMPLETYPE_DEPTH, SAMPLETYPE_INT, SAMPLETYPE_UINT } from '../constants.js'; import { WebgpuUtils } from './webgpu-utils.js'; import { gpuTextureFormats } from './constants.js'; @@ -10,10 +10,19 @@ samplerTypes[SAMPLETYPE_FLOAT] = 'filtering'; samplerTypes[SAMPLETYPE_UNFILTERABLE_FLOAT] = 'non-filtering'; samplerTypes[SAMPLETYPE_DEPTH] = 'comparison'; +// Using 'comparison' instead of 'non-filtering' may seem unusual, but currently we will get a +// validation error if we use 'non-filtering' along with texelFetch/textureLoad. 'comparison' works +// very well for the most common use-case of integer textures, texelFetch. We may be able to change +// how we initialize the sampler elsewhere to support 'non-filtering' in the future. +samplerTypes[SAMPLETYPE_INT] = 'comparison'; +samplerTypes[SAMPLETYPE_UINT] = 'comparison'; + const sampleTypes = []; sampleTypes[SAMPLETYPE_FLOAT] = 'float'; sampleTypes[SAMPLETYPE_UNFILTERABLE_FLOAT] = 'unfilterable-float'; sampleTypes[SAMPLETYPE_DEPTH] = 'depth'; +sampleTypes[SAMPLETYPE_INT] = 'sint'; +sampleTypes[SAMPLETYPE_UINT] = 'uint'; const stringIds = new StringIds(); diff --git a/src/platform/graphics/webgpu/webgpu-texture.js b/src/platform/graphics/webgpu/webgpu-texture.js index 81a17b7c037..705e0fa02cb 100644 --- a/src/platform/graphics/webgpu/webgpu-texture.js +++ b/src/platform/graphics/webgpu/webgpu-texture.js @@ -8,7 +8,7 @@ import { PIXELFORMAT_RGBA16F, PIXELFORMAT_RGBA32F, PIXELFORMAT_DEPTHSTENCIL, SAMPLETYPE_UNFILTERABLE_FLOAT, SAMPLETYPE_DEPTH, FILTER_NEAREST, FILTER_LINEAR, FILTER_NEAREST_MIPMAP_NEAREST, FILTER_NEAREST_MIPMAP_LINEAR, - FILTER_LINEAR_MIPMAP_NEAREST, FILTER_LINEAR_MIPMAP_LINEAR + FILTER_LINEAR_MIPMAP_NEAREST, FILTER_LINEAR_MIPMAP_LINEAR, isIntegerPixelFormat, SAMPLETYPE_INT, SAMPLETYPE_UINT } from '../constants.js'; import { TextureUtils } from '../texture-utils.js'; import { WebgpuDebug } from './webgpu-debug.js'; @@ -215,7 +215,7 @@ class WebgpuTexture { sampleType = SAMPLETYPE_DEPTH; } - if (sampleType === SAMPLETYPE_DEPTH) { + if (sampleType === SAMPLETYPE_DEPTH || sampleType === SAMPLETYPE_INT || sampleType === SAMPLETYPE_UINT) { // depth compare sampling descr.compare = 'less'; @@ -225,7 +225,7 @@ class WebgpuTexture { } else if (sampleType === SAMPLETYPE_UNFILTERABLE_FLOAT) { - // webgpu cannot currently filter float / half float textures + // webgpu cannot currently filter float / half float textures, or integer textures descr.magFilter = 'nearest'; descr.minFilter = 'nearest'; descr.mipmapFilter = 'nearest'; @@ -236,7 +236,7 @@ class WebgpuTexture { // TODO: this is temporary and needs to be made generic if (this.texture.format === PIXELFORMAT_RGBA32F || this.texture.format === PIXELFORMAT_DEPTHSTENCIL || - this.texture.format === PIXELFORMAT_RGBA16F) { + this.texture.format === PIXELFORMAT_RGBA16F || isIntegerPixelFormat(this.texture.format)) { descr.magFilter = 'nearest'; descr.minFilter = 'nearest'; descr.mipmapFilter = 'nearest'; diff --git a/src/scene/graphics/render-pass-shader-quad.js b/src/scene/graphics/render-pass-shader-quad.js index 74c26654cde..da78187ce02 100644 --- a/src/scene/graphics/render-pass-shader-quad.js +++ b/src/scene/graphics/render-pass-shader-quad.js @@ -89,10 +89,29 @@ class RenderPassShaderQuad extends RenderPass { return this._shader; } - createQuadShader(name, fs) { - return createShaderFromCode(this.device, RenderPassShaderQuad.quadVertexShader, fs, name, { - aPosition: SEMANTIC_POSITION - }); + /** + * Creates a quad shader from the supplied fragment shader code. + * + * @param {string} name - A name of the shader. + * @param {string} fs - Fragment shader source code. + * @param {object} [shaderDefinitionOptions] - Additional options that will be added to the + * shader definition. + * @param {boolean} [shaderDefinitionOptions.useTransformFeedback] - Whether to use transform + * feedback. Defaults to false. + * @param {string | string[]} [shaderDefinitionOptions.fragmentOutputTypes] - Fragment shader + * output types, which default to vec4. Passing a string will set the output type for all color + * attachments. Passing an array will set the output type for each color attachment. + * @returns {object} Returns the created shader. + */ + createQuadShader(name, fs, shaderDefinitionOptions = {}) { + return createShaderFromCode( + this.device, + RenderPassShaderQuad.quadVertexShader, + fs, + name, + { aPosition: SEMANTIC_POSITION }, + shaderDefinitionOptions + ); } destroy() { diff --git a/src/scene/shader-lib/utils.js b/src/scene/shader-lib/utils.js index 8ed0eedc013..b4be01c77e4 100644 --- a/src/scene/shader-lib/utils.js +++ b/src/scene/shader-lib/utils.js @@ -12,15 +12,35 @@ import { ShaderGenerator } from './programs/shader-generator.js'; * graphics device. * @param {string} vsName - The vertex shader chunk name. * @param {string} fsName - The fragment shader chunk name. - * @param {boolean} [useTransformFeedback] - Whether to use transform feedback. Defaults to false. + * @param {boolean | Record} [useTransformFeedback] - Whether + * to use transform feedback. Defaults to false. + * @param {object} [shaderDefinitionOptions] - Additional options that will be added to the shader + * definition. + * @param {boolean} [shaderDefinitionOptions.useTransformFeedback] - Whether to use transform + * feedback. Defaults to false. + * @param {string | string[]} [shaderDefinitionOptions.fragmentOutputTypes] - Fragment shader + * output types, which default to vec4. Passing a string will set the output type for all color + * attachments. Passing an array will set the output type for each color attachment. + * @see ShaderUtils.createDefinition * @returns {Shader} The newly created shader. */ -function createShader(device, vsName, fsName, useTransformFeedback = false) { +function createShader(device, vsName, fsName, useTransformFeedback = false, shaderDefinitionOptions = {}) { + + // Normalize arguments to allow passing shaderDefinitionOptions as the 6th argument + if (typeof useTransformFeedback === 'boolean') { + shaderDefinitionOptions.useTransformFeedback = useTransformFeedback; + } else if (typeof useTransformFeedback === 'object') { + shaderDefinitionOptions = { + ...shaderDefinitionOptions, + ...useTransformFeedback + }; + } + return new Shader(device, ShaderUtils.createDefinition(device, { + ...shaderDefinitionOptions, name: `${vsName}_${fsName}`, vertexCode: shaderChunks[vsName], - fragmentCode: shaderChunks[fsName], - useTransformFeedback: useTransformFeedback + fragmentCode: shaderChunks[fsName] })); } @@ -39,23 +59,42 @@ function createShader(device, vsName, fsName, useTransformFeedback = false) { * @param {Object} [attributes] - Object detailing the mapping of vertex shader * attribute names to semantics SEMANTIC_*. This enables the engine to match vertex buffer data as * inputs to the shader. Defaults to undefined, which generates the default attributes. - * @param {boolean} [useTransformFeedback] - Whether to use transform feedback. Defaults to false. + * @param {boolean | Record} [useTransformFeedback] - Whether + * to use transform feedback. Defaults to false. + * @param {object} [shaderDefinitionOptions] - Additional options that will be added to the shader + * definition. + * @param {boolean} [shaderDefinitionOptions.useTransformFeedback] - Whether to use transform + * feedback. Defaults to false. + * @param {string | string[]} [shaderDefinitionOptions.fragmentOutputTypes] - Fragment shader + * output types, which default to vec4. Passing a string will set the output type for all color + * attachments. Passing an array will set the output type for each color attachment. + * @see ShaderUtils.createDefinition * @returns {Shader} The newly created shader. */ -function createShaderFromCode(device, vsCode, fsCode, uniqueName, attributes, useTransformFeedback = false) { +function createShaderFromCode(device, vsCode, fsCode, uniqueName, attributes, useTransformFeedback = false, shaderDefinitionOptions = {}) { // the function signature has changed, fail if called incorrectly Debug.assert(typeof attributes !== 'boolean'); + // Normalize arguments to allow passing shaderDefinitionOptions as the 6th argument + if (typeof useTransformFeedback === 'boolean') { + shaderDefinitionOptions.useTransformFeedback = useTransformFeedback; + } else if (typeof useTransformFeedback === 'object') { + shaderDefinitionOptions = { + ...shaderDefinitionOptions, + ...useTransformFeedback + }; + } + const programLibrary = getProgramLibrary(device); let shader = programLibrary.getCachedShader(uniqueName); if (!shader) { shader = new Shader(device, ShaderUtils.createDefinition(device, { + ...shaderDefinitionOptions, name: uniqueName, vertexCode: vsCode, fragmentCode: fsCode, - attributes: attributes, - useTransformFeedback: useTransformFeedback + attributes: attributes })); programLibrary.setCachedShader(uniqueName, shader); }