danini-the-panini/mittsu-opengl

View on GitHub
lib/mittsu/opengl/program.rb

Summary

Maintainability
A
1 hr
Test Coverage
B
83%
require 'erb'
require 'mittsu/opengl/shader'

module Mittsu
  class OpenGL::Program
    attr_reader :id, :program, :uniforms
    attr_accessor :code, :used_times, :attributes, :vertex_shader, :fragment_shader

    def initialize(renderer, code, material, parameters)
      @id = (@@id ||= 1).tap { @@id += 1 }
      @renderer = renderer

      compile_and_link_program(material, parameters)

      cache_uniform_locations(material.shader[:uniforms] || {}, parameters)
      cache_attribute_locations(material.attributes || {}, parameters)

      @code = code
      @used_times = 2
    end

    private

    def compile_and_link_program(material, parameters)
      @program = GL.CreateProgram

      # TODO: necessary for OpenGL?
      # index0_attribute_name = material.index0_attribute_name
      #
      # if index0_attribute_name.nil? && parameters[:morph_targets]
      #   # programs with morph_targets displace position of attribute 0
      #
      #   index0_attribute_name = 'position'
      # end

      compile_shaders(material, parameters)

      # if !index0_attribute_name.nil?
        # TODO: is this necessary in OpenGL ???
        # Force a particular attribute to index 0.
        # because potentially expensive emulation is done by __browser__ if attribute 0 is disabled. (no browser here!)
        # And, color, for example is often automatically bound to index 0 so disabling it

      #   GL.BindAttributeLocation(program, 0, index0_attribute_name)
      # end

      GL.LinkProgram(@program)
      check_for_link_errors
      post_link_clean_up
    end

    def generate_defines(defines)
      chunks = []

      defines.each do |d, value|
        next if value == false

        chunk = "#define #{d} #{value}"
        chunks << chunk
      end

      chunks.join("\n")
    end

    def link_status
      ptr = ' '*8
      GL.GetProgramiv @program, GL::LINK_STATUS, ptr
      ptr.unpack('L')[0]
    end

    def program_info_log
      ptr = ' '*8
      GL.GetProgramiv @program, GL::INFO_LOG_LENGTH, ptr
      length = ptr.unpack('L')[0]

      if length > 0
        log = ' '*length
        GL.GetProgramInfoLog @program, length, ptr, log
        log.unpack("A#{length}")[0]
      else
        ''
      end
    end

    def check_for_link_errors
      log_info = program_info_log

      if !link_status
        puts "ERROR: Mittsu::OpenGL::Program: shader error: #{GL.GetError}, GL::INVALID_STATUS, #{GL.GetProgramParameter(program, GL::VALIDATE_STATUS)}, GL.GetProgramParameterInfoLog, #{log_info}"
      end

      if !log_info.empty?
        puts "WARNING: Mittsu::OpenGL::Program: GL.GetProgramInfoLog, #{log_info}"
        # TODO: useless in OpenGL ???
        # puts "WARNING: #{GL.GetExtension( 'OPENGL_debug_shaders' ).getTranslatedShaderSource( GL.VertexShader )}"
        # puts "WARNING: #{GL.GetExtension( 'OPENGL_debug_shaders' ).getTranslatedShaderSource( GL.FragmentShader )}"
      end
    end

    def get_shadow_map_define(shadow_map_param)
      case shadow_map_param
      when PCFShadowMap
        'SHADOWMAP_TYPE_PCF'
      when PCFSoftShadowMap
        'SHADOWMAP_TYPE_PCF_SOFT'
      else
        'SHADOWMAP_TYPE_BASIC'
      end
    end

    def get_env_map_type_define(env_map_param, material)
      return 'ENVMAP_TYPE_CUBE' unless env_map_param
      case material.env_map.mapping
      when CubeReflectionMapping, CubeRefractionMapping
        'ENVMAP_TYPE_CUBE'
      when EquirectangularReflectionMapping, EquirectangularRefractionMapping
        'ENVMAP_TYPE_EQUIREC'
      when SphericalReflectionMapping
        'ENVMAP_TYPE_SPHERE'
      else
        'ENVMAP_TYPE_CUBE'
      end
    end

    def get_env_map_mode_define(env_map_param, material)
      return 'ENVMAP_MODE_REFLECTION' unless env_map_param
      case material.env_map.mapping
      when CubeRefractionMapping, EquirectangularRefractionMapping
        'ENVMAP_MODE_REFRACTION'
      else
        'ENVMAP_MODE_REFLECTION'
      end
    end

    def get_env_map_blending_define(env_map_param, material)
      return 'ENVMAP_BLENDING_MULTIPLY' unless env_map_param
      case material.combine
      when MultiplyOperation
        'ENVMAP_BLENDING_MULTIPLY'
      when MixOperation
        'ENVMAP_BLENDING_MIX'
      when AddOperation
        'ENVMAP_BLENDING_ADD'
      else
        'ENVMAP_BLENDING_MULTIPLY'
      end
    end

    def compile_shaders(material, parameters)
      shadow_map_type_define = get_shadow_map_define(parameters[:shadow_map_type])

      env_map_type_define = get_env_map_type_define(parameters[:env_map], material)
      env_map_mode_define = get_env_map_mode_define(parameters[:env_map], material)
      env_map_blending_define = get_env_map_blending_define(parameters[:env_map], material)

      gamma_factor_define = (@renderer.gamma_factor > 0) ? @renderer.gamma_factor : 1.0

      custom_defines = generate_defines(material.defines || {})

      if false # material.is_a?(RawShaderMaterial) # TODO: when RawShaderMaterial exists
        prefix_vertex = ''
        prefix_fragment = ''
      else
        prefix_vertex = File.read(File.expand_path('../shader/templates/vertex.glsl.erb', __FILE__))
        prefix_fragment = File.read(File.expand_path('../shader/templates/fragment.glsl.erb', __FILE__))
      end

      @vertex_shader = OpenGL::Shader.new(GL::VERTEX_SHADER, compile_shader_template(prefix_vertex + material.shader[:vertex_shader], binding))
      @fragment_shader = OpenGL::Shader.new(GL::FRAGMENT_SHADER, compile_shader_template(prefix_fragment + material.shader[:fragment_shader], binding))

      GL.AttachShader(@program, @vertex_shader.shader)
      GL.AttachShader(@program, @fragment_shader.shader)
    end

    def compile_shader_template(template, b)
      ERB.new(template).result(b)
    end

    def post_link_clean_up
      GL.DeleteShader(@vertex_shader.shader)
      GL.DeleteShader(@fragment_shader.shader)
    end

    def cache_uniform_locations(uniforms, parameters)
      identifiers = [
        'viewMatrix',
        'modelViewMatrix',
        'projectionMatrix',
        'normalMatrix',
        'modelMatrix',
        'cameraPosition',
        'morphTargetInfluences',
        'bindMatrix',
        'bindMatrixInverse'
      ]

      if parameters[:use_vertex_texture]
        identifiers << 'boneTexture'
        identifiers << 'boneTextureWidth'
        identifiers << 'boneTextureHeight'
      else
        identifiers << 'boneGlobalMatrices'
      end

      if parameters[:logarithmic_depth_buffer]
        identifiers << 'logDepthBufFC'
      end

      uniforms.each do |k, v|
        identifiers << k.to_s
      end

      @uniforms = {}
      identifiers.each do |id|
        @uniforms[id] = GL.GetUniformLocation(program, id)
      end
    end

    def cache_attribute_locations(attributes, parameters)
      identifiers = [
        'position',
        'normal',
        'uv',
        'uv2',
        'tangent',
        'color',
        'skinIndex',
        'skinWeight',
        'lineDistance'
      ]

      parameters[:max_morph_targets].times do |i|
        identifiers << "morphTarget#{i}"
      end

      parameters[:max_morph_normals].times do |i|
        identifiers << "morphNormal#{i}"
      end

      attributes.each do |k, v|
        identifiers << k
      end

      @attributes = {}
      identifiers.each do |id|
        @attributes[id] = GL.GetAttribLocation(program, id)
      end
    end
  end
end