@driftengine/core 4.8.1 → 4.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.json +86 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -0
- package/dist/input/actionMap.d.ts +6 -1
- package/dist/input/actionMap.js +20 -7
- package/dist/input/input.d.ts +28 -0
- package/dist/input/input.js +71 -9
- package/dist/input/inputHarness.d.ts +9 -0
- package/dist/input/inputHarness.js +34 -2
- package/dist/render/backend/webgl2/renderer.d.ts +52 -3
- package/dist/render/backend/webgl2/renderer.js +151 -5
- package/dist/render/backend/webgpu/buffers.d.ts +2 -0
- package/dist/render/backend/webgpu/buffers.js +2 -0
- package/dist/render/backend/webgpu/flatPass.js +16 -7
- package/dist/render/backend/webgpu/pipelineCache.d.ts +40 -8
- package/dist/render/backend/webgpu/pipelineCache.js +76 -9
- package/dist/render/backend/webgpu/renderer.d.ts +56 -1
- package/dist/render/backend/webgpu/renderer.js +151 -26
- package/dist/render/mapsWithoutUvs.d.ts +24 -0
- package/dist/render/mapsWithoutUvs.js +24 -0
- package/dist/render/mesh.d.ts +2 -0
- package/dist/render/mesh.js +3 -0
- package/dist/render/particlePool.d.ts +15 -0
- package/dist/render/particlePool.js +72 -37
- package/dist/render/pass.d.ts +12 -0
- package/dist/render/renderQuality.d.ts +7 -6
- package/dist/render/shaders/flat/glassTint.d.ts +28 -0
- package/dist/render/shaders/flat/glassTint.js +31 -0
- package/dist/render/shaders/flat/index.d.ts +12 -0
- package/dist/render/shaders/flat/index.js +7 -1
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +60 -25
- package/dist/render/shaders/flat/surfaceEffects.d.ts +1 -1
- package/dist/render/shaders/flat/surfaceEffects.js +6 -2
- package/dist/render/shaders/generated/flat.wgsl.d.ts +64 -0
- package/dist/render/shaders/generated/flat.wgsl.js +102 -38
- package/dist/render/surfaceTexture.d.ts +2 -0
- package/dist/render/surfaceTexture.js +4 -0
- package/dist/render/viewModel.d.ts +29 -0
- package/dist/render/viewModel.js +35 -0
- package/package.json +6 -6
- package/src/index.ts +9 -1
- package/src/input/actionMap.ts +23 -8
- package/src/input/input.ts +79 -9
- package/src/input/inputHarness.ts +27 -2
- package/src/render/backend/webgl2/renderer.ts +166 -13
- package/src/render/backend/webgpu/buffers.ts +5 -0
- package/src/render/backend/webgpu/flatPass.ts +41 -25
- package/src/render/backend/webgpu/pipelineCache.ts +82 -8
- package/src/render/backend/webgpu/renderer.ts +151 -26
- package/src/render/mapsWithoutUvs.ts +43 -0
- package/src/render/mesh.ts +3 -0
- package/src/render/particlePool.ts +101 -54
- package/src/render/pass.ts +12 -0
- package/src/render/renderQuality.ts +7 -6
- package/src/render/shaders/flat/glassTint.ts +48 -0
- package/src/render/shaders/flat/index.ts +19 -1
- package/src/render/shaders/flat/main.ts +60 -25
- package/src/render/shaders/flat/surfaceEffects.ts +6 -2
- package/src/render/shaders/generated/flat.wgsl.ts +102 -38
- package/src/render/surfaceTexture.ts +5 -0
- package/src/render/viewModel.ts +41 -0
|
@@ -38,6 +38,15 @@ export class ParticlePool {
|
|
|
38
38
|
* together.
|
|
39
39
|
*/
|
|
40
40
|
seedOf;
|
|
41
|
+
/**
|
|
42
|
+
* Where each slot's particle sits in the drawn streams, or -1 where it sits nowhere.
|
|
43
|
+
*
|
|
44
|
+
* **What lets `emit` draw a particle the frame it is born.** The streams are compacted, so a
|
|
45
|
+
* slot's entry is wherever `update` last put it; an emit into a slot that has one overwrites
|
|
46
|
+
* that entry, and an emit into one that has none appends. Without it a new particle waited for
|
|
47
|
+
* the next `update` to be drawn at all.
|
|
48
|
+
*/
|
|
49
|
+
drawnAt;
|
|
41
50
|
next = 0;
|
|
42
51
|
constructor(options) {
|
|
43
52
|
this.options = options;
|
|
@@ -60,6 +69,7 @@ export class ParticlePool {
|
|
|
60
69
|
this.spin = new Float32Array(capacity);
|
|
61
70
|
this.scale = new Float32Array(capacity).fill(1);
|
|
62
71
|
this.seedOf = new Float32Array(capacity);
|
|
72
|
+
this.drawnAt = new Int32Array(capacity).fill(-1);
|
|
63
73
|
}
|
|
64
74
|
/** How many particles are alive. */
|
|
65
75
|
get live() {
|
|
@@ -95,6 +105,18 @@ export class ParticlePool {
|
|
|
95
105
|
this.spin[i] = (hash - Math.floor(hash)) * Math.PI * 2;
|
|
96
106
|
this.scale[i] = sizeScale;
|
|
97
107
|
this.seedOf[i] = seed;
|
|
108
|
+
/*
|
|
109
|
+
* Drawn now, at birth, rather than at the next `update`: into the entry this slot already
|
|
110
|
+
* has when it is taking over a particle that was drawn, and onto the end otherwise.
|
|
111
|
+
*/
|
|
112
|
+
const held = this.drawnAt[i];
|
|
113
|
+
const at = held >= 0 ? held : this.particles.count;
|
|
114
|
+
this.writeDrawn(i, at, 0, x, y, z, vx, vy, vz);
|
|
115
|
+
if (held < 0) {
|
|
116
|
+
this.drawnAt[i] = at;
|
|
117
|
+
this.instances.count = at + 1;
|
|
118
|
+
this.particles.count = at + 1;
|
|
119
|
+
}
|
|
98
120
|
}
|
|
99
121
|
/**
|
|
100
122
|
* Advance every particle and rebuild the instance buffer.
|
|
@@ -108,12 +130,15 @@ export class ParticlePool {
|
|
|
108
130
|
let count = 0;
|
|
109
131
|
for (let i = 0; i < this.age.length; i++) {
|
|
110
132
|
const age = this.age[i];
|
|
111
|
-
if (age === Infinity)
|
|
133
|
+
if (age === Infinity) {
|
|
134
|
+
this.drawnAt[i] = -1;
|
|
112
135
|
continue;
|
|
136
|
+
}
|
|
113
137
|
const life = this.life[i];
|
|
114
138
|
const next = age + dt;
|
|
115
139
|
if (next >= life) {
|
|
116
140
|
this.age[i] = Infinity;
|
|
141
|
+
this.drawnAt[i] = -1;
|
|
117
142
|
continue;
|
|
118
143
|
}
|
|
119
144
|
this.age[i] = next;
|
|
@@ -131,50 +156,60 @@ export class ParticlePool {
|
|
|
131
156
|
this.x[i] = px;
|
|
132
157
|
this.y[i] = py;
|
|
133
158
|
this.z[i] = pz;
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const r = o.colorStart[0] + (o.colorEnd[0] - o.colorStart[0]) * t;
|
|
137
|
-
const g = o.colorStart[1] + (o.colorEnd[1] - o.colorStart[1]) * t;
|
|
138
|
-
const b = o.colorStart[2] + (o.colorEnd[2] - o.colorStart[2]) * t;
|
|
139
|
-
/*
|
|
140
|
-
* Colour carries the fade rather than an alpha channel: the instanced
|
|
141
|
-
* path multiplies a tint into the base mesh and has no per-instance
|
|
142
|
-
* opacity, and for an additive puff darkening to nothing *is* fading out.
|
|
143
|
-
* It also means the effect needs no blend-state change.
|
|
144
|
-
*/
|
|
145
|
-
const fade = 1 - t * t;
|
|
146
|
-
writeInstance(this.instances, count, px, py, pz, size, this.spin[i], r * fade, g * fade, b * fade, 0, 0, 0);
|
|
147
|
-
/*
|
|
148
|
-
* The same particle for a material that has a real opacity channel. Colour
|
|
149
|
-
* is written *unfaded* here, because dimming it is the scatter path's
|
|
150
|
-
* workaround and doing both would fade twice — a puff that vanished at half
|
|
151
|
-
* its life while still occluding what was behind it.
|
|
152
|
-
*/
|
|
153
|
-
const p = this.particles;
|
|
154
|
-
const alphaStart = o.alphaStart ?? 1;
|
|
155
|
-
const alphaEnd = o.alphaEnd ?? 0;
|
|
156
|
-
p.positions[count * 3] = px;
|
|
157
|
-
p.positions[count * 3 + 1] = py;
|
|
158
|
-
p.positions[count * 3 + 2] = pz;
|
|
159
|
-
p.sizes[count] = size;
|
|
160
|
-
p.spins[count] = this.spin[i];
|
|
161
|
-
p.colors[count * 3] = r;
|
|
162
|
-
p.colors[count * 3 + 1] = g;
|
|
163
|
-
p.colors[count * 3 + 2] = b;
|
|
164
|
-
p.alphas[count] = alphaStart + (alphaEnd - alphaStart) * t;
|
|
165
|
-
p.ages[count] = t;
|
|
166
|
-
p.seeds[count] = this.seedOf[i];
|
|
167
|
-
p.velocities[count * 3] = vx;
|
|
168
|
-
p.velocities[count * 3 + 1] = vy;
|
|
169
|
-
p.velocities[count * 3 + 2] = vz;
|
|
159
|
+
this.drawnAt[i] = count;
|
|
160
|
+
this.writeDrawn(i, count, next / life, px, py, pz, vx, vy, vz);
|
|
170
161
|
count++;
|
|
171
162
|
}
|
|
172
163
|
this.instances.count = count;
|
|
173
164
|
this.particles.count = count;
|
|
174
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* One particle's entry in both drawn streams, at `t` of its life: what `update` writes for every
|
|
168
|
+
* live particle and `emit` for a new one, so the two cannot disagree about what a particle looks
|
|
169
|
+
* like.
|
|
170
|
+
*/
|
|
171
|
+
writeDrawn(i, count, t, px, py, pz, vx, vy, vz) {
|
|
172
|
+
const o = this.options;
|
|
173
|
+
const size = (o.sizeStart + (o.sizeEnd - o.sizeStart) * t) * this.scale[i];
|
|
174
|
+
const r = o.colorStart[0] + (o.colorEnd[0] - o.colorStart[0]) * t;
|
|
175
|
+
const g = o.colorStart[1] + (o.colorEnd[1] - o.colorStart[1]) * t;
|
|
176
|
+
const b = o.colorStart[2] + (o.colorEnd[2] - o.colorStart[2]) * t;
|
|
177
|
+
/*
|
|
178
|
+
* Colour carries the fade rather than an alpha channel: the instanced
|
|
179
|
+
* path multiplies a tint into the base mesh and has no per-instance
|
|
180
|
+
* opacity, and for an additive puff darkening to nothing *is* fading out.
|
|
181
|
+
* It also means the effect needs no blend-state change.
|
|
182
|
+
*/
|
|
183
|
+
const fade = 1 - t * t;
|
|
184
|
+
writeInstance(this.instances, count, px, py, pz, size, this.spin[i], r * fade, g * fade, b * fade, 0, 0, 0);
|
|
185
|
+
/*
|
|
186
|
+
* The same particle for a material that has a real opacity channel. Colour
|
|
187
|
+
* is written *unfaded* here, because dimming it is the scatter path's
|
|
188
|
+
* workaround and doing both would fade twice — a puff that vanished at half
|
|
189
|
+
* its life while still occluding what was behind it.
|
|
190
|
+
*/
|
|
191
|
+
const p = this.particles;
|
|
192
|
+
const alphaStart = o.alphaStart ?? 1;
|
|
193
|
+
const alphaEnd = o.alphaEnd ?? 0;
|
|
194
|
+
p.positions[count * 3] = px;
|
|
195
|
+
p.positions[count * 3 + 1] = py;
|
|
196
|
+
p.positions[count * 3 + 2] = pz;
|
|
197
|
+
p.sizes[count] = size;
|
|
198
|
+
p.spins[count] = this.spin[i];
|
|
199
|
+
p.colors[count * 3] = r;
|
|
200
|
+
p.colors[count * 3 + 1] = g;
|
|
201
|
+
p.colors[count * 3 + 2] = b;
|
|
202
|
+
p.alphas[count] = alphaStart + (alphaEnd - alphaStart) * t;
|
|
203
|
+
p.ages[count] = t;
|
|
204
|
+
p.seeds[count] = this.seedOf[i];
|
|
205
|
+
p.velocities[count * 3] = vx;
|
|
206
|
+
p.velocities[count * 3 + 1] = vy;
|
|
207
|
+
p.velocities[count * 3 + 2] = vz;
|
|
208
|
+
}
|
|
175
209
|
/** Drop everything, for a respawn or a scene change. */
|
|
176
210
|
clear() {
|
|
177
211
|
this.age.fill(Infinity);
|
|
212
|
+
this.drawnAt.fill(-1);
|
|
178
213
|
this.instances.count = 0;
|
|
179
214
|
this.particles.count = 0;
|
|
180
215
|
this.next = 0;
|
package/dist/render/pass.d.ts
CHANGED
|
@@ -122,6 +122,18 @@ export type PassContext = {
|
|
|
122
122
|
readonly pass: GPURenderPassEncoder;
|
|
123
123
|
/** The frame's jitter, the same array `PrepareContext.jitter` held. */
|
|
124
124
|
readonly jitter: Float32Array;
|
|
125
|
+
/**
|
|
126
|
+
* The colour format, depth format and sample count of the pass this draw lands in, which a
|
|
127
|
+
* pipeline set on it must match exactly.
|
|
128
|
+
*
|
|
129
|
+
* **Not always the device's.** `PassDevice` says what the frame is; a pass drawn after
|
|
130
|
+
* `endFrame` lands on the canvas, at the canvas's format and one sample, where the frame is
|
|
131
|
+
* the scene target at the scene's samples. A pass that is drawn in both places keeps a
|
|
132
|
+
* pipeline for each and chooses here; one built only from the device is refused by the other.
|
|
133
|
+
*/
|
|
134
|
+
readonly format: GPUTextureFormat;
|
|
135
|
+
readonly depthFormat: GPUTextureFormat;
|
|
136
|
+
readonly samples: number;
|
|
125
137
|
});
|
|
126
138
|
/**
|
|
127
139
|
* What a pass is handed once a frame, before the frame's own target exists.
|
|
@@ -185,12 +185,13 @@ export interface RenderQuality {
|
|
|
185
185
|
* existed: a pane casts nothing at all.
|
|
186
186
|
*
|
|
187
187
|
* Nothing is allocated until a glass caster first reaches a shadow, so a scene with no glass pays
|
|
188
|
-
* no memory and no pass whatever this says. **
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
* at 720p on WebGPU, 0.42 ms on WebGL2 and 1.2 ms at 4K
|
|
192
|
-
*
|
|
193
|
-
*
|
|
188
|
+
* no memory and no pass whatever this says. **Nor any shader, since 4.8.2**: the lit shader is
|
|
189
|
+
* built without the glass lookup and rebuilt with it when the first glass caster reaches a shadow.
|
|
190
|
+
* A lookup that finds no glass still cost the lit pass its registers — measured on the courtyard at
|
|
191
|
+
* night, 0.27 ms at 720p on WebGPU, 0.42 ms on WebGL2 and 1.2 ms at 4K — and from 4.5.0 to 4.8.1
|
|
192
|
+
* every scene paid it, glass or none: about 30% of the lit shader's instructions, which on a phone
|
|
193
|
+
* was the lit pass. `'off'` never rebuilds. What would make `'full'` wrong is a phone budget with a
|
|
194
|
+
* lot of glass; that is what `'half'` is for. See `glassShadow.ts`.
|
|
194
195
|
*/
|
|
195
196
|
readonly glassShadows: GlassShadows;
|
|
196
197
|
/** Horizontal world-space reach over which a directional shadow dissolves. */
|
|
@@ -4,3 +4,31 @@
|
|
|
4
4
|
* outside every conditional, so each lookup can call it whichever shadows a permutation compiles.
|
|
5
5
|
*/
|
|
6
6
|
export declare function glassTintGlsl(glassShadows: boolean): string;
|
|
7
|
+
/**
|
|
8
|
+
* The lit stage's switches, by the name of the constant each is: the decision both backends make
|
|
9
|
+
* about which optional code a lit program carries. See `litSwitchesGlsl`.
|
|
10
|
+
*/
|
|
11
|
+
export type LitSwitch = 'GLASS_SHADOWS' | 'CLUSTERED_LIGHTS' | 'LIGHT_FIXTURES' | 'SURFACE_EFFECTS' | 'DRIFT_LIGHT';
|
|
12
|
+
/** Every switch off: what a renderer starts from before its profile and its content say otherwise. */
|
|
13
|
+
export declare function noLitSwitches(): Record<LitSwitch, boolean>;
|
|
14
|
+
/** Which of the lit stage's optional features a build reads. See `litSwitchesGlsl`. */
|
|
15
|
+
export interface LitSwitches {
|
|
16
|
+
readonly clusteredLights: boolean;
|
|
17
|
+
readonly lightFixtures: boolean;
|
|
18
|
+
readonly surfaceEffects: boolean;
|
|
19
|
+
readonly driftLight: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The lit stage's other switches, declared after `GLASS_SHADOWS` so that one keeps the id 0 the
|
|
23
|
+
* pipelines and their tests already name, each a pipeline-overridable constant on the generated
|
|
24
|
+
* WGSL and a written-in value on WebGL2 — the arrangement `GLASS_SHADOWS` has, for its reason:
|
|
25
|
+
* code a branch never runs still costs a shader its registers, and the device compiles it away
|
|
26
|
+
* only where the switch is a constant.
|
|
27
|
+
*
|
|
28
|
+
* **Clustering is the profile's and fixed.** The other three are content, off until a consumer
|
|
29
|
+
* first uses them — a measured profile or a cookie, a material with an effects table, a DriftLight
|
|
30
|
+
* volume — and then on for good; see `PipelineCache.enable` and the WebGL2 renderer's rebuild.
|
|
31
|
+
* Measured on RADV against the lit shader with glass already out: 6,852 instructions and 120
|
|
32
|
+
* registers, 4,641 and 96 with all four out.
|
|
33
|
+
*/
|
|
34
|
+
export declare function litSwitchesGlsl(switches: LitSwitches): string;
|
|
@@ -23,3 +23,34 @@ vec4 glassUnmix(vec4 mip, float centreClarity) {
|
|
|
23
23
|
}
|
|
24
24
|
`;
|
|
25
25
|
}
|
|
26
|
+
/** Every switch off: what a renderer starts from before its profile and its content say otherwise. */
|
|
27
|
+
export function noLitSwitches() {
|
|
28
|
+
return {
|
|
29
|
+
GLASS_SHADOWS: false,
|
|
30
|
+
CLUSTERED_LIGHTS: false,
|
|
31
|
+
LIGHT_FIXTURES: false,
|
|
32
|
+
SURFACE_EFFECTS: false,
|
|
33
|
+
DRIFT_LIGHT: false,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The lit stage's other switches, declared after `GLASS_SHADOWS` so that one keeps the id 0 the
|
|
38
|
+
* pipelines and their tests already name, each a pipeline-overridable constant on the generated
|
|
39
|
+
* WGSL and a written-in value on WebGL2 — the arrangement `GLASS_SHADOWS` has, for its reason:
|
|
40
|
+
* code a branch never runs still costs a shader its registers, and the device compiles it away
|
|
41
|
+
* only where the switch is a constant.
|
|
42
|
+
*
|
|
43
|
+
* **Clustering is the profile's and fixed.** The other three are content, off until a consumer
|
|
44
|
+
* first uses them — a measured profile or a cookie, a material with an effects table, a DriftLight
|
|
45
|
+
* volume — and then on for good; see `PipelineCache.enable` and the WebGL2 renderer's rebuild.
|
|
46
|
+
* Measured on RADV against the lit shader with glass already out: 6,852 instructions and 120
|
|
47
|
+
* registers, 4,641 and 96 with all four out.
|
|
48
|
+
*/
|
|
49
|
+
export function litSwitchesGlsl(switches) {
|
|
50
|
+
return /* glsl */ `
|
|
51
|
+
const bool CLUSTERED_LIGHTS = ${switches.clusteredLights ? 'true' : 'false'}; // wgsl:override
|
|
52
|
+
const bool LIGHT_FIXTURES = ${switches.lightFixtures ? 'true' : 'false'}; // wgsl:override
|
|
53
|
+
const bool SURFACE_EFFECTS = ${switches.surfaceEffects ? 'true' : 'false'}; // wgsl:override
|
|
54
|
+
const bool DRIFT_LIGHT = ${switches.driftLight ? 'true' : 'false'}; // wgsl:override
|
|
55
|
+
`;
|
|
56
|
+
}
|
|
@@ -92,6 +92,18 @@ export interface FlatShaderOptions {
|
|
|
92
92
|
* courtyard at night, measured, and nothing once compiled out.
|
|
93
93
|
*/
|
|
94
94
|
readonly glassShadows?: boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Whether this build has the clustered arm of the light loop: `clusteredLights`, fixed per
|
|
97
|
+
* renderer. True when absent, which is what the generated WGSL is built with. See
|
|
98
|
+
* `litSwitchesGlsl`.
|
|
99
|
+
*/
|
|
100
|
+
readonly clusteredLights?: boolean;
|
|
101
|
+
/** Whether it reads measured profiles and cookies. True when absent. See `litSwitchesGlsl`. */
|
|
102
|
+
readonly lightFixtures?: boolean;
|
|
103
|
+
/** Whether it reads a material's effects table. True when absent. See `litSwitchesGlsl`. */
|
|
104
|
+
readonly surfaceEffects?: boolean;
|
|
105
|
+
/** Whether it reads a DriftLight volume. True when absent. See `litSwitchesGlsl`. */
|
|
106
|
+
readonly driftLight?: boolean;
|
|
95
107
|
/**
|
|
96
108
|
* How many point lights this build declares room for. `MAX_POINT_LIGHTS` when absent.
|
|
97
109
|
*
|
|
@@ -7,7 +7,7 @@ import { preambleGlsl } from './preamble.js';
|
|
|
7
7
|
import { LOBES_GLSL } from './lobes.js';
|
|
8
8
|
import { POINTSHADOW_GLSL } from './pointShadow.js';
|
|
9
9
|
import { DIRECTIONALSHADOW_GLSL } from './directionalShadow.js';
|
|
10
|
-
import { glassTintGlsl } from './glassTint.js';
|
|
10
|
+
import { glassTintGlsl, litSwitchesGlsl } from './glassTint.js';
|
|
11
11
|
import { MORPH_GLSL } from '../morph.js';
|
|
12
12
|
import { SKINNING_GLSL } from '../skinning.js';
|
|
13
13
|
import { SURFACE_GLSL } from './surface.js';
|
|
@@ -359,6 +359,12 @@ export function flatFrag(options) {
|
|
|
359
359
|
/* Depth zero as well: both glass lookups, sun and lamps, call it, and every permutation
|
|
360
360
|
declares its switch, which the pipelines set whether the permutation reads it or not. */
|
|
361
361
|
glassTintGlsl(options.glassShadows ?? true),
|
|
362
|
+
litSwitchesGlsl({
|
|
363
|
+
clusteredLights: options.clusteredLights ?? true,
|
|
364
|
+
lightFixtures: options.lightFixtures ?? true,
|
|
365
|
+
surfaceEffects: options.surfaceEffects ?? true,
|
|
366
|
+
driftLight: options.driftLight ?? true,
|
|
367
|
+
}),
|
|
362
368
|
LOBES_GLSL,
|
|
363
369
|
POINTSHADOW_GLSL,
|
|
364
370
|
DIRECTIONALSHADOW_GLSL,
|