Repository: playcanvas/engine
Pull request #7386 (2025-02-27 12:25:03)

Adjusted syntax of variables the preprocessor uses for injection
use curly brackets around the define variable name to mark it for injection into shader code (we resolve it before passing it to platform compilers)

Before, the define had to be named
```
define _INJECT_COUNT 4
```
and the code using it would be 
```
for (int i = 0; i < _INJECT_COUNT; i++)
```

now we have
```
define {COUNT} 4
```
and the code using it would be 
```
for (int i = 0; i < {COUNT}; i++)
```


Patch:
diff --git a/src/core/preprocessor.js b/src/core/preprocessor.js
index 131565090..e56d50089 100644
--- a/src/core/preprocessor.js
+++ b/src/core/preprocessor.js
@@ -24,8 +24,8 @@ const IF = /(ifdef|ifndef|if)[ \t]*([^\r\n]+)\r?\n/g;
 // #endif/#else or #elif EXPRESSION
 const ENDIF = /(endif|else|elif)(?:[ \t]+([^\r\n]*))?\r?\n?/g;
 
-// identifier
-const IDENTIFIER = /([\w-]+)/;
+// identifier in form of IDENTIFIER or {IDENTIFIER}
+const IDENTIFIER = /\{?[\w-]+\}?/;
 
 // [!]defined(EXPRESSION)
 const DEFINED = /(!|\s)?defined\(([\w-]+)\)/;
@@ -94,8 +94,11 @@ class Preprocessor {
             });
         }
 
+        // extracted defines with name in {} which are to be replaced with their values
+        const injectDefines = new Map();
+
         // preprocess defines / ifdefs ..
-        source = this._preprocess(source, defines, includes, options.stripDefines);
+        source = this._preprocess(source, defines, injectDefines, includes, options.stripDefines);
 
         // extract defines that evaluate to an integer number
         const intDefines = new Map();
@@ -105,14 +108,6 @@ class Preprocessor {
             }
         });
 
-        // extract defines with name starting with _INJECT_
-        const injectDefines = new Map();
-        defines.forEach((value, key) => {
-            if (key.startsWith('_INJECT_')) {
-                injectDefines.set(key, value);
-            }
-        });
-
         // strip comments again after the includes have been resolved
         source = this.stripComments(source);
 
@@ -190,12 +185,14 @@ class Preprocessor {
      * @param {Map<string, string>} defines - Supplied defines which are used in addition to those
      * defined in the source code. Maps a define name to its value. Note that the map is modified
      * by the function.
+     * @param {Map<string, string>} injectDefines - An object to collect defines that are to be
+     * replaced with their values.
      * @param {Map<string, string>} [includes] - An object containing key-value pairs of include names and their
      * content.
      * @param {boolean} [stripDefines] - If true, strips all defines from the source.
      * @returns {string} Returns preprocessed source code.
      */
-    static _preprocess(source, defines = new Map(), includes, stripDefines) {
+    static _preprocess(source, defines = new Map(), injectDefines, includes, stripDefines) {
 
         const originalSource = source;
 
@@ -222,17 +219,29 @@ class Preprocessor {
                     // split it to identifier name and a value
                     IDENTIFIER.lastIndex = define.index;
                     const identifierValue = IDENTIFIER.exec(expression);
-                    const identifier = identifierValue[1];
+                    const identifier = identifierValue[0];
                     let value = expression.substring(identifier.length).trim();
                     if (value === '') value = 'true';
 
                     // are we inside if-blocks that are accepted
                     const keep = Preprocessor._keep(stack);
+                    let stripThisDefine = stripDefines;
 
                     if (keep) {
-                        defines.set(identifier, value);
 
-                        if (stripDefines) {
+                        // replacement identifier (inside {}) - always remove it from code
+                        const replacementDefine = identifier.startsWith('{') && identifier.endsWith('}');
+                        if (replacementDefine) {
+                            stripThisDefine = true;
+                        }
+
+                        if (replacementDefine) {
+                            injectDefines.set(identifier, value);
+                        } else {
+                            defines.set(identifier, value);
+                        }
+
+                        if (stripThisDefine) {
                             // cut out the define line
                             source = source.substring(0, define.index - 1) + source.substring(DEFINE.lastIndex);
 
@@ -244,7 +253,7 @@ class Preprocessor {
                     Debug.trace(TRACEID, `${keyword}: [${identifier}] ${value} ${keep ? '' : 'IGNORED'}`);
 
                     // continue on the next line
-                    if (!stripDefines) {
+                    if (!stripThisDefine) {
                         KEYWORD.lastIndex = define.index + define[0].length;
                     }
                     break;
diff --git a/src/scene/shader-lib/chunks-wgsl/skybox/frag/skybox.js b/src/scene/shader-lib/chunks-wgsl/skybox/frag/skybox.js
index b966cd25e..62b7e46d8 100644
--- a/src/scene/shader-lib/chunks-wgsl/skybox/frag/skybox.js
+++ b/src/scene/shader-lib/chunks-wgsl/skybox/frag/skybox.js
@@ -47,13 +47,13 @@ export default /* wgsl */`
             #endif
 
             dir.x *= -1.0;
-            linear = _INJECT_SKYBOX_DECODE_FNC(textureSample(texture_cubeMap, texture_cubeMap_sampler, dir));
+            linear = {SKYBOX_DECODE_FNC}(textureSample(texture_cubeMap, texture_cubeMap_sampler, dir));
 
         #else // env-atlas
 
             dir = input.vViewDir * vec3f(-1.0, 1.0, 1.0);
             let uv : vec2f = toSphericalUv(normalize(dir));
-            linear = _INJECT_SKYBOX_DECODE_FNC(textureSample(texture_envAtlas, texture_envAtlas_sampler, mapRoughnessUv(uv, uniform.mipLevel)));
+            linear = {SKYBOX_DECODE_FNC}(textureSample(texture_envAtlas, texture_envAtlas_sampler, mapRoughnessUv(uv, uniform.mipLevel)));
 
         #endif
 
diff --git a/src/scene/shader-lib/chunks/lit/frag/clusteredLight.js b/src/scene/shader-lib/chunks/lit/frag/clusteredLight.js
index 76cf5b1db..67032106e 100644
--- a/src/scene/shader-lib/chunks/lit/frag/clusteredLight.js
+++ b/src/scene/shader-lib/chunks/lit/frag/clusteredLight.js
@@ -98,25 +98,51 @@ struct ClusterLightData {
 mat4 lightProjectionMatrix;
 
 // macros for light properties
-#define isClusteredLightCastShadow(light) ( light.shadowIntensity > 0.0 )
-#define isClusteredLightCookie(light) (light.cookie > 0.5 )
-#define isClusteredLightCookieRgb(light) (light.cookieRgb > 0.5 )
-#define isClusteredLightSpot(light) ( light.lightType > 0.5 )
-#define isClusteredLightFalloffLinear(light) ( light.falloffMode < 0.5 )
+bool isClusteredLightCastShadow(in ClusterLightData light) {
+  return light.shadowIntensity > 0.0;
+}
+
+bool isClusteredLightCookie(in ClusterLightData light) {
+  return light.cookie > 0.5;
+}
+
+bool isClusteredLightCookieRgb(in ClusterLightData light) {
+  return light.cookieRgb > 0.5;
+}
+
+bool isClusteredLightSpot(in ClusterLightData light) {
+  return light.lightType > 0.5;
+}
+
+bool isClusteredLightFalloffLinear(in ClusterLightData light) {
+  return light.falloffMode < 0.5;
+}
 
 // macros to test light shape
 // Note: Following functions need to be called serially in listed order as they do not test both '>' and '<'
-#define isClusteredLightArea(light) ( light.shape > 0.1 )
-#define isClusteredLightRect(light) ( light.shape < 0.3 )
-#define isClusteredLightDisk(light) ( light.shape < 0.6 )
+bool isClusteredLightArea(in ClusterLightData light) {
+  return light.shape > 0.1;
+}
+
+bool isClusteredLightRect(in ClusterLightData light) {
+  return light.shape < 0.3;
+}
+
+bool isClusteredLightDisk(in ClusterLightData light) {
+  return light.shape < 0.6;
+}
 
 // macro to test light mask (mesh accepts dynamic vs lightmapped lights)
 #ifdef CLUSTER_MESH_DYNAMIC_LIGHTS
     // accept lights marked as dynamic or both dynamic and lightmapped
-    #define acceptLightMask(light) ( light.mask < 0.75)
+    bool acceptLightMask(in ClusterLightData light) {
+        return light.mask < 0.75;
+    }
 #else
     // accept lights marked as lightmapped or both dynamic and lightmapped
-    #define acceptLightMask(light) ( light.mask > 0.25)
+    bool acceptLightMask(in ClusterLightData light) {
+        return light.mask > 0.25;
+    }
 #endif
 
 vec4 decodeClusterLowRange4Vec4(vec4 d0, vec4 d1, vec4 d2, vec4 d3) {
diff --git a/src/scene/shader-lib/chunks/lit/vert/uvTransform.js b/src/scene/shader-lib/chunks/lit/vert/uvTransform.js
index 06957fdbd..109870cff 100644
--- a/src/scene/shader-lib/chunks/lit/vert/uvTransform.js
+++ b/src/scene/shader-lib/chunks/lit/vert/uvTransform.js
@@ -1,7 +1,7 @@
 // chunk that generates uv coordinate transformed by uv transform matrix
 export default /* glsl */`
-vUV_INJECT_TRANSFORM_UV_{i}__INJECT_TRANSFORM_ID_{i} = vec2(
-    dot(vec3(uv_INJECT_TRANSFORM_UV_{i}, 1), _INJECT_TRANSFORM_NAME_{i}0),
-    dot(vec3(uv_INJECT_TRANSFORM_UV_{i}, 1), _INJECT_TRANSFORM_NAME_{i}1)
+vUV{TRANSFORM_UV_{i}}_{TRANSFORM_ID_{i}} = vec2(
+    dot(vec3(uv{TRANSFORM_UV_{i}}, 1), {TRANSFORM_NAME_{i}}0),
+    dot(vec3(uv{TRANSFORM_UV_{i}}, 1), {TRANSFORM_NAME_{i}}1)
 );
 `;
diff --git a/src/scene/shader-lib/chunks/lit/vert/uvTransformUniforms.js b/src/scene/shader-lib/chunks/lit/vert/uvTransformUniforms.js
index 7f5212f7d..ef9b5cdd9 100644
--- a/src/scene/shader-lib/chunks/lit/vert/uvTransformUniforms.js
+++ b/src/scene/shader-lib/chunks/lit/vert/uvTransformUniforms.js
@@ -1,5 +1,5 @@
 // uniform declaration for uv transform matrix, 3x2 matrix
 export default /* glsl */`
-    uniform vec3 _INJECT_TRANSFORM_NAME_{i}0;
-    uniform vec3 _INJECT_TRANSFORM_NAME_{i}1;
+    uniform vec3 {TRANSFORM_NAME_{i}}0;
+    uniform vec3 {TRANSFORM_NAME_{i}}1;
 `;
diff --git a/src/scene/shader-lib/chunks/skybox/frag/skybox.js b/src/scene/shader-lib/chunks/skybox/frag/skybox.js
index a84bebcd5..0eec97b63 100644
--- a/src/scene/shader-lib/chunks/skybox/frag/skybox.js
+++ b/src/scene/shader-lib/chunks/skybox/frag/skybox.js
@@ -43,14 +43,14 @@ export default /* glsl */`
             #endif
 
             dir.x *= -1.0;
-            vec3 linear = _INJECT_SKYBOX_DECODE_FNC(textureCube(texture_cubeMap, dir));
+            vec3 linear = {SKYBOX_DECODE_FNC}(textureCube(texture_cubeMap, dir));
 
         #else // env-atlas
 
             vec3 dir = vViewDir * vec3(-1.0, 1.0, 1.0);
             vec2 uv = toSphericalUv(normalize(dir));
 
-            vec3 linear = _INJECT_SKYBOX_DECODE_FNC(texture2D(texture_envAtlas, mapRoughnessUv(uv, mipLevel)));
+            vec3 linear = {SKYBOX_DECODE_FNC}(texture2D(texture_envAtlas, mapRoughnessUv(uv, mipLevel)));
 
         #endif
 
diff --git a/src/scene/shader-lib/programs/lit-shader.js b/src/scene/shader-lib/programs/lit-shader.js
index c1fe6c5b6..0419da072 100644
--- a/src/scene/shader-lib/programs/lit-shader.js
+++ b/src/scene/shader-lib/programs/lit-shader.js
@@ -256,9 +256,9 @@ class LitShader {
 
                 // defines used by the included chunks
                 const varName = `texture_${name}MapTransform`;
-                vDefines.set(`_INJECT_TRANSFORM_NAME_${numTransforms}`, varName);
-                vDefines.set(`_INJECT_TRANSFORM_UV_${numTransforms}`, uv);
-                vDefines.set(`_INJECT_TRANSFORM_ID_${numTransforms}`, id);
+                vDefines.set(`{TRANSFORM_NAME_${numTransforms}}`, varName);
+                vDefines.set(`{TRANSFORM_UV_${numTransforms}}`, uv);
+                vDefines.set(`{TRANSFORM_ID_${numTransforms}}`, id);
 
                 numTransforms++;
             }
diff --git a/src/scene/skybox/sky-mesh.js b/src/scene/skybox/sky-mesh.js
index f7cc44c5f..8582ac457 100644
--- a/src/scene/skybox/sky-mesh.js
+++ b/src/scene/skybox/sky-mesh.js
@@ -46,7 +46,7 @@ class SkyMesh {
         });
 
         // defines
-        material.setDefine('_INJECT_SKYBOX_DECODE_FNC', ChunkUtils.decodeFunc(texture.encoding));
+        material.setDefine('{SKYBOX_DECODE_FNC}', ChunkUtils.decodeFunc(texture.encoding));
         if (type !== SKYTYPE_INFINITE) material.setDefine('SKYMESH', '');
         if (texture.cubemap) material.setDefine('SKY_CUBEMAP', '');
 


Test patch:
diff --git a/test/core/preprocessor.test.mjs b/test/core/preprocessor.test.mjs
index a70232948..59664d0c1 100644
--- a/test/core/preprocessor.test.mjs
+++ b/test/core/preprocessor.test.mjs
@@ -18,8 +18,8 @@ describe('Preprocessor', function () {
     const srcData = `
         
         #define LOOP_COUNT 3
-        #define _INJECT_COUNT 2
-        #define _INJECT_STRING hello
+        #define {COUNT} 2
+        #define {STRING} hello
         #define FEATURE1
         #define FEATURE2
         #define AND1
@@ -126,8 +126,8 @@ describe('Preprocessor', function () {
             CMP5
         #endif
 
-        TESTINJECTION _INJECT_COUNT
-        INJECTSTRING _INJECT_STRING(x)
+        TESTINJECTION {COUNT}
+        INJECTSTRING {STRING}(x)
     `;
 
     it('returns false for MORPH_A', function () {