lib/mittsu/opengl/plugins/shadow_map_plugin.rb
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