danini-the-panini/mittsu-opengl

View on GitHub
lib/mittsu/opengl_implementation/materials/material.rb

Summary

Maintainability
A
2 hrs
Test Coverage
B
86%
module Mittsu
  class Material
    # TODO: init_shader for these material-types
    # MeshDepthMaterial => :depth, # TODO...
    # MeshNormalMaterial => :normal, # TODO...
    # LineDashedMaterial => :dashed, # TODO...
    # PointCloudMaterial => :particle_basic # TODO...

    attr_accessor :shadow_pass
    attr_reader :shader, :uniforms_list

    def init(lights, fog, object, renderer)
      @renderer = renderer

      add_event_listener(:dispose, @renderer.method(:on_material_dispose))

      init_shader

      self.program = find_or_create_program(lights, fog, object)

      count_supported_morph_attributes(program.attributes)

      @uniforms_list = get_uniforms_list
    end

    def set(renderer)
      @renderer = renderer

      if transparent
        @renderer.state.set_blending(blending, blend_equation, blend_src, blend_dst, blend_equation_alpha, blend_src_alpha, blend_dst_alpha)
      else
        @renderer.state.set_blending(NoBlending)
      end

      @renderer.state.set_depth_test(depth_test)
      @renderer.state.set_depth_write(depth_write)
      @renderer.state.set_color_write(color_write)
      @renderer.state.set_polygon_offset(polygon_offset, polygon_offset_factor, polygon_offset_units)
    end

    def needs_face_normals?
      shading == FlatShading
    end

    def clear_custom_attributes
      attributes.each do |attribute|
        attribute.needs_update = false
      end
    end

    def custom_attributes_dirty?
      attributes.each do |attribute|
        return true if attribute.needs_update
      end
      false
    end

    def refresh_uniforms(_)
      # NOOP
    end

    def needs_camera_position_uniform?
      env_map
    end

    def needs_view_matrix_uniform?
      skinning
    end

    def needs_lights?
      lights
    end

    protected

    def init_shader
      @shader = {
        uniforms: uniforms,
        vertex_shader: vertex_shader,
        fragment_shader: fragment_shader
      }
    end

    def shader_id
      nil
    end

    private

    def allocate_lights(lights)
      lights.reject { |light|
        light.only_shadow || !light.visible
      }.each_with_object({
        directional: 0, point: 0, spot: 0, hemi: 0, other: 0
      }) { |light, counts|
        counts[light.to_sym] += 1
      }
    end

    def allocate_shadows(lights)
      max_shadows = 0

      lights.each do |light|
        next unless light.cast_shadow

        max_shadows += 1 if light.is_a?(SpotLight)
        max_shadows += 1 if light.is_a?(DirectionalLight) && !light.shadow_cascade
      end

      max_shadows
    end

    def allocate_bones(object = nil)
      if @renderer.supports_bone_textures? && object && object.skeleton && object.skeleton.use_vertex_texture
        return 1024
      end

      # default for when object is not specified
      # ( for example when prebuilding shader
      #   to be used with multiple objects )
      #
      #  - leave some extra space for other uniforms
      #  - limit here is ANGLE's 254 max uniform vectors
      #    (up to 54 should be safe)

      n_vertex_uniforms = (GL.GetParameter(GL::MAX_VERTEX_UNIFORM_COMPONENTS) / 4.0).floor
      n_vertex_matrices = ((n_vertex_uniforms - 20) / 4.0).floor

      max_bones = n_vertex_matrices

      # TODO: when SkinnedMesh exists
      # if !object.nil? && object.is_a?(SkinnedMesh)
      #   max_bones = [object.skeleton.bones.length, max_bones].min
      #
      #   if max_bones < object.skeleton.bones.length
      #     puts "WARNING: OpenGL::Renderer: too many bones - #{object.skeleton.bones.length}, this GPU supports just #{max_bones}"
      #   end
      # end

      max_bones
    end

    def count_supported_morph_attributes(attributes)
      if morph_targets
        self.num_supported_morph_normals = count_supported_morph_attribute(attributes, 'morphTarget', @renderer.max_morph_normals)
      end
      if morph_normals
        self.num_supported_morph_normals = count_supported_morph_attribute(attributes, 'morphNormal', @renderer.max_morph_normals)
      end
    end

    def count_supported_morph_attribute(attributes, base, max)
      max.times.reduce do |num, i|
        attribute = attributes["#{base}#{i}"]
        attribute && attribute >= 0 ? num + 1 : num
      end
    end

    def get_uniforms_list
      @shader[:uniforms].map { |(key, uniform)|
        location = program.uniforms[key]
        if location
          [uniform, location]
        end
      }.compact
    end

    def program_parameters(lights, fog, object)
      # heuristics to create shader paramaters according to lights in the scene
      # (not to blow over max_lights budget)

      max_light_count = allocate_lights(lights)
      max_shadows = allocate_shadows(lights)
      max_bones = allocate_bones(object)

      {
        supports_vertex_textures: @renderer.supports_vertex_textures?,

        map: !!map,
        env_map: !!env_map,
        env_map_mode: env_map && env_map.mapping,
        light_map: !!light_map,
        bump_map: !!light_map,
        normal_map: !!normal_map,
        specular_map: !!specular_map,
        alpha_map: !!alpha_map,

        combine: combine,

        vertex_colors: vertex_colors,

        fog: fog,
        use_fog: fog,
        # fog_exp: fog.is_a?(FogExp2), # TODO: when FogExp2 exists

        flat_shading: shading == FlatShading,

        size_attenuation: size_attenuation,
        logarithmic_depth_buffer: @renderer.logarithmic_depth_buffer,

        skinning: skinning,
        max_bones: max_bones,
        use_vertex_texture: @renderer.supports_bone_textures?,

        morph_targets: morph_targets,
        morph_normals: morph_normals,
        max_morph_targets: @renderer.max_morph_targets,
        max_morph_normals: @renderer.max_morph_normals,

        max_dir_lights: max_light_count[:directional],
        max_point_lights: max_light_count[:point],
        max_spot_lights: max_light_count[:spot],
        max_hemi_lights: max_light_count[:hemi],

        max_shadows: max_shadows,
        shadow_map_enabled: @renderer.shadow_map_enabled? && object.receive_shadow && max_shadows > 0,
        shadow_map_type: @renderer.shadow_map_type,
        shadow_map_debug: @renderer.shadow_map_debug,
        shadow_map_cascade: @renderer.shadow_map_cascade,

        alpha_test: alpha_test,
        metal: metal,
        wrap_around: wrap_around,
        double_sided: side == DoubleSide,
        flip_sided: side == BackSide
      }
    end

    def program_slug(parameters)
      chunks = []

      if shader_id
        chunks << shader_id
      else
        chunks << fragment_shader
        chunks << vertex_shader
      end

      if !defines.nil?
        defines.each do |(name, define)|
          chunks << name
          chunks << define
        end
      end

      parameters.each do |(name, parameter)|
        chunks << name
        chunks << parameter
      end

      chunks.join
    end

    def find_or_create_program(lights, fog, object)
      parameters = program_parameters(lights, fog, object)
      code = program_slug(parameters)

      program = @renderer.programs.find do |program_info|
        program_info.code == code
      end

      if program.nil?
        program = OpenGL::Program.new(@renderer, code, self, parameters)
        @renderer.programs.push(program)

        @renderer.info[:memory][:programs] = @renderer.programs.length
      else
        program.used_times += 1
      end

      program
    end
  end
end