diff --git a/examples/shadow.cpp b/examples/shadow.cpp new file mode 100644 index 00000000..24b18639 --- /dev/null +++ b/examples/shadow.cpp @@ -0,0 +1,777 @@ +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include + +using namespace psemek; + +static char const phong_vertex_source[] = +R"(#version 330 + +uniform mat4 u_transform; + +layout (location = 0) in vec4 in_position; +layout (location = 1) in vec3 in_normal; +layout (location = 2) in vec4 in_color; + +out vec3 position; +out vec4 color; +out vec3 normal; + +void main() +{ + position = in_position.xyz; + gl_Position = u_transform * in_position; + color = in_color; + normal = in_normal; +} +)"; + +static char const phong_fragment_source[] = +R"(#version 330 + +uniform vec4 u_light_position; +uniform vec4 u_camera_position; + +uniform vec4 u_material; + +in vec3 position; +in vec4 color; +in vec3 normal; + +out vec4 out_color; + +void main() +{ + vec3 n = normalize(normal); + + vec3 r = u_light_position.xyz - u_light_position.w * position; + r = normalize(r); + + vec3 v = u_camera_position.xyz - u_camera_position.w * position; + v = normalize(v); + + vec3 d = 2.0 * dot(r, n) * n - r; + + vec3 light_color = vec3(1.0, 1.0, 1.0); + vec3 specular_color = vec3(1.0, 1.0, 1.0); + + vec3 col = u_material.x * color.rgb + u_material.y * max(0.0, dot(n, r)) * color.rgb * light_color + u_material.z * (pow(max(0.0, dot(d, v)), u_material.w)) * specular_color; + + out_color = vec4(col, color.a); +} +)"; + +struct phong_renderer +{ + struct material + { + float ambient; + float diffuse; + float specular; + float shininess; + }; + + struct render_state + { + struct material material; + gfx::mesh const * mesh; + }; + + struct light + { + geom::vector position; + }; + + struct render_options + { + gfx::framebuffer const * framebuffer; + GLenum draw_buffer; + geom::box viewport; + geom::matrix transform; + geom::point camera_position; + struct light light; + }; + + void push(render_state const & state); + + void render(render_options const & options); + +private: + std::vector render_states_; + gfx::program program_{phong_vertex_source, phong_fragment_source}; +}; + +void phong_renderer::push(render_state const & state) +{ + render_states_.push_back(state); +} + +void phong_renderer::render(render_options const & options) +{ + if (!options.framebuffer) return; + + options.framebuffer->bind(); + gl::DrawBuffer(options.draw_buffer); + gl::Viewport(options.viewport[0].min, options.viewport[1].min, options.viewport[0].length(), options.viewport[1].length()); + + program_.bind(); + program_["u_transform"] = options.transform; + program_["u_light_position"] = options.light.position; + program_["u_camera_position"] = geom::homogeneous(options.camera_position); + + for (auto const & state : render_states_) + { + if (!state.mesh) continue; + + program_["u_material"] = geom::vector{state.material.ambient, state.material.diffuse, state.material.specular, state.material.shininess}; + state.mesh->draw(); + } + + render_states_.clear(); +} + +static char const shadow_builder_vertex_source[] = +R"(#version 330 + +uniform mat4 u_transform; + +layout (location = 0) in vec4 in_position; + +void main() +{ + gl_Position = u_transform * in_position; +} +)"; + +static char const shadow_builder_fragment_source[] = +R"(#version 330 + +layout (location = 0) out float depth; + +void main() +{ + depth = gl_FragCoord.z; +} + +)"; + +struct shadow_map_builder +{ + struct render_state + { + gfx::mesh const * mesh; + geom::box bbox; + }; + + struct render_options + { + geom::vector light; + }; + + shadow_map_builder(std::size_t width, std::size_t height); + + void push(render_state const & state); + + void build(render_options const & options); + + geom::matrix const & transform() const; + + gfx::texture_2d const & texture() const; + +private: + std::vector render_states_; + gfx::program program_{shadow_builder_vertex_source, shadow_builder_fragment_source}; + gfx::framebuffer framebuffer_; + gfx::texture_2d depth_texture_; + + geom::matrix transform_; +}; + +shadow_map_builder::shadow_map_builder(std::size_t width, std::size_t height) +{ + depth_texture_.load({width, height}); + gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MIN_FILTER, gl::LINEAR); + gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MAG_FILTER, gl::LINEAR); + gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_S, gl::CLAMP_TO_EDGE); + gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_T, gl::CLAMP_TO_EDGE); + gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_COMPARE_MODE, gl::COMPARE_REF_TO_TEXTURE); + gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_COMPARE_FUNC, gl::LEQUAL); + framebuffer_.depth(depth_texture_); + framebuffer_.assert_complete(); +} + +void shadow_map_builder::push(render_state const & state) +{ + render_states_.push_back(state); +} + +void shadow_map_builder::build(render_options const & options) +{ + geom::vector light_axes[3]; + + light_axes[2] = -options.light; + light_axes[1] = {0.f, 0.f, 1.f}; + + if (light_axes[2][2] > 0.5f) + light_axes[1] = {0.f, 1.f, 0.f}; + + geom::gram_schmidt(light_axes[2], light_axes[1]); + + light_axes[0] = geom::cross(light_axes[2], light_axes[1]); + + geom::box light_bbox; + + geom::point origin = geom::point::zero(); + + for (auto const & state : render_states_) + { + for (auto const & v : geom::vertices(state.bbox)) + { + for (std::size_t i = 0; i < 3; ++i) + light_bbox[i] |= geom::dot(light_axes[i], v - origin); + } + } + + transform_ = geom::orthographic_camera{light_bbox}.projection() * geom::homogeneous(geom::by_rows(light_axes[0], light_axes[1], light_axes[2])); + + framebuffer_.bind(); + gl::DrawBuffer(gl::NONE); + + gl::Viewport(0, 0, depth_texture_.width(), depth_texture_.height()); + + gl::Clear(gl::DEPTH_BUFFER_BIT); + + gl::Enable(gl::DEPTH_TEST); + gl::DepthFunc(gl::LEQUAL); + + gl::Enable(gl::CULL_FACE); + gl::CullFace(gl::FRONT); + + program_.bind(); + program_["u_transform"] = transform_; + gfx::check_error(); + + for (auto const & state: render_states_) + { + state.mesh->draw(); + gfx::check_error(); + } + + render_states_.clear(); + +/* + framebuffer_.null().bind(); + + gfx::pixmap_float pixmap({depth_texture_.width(), depth_texture_.height()}); + depth_texture_.pixels(gl::DEPTH_COMPONENT, gl::FLOAT, pixmap.data()); + + auto pixels = util::map([](float x){ return std::uint8_t(x * 255); }, pixmap); + + std::ofstream out{"depth.pgm"}; + gfx::write_pgm(pixels, out); +*/ +} + +geom::matrix const & shadow_map_builder::transform() const +{ + return transform_; +} + +gfx::texture_2d const & shadow_map_builder::texture() const +{ + return depth_texture_; +} + +static char const shadow_vertex_source[] = +R"(#version 330 + +uniform mat4 u_transform; + +layout (location = 0) in vec4 in_position; +layout (location = 1) in vec3 in_normal; +layout (location = 2) in vec4 in_color; + +out vec3 position; +out vec4 color; +out vec3 normal; + +void main() +{ + position = in_position.xyz; + gl_Position = u_transform * in_position; + color = in_color; + normal = in_normal; +} +)"; + +static char const shadow_fragment_source[] = +R"(#version 330 + +uniform vec4 u_light_position; +uniform vec4 u_camera_position; + +uniform mat4 u_shadow_transform; +uniform sampler2DShadow u_shadow_map; + +uniform vec4 u_material; + +in vec3 position; +in vec4 color; +in vec3 normal; + +out vec4 out_color; + +void main() +{ + vec3 n = normalize(normal); + + vec3 r = u_light_position.xyz - u_light_position.w * position; + r = normalize(r); + + vec3 v = u_camera_position.xyz - u_camera_position.w * position; + v = normalize(v); + + vec3 d = 2.0 * dot(r, n) * n - r; + + vec3 light_color = vec3(1.0, 1.0, 1.0); + vec3 specular_color = vec3(1.0, 1.0, 1.0); + + float light_dot = dot(n, r); + + vec3 ambient = u_material.x * color.rgb; + vec3 diffuse = u_material.y * max(0.0, light_dot) * color.rgb * light_color; + vec3 specular = u_material.z * (pow(max(0.0, dot(d, v)), u_material.w)) * specular_color; + + vec3 result = ambient; + + vec4 shadow_space = u_shadow_transform * vec4(position, 1.0); + + vec3 tc = shadow_space.xyz / shadow_space.w; + tc = tc * 0.5 + vec3(0.5); + + float fragment_depth = tc.z; + + float shadow_value = 1.0; + + if (tc.x >= 0.0 && tc.x <= 1.0 && tc.y >= 0.0 && tc.y <= 1.0 && tc.z >= 0.0) + { + if (light_dot > 0.0) + { + shadow_value = texture(u_shadow_map, tc); + } + } + + result += shadow_value * (diffuse + specular); + + out_color = vec4(result, color.a); +} +)"; + +struct shadow_renderer +{ + struct material + { + float ambient; + float diffuse; + float specular; + float shininess; + }; + + struct render_state + { + struct material material; + gfx::mesh const * mesh; + bool casts_shadow; + std::optional> bbox; + }; + + struct light + { + geom::vector position; + }; + + struct render_options + { + gfx::framebuffer const * framebuffer; + GLenum draw_buffer; + geom::box viewport; + geom::matrix transform; + geom::point camera_position; + struct light light; + }; + + void push(render_state const & state); + + void render(render_options const & options); + + gfx::texture_2d const & depth_texture() const { return builder_.texture(); } + +private: + std::vector render_states_; + gfx::program program_{shadow_vertex_source, shadow_fragment_source}; + + shadow_map_builder builder_{1024, 1024}; +}; + +void shadow_renderer::push(render_state const & state) +{ + render_states_.push_back(state); +} + +void shadow_renderer::render(render_options const & options) +{ + for (auto const & state : render_states_) + { + if (state.casts_shadow && state.bbox) + builder_.push({state.mesh, *state.bbox}); + } + + shadow_map_builder::render_options build_options; + build_options.light = {options.light.position[0], options.light.position[1], options.light.position[2]}; + builder_.build(build_options); + + options.framebuffer->bind(); + gl::DrawBuffer(options.draw_buffer); + gl::Viewport(options.viewport[0].min, options.viewport[1].min, options.viewport[0].length(), options.viewport[1].length()); + + gl::Enable(gl::DEPTH_TEST); + gl::DepthFunc(gl::LEQUAL); + + gl::Enable(gl::CULL_FACE); + gl::CullFace(gl::BACK); + + program_.bind(); + program_["u_transform"] = options.transform; + program_["u_light_position"] = options.light.position; + program_["u_camera_position"] = geom::homogeneous(options.camera_position); + program_["u_shadow_transform"] = builder_.transform(); + program_["u_shadow_map"] = 0; + + gl::ActiveTexture(gl::TEXTURE0); + builder_.texture().bind(); + + for (auto const & state : render_states_) + { + program_["u_material"] = geom::vector{state.material.ambient, state.material.diffuse, state.material.specular, state.material.shininess}; + state.mesh->draw(); + } + + render_states_.clear(); +} + +struct vertex +{ + geom::point position; + geom::vector normal; + gfx::color_rgba color; +}; + +struct shadow_app + : app::app +{ + geom::spherical_camera camera; + + shadow_renderer renderer; + + gfx::mesh plane_mesh; + shadow_renderer::material plane_material; + + gfx::mesh cube_mesh; + shadow_renderer::material cube_material; + geom::box cube_bbox; + + gfx::mesh sphere_mesh; + shadow_renderer::material sphere_material; + geom::box sphere_bbox; + + gfx::mesh torus_mesh; + shadow_renderer::material torus_material; + geom::box torus_bbox; + + util::clock> clock; + float time = 0.f; + bool paused = false; + + shadow_app(); + + void on_resize(int width, int height) override; + + void on_mouse_move(int x, int y, int dx, int dy) override; + + void on_mouse_wheel(int delta) override; + + void on_key_down(SDL_Keycode key) override; + + void render() override; +}; + +shadow_app::shadow_app() + : app("Shadow") +{ + vsync(true); + + camera.near_clip = 0.1f; + camera.far_clip = 1000.f; + camera.fov_y = geom::rad(45.f); + + camera.azimuthal_angle = 0.f; + camera.elevation_angle = geom::rad(30.f); + camera.target = {0.f, 0.f, 0.f}; + camera.distance = 10.f; + + { + + std::vector vertices; + + vertices.push_back({{-10.f, -10.f, 0.f}, {0.f, 0.f, 1.f}, {127, 127, 127, 255}}); + vertices.push_back({{ 10.f, -10.f, 0.f}, {0.f, 0.f, 1.f}, {127, 127, 127, 255}}); + vertices.push_back({{-10.f, 10.f, 0.f}, {0.f, 0.f, 1.f}, {127, 127, 127, 255}}); + vertices.push_back({{-10.f, 10.f, 0.f}, {0.f, 0.f, 1.f}, {127, 127, 127, 255}}); + vertices.push_back({{ 10.f, -10.f, 0.f}, {0.f, 0.f, 1.f}, {127, 127, 127, 255}}); + vertices.push_back({{ 10.f, 10.f, 0.f}, {0.f, 0.f, 1.f}, {127, 127, 127, 255}}); + + plane_mesh.setup, geom::vector, gfx::normalized>(); + plane_mesh.load(vertices, gl::TRIANGLES, gl::STATIC_DRAW); + + plane_material.ambient = 0.2f; + plane_material.diffuse = 1.f; + plane_material.specular = 0.f; + plane_material.shininess = 1.f; + } + + { + auto cube = geom::box{{{-1.f, 1.f}, {-1.f, 1.f}, {0.f, 5.f}}}; + + auto vertices = geom::vertices(cube); + auto faces = geom::faces(cube); + auto normals = geom::flat_normals(vertices, faces); + auto flat_vertices = geom::deindex(vertices, faces); + + std::vector mesh_vertices; + + for (std::size_t i = 0; i < flat_vertices.size(); ++i) + { + mesh_vertices.push_back({flat_vertices[i][0], normals[i], {255, 127, 127, 255}}); + mesh_vertices.push_back({flat_vertices[i][1], normals[i], {255, 127, 127, 255}}); + mesh_vertices.push_back({flat_vertices[i][2], normals[i], {255, 127, 127, 255}}); + } + + cube_mesh.setup, geom::vector, gfx::normalized>(); + cube_mesh.load(mesh_vertices, gl::TRIANGLES, gl::STATIC_DRAW); + + cube_material.ambient = 0.2f; + cube_material.diffuse = 1.f; + cube_material.specular = 0.6f; + cube_material.shininess = 10000.f; + + cube_bbox = cube; + } + + { + std::vector vertices; + + geom::point const position = {3.f, 2.f, 3.f}; + + float const radius = 1.f; + + int const N = 24; + + gfx::color_rgba color { 63, 63, 191, 255 }; + + for (int j = - N + 1; j < N; ++j) + { + for (int i = 0; i < 4 * N; ++i) + { + float a = (geom::pi * i) / (2 * N); + float b = (geom::pi * j) / (2 * N); + + geom::vector n{std::cos(a) * std::cos(b), std::sin(a) * std::cos(b), std::sin(b)}; + + vertices.push_back({position + radius * n, n, color}); + } + } + + vertices.push_back({position + geom::vector{0.f, 0.f, -radius}, {0.f, 0.f, -1.f}, color}); + vertices.push_back({position + geom::vector{0.f, 0.f, radius}, {0.f, 0.f, 1.f}, color}); + + std::vector> indices; + + auto idx = [](int i, int j) -> std::uint32_t { return (i % (4 * N)) + 4 * N * (j + N - 1); }; + + for (int j = - N + 1; j + 1 < N; ++j) + { + for (int i = 0; i < 4 * N; ++i) + { + indices.push_back({idx(i, j), idx(i + 1, j), idx(i, j + 1)}); + indices.push_back({idx(i, j + 1), idx(i + 1, j), idx(i + 1, j + 1)}); + } + } + + for (int i = 0; i < 4 * N; ++i) + { + indices.push_back({idx(i, 1 - N), (2 * N - 1) * (4 * N), idx(i + 1, 1 - N)}); + } + + for (int i = 0; i < 4 * N; ++i) + { + indices.push_back({idx(i, N - 1), idx(i + 1, N - 1), (2 * N - 1) * (4 * N) + 1}); + } + + sphere_mesh.setup, geom::vector, gfx::normalized>(); + sphere_mesh.load(vertices, indices, gl::STATIC_DRAW); + + sphere_material.ambient = 0.2f; + sphere_material.diffuse = 1.f; + sphere_material.specular = 1.f; + sphere_material.shininess = 100.f; + + sphere_bbox[0] = {position[0] - radius, position[0] + radius}; + sphere_bbox[1] = {position[1] - radius, position[1] + radius}; + sphere_bbox[2] = {position[2] - radius, position[2] + radius}; + } + + { + std::vector vertices; + + geom::point const position = {-3.f, 2.f, 3.f}; + + float const radius1 = 1.f; + float const radius2 = 0.2f; + + int const N = 72; + int const M = 24; + + gfx::color_rgba color { 63, 63, 191, 255 }; + + for (int j = 0; j < M; ++j) + { + for (int i = 0; i < N; ++i) + { + float a = (2.f * geom::pi * i) / N; + float b = (2.f * geom::pi * j) / M; + + geom::vector r{std::cos(a), std::sin(a), 0.f}; + + geom::vector n{std::cos(a) * std::cos(b), std::sin(a) * std::cos(b), std::sin(b)}; + + vertices.push_back({position + radius1 * r + radius2 * n, n, color}); + } + } + + std::vector> indices; + + auto idx = [](int i, int j) -> std::uint32_t { return (i % N) + N * (j % M); }; + + for (int j = 0; j < M; ++j) + { + for (int i = 0; i < N; ++i) + { + indices.push_back({idx(i, j), idx(i + 1, j), idx(i, j + 1)}); + indices.push_back({idx(i, j + 1), idx(i + 1, j), idx(i + 1, j + 1)}); + } + } + + torus_mesh.setup, geom::vector, gfx::normalized>(); + torus_mesh.load(vertices, indices, gl::STATIC_DRAW); + + torus_material.ambient = 0.2f; + torus_material.diffuse = 1.f; + torus_material.specular = 1.f; + torus_material.shininess = 100.f; + + torus_bbox[0] = {position[0] - radius1 - radius2, position[0] + radius1 + radius2}; + torus_bbox[1] = {position[1] - radius1 - radius2, position[1] + radius1 + radius2}; + torus_bbox[2] = {position[2] - radius2, position[2] + radius2}; + } +} + +void shadow_app::on_resize(int width, int height) +{ + app::on_resize(width, height); + + camera.set_fov(camera.fov_y, (1.f * width) / height); +} + +void shadow_app::on_mouse_move(int x, int y, int dx, int dy) +{ + app::on_mouse_move(x, y, dx, dy); + + if (is_middle_button_down()) + { + camera.azimuthal_angle -= dx * 0.01f; + camera.elevation_angle += dy * 0.01f; + } +} + +void shadow_app::on_mouse_wheel(int delta) +{ + app::on_mouse_wheel(delta); + + camera.distance *= std::pow(0.8f, delta); +} + +void shadow_app::on_key_down(SDL_Keycode key) +{ + if (key == SDLK_SPACE) + paused = !paused; +} + +void shadow_app::render() +{ + if (!paused) + time += clock.restart().count() / 4.f; + else + clock.restart(); + + geom::vector light_dir = {std::cos(time), std::sin(time), 0.5f}; + + gfx::framebuffer::null().bind(); + gl::DrawBuffer(gl::BACK); + + gl::Viewport(0, 0, width(), height()); + + gl::ClearColor(0.7f, 0.7f, 1.f, 0.f); + gl::Clear(gl::COLOR_BUFFER_BIT | gl::DEPTH_BUFFER_BIT); + + gl::Enable(gl::DEPTH_TEST); + gl::DepthFunc(gl::LEQUAL); + + renderer.push({plane_material, &plane_mesh, false, std::nullopt}); + renderer.push({sphere_material, &sphere_mesh, true, sphere_bbox}); + renderer.push({cube_material, &cube_mesh, true, cube_bbox}); + renderer.push({torus_material, &torus_mesh, true, torus_bbox}); + + shadow_renderer::render_options options; + options.framebuffer = &gfx::framebuffer::null(); + options.viewport = {{{0, width()}, {0, height()}}}; + options.draw_buffer = gl::BACK; + options.transform = camera.transform(); + options.light.position = geom::homogeneous(light_dir); + options.camera_position = camera.position(); + + renderer.render(options); + + gfx::check_error(); +} + +int main() +{ + return app::main(); +}