Last active
March 17, 2024 09:25
-
-
Save jmiskovic/3b687bb3052d930f03c2648c54366a12 to your computer and use it in GitHub Desktop.
Directional light shadow mapping for lovr
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- directional light shadow mapping for lovr 0.18 (currenly on post 0.17.1 dev branch) | |
local m = {} | |
m.near = 0.01 | |
m.far = 500 | |
m.orthographic_span = 40 | |
m.perspective_fov = 90 | |
m.view_pose = lovr.math.newMat4() | |
:lookAt( | |
vec3(10, 70, -10), | |
vec3(0, 0, 0)) | |
m.projection = Mat4() | |
:orthographic(-m.orthographic_span, m.orthographic_span, m.orthographic_span, -m.orthographic_span, m.near, m.far) | |
--:perspective(math.rad(m.perspective_fov), 1, m.near, m.far) | |
local shadowmapper = lovr.graphics.newShader('unlit', | |
[[ //glsl | |
uniform mat4 LightSpaceMatrix; | |
uniform texture2D DepthBuffer; | |
/* on lovr 0.17.1 the above uniforms should be replaced with: | |
layout(set = 2, binding = 0) uniform texture2D DepthBuffer; | |
layout( push_constant ) uniform constants { | |
mat4 LightSpaceMatrix; | |
} Push; | |
*/ | |
vec2 pcf_offset[20] = vec2[]( | |
vec2(0.0, 0.0), vec2(0.1, -0.1), vec2(-0.4, 0.7), vec2(-0.5, 0.4), | |
vec2(0.9, -0.9), vec2(0.8, 0.1), vec2(-0.6, -1.0), vec2(0.1, 0.5), | |
vec2(0.2, -0.6), vec2(0.2, -0.5), vec2(-0.2, -0.3), vec2(0.7, -0.1), | |
vec2(1.0, 0.3), vec2(0.3, -0.9), vec2(-0.9, 0.7), vec2(-0.4, -0.4), | |
vec2(0.1, 0.3), vec2(-0.1, -0.8), vec2(-0.4, -0.0), vec2(0.3, 0.2) | |
); | |
vec4 lovrmain() { | |
vec4 color = DefaultColor; | |
const int pcf_samples = 20; // only up to 20 | |
vec4 PositionLightSpace = LightSpaceMatrix * vec4(PositionWorld, 1.); | |
vec2 lightSpaceUV = PositionLightSpace.xy / PositionLightSpace.w * 0.5 + 0.5; | |
float depth_biased = 8e-7; | |
float currentDepth = PositionLightSpace.z / PositionLightSpace.w + depth_biased; | |
float shadowing = 0.; // collect shadow across pcf samples | |
for (int i = 0; i < pcf_samples; ++i) { | |
int index = i; | |
lightSpaceUV += pcf_offset[index] * 2e-3; | |
float closestDepth; | |
if ((lightSpaceUV.x > 0.) && (lightSpaceUV.x < 1.) && (lightSpaceUV.y > 0.) && (lightSpaceUV.y < 1.)) { | |
closestDepth = getPixel(DepthBuffer, lightSpaceUV).r; | |
} else { | |
closestDepth = 1e9; | |
} | |
if (currentDepth < closestDepth) | |
shadowing += 1.0 / pcf_samples; | |
} | |
shadowing = -0.2 + pow(1.8 * shadowing, 0.4); | |
color.rgb *= vec3(shadowing); | |
return color; | |
} | |
]]) | |
function m.load(resolution, pose) | |
if pose then | |
m.view_pose:set(pose):invert() | |
end | |
m.resolution = resolution or 1024 | |
m.texture = lovr.graphics.newTexture(m.resolution, m.resolution, | |
{ format = 'd32f', mipmaps = false, linear = true, usage = {'render', 'sample'}} ) | |
m.pass = lovr.graphics.newPass{ depth = m.texture, samples = 1 } | |
end | |
function m.getPass() | |
-- render to depth buffer from light perspective | |
m.pass:setClear({ depth = 1 }) | |
m.pass:reset() | |
m.pass:setCullMode('front') | |
m.pass:setDepthTest('lequal') | |
m.pass:setProjection(1, m.projection) | |
m.pass:setViewPose(1, m.view_pose, true) | |
return m.pass | |
end | |
function m.setShader(pass) | |
pass:setCullMode('back') | |
pass:setShader(shadowmapper) | |
local light_space_matrix = m.projection * m.view_pose | |
pass:send('LightSpaceMatrix', light_space_matrix) | |
pass:send('DepthBuffer', m.texture) | |
end | |
function m.debugDraw(pass) | |
pass:setColor(1, 0.8, 0.5) | |
local pose = mat4(m.view_pose):invert() | |
pass:cone(vec3(pose:mul(0, 0, -0.3)), vec3(pose), 0.2) | |
end | |
function m.setPose(pose_or_origin, target) | |
if not target then | |
local pose = pose_or_origin | |
m.view_pose:set(pose):invert() | |
else | |
local origin = pose_or_origin | |
m.view_pose:lookAt(origin, target) | |
end | |
end | |
-- end of library code; now follows a testing scene, accessed with `lovr dir_light.lua` | |
if string.find(arg[0], 'dir_light') then | |
-- in main.lua this would be: local dir_light = require('dir_light') | |
local dir_light = m | |
dir_light.load(1024 * 4) | |
local palette = { -- https://lospec.com/palette-list/breathless-calm | |
{0.200, 0.247, 0.345}, | |
{0.933, 0.525, 0.584}, | |
{0.655, 0.322, 0.322}, | |
{0.976, 0.976, 0.976}, | |
{0.467, 0.227, 0.302}, | |
{0.812, 0.471, 0.384}, | |
{0.290, 0.478, 0.588}, | |
{0.329, 0.196, 0.275}, | |
{0.161, 0.157, 0.192}, | |
} | |
lovr.graphics.setBackgroundColor(palette[#palette]) | |
local is_wobbling = false -- togglable with ` or tilda key | |
local is_hiding_hints = false -- togglable with ESC | |
local is_orthographic = true -- p for projection, o for orthographic | |
local function scene(pass) | |
pass:setColor(1,1,1) | |
pass:setColor(palette[1]) | |
pass:circle(0, 0, 0, 500, -math.pi / 2, 1,0,0) | |
lovr.math.setRandomSeed(0) | |
for i = 1, 50 do | |
local x = (lovr.math.random() - 0.5) * 100 | |
local z = (lovr.math.random() - 0.5) * 100 | |
local s = 4 * math.log(1.1 + 0.4 * lovr.math.random()) | |
local pose = mat4( | |
x, 1.5 * s, z, | |
10 * s, 1, 0.8 * s, | |
lovr.math.random() * math.pi * 2, 0,1,0) | |
pass:setColor(palette[2 + i % (#palette - 2)]) | |
pass:torus(pose, 6, 6) | |
pass:torus(pose:translate(0, - s, -15 * s):scale(0.7), 6, 6) | |
end | |
for i = 1, 50 do | |
local x = (lovr.math.random() - 0.5) * 120 | |
local z = (lovr.math.random() - 0.5) * 120 | |
local s = 0.5 + 15 * lovr.math.random() | |
pass:setColor(palette[2 + i % (#palette - 2)]) | |
local o = vec3(x, 0, z) | |
local t = vec3(x, s, z) | |
pass:cylinder(o, t, 1, true, nil, nil, 6, 6) | |
local o = vec3(x + 6 * (-0.5 + lovr.math.random()), 0, z + 6 * (-0.5 + lovr.math.random())) | |
local t = vec3(o):add(0, s * 0.3, 0) | |
pass:cylinder(o, t, 2.5, true, nil, nil, 6, 6) | |
end | |
end | |
function lovr.draw(pass) | |
local viewer_pose = mat4(pass:getViewPose(1)) | |
if is_wobbling then | |
local pose = mat4(dir_light.view_pose):invert() | |
local o = vec3(pose) | |
o:add( | |
0.01 * math.cos(1.7 * lovr.timer.getTime()), | |
0.03 * math.sin(1.7 * lovr.timer.getTime()), | |
0) | |
t = vec3(pose:mul(0, 0, -10)) | |
dir_light.setPose(o, t) | |
end | |
-- set light pose to current camera when G is pressed | |
if lovr.system.isKeyDown('space') then | |
local pose = mat4(viewer_pose):translate(0, -0.2, -0.1) | |
dir_light.setPose(pose) | |
end | |
-- draw only depth texture from light perspective while SPACE is held | |
if lovr.system.isKeyDown('g') then | |
pass:fill(dir_light.texture) | |
end | |
if not is_hiding_hints then | |
local color_text = palette[3] | |
local color_value = palette[12] | |
pass:text({ | |
color_text, 'wobble is ', | |
color_value, is_wobbling and 'on' or 'off', | |
color_text, ' (use ` or ~ tilda to toggle)\n', | |
color_text, 'camera is ', | |
color_value, is_orthographic and 'orthographic' or 'perspective', | |
color_text, ' (use p and o)\n', | |
color_text, '(hold SPACE to set camera pose)\n', | |
color_text, '(hold G to see light depth map)\n', | |
color_text, '(press ESC to hide)\n', | |
}, mat4(viewer_pose):translate(-0.2, 0.4, -1):scale(0.06)) | |
end | |
-- the most important part: | |
local gpass = dir_light.getPass() | |
scene(gpass) | |
dir_light.debugDraw(pass) | |
dir_light.setShader(pass) | |
scene(pass) | |
return lovr.graphics.submit(gpass, pass) | |
end | |
function lovr.keypressed(key) | |
if key == 'p' then | |
dir_light.projection:perspective(math.rad(m.perspective_fov), 1, m.near, m.far) | |
is_orthographic = false | |
elseif key == 'o' then | |
dir_light.projection:orthographic(-m.orthographic_span, m.orthographic_span, m.orthographic_span, -m.orthographic_span, m.near, m.far) | |
is_orthographic = true | |
elseif key == '`' then | |
is_wobbling = not is_wobbling -- ~~ | |
elseif key == 'escape' then | |
is_hiding_hints = not is_hiding_hints | |
end | |
end | |
end -- this would be end of main.lua improting this lib | |
return m |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment