lib/cyberarm_engine/shader.rb in cyberarm_engine-0.13.0 vs lib/cyberarm_engine/shader.rb in cyberarm_engine-0.13.1

- old
+ new

@@ -1,262 +1,398 @@ -module CyberarmEngine - # Ref: https://github.com/vaiorabbit/ruby-opengl/blob/master/sample/OrangeBook/brick.rb - class Shader - include OpenGL - @@shaders = {} - PREPROCESSOR_CHARACTER = "@" - - def self.add(name, instance) - @@shaders[name] = instance - end - - def self.use(name, &block) - shader = @@shaders.dig(name) - if shader - shader.use(&block) - else - raise ArgumentError, "Shader '#{name}' not found!" - end - end - - def self.available?(name) - @@shaders.dig(name).is_a?(Shader) - end - - def self.get(name) - @@shaders.dig(name) - end - - def self.active_shader - @active_shader - end - - def self.active_shader=(instance) - @active_shader = instance - end - - def self.stop - shader = Shader.active_shader - - if shader - shader.stop - else - raise ArgumentError, "No active shader to stop!" - end - end - - def self.attribute_location(variable) - raise RuntimeError, "No active shader!" unless Shader.active_shader - Shader.active_shader.attribute_location(variable) - end - - def self.set_uniform(variable, value) - raise RuntimeError, "No active shader!" unless Shader.active_shader - Shader.active_shader.set_uniform(variable, value) - end - - attr_reader :name, :program - def initialize(name:, includes_dir: nil, vertex: "shaders/default.vert", fragment:) - raise "Shader name can not be blank" if name.length == 0 - - @name = name - @includes_dir = includes_dir - @compiled = false - - @program = nil - - @error_buffer_size = 1024 - @variable_missing = {} - - @data = {shaders: {}} - - unless shader_files_exist?(vertex: vertex, fragment: fragment) - raise ArgumentError, "Shader files not found: #{vertex} or #{fragment}" - end - - create_shader(type: :vertex, source: File.read(vertex)) - create_shader(type: :fragment, source: File.read(fragment)) - - compile_shader(type: :vertex) - compile_shader(type: :fragment) - link_shaders - - # Only add shader if it successfully compiles - if @compiled - puts "compiled!" - puts "Compiled shader: #{@name}" - Shader.add(@name, self) - else - warn "FAILED to compile shader: #{@name}", "" - end - end - - def shader_files_exist?(vertex:, fragment:) - File.exist?(vertex) && File.exist?(fragment) - end - - def create_shader(type:, source:) - _shader = nil - - case type - when :vertex - _shader = glCreateShader(GL_VERTEX_SHADER) - when :fragment - _shader = glCreateShader(GL_FRAGMENT_SHADER) - else - warn "Unsupported shader type: #{type.inspect}" - end - - processed_source = preprocess_source(source: source) - - _source = [processed_source].pack("p") - _size = [processed_source.length].pack("I") - glShaderSource(_shader, 1, _source, _size) - - @data[:shaders][type] =_shader - end - - def preprocess_source(source:) - lines = source.lines - - lines.each_with_index do |line, i| - if line.start_with?(PREPROCESSOR_CHARACTER) - preprocessor = line.strip.split(" ") - lines.delete(line) - - case preprocessor.first - when "@include" - raise ArgumentError, "Shader preprocessor include directory was not given for shader #{@name}" unless @includes_dir - - preprocessor[1..preprocessor.length - 1].join.scan(/"([^"]*)"/).flatten.each do |file| - source = File.read("#{@includes_dir}/#{file}.glsl") - - lines.insert(i, source) - end - else - warn "Unsupported preprocessor #{preprocessor.first} for #{@name}" - end - end - end - - lines.join - end - - def compile_shader(type:) - _compiled = false - _shader = @data[:shaders][type] - raise ArgumentError, "No shader for #{type.inspect}" unless _shader - - glCompileShader(_shader) - buffer = ' ' - glGetShaderiv(_shader, GL_COMPILE_STATUS, buffer) - compiled = buffer.unpack('L')[0] - - if compiled == 0 - log = ' ' * @error_buffer_size - glGetShaderInfoLog(_shader, @error_buffer_size, nil, log) - puts "Shader Error: Program \"#{@name}\"" - puts " #{type.to_s.capitalize} Shader InfoLog:", " #{log.strip.split("\n").join("\n ")}\n\n" - puts " Shader Compiled status: #{compiled}" - puts " NOTE: assignment of uniforms in shaders is illegal!" - puts - else - _compiled = true - end - - return _compiled - end - - def link_shaders - @program = glCreateProgram - @data[:shaders].values.each do |_shader| - glAttachShader(@program, _shader) - end - glLinkProgram(@program) - - buffer = ' ' - glGetProgramiv(@program, GL_LINK_STATUS, buffer) - linked = buffer.unpack('L')[0] - - if linked == 0 - log = ' ' * @error_buffer_size - glGetProgramInfoLog(@program, @error_buffer_size, nil, log) - puts "Shader Error: Program \"#{@name}\"" - puts " Program InfoLog:", " #{log.strip.split("\n").join("\n ")}\n\n" - end - - @compiled = linked == 0 ? false : true - end - - # Returns the location of a uniform variable - def variable(variable) - loc = glGetUniformLocation(@program, variable) - if (loc == -1) - puts "Shader Error: Program \"#{@name}\" has no such uniform named \"#{variable}\"", " Is it used in the shader? GLSL may have optimized it out.", " Is it miss spelled?" unless @variable_missing[variable] - @variable_missing[variable] = true - end - return loc - end - - def use(&block) - return unless compiled? - raise "Another shader is already in use! #{Shader.active_shader.name.inspect}" if Shader.active_shader - Shader.active_shader=self - - glUseProgram(@program) - - if block - block.call(self) - stop - end - end - - def stop - Shader.active_shader = nil if Shader.active_shader == self - glUseProgram(0) - end - - def compiled? - @compiled - end - - def attribute_location(variable) - glGetUniformLocation(@program, variable) - end - - def uniform_transform(variable, value, location = nil) - attr_loc = location ? location : attribute_location(variable) - - glUniformMatrix4fv(attr_loc, 1, GL_FALSE, value.to_gl.pack("F16")) - end - - def uniform_boolean(variable, value, location = nil) - attr_loc = location ? location : attribute_location(variable) - - glUniform1i(attr_loc, value ? 1 : 0) - end - - def uniform_integer(variable, value, location = nil) - attr_loc = location ? location : attribute_location(variable) - - glUniform1i(attr_loc, value) - end - - def uniform_float(variable, value, location = nil) - attr_loc = location ? location : attribute_location(variable) - - glUniform1f(attr_loc, value) - end - - def uniform_vec3(variable, value, location = nil) - attr_loc = location ? location : attribute_location(variable) - - glUniform3f(attr_loc, *value.to_a[0..2]) - end - - def uniform_vec4(variable, value, location = nil) - attr_loc = location ? location : attribute_location(variable) - - glUniform4f(attr_loc, *value.to_a) - end - end -end +module CyberarmEngine + # Ref: https://github.com/vaiorabbit/ruby-opengl/blob/master/sample/OrangeBook/brick.rb + class Shader + include OpenGL + @@shaders = {} # Cache for {Shader} instances + PREPROCESSOR_CHARACTER = "@".freeze # magic character for preprocessor phase of {Shader} compilation + + # add instance of {Shader} to cache + # + # @param name [String] + # @param instance [Shader] + def self.add(name, instance) + @@shaders[name] = instance + end + + # removes {Shader} from cache and cleans up + # + # @param name [String] + def self.delete(name) + shader = @@shaders.dig(name) + + if shader + @@shaders.delete(name) + + if shader.compiled? + glDeleteProgram(shader.program) + end + end + end + + ## + # runs _block_ using {Shader} with _name_ + # + # @example + # + # CyberarmEngine::Shader.use("blur") do |shader| + # shader.uniform_float("radius", 20.0) + # # OpenGL Code that uses shader + # end + # + # @param name [String] name of {Shader} to use + # @return [void] + def self.use(name, &block) + shader = @@shaders.dig(name) + if shader + shader.use(&block) + else + raise ArgumentError, "Shader '#{name}' not found!" + end + end + + # returns whether {Shader} with _name_ is in cache + # + # @param name [String] + # @return [Boolean] + def self.available?(name) + @@shaders.dig(name).is_a?(Shader) + end + + # returns instance of {Shader}, if it exists + # + # @param name [String] + # @return [Shader?] + def self.get(name) + @@shaders.dig(name) + end + + # returns currently active {Shader}, if one is active + # + # @return [Shader?] + def self.active_shader + @active_shader + end + + # sets currently active {Shader} + # + # @param instance [Shader] instance of {Shader} to set as active + def self.active_shader=(instance) + @active_shader = instance + end + + # stops using currently active {Shader} + def self.stop + shader = Shader.active_shader + + if shader + shader.stop + else + raise ArgumentError, "No active shader to stop!" + end + end + + # returns location of OpenGL Shader uniform + # + # @param variable [String] + def self.attribute_location(variable) + raise RuntimeError, "No active shader!" unless Shader.active_shader + Shader.active_shader.attribute_location(variable) + end + + # sets _variable_ to _value_ + # + # @param variable [String] + # @param value + def self.set_uniform(variable, value) + raise RuntimeError, "No active shader!" unless Shader.active_shader + Shader.active_shader.set_uniform(variable, value) + end + + attr_reader :name, :program + def initialize(name:, includes_dir: nil, vertex: "shaders/default.vert", fragment:) + raise "Shader name can not be blank" if name.length == 0 + + @name = name + @includes_dir = includes_dir + @compiled = false + + @program = nil + + @error_buffer_size = 1024 * 8 + @variable_missing = {} + + @data = {shaders: {}} + + unless shader_files_exist?(vertex: vertex, fragment: fragment) + raise ArgumentError, "Shader files not found: #{vertex} or #{fragment}" + end + + create_shader(type: :vertex, source: File.read(vertex)) + create_shader(type: :fragment, source: File.read(fragment)) + + compile_shader(type: :vertex) + compile_shader(type: :fragment) + link_shaders + + @data[:shaders].each { |key, id| glDeleteShader(id) } + + # Only add shader if it successfully compiles + if @compiled + puts "compiled!" + puts "Compiled shader: #{@name}" + Shader.add(@name, self) + else + glDeleteProgram(@program) + warn "FAILED to compile shader: #{@name}", "" + end + end + + # whether vertex and fragment files exist on disk + # + # @return [Boolean] + def shader_files_exist?(vertex:, fragment:) + File.exist?(vertex) && File.exist?(fragment) + end + + # creates an OpenGL Shader of _type_ using _source_ + # + # @param type [Symbol] valid values are: :vertex, :fragment + # @param source [String] source code for shader + def create_shader(type:, source:) + _shader = nil + + case type + when :vertex + _shader = glCreateShader(GL_VERTEX_SHADER) + when :fragment + _shader = glCreateShader(GL_FRAGMENT_SHADER) + else + raise ArgumentError, "Unsupported shader type: #{type.inspect}" + end + + processed_source = preprocess_source(source: source) + + _source = [processed_source].pack("p") + _size = [processed_source.length].pack("I") + glShaderSource(_shader, 1, _source, _size) + + @data[:shaders][type] =_shader + end + + # evaluates shader preprocessors + # + # currently supported preprocessors: + # + # @include "file/path" "another/file/path" # => Replace line with contents of file; Shader includes_dir must be specified in constructor + # + # @example + # # Example Vertex Shader # + # # #version 330 core + # # @include "material_struct" + # # void main() { + # # gl_Position = vec4(1, 1, 1, 1); + # # } + # + # Shader.new(name: "model_renderer", includes_dir: "path/to/includes", vertex: "path/to/vertex_shader.glsl") + # + # @param source shader source code + def preprocess_source(source:) + lines = source.lines + + lines.each_with_index do |line, i| + if line.start_with?(PREPROCESSOR_CHARACTER) + preprocessor = line.strip.split(" ") + lines.delete(line) + + case preprocessor.first + when "@include" + raise ArgumentError, "Shader preprocessor include directory was not given for shader #{@name}" unless @includes_dir + + preprocessor[1..preprocessor.length - 1].join.scan(/"([^"]*)"/).flatten.each do |file| + source = File.read("#{@includes_dir}/#{file}.glsl") + + lines.insert(i, source) + end + else + warn "Unsupported preprocessor #{preprocessor.first} for #{@name}" + end + end + end + + lines.join + end + + # compile OpenGL Shader of _type_ + # + # @return [Boolean] whether compilation succeeded + def compile_shader(type:) + _compiled = false + _shader = @data[:shaders][type] + raise ArgumentError, "No shader for #{type.inspect}" unless _shader + + glCompileShader(_shader) + buffer = ' ' + glGetShaderiv(_shader, GL_COMPILE_STATUS, buffer) + compiled = buffer.unpack('L')[0] + + if compiled == 0 + log = ' ' * @error_buffer_size + glGetShaderInfoLog(_shader, @error_buffer_size, nil, log) + puts "Shader Error: Program \"#{@name}\"" + puts " #{type.to_s.capitalize} Shader InfoLog:", " #{log.strip.split("\n").join("\n ")}\n\n" + puts " Shader Compiled status: #{compiled}" + puts " NOTE: assignment of uniforms in shaders is illegal!" + puts + else + _compiled = true + end + + return _compiled + end + + # link compiled OpenGL Shaders in to a OpenGL Program + # + # @note linking must succeed or shader cannot be used + # + # @return [Boolean] whether linking succeeded + def link_shaders + @program = glCreateProgram + @data[:shaders].values.each do |_shader| + glAttachShader(@program, _shader) + end + glLinkProgram(@program) + + buffer = ' ' + glGetProgramiv(@program, GL_LINK_STATUS, buffer) + linked = buffer.unpack('L')[0] + + if linked == 0 + log = ' ' * @error_buffer_size + glGetProgramInfoLog(@program, @error_buffer_size, nil, log) + puts "Shader Error: Program \"#{@name}\"" + puts " Program InfoLog:", " #{log.strip.split("\n").join("\n ")}\n\n" + end + + @compiled = linked == 0 ? false : true + end + + # Returns the location of a uniform _variable_ + # + # @param variable [String] + # @return [Integer] location of uniform + def variable(variable) + loc = glGetUniformLocation(@program, variable) + if (loc == -1) + puts "Shader Error: Program \"#{@name}\" has no such uniform named \"#{variable}\"", " Is it used in the shader? GLSL may have optimized it out.", " Is it miss spelled?" unless @variable_missing[variable] + @variable_missing[variable] = true + end + return loc + end + + # @see Shader.use Shader.use + def use(&block) + return unless compiled? + raise "Another shader is already in use! #{Shader.active_shader.name.inspect}" if Shader.active_shader + Shader.active_shader=self + + glUseProgram(@program) + + if block + block.call(self) + stop + end + end + + # stop using shader, if shader is active + def stop + Shader.active_shader = nil if Shader.active_shader == self + glUseProgram(0) + end + + # @return [Boolean] whether {Shader} successfully compiled + def compiled? + @compiled + end + + # returns location of a uniform _variable_ + # + # @note Use {#variable} for friendly error handling + # @see #variable Shader#variable + # + # @param variable [String] + # @return [Integer] + def attribute_location(variable) + glGetUniformLocation(@program, variable) + end + + # send {Transform} to {Shader} + # + # @param variable [String] + # @param value [Transform] + # @param location [Integer] + # @return [void] + def uniform_transform(variable, value, location = nil) + attr_loc = location ? location : attribute_location(variable) + + glUniformMatrix4fv(attr_loc, 1, GL_FALSE, value.to_gl.pack("F16")) + end + + # send Boolean to {Shader} + # + # @param variable [String] + # @param value [Boolean] + # @param location [Integer] + # @return [void] + def uniform_boolean(variable, value, location = nil) + attr_loc = location ? location : attribute_location(variable) + + glUniform1i(attr_loc, value ? 1 : 0) + end + + # send Integer to {Shader} + # @param variable [String] + # @param value [Integer] + # @param location [Integer] + # @return [void] + def uniform_integer(variable, value, location = nil) + attr_loc = location ? location : attribute_location(variable) + + glUniform1i(attr_loc, value) + end + + # send Float to {Shader} + # + # @param variable [String] + # @param value [Float] + # @param location [Integer] + # @return [void] + def uniform_float(variable, value, location = nil) + attr_loc = location ? location : attribute_location(variable) + + glUniform1f(attr_loc, value) + end + + # send {Vector} (x, y, z) to {Shader} + # + # @param variable [String] + # @param value [Vector] + # @param location [Integer] + # @return [void] + def uniform_vec3(variable, value, location = nil) + attr_loc = location ? location : attribute_location(variable) + + glUniform3f(attr_loc, *value.to_a[0..2]) + end + + # send {Vector} to {Shader} + # + # @param variable [String] + # @param value [Vector] + # @param location [Integer] + # @return [void] + def uniform_vec4(variable, value, location = nil) + attr_loc = location ? location : attribute_location(variable) + + glUniform4f(attr_loc, *value.to_a) + end + end +end