danini-the-panini/mittsu-opengl

View on GitHub
lib/mittsu/opengl/plugins/shadow_map_plugin.rb

Summary

Maintainability
D
2 days
Test Coverage
F
52%
require 'mittsu/math'

module Mittsu
  class ShadowMapPlugin
    def initialize(renderer, lights, opengl_objects, opengl_objects_immediate)
      @renderer, @lights = renderer, lights
      @opengl_objects = opengl_objects
      @opengl_objects_immediate = opengl_objects_immediate

      @frustum = Frustum.new
      @proj_screen_matrix = Matrix4.new

      @min = Vector3.new
      @max = Vector3.new

      @matrix_position = Vector3.new

      @render_list = []

      depth_shader = OpenGL::Shader::Lib[:depth_rgba]
      depth_uniforms = OpenGL::Shader::UniformsUtils.clone(depth_shader.uniforms)

      @depth_material = ShaderMaterial.new(
        uniforms: depth_uniforms,
        vertex_shader: depth_shader.vertex_shader,
        fragment_shader: depth_shader.fragment_shader
      )

      @depth_material_morph = ShaderMaterial.new(
        uniforms: depth_uniforms,
        vertex_shader: depth_shader.vertex_shader,
        fragment_shader: depth_shader.fragment_shader,
        morph_targets: true
      )

      @depth_material_skin = ShaderMaterial.new(
        uniforms: depth_uniforms,
        vertex_shader: depth_shader.vertex_shader,
        fragment_shader: depth_shader.fragment_shader,
        skinning: true
      )

      @depth_material_morph_skin = ShaderMaterial.new(
        uniforms: depth_uniforms,
        vertex_shader: depth_shader.vertex_shader,
        fragment_shader: depth_shader.fragment_shader,
        morph_targets: true,
        skinning: true
      )

      @depth_material.shadow_pass = true
      @depth_material_morph.shadow_pass = true
      @depth_material_skin.shadow_pass = true
      @depth_material_morph_skin.shadow_pass = true
    end

    def render(scene, camera)
      return unless @renderer.shadow_map_enabled

      lights = []
      fog = nil

      # set GL state for depth map

      GL.ClearColor(1.0, 1.0, 1.0, 1.0)
      GL.Disable(GL::BLEND)

      GL.Enable(GL::CULL_FACE)
      GL.FrontFace(GL::CCW)

      if @renderer.shadow_map_cull_face = CullFaceFront
        GL.CullFace(GL::FRONT)
      else
        GL.CullFace(GL::BACK)
      end

      @renderer.state.set_depth_test(true)

      # process lights
      #  - skip lights that are not casting shadows
      #  - create virtual lights for cascaded shadow maps

      @lights.select(&:cast_shadow).each do |light|
        if light.is_a?(DirectionalLight) && light.shadow_cascade
          light.shadow_cascade_count.times do |n|
            if !light.shadow_cascade_array[n]
              virtual_light = create_virtual_light(light, n)
              virtual_light.original_camera = camera

              gyro = Gyroscope.new
              gyro.position.copy(light.shadow_cascade_offset)

              gyro.add(virtual_light)
              gyro.add(virtual_light.target)

              camera.add(gyro)

              light.shadow_cascade_array[n] = virtual_light
            else
              virtual_light = light.shadow_cascade_array[n]
            end

            update_virtual_light(light, n)

            lights << virtual_light
          end
        else
          lights << light
        end
      end

      # render depth map

      lights.each do |light|
        if !light.shadow_map
          shadow_filter = LinearFilter
          if @renderer.shadow_map_type == PCFSoftShadowMap
            shadow_filter = NearestFilter
          end

          pars = { min_filter: shadow_filter, mag_filter: shadow_filter, format: RGBAFormat }

          light.shadow_map = RenderTarget.new(light.shadow_map_width, light.shadow_map_height, pars)
          light.shadow_map.renderer = @renderer
          light.shadow_map_size = Vector2.new(light.shadow_map_width, light.shadow_map_height)

          light.shadow_matrix = Matrix4.new
        end

        if !light.shadow_camera
          case light
          when SpotLight
            light.shadow_camera = PerspectiveCamera.new(light.shadow_camera_fov, light.shadow_map_width / light.shadow_map_height, light.shadow_camera_near, light.shadow_camera_far)
          when DirectionalLight
            light.shadow_camera = OrthographicCamera.new(light.shadow_camera_left, light.shadow_camera_right, light.shadow_camera_top, light.shadow_camera_bottom, light.shadow_camera_near, light.shadow_camera_far)
          else
            puts "ERROR: Mittsu::ShadowMapPlugin: Unsupported light type for shadow #{light.inspect}"
            next
          end

          scene.add(light.shadow_camera)
          scene.update_matrix_world if scene.auto_update
        end

        if light.shadow_camera_visible && !light.camera_helper
          light.camera_helper = CameraHelper.new(light.shadow_camera)
          scene.add(light.camera_helper)
        end

        if light.virtual? && virtual_light.original_camera == camera
          update_shadow_camera(camera, light)
        end

        shadow_map = light.shadow_map
        shadow_matrix = light.shadow_matrix
        shadow_camera = light.shadow_camera

        #

        shadow_camera.position.set_from_matrix_position(light.matrix_world)
        @matrix_position.set_from_matrix_position(light.target.matrix_world)
        shadow_camera.look_at(@matrix_position)
        shadow_camera.update_matrix_world

        shadow_camera.matrix_world_inverse.inverse(shadow_camera.matrix_world)

        #


        light.camera_helper.visible = light.shadow_camera_visible if light.camera_helper
        light.camera_helper.update_points if light.shadow_camera_visible

        # compute shadow matrix

        shadow_matrix.set(
          0.5, 0.0, 0.0, 0.5,
          0.0, 0.5, 0.0, 0.5,
          0.0, 0.0, 0.5, 0.5,
          0.0, 0.0, 0.0, 1.0
        )

        shadow_matrix.multiply(shadow_camera.projection_matrix)
        shadow_matrix.multiply(shadow_camera.matrix_world_inverse)

        # update camera matrices and frustum

        @proj_screen_matrix.multiply_matrices(shadow_camera.projection_matrix, shadow_camera.matrix_world_inverse)
        @frustum.set_from_matrix(@proj_screen_matrix)

        # render shadow map

        @renderer.set_render_target(shadow_map)
        @renderer.clear

        # set object matrices & frustum culling

        @render_list.clear

        project_object(scene, scene, shadow_camera)

        # render regular obejcts

        @render_list.each do |opengl_object|
          object = opengl_object[:object]
          buffer = opengl_object[:buffer]

          # culling is overridden globally for all objects
          # while rendering depth map

          # need to deal with MeshFaceMaterial somehow
          # in that case just use the first of material.materials for now
          # (proper solution would require to break objects by materials
          #  similarly to regular rendering and then set corresponding
          #  depth materials per each chunk instead of just once per object)

          object_material = get_object_material(object)

          # TODO: SkinnedMesh/morph_targets
          # use_morphing = !object.geometry.morph_targets.nil? && !object.geometry.morph_targets.empty?
          # use_skinning = object.is_a?(SkinnedMesh) && object_material.skinning

          # TODO: SkinnedMesh/morph_targets
          # if object.custom_depth_material
          #   material = object.custom_depth_material
          # elsif use_skinning
          #   material = use_morphing ? @depth_material_morph_skin : @depth_material_skin
          # elsif use_morphing
          #   material = @deptth_material_morph
          # else
            material = @depth_material
          # end

          @renderer.set_material_faces(object_material)

          if buffer.is_a?(BufferGeometry)
            @renderer.render_buffer_direct(shadow_camera, @lights, fog, material, buffer, object)
          else
            @renderer.render_buffer(shadow_camera, @lights, fog, material, buffer, object)
          end
        end

        # set materices and rendr immeidate objects

        @opengl_objects_immediate.each do |opengl_object_immediate|
          opengl_object = opengl_object_immediate
          object = opengl_object[:object]

          if object.visible && object.cast_shadow
            object[:_model_view_matrix].multiply_matrices(shadow_camera.matrix_womatrix_world_inverse, object.matrix_world)
            @renderer.render_immediate_object(shadow_camera, @lights, fog, @depth_material, object)
          end
        end
      end

      # restore GL state

      clear_color = @renderer.get_clear_color
      clear_alpha = @renderer.get_clear_alpha

      GL.ClearColor(clear_color.r, clear_color.g, clear_color.b, clear_alpha)
      GL.Enable(GL::BLEND)

      if @renderer.shadow_map_cull_face == CullFaceFront
        GL.CullFace(GL::BACK)
      end

      @renderer.reset_gl_state
    end

    def project_object(scene, object, shadow_camera)
      if object.visible
        opengl_objects = @opengl_objects[object.id]

        if opengl_objects && object.cast_shadow && (object.frustum_culled == false || @frustum.intersects_object?(object) == true)
          opengl_objects.each do |opengl_object|
            object.model_view_matrix.multiply_matrices(shadow_camera.matrix_world_inverse, object.matrix_world)
            @render_list << opengl_object
          end
        end

        object.children.each do |child|
          project_object(scene, child, shadow_camera)
        end
      end
    end

    def create_virtual_light(light, cascade)
      DirectionalLight.new.tap do |virtual_light|
        virtual_light.is_virtual = true

        virtual_light.only_shadow = true
        virtual_light.cast_shadow = true

        virtual_light.shadow_camera_near = light.shadow_camera_near
        virtual_light.shadow_camera_far = light.shadow_camera_far

        virtual_light.shadow_camera_left = light.shadow_camera_left
        virtual_light.shadow_camera_right = light.shadow_camera_right
        virtual_light.shadow_camera_bottom = light.shadow_camera_bottom
        virtual_light.shadow_camera_top = light.shadow_camera_top

        virtual_light.shadow_camera_visible = light.shadow_camera_visible

        virtual_light.shadow_darkness = light.shadow_darkness

        virtual_light.shadow_darkness = light.shadow_darkness

        virtual_light.shadow_bias = light.shadow_cascade_bias[cascade]
        virtual_light.shadow_map_width = light.shadow_cascade_width[cascade]
        virtual_light.shadow_map_height = light.shadow_cascade_height[cascade]

        points_world = virtual_light.points_world = []
        points_frustum = virtual_light.points_frustum = []

        8.times do
          points_world << Vector3.new
          points_frustum << Vector3.new
        end

        near_z = light.shadow_cascade_near_z[cascade]
        far_z = light.shadow_cascade_far_z[cascade]

        points_frustum[0].set(-1.0, -1.0, near_z)
        points_frustum[1].set( 1.0, -1.0, near_z)
        points_frustum[2].set(-1.0,  1.0, near_z)
        points_frustum[3].set( 1.0,  1.0, near_z)

        points_frustum[4].set(-1.0, -1.0, far_z)
        points_frustum[5].set( 1.0, -1.0, far_z)
        points_frustum[6].set(-1.0,  1.0, far_z)
        points_frustum[7].set( 1.0,  1.0, far_z)
      end
    end

    # synchronize virtual light with the original light

    def update_virtual_light(light, cascade)
      virtual_light = light.shadow_cascade_array[cascade]

      virtual_light.position.copy(light.position)
      virtual_light.target.position.copy(light.target.position)
      virtual_light.look_at(virtual_light.target)

      virtual_light.shadow_camera_visible = light.shadow_camera_visible
      virtual_light.shadow_darkness = light.shadow_darkness

      virtual_light.shadow_bias = light.shadow_cascade_bias[cascade]

      near_z = light.shadow_cascade_near_z[cascade]
      far_z = light.shadow_cascade_far_z[cascade]

      points_frustum = virtual_light.points_frustum

      points_frustum[0].z = near_z
      points_frustum[1].z = near_z
      points_frustum[2].z = near_z
      points_frustum[3].z = near_z

      points_frustum[4].z = far_z
      points_frustum[5].z = far_z
      points_frustum[6].z = far_z
      points_frustum[7].z = far_z
    end

    # fit shadow camera's ortho frustum to camera frustum

    def update_shadow_camera(camera, light)
      shadow_camera = light.shadow_camera
      points_frustum = light.pointa_frustum
      points_world = light.points_world

      @min.set(Float::INFINITY, Float::INFINITY, Float::INFINITY)
      @max.set(-Float::INFINITY, -Float::INFINITY, -Float::INFINITY)

      8.times do |i|
        p = points_world[i]

        p.copy(points_frustum[i])
        p.unproject(camera)

        p.apply_matrix4(shadow_camera.matrix_world_inverse)

        @min.x = p.x if (p.x < @min.x)
        @max.x = p.x if (p.x > @max.x)

        @min.y = p.y if (p.y < @min.y)
        @max.y = p.y if (p.y > @max.y)

        @min.z = p.z if (p.z < @min.z)
        @max.z = p.z if (p.z > @max.z)
      end

      shadow_camera.left = @min.x
      shadow_camera.right = @max.x
      shadow_camera.top = @max.y
      shadow_camera.bottom = @min.y

      # can't really fit near/far
      # shadow_camera.near = @min.x
      # shadow_camera.far = @max.z

      shadow_camera.update_projection_matrix
    end

    # For the moment just ignore objects that have multiple materials with different animation methods
    # Only the frst material will be taken into account for deciding which depth material to use for shadow maps

    def get_object_material(object)
      if object.material.is_a?(MeshFaceMaterial)
        object.material.materials[0]
      else
        object.material
      end
    end
  end
end