Repository: playcanvas/engine
Pull request #7134 (2024-11-22 15:29:47)

WebGPU support for ClipDistances feature
see https://developer.chrome.com/blog/new-in-webgpu-131#clip_distances_in_wgsl

when the feature is supported, request it, allowing users to use it.

availability exposed in public API:
```
GraphicsDevice.supportsClipDistances 
```


Patch:
diff --git a/src/core/preprocessor.js b/src/core/preprocessor.js
index 3f5f271e6..bd8871a36 100644
--- a/src/core/preprocessor.js
+++ b/src/core/preprocessor.js
@@ -30,8 +30,11 @@ const IDENTIFIER = /([\w-]+)/;
 // [!]defined(EXPRESSION)
 const DEFINED = /(!|\s)?defined\(([\w-]+)\)/;
 
+// Matches comparison operators like ==, !=, <, <=, >, >=
+const COMPARISON = /([a-z_]\w*)\s*(==|!=|<|<=|>|>=)\s*([\w"']+)/i;
+
 // currently unsupported characters in the expression: | & < > = + -
-const INVALID = /[><=|&+-]/g;
+const INVALID = /[|&+-]/g;
 
 // #include "identifier"
 const INCLUDE = /include[ \t]+"([\w-]+)"\r?(?:\n|$)/g;
@@ -374,6 +377,7 @@ class Preprocessor {
      * - expression
      * - defined(expression)
      * - !defined(expression)
+     * - simple comparisons like "XX == 3" or "XX != test"
      *
      * But does not handle more complex cases, which would require more complex system:
      *
@@ -388,7 +392,7 @@ class Preprocessor {
         const correct = INVALID.exec(expression) === null;
         Debug.assert(correct, `Resolving expression like this is not supported: ${expression}`);
 
-        // if the format is defined(expression), extract expression
+        // if the format is 'defined(expression)', extract expression
         let invert = false;
         const defined = DEFINED.exec(expression);
         if (defined) {
@@ -396,6 +400,29 @@ class Preprocessor {
             expression = defined[2];
         }
 
+        // if the expression is a comparison, evaluate it
+        const comparison = COMPARISON.exec(expression);
+        if (comparison) {
+            const left = defines.get(comparison[1]) ?? comparison[1];
+            const right = defines.get(comparison[3]) ?? comparison[3];
+            const operator = comparison[2];
+
+            let result = false;
+            switch (operator) {
+                case '==': result = left === right; break;
+                case '!=': result = left !== right; break;
+                case '<': result = left < right; break;
+                case '<=': result = left <= right; break;
+                case '>': result = left > right; break;
+                case '>=': result = left >= right; break;
+            }
+
+            return {
+                result,
+                error: !correct
+            };
+        }
+
         // test if expression define exists
         expression = expression.trim();
         let exists = defines.has(expression);
diff --git a/src/framework/xr/xr-joint.js b/src/framework/xr/xr-joint.js
index 35d980042..4d98aa89b 100644
--- a/src/framework/xr/xr-joint.js
+++ b/src/framework/xr/xr-joint.js
@@ -35,7 +35,7 @@ class XrJoint {
     _index;
 
     /**
-     * @type {string}
+     * @type {XRHandJoint}
      * @private
      */
     _id;
@@ -47,7 +47,7 @@ class XrJoint {
     _hand;
 
     /**
-     * @type {XrFinger}
+     * @type {XrFinger|null}
      * @private
      */
     _finger;
@@ -65,7 +65,7 @@ class XrJoint {
     _tip;
 
     /**
-     * @type {number}
+     * @type {number|null}
      * @private
      */
     _radius = null;
@@ -116,7 +116,7 @@ class XrJoint {
      * Create an XrJoint instance.
      *
      * @param {number} index - Index of a joint within a finger.
-     * @param {string} id - Id of a joint based on WebXR Hand Input Specs.
+     * @param {XRHandJoint} id - Id of a joint based on WebXR Hand Input Specs.
      * @param {XrHand} hand - Hand that joint relates to.
      * @param {XrFinger|null} finger - Finger that joint is related to. Can be null in the case of
      * the wrist joint.
@@ -181,6 +181,15 @@ class XrJoint {
         return this._rotation;
     }
 
+    /**
+     * Id of a joint based on WebXR Hand Input Specs.
+     *
+     * @type {XRHandJoint}
+     */
+    get id() {
+        return this._id;
+    }
+
     /**
      * Index of a joint within a finger, starting from 0 (root of a finger) all the way to tip of
      * the finger.
diff --git a/src/platform/graphics/graphics-device.js b/src/platform/graphics/graphics-device.js
index 11abc4388..6e9bc869b 100644
--- a/src/platform/graphics/graphics-device.js
+++ b/src/platform/graphics/graphics-device.js
@@ -275,6 +275,14 @@ class GraphicsDevice extends EventHandler {
      */
     supportsUniformBuffers = false;
 
+    /**
+     * True if the device supports clip distances (WebGPU only). Clip distances allow you to restrict
+     * primitives' clip volume with user-defined half-spaces in the output of vertex stage.
+     *
+     * @type {boolean}
+     */
+    supportsClipDistances = false;
+
     /**
      * True if 32-bit floating-point textures can be used as a frame buffer.
      *
diff --git a/src/platform/graphics/webgpu/webgpu-graphics-device.js b/src/platform/graphics/webgpu/webgpu-graphics-device.js
index 1841c9c8f..449b0b090 100644
--- a/src/platform/graphics/webgpu/webgpu-graphics-device.js
+++ b/src/platform/graphics/webgpu/webgpu-graphics-device.js
@@ -210,7 +210,6 @@ class WebgpuGraphicsDevice extends GraphicsDevice {
          */
         this.gpuAdapter = await window.navigator.gpu.requestAdapter(adapterOptions);
 
-
         // request optional features
         const requiredFeatures = [];
         const requireFeature = (feature) => {
@@ -231,6 +230,7 @@ class WebgpuGraphicsDevice extends GraphicsDevice {
         this.supportsShaderF16 = requireFeature('shader-f16');
         this.supportsStorageRGBA8 = requireFeature('bgra8unorm-storage');
         this.textureRG11B10Renderable = requireFeature('rg11b10ufloat-renderable');
+        this.supportsClipDistances = requireFeature('clip-distances');
         Debug.log(`WEBGPU features: ${requiredFeatures.join(', ')}`);
 
         // copy all adapter limits to the requiredLimits object - to created a device with the best feature sets available


Test patch:
diff --git a/test/core/preprocessor.test.mjs b/test/core/preprocessor.test.mjs
index 79503ca4b..d722980cc 100644
--- a/test/core/preprocessor.test.mjs
+++ b/test/core/preprocessor.test.mjs
@@ -73,6 +73,28 @@ describe('Preprocessor', function () {
         #ifdef (UNKNOWN)
             #define TEST14  // this should not be defined
         #endif
+
+        #define INDEX 3
+        #if INDEX == 3
+            CMP1
+        #endif
+
+        #if INDEX != 3
+            CMP2
+        #endif
+
+        #if INDEX > 2
+            CMP3
+        #endif
+
+        #define NAME hello
+        #if NAME == hello
+            CMP4
+        #endif
+
+        #if NAME != hello
+            CMP5
+        #endif
     `;
 
     it('returns false for MORPH_A', function () {
@@ -154,4 +176,25 @@ describe('Preprocessor', function () {
     it('returns true for nested', function () {
         expect(Preprocessor.run(srcData, includes).includes('nested')).to.equal(true);
     });
+
+    it('returns true for CMP1', function () {
+        expect(Preprocessor.run(srcData, includes).includes('CMP1')).to.equal(true);
+    });
+
+    it('returns false for CMP2', function () {
+        expect(Preprocessor.run(srcData, includes).includes('CMP2')).to.equal(false);
+    });
+
+    it('returns true for CMP3', function () {
+        expect(Preprocessor.run(srcData, includes).includes('CMP3')).to.equal(true);
+    });
+
+    it('returns true for CMP4', function () {
+        expect(Preprocessor.run(srcData, includes).includes('CMP4')).to.equal(true);
+    });
+
+    it('returns false for CMP5', function () {
+        expect(Preprocessor.run(srcData, includes).includes('CMP5')).to.equal(false);
+    });
+
 });