lib/gamnit: intro graphic programs abstraction layer
[nit.git] / lib / gamnit / programs.nit
1 # This file is part of NIT (http://www.nitlanguage.org).
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 # Services for graphical programs with shaders, attributes and uniforms
16 module programs
17
18 import display
19 import matrix
20
21 private import more_collections
22
23 # `Uniform` or `Attribute` of a `GamnitProgram`
24 class ShaderVariable
25
26 # The `GamnitProgram` to which `self` belongs
27 var program: GLProgram
28
29 # Name of `self` in the `program` source
30 var name: String
31
32 # Location of `self` in the compiled `program`
33 var location: Int
34
35 # Number of elements in this array (1 for scalars and more for vectors)
36 var size: Int
37
38 # Is `self` an active uniform or attribute in the `program`?
39 #
40 # If `false`, the variable may have been optimized out by the compiler.
41 fun is_active: Bool do return true
42
43 redef fun to_s do return "<{class_name} name:{name} location:{location} size:{size}"
44 end
45
46 # Inactive shader variable, either optimized out or simple absent from the program
47 #
48 # Returned by `GamnitProgram::uniforms` or `GamnitProgram::attributes` when
49 # the uniform has not been identified as active by the driver.
50 # Operations on instances of this class have no effects.
51 #
52 # Act as a compatibility when a program expect a uniform to exist even in
53 # a context where the driver's compiler may have optimized it out.
54 # You must be careful when receiving an `InactiveVariable` as it may also
55 # silence real program errors, such type in variable name.
56 abstract class InactiveVariable
57 super ShaderVariable
58
59 redef fun is_active do return false
60 end
61
62 # Shader attribute
63 #
64 # It will use either the `uniform` value or the data at `array_pointer` if
65 # and only if `array_enabled`.
66 class Attribute
67 super ShaderVariable
68
69 private var array_enabled_cache = false
70
71 # Is the array attribute enabled?
72 fun array_enabled: Bool do return array_enabled_cache
73
74 # Set whether to use the data at `array_pointer` over `uniform`.
75 fun array_enabled=(value: Bool)
76 do
77 if not is_active then return
78
79 glUseProgram program
80
81 self.array_enabled_cache = value
82 if value then
83 glEnableVertexAttribArray location
84 else glDisableVertexAttribArray location
85 end
86
87 # Define the `array` of vertex data
88 fun array(array: Array[Float], data_per_vertex: Int)
89 do
90 # TODO move this and native_float_array to a subclass specific to float
91
92 if not is_active then return
93
94 var native = native_float_array
95 if native == null or array.length > native.length then
96 if native != null then native.destroy
97 native = new GLfloatArray.from(array)
98 self.native_float_array = native
99 else
100 native.fill_from(array)
101 end
102
103 glVertexAttribPointer(location, data_per_vertex, gl_FLOAT, false, 0, native.native_array)
104 end
105
106 private var native_float_array: nullable GLfloatArray = null
107 end
108
109 # Shader attribute of GLSL type `float`
110 class AttributeFloat
111 super Attribute
112
113 # Set the uniform value to use when the vertex array is disabled
114 fun uniform(x: Float) do if is_active then glVertexAttrib1f(location, x)
115 end
116
117 # Shader attribute of GLSL type `vec2`
118 class AttributeVec2
119 super Attribute
120
121 # Set the uniform value to use when the vertex array is disabled
122 fun uniform(x, y: Float) do if is_active then glVertexAttrib2f(location, x, y)
123 end
124
125 # Shader attribute of GLSL type `vec3`
126 class AttributeVec3
127 super Attribute
128
129 # Set the uniform value to use when the vertex array is disabled
130 fun uniform(x, y, z: Float) do if is_active then glVertexAttrib3f(location, x, y, z)
131 end
132
133 # Shader attribute of GLSL type `vec4`
134 class AttributeVec4
135 super Attribute
136
137 # Set the uniform value to use when the vertex array is disabled
138 fun uniform(x, y, z, w: Float) do if is_active then glVertexAttrib4f(location, x, y, z, w)
139 end
140
141 # `Attribute` that does not exist or that has been optimized out
142 class InactiveAttribute
143 super InactiveVariable
144 super AttributeFloat
145 super AttributeVec2
146 super AttributeVec3
147 super AttributeVec4
148 end
149
150 # Shader uniform
151 class Uniform
152 super ShaderVariable
153
154 private fun uniform_1i(index, x: Int) `{ glUniform1i(index, x); `}
155 end
156
157 # Shader uniform of GLSL type `bool`
158 class UniformBool
159 super Uniform
160
161 # Set this uniform value
162 fun uniform(val: Bool) do uniform_1i(location, if val then 1 else 0)
163 end
164
165 # Shader uniform of GLSL type `vec4`
166 class UniformFloat
167 super Uniform
168
169 # Set this uniform value
170 fun uniform(x: Float) do glUniform1f(location, x)
171 end
172
173 # Shader uniform of GLSL type `vec4`
174 class UniformVec2
175 super Uniform
176
177 # Set this uniform value
178 fun uniform(x, y: Float) do glUniform2f(location, x, y)
179 end
180
181 # Shader uniform of GLSL type `vec4`
182 class UniformVec3
183 super Uniform
184
185 # Set this uniform value
186 fun uniform(x, y, z: Float) do glUniform3f(location, x, y, z)
187 end
188
189 # Shader uniform of GLSL type `vec4`
190 class UniformVec4
191 super Uniform
192
193 # Set this uniform value
194 fun uniform(x, y, z, w: Float) do glUniform4f(location, x, y, z, w)
195 end
196
197 # Shader uniform of GLSL type `sampler2D`
198 class UniformSampler2D
199 super Uniform
200
201 # Set this uniform value
202 fun uniform(val: Int) do uniform_1i(location, val)
203 end
204
205 # Shader uniform of GLSL type `mat4`
206 class UniformMat4
207 super Uniform
208
209 private var native_matrix_cache: nullable NativeGLfloatArray = null
210
211 # Set this uniform value
212 fun uniform(matrix: Matrix)
213 do
214 var native = native_matrix_cache
215 if native == null then
216 native = new NativeGLfloatArray.matrix
217 self.native_matrix_cache = native
218 end
219
220 matrix.fill_native(native)
221 uniform_matrix_4f(location, 1, false, native)
222 end
223
224 private fun uniform_matrix_4f(index, count: Int, transpose: Bool, data: NativeGLfloatArray) `{
225 glUniformMatrix4fv(index, count, transpose, data);
226 `}
227 end
228
229 # `Uniform` that does not exist or that has been optimized out
230 class InactiveUniform
231 super InactiveVariable
232 super UniformBool
233 super UniformFloat
234 super UniformSampler2D
235 super UniformVec2
236 super UniformVec3
237 super UniformVec4
238 super UniformMat4
239
240 redef fun is_active do return false
241 end
242
243 # Gamnit shader
244 abstract class Shader
245
246 # TODO add alternative init to load shaders from binary
247
248 # Index of this shader in OpenGL
249 private var gl_shader: GLShader is noinit
250
251 # Latest error raised by operations of this shader
252 var error: nullable Error = null
253
254 # Source code of this shader
255 var source: Text
256
257 # Low-level type of this shader
258 private fun gl_shader_type: GLShaderType is abstract
259
260 # Compile this shader and report any errors in the attribute `error`
261 fun compile
262 do
263 # Create
264 var gl_shader = glCreateShader(gl_shader_type)
265 if not glIsShader(gl_shader) then
266 self.error = new Error("Shader creation failed: {glGetError}")
267 return
268 end
269 self.gl_shader = gl_shader
270
271 glShaderSource(gl_shader, source.to_cstring)
272
273 # Compile
274 glCompileShader gl_shader
275 if not gl_shader.is_compiled then
276 self.error = new Error("Shader compilation failed: {glGetShaderInfoLog(gl_shader)}")
277 return
278 end
279
280 error = gammit_gl_error
281 end
282
283 # Has this shader been deleted?
284 var deleted = false
285
286 # Delete this shader and free its resources
287 fun delete
288 do
289 if deleted then return
290
291 glDeleteShader gl_shader
292 deleted = true
293 end
294 end
295
296 # Gamnit vertex shader
297 class VertexShader
298 super Shader
299
300 redef fun gl_shader_type do return gl_VERTEX_SHADER
301 end
302
303 # Gamnit fragment shader
304 class FragmentShader
305 super Shader
306
307 redef fun gl_shader_type do return gl_FRAGMENT_SHADER
308 end
309
310 # Gamnit graphical program
311 #
312 # Subclasses should implement both `vertex_shader` and `fragment_shader`.
313 abstract class GamnitProgram
314
315 # Vertex shader to attach to this program
316 fun vertex_shader: VertexShader is abstract
317
318 # Fragment shader to attach to this program
319 fun fragment_shader: FragmentShader is abstract
320
321 # Index to the OpenGL ES program, set by `compile_and_link`
322 private var gl_program: nullable GLProgram = null
323
324 # Last error raised by `compile_and_link`
325 var error: nullable Error = null is protected writable
326
327 # Compile the shaders, and this program, then link and report any errors
328 fun compile_and_link
329 do
330 # Get an index
331 var gl_program = glCreateProgram
332 if not glIsProgram(gl_program) then
333 self.error = new Error("Program creation failed: {glGetError.to_s}")
334 return
335 end
336 self.gl_program = gl_program
337
338 # Vertex shader
339 var vertex_shader = vertex_shader
340 vertex_shader.compile
341 if vertex_shader.error != null then
342 self.error = vertex_shader.error
343 return
344 end
345
346 # Fragment shader
347 var fragment_shader = fragment_shader
348 fragment_shader.compile
349 if fragment_shader.error != null then
350 self.error = fragment_shader.error
351 return
352 end
353
354 # Attach shaders
355 glAttachShader(gl_program, vertex_shader.gl_shader)
356 glAttachShader(gl_program, fragment_shader.gl_shader)
357
358 # Catch any errors up to here
359 var error = gammit_gl_error
360 if error != null then
361 self.error = error
362 return
363 end
364
365 # Link
366 glLinkProgram gl_program
367 if not gl_program.is_linked then
368 self.error = new Error("Linking failed: {glGetProgramInfoLog(gl_program)}")
369 return
370 end
371
372 # Fill the attribute and uniform lists
373 var n_attribs = glGetProgramiv(gl_program, gl_ACTIVE_ATTRIBUTES)
374 for a in [0..n_attribs[ do
375 var name = gl_program.active_attrib_name(a)
376 var size = gl_program.active_attrib_size(a)
377 var typ = gl_program.active_attrib_type(a)
378 var location = gl_program.attrib_location(name)
379
380 # FIXME location may be invalid at this point because
381 # attrib_location does not work with truncated names,
382 # as returned by `active_attrib_name`.
383
384 var attribute
385 if typ == gl_FLOAT then
386 attribute = new AttributeFloat(gl_program, name, location, size)
387 else if typ == gl_FLOAT_VEC2 then
388 attribute = new AttributeVec2(gl_program, name, location, size)
389 else if typ == gl_FLOAT_VEC3 then
390 attribute = new AttributeVec3(gl_program, name, location, size)
391 else if typ == gl_FLOAT_VEC4 then
392 attribute = new AttributeVec4(gl_program, name, location, size)
393 else
394 attribute = new Attribute(gl_program, name, location, size)
395 end
396 # TODO missing types
397 attributes[name] = attribute
398 end
399
400 var n_uniforms = glGetProgramiv(gl_program, gl_ACTIVE_UNIFORMS)
401 for a in [0..n_uniforms[ do
402
403 var name = gl_program.active_uniform_name(a)
404 var size = gl_program.active_uniform_size(a)
405 var typ = gl_program.active_uniform_type(a)
406 var location = gl_program.uniform_location(name)
407
408 var uniform
409 if typ == gl_BOOL then
410 uniform = new UniformBool(gl_program, name, location, size)
411 else if typ == gl_SAMPLER_2D then
412 uniform = new UniformSampler2D(gl_program, name, location, size)
413 else if typ == gl_FLOAT then
414 uniform = new UniformFloat(gl_program, name, location, size)
415 else if typ == gl_FLOAT_VEC2 then
416 uniform = new UniformVec2(gl_program, name, location, size)
417 else if typ == gl_FLOAT_VEC3 then
418 uniform = new UniformVec3(gl_program, name, location, size)
419 else if typ == gl_FLOAT_VEC4 then
420 uniform = new UniformVec4(gl_program, name, location, size)
421 else if typ == gl_FLOAT_MAT4 then
422 uniform = new UniformMat4(gl_program, name, location, size)
423 else
424 uniform = new Uniform(gl_program, name, location, size)
425 end
426 # TODO missing types
427 uniforms[name] = uniform
428 end
429 end
430
431 # Attributes of this program organized by name
432 #
433 # Active attributes are gathered at `compile_and_link`.
434 # Upon request, inactive attributes are returned as a new `InactiveAttribute`.
435 var attributes = new AttributeMap(self)
436
437 # Uniforms of this program organized by name
438 #
439 # Active uniforms are gathered at `compile_and_link`.
440 # Upon request, inactive attributes are returned as a new `InactiveUniform`.
441 var uniforms = new UniformMap(self)
442
443 # Notify the GPU to use this program
444 fun use
445 do
446 var gl_program = gl_program
447 assert gl_program != null # TODO error not compiled, or compile it
448 glUseProgram gl_program
449 end
450
451 # Has this program been deleted?
452 var deleted = false
453
454 # Delete this program if it has not already been deleted
455 fun delete
456 do
457 if deleted then return
458
459 glDeleteProgram gl_program.as(not null)
460 deleted = true
461 end
462 end
463
464 # Gamnit graphical program from the shaders source code
465 class GamnitProgramFromSource
466 super GamnitProgram
467
468 # Source code of the vertex shader
469 fun vertex_shader_source: Text is abstract
470
471 redef var vertex_shader = new VertexShader(vertex_shader_source) is lazy
472
473 # Source code of the fragment shader
474 fun fragment_shader_source: Text is abstract
475
476 redef var fragment_shader = new FragmentShader(fragment_shader_source) is lazy
477 end
478
479 # Map to organize `ShaderVariable` instances by their name
480 abstract class ShaderVariableMap[A: ShaderVariable]
481 super HashMap[String, A]
482
483 private var program: GamnitProgram
484
485 redef fun [](key)
486 do
487 # Alter the user specified name to fit the truncated name
488 var max_len = max_name_length - 1
489 if key isa Text and key.length > max_len then key = key.substring(0, max_len)
490 return super(key)
491 end
492
493 private fun max_name_length: Int is abstract
494 end
495
496 # Map to organize `Attribute` instances by their name
497 class AttributeMap
498 super ShaderVariableMap[Attribute]
499
500 redef fun provide_default_value(key) do
501 return new InactiveAttribute(program.gl_program.as(not null), "", -1, 0)
502 end
503
504 redef fun max_name_length do return glGetProgramiv(program.gl_program.as(not null), gl_ACTIVE_ATTRIBUTE_MAX_LENGTH)
505
506 redef fun [](key)
507 do
508 # Update the location of this attribute from the user specified name
509 var item = super
510 if key isa Text then item.location = program.gl_program.attrib_location(key.to_s)
511 return item
512 end
513 end
514
515 # Map to organize `Uniform` instances by their name
516 class UniformMap
517 super ShaderVariableMap[Uniform]
518
519 redef fun provide_default_value(key) do
520 return new InactiveUniform(program.gl_program.as(not null), "", -1, 0)
521 end
522
523 redef fun max_name_length do return glGetProgramiv(program.gl_program.as(not null), gl_ACTIVE_UNIFORM_MAX_LENGTH)
524
525 redef fun [](key)
526 do
527 var item = super
528 if key isa Text then item.location = program.gl_program.uniform_location(key.to_s)
529 return item
530 end
531 end
532
533 redef extern class NativeGLfloatArray
534
535 # Allocate a new matrix
536 new matrix `{ return malloc(4*4*sizeof(GLfloat)); `}
537
538 # Overwrite this matrix with the identity matrix
539 fun set_identity
540 do
541 for i in 4.times do
542 for j in 4.times do
543 matrix_set(i, j, if i == j then 1.0 else 0.0)
544 end
545 end
546 end
547
548 # Get the element at `x, y`
549 fun matrix_get(x, y: Int): Float `{ return self[y*4+x]; `}
550
551 # Set the element at `x, y`
552 fun matrix_set(x, y: Int, val: Float) `{ self[y*4+x] = val; `}
553 end
554
555 redef class Matrix
556 # Copy content of this matrix to a `NativeGLfloatArray`
557 fun fill_native(native: NativeGLfloatArray)
558 do
559 for i in width.times do
560 for j in height.times do
561 native.matrix_set(i, j, self[i, j])
562 end
563 end
564 end
565 end
566
567 private fun gammit_gl_error: nullable Error
568 do
569 var gl_error = glGetError
570 if gl_error == gl_NO_ERROR then return null
571 return new Error("GL error: {gl_error}")
572 end