Repository: playcanvas/engine
Pull request #7346 (2025-02-12 13:47:40)

Processor handles loop of includes
Includes support loop, with an index injection into the included text. This will be used for shader generation of looped objects, for example lights.

This syntax can be now used:
```
#include "lightChunk, LIGHTCOUNT"
```

from JS, it can be driven this way:
```
defines.set('LIGHTCOUNT', lightCount);
for (i = 0, i < lightCount; i++) {
	defines.set(`LIGHTTYPE${i}`, 'omni');     // #define LIGHTTYPE0 omni
```

lightChunk example (part that gets included). All instances of `{i}` are replaced by the loop index:
```
#ifdef LIGHTTYPE{i} == omni
	uniform vec3 light{i}Color
#endif

```



Patch:
diff --git a/src/core/preprocessor.js b/src/core/preprocessor.js
index de80adeae..e4ddffeb4 100644
--- a/src/core/preprocessor.js
+++ b/src/core/preprocessor.js
@@ -36,8 +36,11 @@ const COMPARISON = /([a-z_]\w*)\s*(==|!=|<|<=|>|>=)\s*([\w"']+)/i;
 // currently unsupported characters in the expression: | & < > = + -
 const INVALID = /[|&+-]/g;
 
-// #include "identifier"
-const INCLUDE = /include[ \t]+"([\w-]+)"\r?(?:\n|$)/g;
+// #include "identifier" or optional second identifier #include "identifier1, identifier2"
+const INCLUDE = /include[ \t]+"([\w-]+)(?:\s*,\s*([\w-]+))?"\r?(?:\n|$)/g;
+
+// loop index to replace, in the format {i}
+const LOOP_INDEX = /\{i\}/g;
 
 /**
  * Pure static class implementing subset of C-style preprocessor.
@@ -388,6 +391,7 @@ class Preprocessor {
                     error ||= include === null;
                     Debug.assert(include, `Invalid [${keyword}]: ${source.substring(match.index, match.index + 100)}...`);
                     const identifier = include[1].trim();
+                    const countIdentifier = include[2]?.trim();
 
                     // are we inside if-blocks that are accepted
                     const keep = Preprocessor._keep(stack);
@@ -395,8 +399,29 @@ class Preprocessor {
                     if (keep) {
 
                         // cut out the include line and replace it with the included string
-                        const includeSource = includes?.get(identifier);
+                        let includeSource = includes?.get(identifier);
                         if (includeSource !== undefined) {
+
+                            // handle second identifier specifying loop count
+                            if (countIdentifier) {
+                                const countString = defines.get(countIdentifier);
+                                const count = parseFloat(countString);
+                                if (Number.isInteger(count)) {
+
+                                    // add the include count times
+                                    let result = '';
+                                    for (let i = 0; i < count; i++) {
+                                        result += includeSource.replace(LOOP_INDEX, String(i));
+                                    }
+                                    includeSource = result;
+
+                                } else {
+                                    console.error(`Include Count identifier "${countIdentifier}" not resolved while preprocessing ${Preprocessor.sourceName} on line: ${source.substring(match.index, match.index + 100)}...`, { source: originalSource });
+                                    error = true;
+                                }
+                            }
+
+                            // replace the include by the included string
                             source = source.substring(0, include.index - 1) + includeSource + source.substring(INCLUDE.lastIndex);
 
                             // process the just included test


Test patch:
diff --git a/test/core/preprocessor.test.mjs b/test/core/preprocessor.test.mjs
index 843752fa7..16d285830 100644
--- a/test/core/preprocessor.test.mjs
+++ b/test/core/preprocessor.test.mjs
@@ -11,16 +11,20 @@ describe('Preprocessor', function () {
                 nested
             #endif
         `],
-        ['inc2', 'block2']
+        ['inc2', 'block2'],
+        ['incLoop', 'inserted{i}\n']
     ]);
 
     const srcData = `
         
+        #define LOOP_COUNT 3
         #define __INJECT_COUNT 2
         #define __INJECT_STRING hello
         #define FEATURE1
         #define FEATURE2
 
+        #include "incLoop, LOOP_COUNT"
+
         #ifdef FEATURE1
             TEST1
             #include "inc1"
@@ -214,4 +218,11 @@ describe('Preprocessor', function () {
         expect(Preprocessor.run(srcData, includes).includes('INJECTSTRING hello(x)')).to.equal(true);
     });
 
+    it('returns true for loop injection', function () {
+        expect(Preprocessor.run(srcData, includes).includes('inserted0')).to.equal(true);
+        expect(Preprocessor.run(srcData, includes).includes('inserted1')).to.equal(true);
+        expect(Preprocessor.run(srcData, includes).includes('inserted2')).to.equal(true);
+        expect(Preprocessor.run(srcData, includes).includes('inserted3')).to.equal(false);
+    });
+
 });