/
shader.ts
201 lines (169 loc) · 7.12 KB
/
shader.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
/* spellchecker: disable */
import { assert, log, LogLevel } from './auxiliaries';
import { Context } from './context';
import { AbstractObject } from './object';
/* spellchecker: enable */
/**
* WebGL shader wrapper encapsulating shader creation, compilation, and deletion. A shader can be attached to multiple
* Programs for linking, and can be deleted if detached from all (linked) programs. The expected default behavior is to
* create a shader, attach it to programs, and discard is immediately after all programs are created (linked). If,
* however, the source of a shader needs to be changed, e.g., for replacements or other modifications, the shader
* object should be kept and, on change, all programs that have the shader attached have to be invalidated/relinked
* manually.
*
* ```
* var frag = new gloperate.Shader(context, context.gl.FRAGMENT_SHADER, 'EmptyFragmentShader');
* var vert = new gloperate.Shader(context, context.gl.VERTEX_SHADER, 'EmptyVertexShader');
* vert.initialize('void main() { }');
* frag.initialize('void main() { }');
*
* var prog = new gloperate.Program(context, 'EmptyProgram');
* prog.initialize([frag, vert]);
* ```
*/
export class Shader extends AbstractObject<WebGLShader> {
/** @see {@link type} */
protected _type: GLenum;
/** @see {@link source} */
protected _source: string;
/** @see {@link compiled} */
protected _compiled = false;
/**
* Map of replacement strings and the value to replace them with.
*/
protected _replacements: undefined | Map<string, string>;
/**
* Object constructor, requires a context and a valid identifier.
* @param context - Valid context to create the object for.
* @param type - Either GL_VERTEX_SHADER or GL_FRAGMENT_SHADER.
* @param identifier - Meaningful name for identification of this instance.
*/
constructor(context: Context, type: GLenum, identifier?: string) {
super(context, identifier);
const gl = context.gl;
if (identifier === undefined) {
switch (type) {
case context.gl.FRAGMENT_SHADER:
identifier = 'FragmentShader';
break;
case context.gl.VERTEX_SHADER:
identifier = 'VertexShader';
break;
default:
assert(false, `expected either a FRAGMENT_SHADER (${gl.FRAGMENT_SHADER}) ` +
`or a VERTEX_SHADER (${gl.VERTEX_SHADER}), given ${type}`);
}
}
this._type = type;
}
/**
* Creates a shader, sets the shader source, and compiles the shader. If the shader source cannot be compiled, the
* identifier and an info log are logged to console and the shader object is deleted. Note that a '#version 300 es'
* is added in case the shader source is compiled in a WebGL2 context.
* @param source - Shader source.
* @param compile - Whether or not to compile the shader immediately if a source is provided.
* @returns - Either a new shader or undefined if compilation failed.
*/
protected create(source?: string, compile: boolean = true): WebGLShader | undefined {
const gl = this._context.gl;
this._object = gl.createShader(this._type);
this._valid = gl.isShader(this._object);
this._compiled = false;
assert(this._object instanceof WebGLShader, `expected WebGLShader object to be created`);
if (source) {
this.source = source;
}
if (source && compile) {
this.compile();
}
return this._object;
}
/**
* Delete the shader object. This should have the reverse effect of `create`.
*/
protected delete(): void {
assert(this._object !== undefined, `expected WebGLShader object`);
this._context.gl.deleteShader(this._object);
this._object = undefined;
this._valid = false;
this._compiled = false;
}
/**
* Triggers recompilation of a shader. This is usually used internally automatically, but exposed here for leaky
* abstraction. It should not be required to invoke this manually in most cases. The shader object is marked as
* compiled iff the source compiled successfully. Note that invalidation of all programs this shader is attached
* to needs to be done manually.
*/
compile(): void {
const gl = this._context.gl;
let source = this.sourceWithReplacements;
if (this._context.isWebGL2) {
source = `#version 300 es\n${source}`;
}
gl.shaderSource(this._object, source);
gl.compileShader(this._object);
this._compiled = gl.getShaderParameter(this._object, gl.COMPILE_STATUS);
if (!this._compiled) {
const infoLog: string = gl.getShaderInfoLog(this._object);
log(LogLevel.Error, `compilation of shader '${this._identifier}' failed: ${infoLog}`);
}
}
/**
* Adds a search-replacement-pair that is processed every time the shaders is recompiled. Note that recompilation
* has to be triggered manually. Internally, all replacements are stored as a Map of search and replacement values.
* Thus, specifying a replacement value overrides an existing search value.
* @param searchValue - String that is to be searched (all occurrences) and replaced by replace value.
* @param replaceValue - The value to be used as replacement for all search value occurrences.
*/
replace(searchValue: string, replaceValue: string): void {
if (this._replacements === undefined) {
this._replacements = new Map<string, string>();
}
this._replacements.set(searchValue, replaceValue);
}
/**
* Either VERTEX_SHADER or FRAGMENT_SHADER.
*/
get type(): GLenum {
this.assertInitialized();
return this._type;
}
/**
* Allows to change the shader's source. Note that this will not recompile the shader.
*/
set source(source: string) {
if (this._source === source) {
return;
}
this._source = source;
}
/**
* Read access to the shader's source (without replacements applied).
*/
get source(): string {
this.assertInitialized();
return this._source;
}
/**
* Processes all search values and replaces them with the replace value on the source.
* @returns The source with all replacements applied.
*/
get sourceWithReplacements(): string {
if (this._replacements === undefined) {
return this._source;
}
let source = this._source;
this._replacements.forEach((replaceValue: string, searchValue: string) => {
const searchRegex = searchValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
source = source.replace(new RegExp(searchRegex, 'g'), replaceValue);
});
return source;
}
/**
* Read access the the shader's compile status. True if last compilation was successful.
*/
get compiled(): boolean {
this.assertInitialized();
return this._compiled;
}
}