1 | const twgl = require('twgl.js');
|
2 |
|
3 |
|
4 | class ShaderManager {
|
5 | /**
|
6 | * @param {WebGLRenderingContext} gl WebGL rendering context to create shaders for
|
7 | * @constructor
|
8 | */
|
9 | constructor (gl) {
|
10 | this._gl = gl;
|
11 |
|
12 | /**
|
13 | * The cache of all shaders compiled so far, filled on demand.
|
14 | * @type {Object<ShaderManager.DRAW_MODE, Array<ProgramInfo>>}
|
15 | * @private
|
16 | */
|
17 | this._shaderCache = {};
|
18 | for (const modeName in ShaderManager.DRAW_MODE) {
|
19 | if (ShaderManager.DRAW_MODE.hasOwnProperty(modeName)) {
|
20 | this._shaderCache[modeName] = [];
|
21 | }
|
22 | }
|
23 | }
|
24 |
|
25 | /**
|
26 | * Fetch the shader for a particular set of active effects.
|
27 | * Build the shader if necessary.
|
28 | * @param {ShaderManager.DRAW_MODE} drawMode Draw normally, silhouette, etc.
|
29 | * @param {int} effectBits Bitmask representing the enabled effects.
|
30 | * @returns {ProgramInfo} The shader's program info.
|
31 | */
|
32 | getShader (drawMode, effectBits) {
|
33 | const cache = this._shaderCache[drawMode];
|
34 | if (drawMode === ShaderManager.DRAW_MODE.silhouette) {
|
35 | // Silhouette mode isn't affected by these effects.
|
36 | effectBits &= ~(ShaderManager.EFFECT_INFO.color.mask | ShaderManager.EFFECT_INFO.brightness.mask);
|
37 | }
|
38 | let shader = cache[effectBits];
|
39 | if (!shader) {
|
40 | shader = cache[effectBits] = this._buildShader(drawMode, effectBits);
|
41 | }
|
42 | return shader;
|
43 | }
|
44 |
|
45 | /**
|
46 | * Build the shader for a particular set of active effects.
|
47 | * @param {ShaderManager.DRAW_MODE} drawMode Draw normally, silhouette, etc.
|
48 | * @param {int} effectBits Bitmask representing the enabled effects.
|
49 | * @returns {ProgramInfo} The new shader's program info.
|
50 | * @private
|
51 | */
|
52 | _buildShader (drawMode, effectBits) {
|
53 | const numEffects = ShaderManager.EFFECTS.length;
|
54 |
|
55 | const defines = [
|
56 | `#define DRAW_MODE_${drawMode}`
|
57 | ];
|
58 | for (let index = 0; index < numEffects; ++index) {
|
59 | if ((effectBits & (1 << index)) !== 0) {
|
60 | defines.push(`#define ENABLE_${ShaderManager.EFFECTS[index]}`);
|
61 | }
|
62 | }
|
63 |
|
64 | const definesText = `${defines.join('\n')}\n`;
|
65 |
|
66 | /* eslint-disable global-require */
|
67 | const vsFullText = definesText + require('raw-loader!./shaders/sprite.vert');
|
68 | const fsFullText = definesText + require('raw-loader!./shaders/sprite.frag');
|
69 | /* eslint-enable global-require */
|
70 |
|
71 | return twgl.createProgramInfo(this._gl, [vsFullText, fsFullText]);
|
72 | }
|
73 | }
|
74 |
|
75 | /**
|
76 | * @typedef {object} ShaderManager.Effect
|
77 | * @prop {int} mask - The bit in 'effectBits' representing the effect.
|
78 | * @prop {function} converter - A conversion function which takes a Scratch value (generally in the range
|
79 | * 0..100 or -100..100) and maps it to a value useful to the shader. This
|
80 | * mapping may not be reversible.
|
81 | * @prop {boolean} shapeChanges - Whether the effect could change the drawn shape.
|
82 | */
|
83 |
|
84 | /**
|
85 | * Mapping of each effect name to info about that effect.
|
86 | * @enum {ShaderManager.Effect}
|
87 | */
|
88 | ShaderManager.EFFECT_INFO = {
|
89 | /** Color effect */
|
90 | color: {
|
91 | uniformName: 'u_color',
|
92 | mask: 1 << 0,
|
93 | converter: x => (x / 200) % 1,
|
94 | shapeChanges: false
|
95 | },
|
96 | /** Fisheye effect */
|
97 | fisheye: {
|
98 | uniformName: 'u_fisheye',
|
99 | mask: 1 << 1,
|
100 | converter: x => Math.max(0, (x + 100) / 100),
|
101 | shapeChanges: true
|
102 | },
|
103 | /** Whirl effect */
|
104 | whirl: {
|
105 | uniformName: 'u_whirl',
|
106 | mask: 1 << 2,
|
107 | converter: x => -x * Math.PI / 180,
|
108 | shapeChanges: true
|
109 | },
|
110 | /** Pixelate effect */
|
111 | pixelate: {
|
112 | uniformName: 'u_pixelate',
|
113 | mask: 1 << 3,
|
114 | converter: x => Math.abs(x) / 10,
|
115 | shapeChanges: true
|
116 | },
|
117 | /** Mosaic effect */
|
118 | mosaic: {
|
119 | uniformName: 'u_mosaic',
|
120 | mask: 1 << 4,
|
121 | converter: x => {
|
122 | x = Math.round((Math.abs(x) + 10) / 10);
|
123 | /** @todo cap by Math.min(srcWidth, srcHeight) */
|
124 | return Math.max(1, Math.min(x, 512));
|
125 | },
|
126 | shapeChanges: true
|
127 | },
|
128 | /** Brightness effect */
|
129 | brightness: {
|
130 | uniformName: 'u_brightness',
|
131 | mask: 1 << 5,
|
132 | converter: x => Math.max(-100, Math.min(x, 100)) / 100,
|
133 | shapeChanges: false
|
134 | },
|
135 | /** Ghost effect */
|
136 | ghost: {
|
137 | uniformName: 'u_ghost',
|
138 | mask: 1 << 6,
|
139 | converter: x => 1 - (Math.max(0, Math.min(x, 100)) / 100),
|
140 | shapeChanges: false
|
141 | }
|
142 | };
|
143 |
|
144 | /**
|
145 | * The name of each supported effect.
|
146 | * @type {Array}
|
147 | */
|
148 | ShaderManager.EFFECTS = Object.keys(ShaderManager.EFFECT_INFO);
|
149 |
|
150 | /**
|
151 | * The available draw modes.
|
152 | * @readonly
|
153 | * @enum {string}
|
154 | */
|
155 | ShaderManager.DRAW_MODE = {
|
156 | /**
|
157 | * Draw normally. Its output will use premultiplied alpha.
|
158 | */
|
159 | default: 'default',
|
160 |
|
161 | /**
|
162 | * Draw with non-premultiplied alpha. Useful for reading pixels from GL into an ImageData object.
|
163 | */
|
164 | straightAlpha: 'straightAlpha',
|
165 |
|
166 | /**
|
167 | * Draw a silhouette using a solid color.
|
168 | */
|
169 | silhouette: 'silhouette',
|
170 |
|
171 | /**
|
172 | * Draw only the parts of the drawable which match a particular color.
|
173 | */
|
174 | colorMask: 'colorMask',
|
175 |
|
176 | /**
|
177 | * Draw a line with caps.
|
178 | */
|
179 | line: 'line',
|
180 |
|
181 | /**
|
182 | * Draw the background in a certain color. Must sometimes be used instead of gl.clear.
|
183 | */
|
184 | background: 'background'
|
185 | };
|
186 |
|
187 | module.exports = ShaderManager;
|