diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7b1f667cca..cba6c71603 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1104,6 +1104,8 @@ set (PCH_SOURCES gl/dynlights/gl_dynlight.cpp gl/dynlights/gl_glow.cpp gl/dynlights/gl_lightbuffer.cpp + gl/dynlights/gl_aabbtree.cpp + gl/dynlights/gl_shadowmap.cpp gl/models/gl_models_md3.cpp gl/models/gl_models_md2.cpp gl/models/gl_voxels.cpp @@ -1117,6 +1119,7 @@ set (PCH_SOURCES gl/shaders/gl_shader.cpp gl/shaders/gl_texshader.cpp gl/shaders/gl_shaderprogram.cpp + gl/shaders/gl_shadowmapshader.cpp gl/shaders/gl_presentshader.cpp gl/shaders/gl_present3dRowshader.cpp gl/shaders/gl_bloomshader.cpp diff --git a/src/gl/dynlights/gl_aabbtree.cpp b/src/gl/dynlights/gl_aabbtree.cpp new file mode 100644 index 0000000000..7108a7278a --- /dev/null +++ b/src/gl/dynlights/gl_aabbtree.cpp @@ -0,0 +1,288 @@ +// +//--------------------------------------------------------------------------- +// AABB-tree used for ray testing +// Copyright(C) 2017 Magnus Norddahl +// All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see http://www.gnu.org/licenses/ +// +//-------------------------------------------------------------------------- +// + +#include "gl/system/gl_system.h" +#include "gl/shaders/gl_shader.h" +#include "gl/dynlights/gl_aabbtree.h" +#include "gl/system/gl_interface.h" +#include "r_state.h" +#include "g_levellocals.h" + +LevelAABBTree::LevelAABBTree() +{ + // Calculate the center of all lines + TArray centroids; + for (unsigned int i = 0; i < level.lines.Size(); i++) + { + FVector2 v1 = { (float)level.lines[i].v1->fX(), (float)level.lines[i].v1->fY() }; + FVector2 v2 = { (float)level.lines[i].v2->fX(), (float)level.lines[i].v2->fY() }; + centroids.Push((v1 + v2) * 0.5f); + } + + // Create a list of level lines we want to add: + TArray line_elements; + for (unsigned int i = 0; i < level.lines.Size(); i++) + { + if (!level.lines[i].backsector) + { + line_elements.Push(i); + } + } + + // GenerateTreeNode needs a buffer where it can store line indices temporarily when sorting lines into the left and right child AABB buckets + TArray work_buffer; + work_buffer.Resize(line_elements.Size() * 2); + + // Generate the AABB tree + GenerateTreeNode(&line_elements[0], (int)line_elements.Size(), ¢roids[0], &work_buffer[0]); + + // Add the lines referenced by the leaf nodes + lines.Resize(level.lines.Size()); + for (unsigned int i = 0; i < level.lines.Size(); i++) + { + const auto &line = level.lines[i]; + auto &treeline = lines[i]; + + treeline.x = (float)line.v1->fX(); + treeline.y = (float)line.v1->fY(); + treeline.dx = (float)line.v2->fX() - treeline.x; + treeline.dy = (float)line.v2->fY() - treeline.y; + } +} + +double LevelAABBTree::RayTest(const DVector3 &ray_start, const DVector3 &ray_end) +{ + // Precalculate some of the variables used by the ray/line intersection test + DVector2 raydelta = ray_end - ray_start; + double raydist2 = raydelta | raydelta; + DVector2 raynormal = DVector2(raydelta.Y, -raydelta.X); + double rayd = raynormal | ray_start; + if (raydist2 < 1.0) + return 1.0f; + + double hit_fraction = 1.0; + + // Walk the tree nodes + int stack[16]; + int stack_pos = 1; + stack[0] = nodes.Size() - 1; // root node is the last node in the list + while (stack_pos > 0) + { + int node_index = stack[stack_pos - 1]; + + if (!OverlapRayAABB(ray_start, ray_end, nodes[node_index])) + { + // If the ray doesn't overlap this node's AABB we're done for this subtree + stack_pos--; + } + else if (nodes[node_index].line_index != -1) // isLeaf(node_index) + { + // We reached a leaf node. Do a ray/line intersection test to see if we hit the line. + hit_fraction = MIN(IntersectRayLine(ray_start, ray_end, nodes[node_index].line_index, raydelta, rayd, raydist2), hit_fraction); + stack_pos--; + } + else if (stack_pos == 16) + { + stack_pos--; // stack overflow - tree is too deep! + } + else + { + // The ray overlaps the node's AABB. Examine its child nodes. + stack[stack_pos - 1] = nodes[node_index].left_node; + stack[stack_pos] = nodes[node_index].right_node; + stack_pos++; + } + } + + return hit_fraction; +} + +bool LevelAABBTree::OverlapRayAABB(const DVector2 &ray_start2d, const DVector2 &ray_end2d, const AABBTreeNode &node) +{ + // To do: simplify test to use a 2D test + DVector3 ray_start = DVector3(ray_start2d, 0.0); + DVector3 ray_end = DVector3(ray_end2d, 0.0); + DVector3 aabb_min = DVector3(node.aabb_left, node.aabb_top, -1.0); + DVector3 aabb_max = DVector3(node.aabb_right, node.aabb_bottom, 1.0); + + // Standard 3D ray/AABB overlapping test. + // The details for the math here can be found in Real-Time Rendering, 3rd Edition. + // We could use a 2D test here instead, which would probably simplify the math. + + DVector3 c = (ray_start + ray_end) * 0.5f; + DVector3 w = ray_end - c; + DVector3 h = (aabb_max - aabb_min) * 0.5f; // aabb.extents(); + + c -= (aabb_max + aabb_min) * 0.5f; // aabb.center(); + + DVector3 v = DVector3(abs(w.X), abs(w.Y), abs(w.Z)); + + if (abs(c.X) > v.X + h.X || abs(c.Y) > v.Y + h.Y || abs(c.Z) > v.Z + h.Z) + return false; // disjoint; + + if (abs(c.Y * w.Z - c.Z * w.Y) > h.Y * v.Z + h.Z * v.Y || + abs(c.X * w.Z - c.Z * w.X) > h.X * v.Z + h.Z * v.X || + abs(c.X * w.Y - c.Y * w.X) > h.X * v.Y + h.Y * v.X) + return false; // disjoint; + + return true; // overlap; +} + +double LevelAABBTree::IntersectRayLine(const DVector2 &ray_start, const DVector2 &ray_end, int line_index, const DVector2 &raydelta, double rayd, double raydist2) +{ + // Check if two line segments intersects (the ray and the line). + // The math below does this by first finding the fractional hit for an infinitely long ray line. + // If that hit is within the line segment (0 to 1 range) then it calculates the fractional hit for where the ray would hit. + // + // This algorithm is homemade - I would not be surprised if there's a much faster method out there. + + const double epsilon = 0.0000001; + const AABBTreeLine &line = lines[line_index]; + + DVector2 raynormal = DVector2(raydelta.Y, -raydelta.X); + + DVector2 line_pos(line.x, line.y); + DVector2 line_delta(line.dx, line.dy); + + double den = raynormal | line_delta; + if (abs(den) > epsilon) + { + double t_line = (rayd - (raynormal | line_pos)) / den; + if (t_line >= 0.0 && t_line <= 1.0) + { + DVector2 linehitdelta = line_pos + line_delta * t_line - ray_start; + double t = (raydelta | linehitdelta) / raydist2; + return t > 0.0 ? t : 1.0; + } + } + + return 1.0; +} + +int LevelAABBTree::GenerateTreeNode(int *lines, int num_lines, const FVector2 *centroids, int *work_buffer) +{ + if (num_lines == 0) + return -1; + + // Find bounding box and median of the lines + FVector2 median = FVector2(0.0f, 0.0f); + FVector2 aabb_min, aabb_max; + aabb_min.X = (float)level.lines[lines[0]].v1->fX(); + aabb_min.Y = (float)level.lines[lines[0]].v1->fY(); + aabb_max = aabb_min; + for (int i = 0; i < num_lines; i++) + { + float x1 = (float)level.lines[lines[i]].v1->fX(); + float y1 = (float)level.lines[lines[i]].v1->fY(); + float x2 = (float)level.lines[lines[i]].v2->fX(); + float y2 = (float)level.lines[lines[i]].v2->fY(); + + aabb_min.X = MIN(aabb_min.X, x1); + aabb_min.X = MIN(aabb_min.X, x2); + aabb_min.Y = MIN(aabb_min.Y, y1); + aabb_min.Y = MIN(aabb_min.Y, y2); + aabb_max.X = MAX(aabb_max.X, x1); + aabb_max.X = MAX(aabb_max.X, x2); + aabb_max.Y = MAX(aabb_max.Y, y1); + aabb_max.Y = MAX(aabb_max.Y, y2); + + median += centroids[lines[i]]; + } + median /= (float)num_lines; + + if (num_lines == 1) // Leaf node + { + nodes.Push(AABBTreeNode(aabb_min, aabb_max, lines[0])); + return (int)nodes.Size() - 1; + } + + // Find the longest axis + float axis_lengths[2] = + { + aabb_max.X - aabb_min.X, + aabb_max.Y - aabb_min.Y + }; + int axis_order[2] = { 0, 1 }; + FVector2 axis_plane[2] = { FVector2(1.0f, 0.0f), FVector2(0.0f, 1.0f) }; + std::sort(axis_order, axis_order + 2, [&](int a, int b) { return axis_lengths[a] > axis_lengths[b]; }); + + // Try sort at longest axis, then if that fails then the other one. + // We place the sorted lines into work_buffer and then move the result back to the lines list when done. + int left_count, right_count; + FVector2 axis; + for (int attempt = 0; attempt < 2; attempt++) + { + // Find the sort plane for axis + FVector2 axis = axis_plane[axis_order[attempt]]; + FVector3 plane(axis, -(median | axis)); + + // Sort lines into two based ib whether the line center is on the front or back side of a plane + left_count = 0; + right_count = 0; + for (int i = 0; i < num_lines; i++) + { + int line_index = lines[i]; + + float side = FVector3(centroids[lines[i]], 1.0f) | plane; + if (side >= 0.0f) + { + work_buffer[left_count] = line_index; + left_count++; + } + else + { + work_buffer[num_lines + right_count] = line_index; + right_count++; + } + } + + if (left_count != 0 && right_count != 0) + break; + } + + // Check if something went wrong when sorting and do a random sort instead + if (left_count == 0 || right_count == 0) + { + left_count = num_lines / 2; + right_count = num_lines - left_count; + } + else + { + // Move result back into lines list: + for (int i = 0; i < left_count; i++) + lines[i] = work_buffer[i]; + for (int i = 0; i < right_count; i++) + lines[i + left_count] = work_buffer[num_lines + i]; + } + + // Create child nodes: + int left_index = -1; + int right_index = -1; + if (left_count > 0) + left_index = GenerateTreeNode(lines, left_count, centroids, work_buffer); + if (right_count > 0) + right_index = GenerateTreeNode(lines + left_count, right_count, centroids, work_buffer); + + // Store resulting node and return its index + nodes.Push(AABBTreeNode(aabb_min, aabb_max, left_index, right_index)); + return (int)nodes.Size() - 1; +} diff --git a/src/gl/dynlights/gl_aabbtree.h b/src/gl/dynlights/gl_aabbtree.h new file mode 100644 index 0000000000..0de075be58 --- /dev/null +++ b/src/gl/dynlights/gl_aabbtree.h @@ -0,0 +1,59 @@ + +#pragma once + +#include "vectors.h" + +// Node in a binary AABB tree +struct AABBTreeNode +{ + AABBTreeNode(const FVector2 &aabb_min, const FVector2 &aabb_max, int line_index) : aabb_left(aabb_min.X), aabb_top(aabb_min.Y), aabb_right(aabb_max.X), aabb_bottom(aabb_max.Y), left_node(-1), right_node(-1), line_index(line_index) { } + AABBTreeNode(const FVector2 &aabb_min, const FVector2 &aabb_max, int left, int right) : aabb_left(aabb_min.X), aabb_top(aabb_min.Y), aabb_right(aabb_max.X), aabb_bottom(aabb_max.Y), left_node(left), right_node(right), line_index(-1) { } + + // Axis aligned bounding box for the node + float aabb_left, aabb_top; + float aabb_right, aabb_bottom; + + // Child node indices + int left_node; + int right_node; + + // AABBTreeLine index if it is a leaf node. Index is -1 if it is not. + int line_index; + + // Padding to keep 16-byte length (this structure is uploaded to the GPU) + int padding; +}; + +// Line segment for leaf nodes in an AABB tree +struct AABBTreeLine +{ + float x, y; + float dx, dy; +}; + +// Axis aligned bounding box tree used for ray testing lines. +class LevelAABBTree +{ +public: + // Constructs a tree for the current level + LevelAABBTree(); + + // Nodes in the AABB tree. Last node is the root node. + TArray nodes; + + // Line segments for the leaf nodes in the tree. + TArray lines; + + // Shoot a ray from ray_start to ray_end and return the closest hit as a fractional value between 0 and 1. Returns 1 if no line was hit. + double RayTest(const DVector3 &ray_start, const DVector3 &ray_end); + +private: + // Test if a ray overlaps an AABB node or not + bool OverlapRayAABB(const DVector2 &ray_start2d, const DVector2 &ray_end2d, const AABBTreeNode &node); + + // Intersection test between a ray and a line segment + double IntersectRayLine(const DVector2 &ray_start, const DVector2 &ray_end, int line_index, const DVector2 &raydelta, double rayd, double raydist2); + + // Generate a tree node and its children recursively + int GenerateTreeNode(int *lines, int num_lines, const FVector2 *centroids, int *work_buffer); +}; diff --git a/src/gl/dynlights/gl_dynlight1.cpp b/src/gl/dynlights/gl_dynlight1.cpp index 022aebb134..eb6a5bb567 100644 --- a/src/gl/dynlights/gl_dynlight1.cpp +++ b/src/gl/dynlights/gl_dynlight1.cpp @@ -60,6 +60,7 @@ CVAR (Bool, gl_attachedlights, true, CVAR_ARCHIVE | CVAR_GLOBALCONFIG); CVAR (Bool, gl_lights_checkside, true, CVAR_ARCHIVE | CVAR_GLOBALCONFIG); CVAR (Bool, gl_light_sprites, true, CVAR_ARCHIVE | CVAR_GLOBALCONFIG); CVAR (Bool, gl_light_particles, true, CVAR_ARCHIVE | CVAR_GLOBALCONFIG); +CVAR (Bool, gl_light_shadowmap, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG); //========================================================================== // @@ -109,6 +110,11 @@ bool gl_GetLight(int group, Plane & p, ADynamicLight * light, bool checkside, FD i = 1; } + // Store attenuate flag in the sign bit of the float. + float shadowIndex = GLRenderer->mShadowMap.ShadowMapIndex(light) + 1.0f; + if (!!(light->flags4 & MF4_ATTENUATE)) + shadowIndex = -shadowIndex; + float *data = &ldata.arrays[i][ldata.arrays[i].Reserve(8)]; data[0] = pos.X; data[1] = pos.Z; @@ -117,7 +123,7 @@ bool gl_GetLight(int group, Plane & p, ADynamicLight * light, bool checkside, FD data[4] = r; data[5] = g; data[6] = b; - data[7] = !!(light->flags4 & MF4_ATTENUATE); + data[7] = shadowIndex; return true; } diff --git a/src/gl/dynlights/gl_shadowmap.cpp b/src/gl/dynlights/gl_shadowmap.cpp new file mode 100644 index 0000000000..9a23e41145 --- /dev/null +++ b/src/gl/dynlights/gl_shadowmap.cpp @@ -0,0 +1,206 @@ +// +//--------------------------------------------------------------------------- +// 1D dynamic shadow maps +// Copyright(C) 2017 Magnus Norddahl +// All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see http://www.gnu.org/licenses/ +// +//-------------------------------------------------------------------------- +// + +#include "gl/system/gl_system.h" +#include "gl/shaders/gl_shader.h" +#include "gl/dynlights/gl_shadowmap.h" +#include "gl/dynlights/gl_dynlight.h" +#include "gl/system/gl_interface.h" +#include "gl/system/gl_debug.h" +#include "gl/system/gl_cvars.h" +#include "gl/renderer/gl_renderer.h" +#include "gl/renderer/gl_postprocessstate.h" +#include "gl/renderer/gl_renderbuffers.h" +#include "gl/shaders/gl_shadowmapshader.h" +#include "r_state.h" + +/* + The 1D shadow maps are stored in a 1024x1024 texture as float depth values (R32F). + + Each line in the texture is assigned to a single light. For example, to grab depth values for light 20 + the fragment shader (main.fp) needs to sample from row 20. That is, the V texture coordinate needs + to be 20.5/1024. + + mLightToShadowmap is a hash map storing which line each ADynamicLight is assigned to. The public + ShadowMapIndex function allows the main rendering to find the index and upload that along with the + normal light data. From there, the main.fp shader can sample from the shadow map texture, which + is currently always bound to texture unit 16. + + The texel row for each light is split into four parts. One for each direction, like a cube texture, + but then only in 2D where this reduces itself to a square. When main.fp samples from the shadow map + it first decides in which direction the fragment is (relative to the light), like cubemap sampling does + for 3D, but once again just for the 2D case. + + Texels 0-255 is Y positive, 256-511 is X positive, 512-767 is Y negative and 768-1023 is X negative. + + Generating the shadow map itself is done by FShadowMap::Update(). The shadow map texture's FBO is + bound and then a screen quad is drawn to make a fragment shader cover all texels. For each fragment + it shoots a ray and collects the distance to what it hit. + + The shadowmap.fp shader knows which light and texel it is processing by mapping gl_FragCoord.y back + to the light index, and it knows which direction to ray trace by looking at gl_FragCoord.x. For + example, if gl_FragCoord.y is 20.5, then it knows its processing light 20, and if gl_FragCoord.x is + 127.5, then it knows we are shooting straight ahead for the Y positive direction. + + Ray testing is done by uploading two GPU storage buffers - one holding AABB tree nodes, and one with + the line segments at the leaf nodes of the tree. The fragment shader then performs a test same way + as on the CPU, except everything uses indexes as pointers are not allowed in GLSL. +*/ + +void FShadowMap::Update() +{ + if (!IsEnabled()) + return; + + UploadAABBTree(); + UploadLights(); + + FGLDebug::PushGroup("ShadowMap"); + FGLPostProcessState savedState; + + GLRenderer->mBuffers->BindShadowMapFB(); + + GLRenderer->mShadowMapShader->Bind(); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 4, mLightList); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, mNodesBuffer); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, mLinesBuffer); + + glViewport(0, 0, 1024, 1024); + GLRenderer->RenderScreenQuad(); + + const auto &viewport = GLRenderer->mScreenViewport; + glViewport(viewport.left, viewport.top, viewport.width, viewport.height); + + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 4, 0); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, 0); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, 0); + + GLRenderer->mBuffers->BindShadowMapTexture(16); + + FGLDebug::PopGroup(); +} + +bool FShadowMap::ShadowTest(ADynamicLight *light, const DVector3 &pos) +{ + if (IsEnabled() && mAABBTree) + return mAABBTree->RayTest(light->Pos(), pos) >= 1.0f; + else + return true; +} + +bool FShadowMap::IsEnabled() const +{ + return gl_light_shadowmap && !!(gl.flags & RFL_SHADER_STORAGE_BUFFER); +} + +int FShadowMap::ShadowMapIndex(ADynamicLight *light) +{ + if (IsEnabled()) + return mLightToShadowmap[light]; + else + return 1024; +} + +void FShadowMap::UploadLights() +{ + mLights.Clear(); + mLightToShadowmap.Clear(mLightToShadowmap.CountUsed() * 2); // To do: allow clearing a TMap while building up a reserve + + TThinkerIterator it(STAT_DLIGHT); + while (true) + { + ADynamicLight *light = it.Next(); + if (!light) break; + + mLightToShadowmap[light] = mLights.Size() / 4; + + mLights.Push(light->X()); + mLights.Push(light->Y()); + mLights.Push(light->Z()); + mLights.Push(light->GetRadius()); + + if (mLights.Size() == 1024) // Only 1024 lights for now + break; + } + + while (mLights.Size() < 1024 * 4) + mLights.Push(0.0f); + + if (mLightList == 0) + glGenBuffers(1, (GLuint*)&mLightList); + + int oldBinding = 0; + glGetIntegerv(GL_SHADER_STORAGE_BUFFER_BINDING, &oldBinding); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, mLightList); + glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(float) * mLights.Size(), &mLights[0], GL_STATIC_DRAW); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, oldBinding); +} + +void FShadowMap::UploadAABBTree() +{ + if (numnodes != mLastNumNodes || numsegs != mLastNumSegs) // To do: there is probably a better way to detect a map change than this.. + Clear(); + + if (mAABBTree) + return; + + mAABBTree.reset(new LevelAABBTree()); + + int oldBinding = 0; + glGetIntegerv(GL_SHADER_STORAGE_BUFFER_BINDING, &oldBinding); + + glGenBuffers(1, (GLuint*)&mNodesBuffer); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, mNodesBuffer); + glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(AABBTreeNode) * mAABBTree->nodes.Size(), &mAABBTree->nodes[0], GL_STATIC_DRAW); + + glGenBuffers(1, (GLuint*)&mLinesBuffer); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, mLinesBuffer); + glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(AABBTreeLine) * mAABBTree->lines.Size(), &mAABBTree->lines[0], GL_STATIC_DRAW); + + glBindBuffer(GL_SHADER_STORAGE_BUFFER, oldBinding); +} + +void FShadowMap::Clear() +{ + if (mLightList != 0) + { + glDeleteBuffers(1, (GLuint*)&mLightList); + mLightList = 0; + } + + if (mNodesBuffer != 0) + { + glDeleteBuffers(1, (GLuint*)&mNodesBuffer); + mNodesBuffer = 0; + } + + if (mLinesBuffer != 0) + { + glDeleteBuffers(1, (GLuint*)&mLinesBuffer); + mLinesBuffer = 0; + } + + mAABBTree.reset(); + + mLastNumNodes = numnodes; + mLastNumSegs = numsegs; +} diff --git a/src/gl/dynlights/gl_shadowmap.h b/src/gl/dynlights/gl_shadowmap.h new file mode 100644 index 0000000000..e2b5b371fb --- /dev/null +++ b/src/gl/dynlights/gl_shadowmap.h @@ -0,0 +1,60 @@ + +#pragma once + +#include "gl/dynlights/gl_aabbtree.h" +#include "tarray.h" +#include + +class ADynamicLight; + +class FShadowMap +{ +public: + FShadowMap() { } + ~FShadowMap() { Clear(); } + + // Release resources + void Clear(); + + // Update shadow map texture + void Update(); + + // Return the assigned shadow map index for a given light + int ShadowMapIndex(ADynamicLight *light); + + // Test if a world position is in shadow relative to the specified light and returns false if it is + bool ShadowTest(ADynamicLight *light, const DVector3 &pos); + + // Returns true if gl_light_shadowmap is enabled and supported by the hardware + bool IsEnabled() const; + +private: + // Upload the AABB-tree to the GPU + void UploadAABBTree(); + + // Upload light list to the GPU + void UploadLights(); + + // OpenGL storage buffer with the list of lights in the shadow map texture + int mLightList = 0; + + // Working buffer for creating the list of lights. Stored here to avoid allocating memory each frame + TArray mLights; + + // The assigned shadow map index for each light + TMap mLightToShadowmap; + + // OpenGL storage buffers for the AABB tree + int mNodesBuffer = 0; + int mLinesBuffer = 0; + + // Used to detect when a level change requires the AABB tree to be regenerated + int mLastNumNodes = 0; + int mLastNumSegs = 0; + + // AABB-tree of the level, used for ray tests + std::unique_ptr mAABBTree; + + FShadowMap(const FShadowMap &) = delete; + FShadowMap &operator=(FShadowMap &) = delete; +}; diff --git a/src/gl/renderer/gl_renderbuffers.cpp b/src/gl/renderer/gl_renderbuffers.cpp index a3635ca0b9..74dc36e729 100644 --- a/src/gl/renderer/gl_renderbuffers.cpp +++ b/src/gl/renderer/gl_renderbuffers.cpp @@ -740,6 +740,49 @@ void FGLRenderBuffers::BindEyeFB(int eye, bool readBuffer) glBindFramebuffer(readBuffer ? GL_READ_FRAMEBUFFER : GL_FRAMEBUFFER, mEyeFBs[eye]); } +//========================================================================== +// +// Shadow map texture and frame buffers +// +//========================================================================== + +void FGLRenderBuffers::BindShadowMapFB() +{ + CreateShadowMap(); + glBindFramebuffer(GL_FRAMEBUFFER, mShadowMapFB); +} + +void FGLRenderBuffers::BindShadowMapTexture(int texunit) +{ + CreateShadowMap(); + glActiveTexture(GL_TEXTURE0 + texunit); + glBindTexture(GL_TEXTURE_2D, mShadowMapTexture); +} + +void FGLRenderBuffers::CreateShadowMap() +{ + if (mShadowMapTexture != 0) + return; + + GLint activeTex, textureBinding, frameBufferBinding; + glGetIntegerv(GL_ACTIVE_TEXTURE, &activeTex); + glActiveTexture(GL_TEXTURE0); + glGetIntegerv(GL_TEXTURE_BINDING_2D, &textureBinding); + glGetIntegerv(GL_FRAMEBUFFER_BINDING, &frameBufferBinding); + + mShadowMapTexture = Create2DTexture("ShadowMap", GL_R32F, 1024, 1024); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + mShadowMapFB = CreateFrameBuffer("ShadowMapFB", mShadowMapTexture); + + glBindTexture(GL_TEXTURE_2D, textureBinding); + glActiveTexture(activeTex); + glBindFramebuffer(GL_FRAMEBUFFER, frameBufferBinding); +} + //========================================================================== // // Makes the scene frame buffer active (multisample, depth, stecil, etc.) diff --git a/src/gl/renderer/gl_renderbuffers.h b/src/gl/renderer/gl_renderbuffers.h index f2f7b7cb9f..5df3bcca05 100644 --- a/src/gl/renderer/gl_renderbuffers.h +++ b/src/gl/renderer/gl_renderbuffers.h @@ -49,6 +49,9 @@ public: void BindEyeTexture(int eye, int texunit); void BindEyeFB(int eye, bool readBuffer = false); + void BindShadowMapFB(); + void BindShadowMapTexture(int index); + enum { NumBloomLevels = 4 }; FGLBloomTextureLevel BloomLevels[NumBloomLevels]; @@ -89,6 +92,7 @@ private: void CreateBloom(int width, int height); void CreateExposureLevels(int width, int height); void CreateEyeBuffers(int eye); + void CreateShadowMap(); void CreateAmbientOcclusion(int width, int height); GLuint Create2DTexture(const FString &name, GLuint format, int width, int height, const void *data = nullptr); GLuint Create2DMultisampleTexture(const FString &name, GLuint format, int width, int height, int samples, bool fixedSampleLocations); @@ -133,6 +137,10 @@ private: TArray mEyeTextures; TArray mEyeFBs; + // Shadow map texture + GLuint mShadowMapTexture = 0; + GLuint mShadowMapFB = 0; + static bool FailedCreate; static bool BuffersActive; }; diff --git a/src/gl/renderer/gl_renderer.cpp b/src/gl/renderer/gl_renderer.cpp index 6021411bee..b68ef57f9b 100644 --- a/src/gl/renderer/gl_renderer.cpp +++ b/src/gl/renderer/gl_renderer.cpp @@ -60,6 +60,7 @@ #include "gl/shaders/gl_fxaashader.h" #include "gl/shaders/gl_presentshader.h" #include "gl/shaders/gl_present3dRowshader.h" +#include "gl/shaders/gl_shadowmapshader.h" #include "gl/stereo3d/gl_stereo3d.h" #include "gl/textures/gl_texture.h" #include "gl/textures/gl_translate.h" @@ -125,6 +126,7 @@ FGLRenderer::FGLRenderer(OpenGLFrameBuffer *fb) mSSAOCombineShader = nullptr; mFXAAShader = nullptr; mFXAALumaShader = nullptr; + mShadowMapShader = nullptr; } void gl_LoadModels(); @@ -153,6 +155,7 @@ void FGLRenderer::Initialize(int width, int height) mPresent3dCheckerShader = new FPresent3DCheckerShader(); mPresent3dColumnShader = new FPresent3DColumnShader(); mPresent3dRowShader = new FPresent3DRowShader(); + mShadowMapShader = new FShadowMapShader(); m2DDrawer = new F2DDrawer; // needed for the core profile, because someone decided it was a good idea to remove the default VAO. @@ -223,6 +226,7 @@ FGLRenderer::~FGLRenderer() if (mTonemapPalette) delete mTonemapPalette; if (mColormapShader) delete mColormapShader; if (mLensShader) delete mLensShader; + if (mShadowMapShader) delete mShadowMapShader; delete mFXAAShader; delete mFXAALumaShader; } diff --git a/src/gl/renderer/gl_renderer.h b/src/gl/renderer/gl_renderer.h index 67f7aa0cff..83055689b2 100644 --- a/src/gl/renderer/gl_renderer.h +++ b/src/gl/renderer/gl_renderer.h @@ -6,6 +6,7 @@ #include "vectors.h" #include "r_renderer.h" #include "gl/data/gl_matrix.h" +#include "gl/dynlights/gl_shadowmap.h" struct particle_t; class FCanvasTexture; @@ -40,6 +41,7 @@ class FPresent3DColumnShader; class FPresent3DRowShader; class F2DDrawer; class FHardwareTexture; +class FShadowMapShader; inline float DEG2RAD(float deg) { @@ -122,6 +124,9 @@ public: FPresent3DCheckerShader *mPresent3dCheckerShader; FPresent3DColumnShader *mPresent3dColumnShader; FPresent3DRowShader *mPresent3dRowShader; + FShadowMapShader *mShadowMapShader; + + FShadowMap mShadowMap; FTexture *gllight; FTexture *glpart2; diff --git a/src/gl/scene/gl_scene.cpp b/src/gl/scene/gl_scene.cpp index d5c1838609..a99423e5e6 100644 --- a/src/gl/scene/gl_scene.cpp +++ b/src/gl/scene/gl_scene.cpp @@ -950,6 +950,8 @@ void FGLRenderer::RenderView (player_t* player) TThinkerIterator it(STAT_DLIGHT); GLRenderer->mLightCount = ((it.Next()) != NULL); + GLRenderer->mShadowMap.Update(); + sector_t * viewsector = RenderViewpoint(player->camera, NULL, FieldOfView.Degrees, ratio, fovratio, true, true); All.Unclock(); diff --git a/src/gl/scene/gl_spritelight.cpp b/src/gl/scene/gl_spritelight.cpp index 6efa078ca9..3283f00862 100644 --- a/src/gl/scene/gl_spritelight.cpp +++ b/src/gl/scene/gl_spritelight.cpp @@ -91,7 +91,7 @@ void gl_SetDynSpriteLight(AActor *self, float x, float y, float z, subsector_t * frac = 1.0f - (dist / radius); - if (frac > 0) + if (frac > 0 && GLRenderer->mShadowMap.ShadowTest(light, { x, y, z })) { lr = light->GetRed() / 255.0f; lg = light->GetGreen() / 255.0f; diff --git a/src/gl/shaders/gl_shader.cpp b/src/gl/shaders/gl_shader.cpp index 421139050c..9cb5e9aab3 100644 --- a/src/gl/shaders/gl_shader.cpp +++ b/src/gl/shaders/gl_shader.cpp @@ -105,6 +105,11 @@ bool FShader::Load(const char * name, const char * vert_prog_lump, const char * vp_comb << "#define USE_QUAD_DRAWER\n"; } + if (!!(gl.flags & RFL_SHADER_STORAGE_BUFFER)) + { + vp_comb << "#define SUPPORTS_SHADOWMAPS\n"; + } + vp_comb << defines << i_data.GetString().GetChars(); FString fp_comb = vp_comb; @@ -270,6 +275,9 @@ bool FShader::Load(const char * name, const char * vert_prog_lump, const char * if (tempindex > 0) glUniform1i(tempindex, i - 1); } + int shadowmapindex = glGetUniformLocation(hShader, "ShadowMap"); + if (shadowmapindex > 0) glUniform1i(shadowmapindex, 16); + glUseProgram(0); return !!linked; } diff --git a/src/gl/shaders/gl_shadowmapshader.cpp b/src/gl/shaders/gl_shadowmapshader.cpp new file mode 100644 index 0000000000..674d0a04f2 --- /dev/null +++ b/src/gl/shaders/gl_shadowmapshader.cpp @@ -0,0 +1,45 @@ +// +//--------------------------------------------------------------------------- +// +// Copyright(C) 2016 Magnus Norddahl +// All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see http://www.gnu.org/licenses/ +// +//-------------------------------------------------------------------------- +// + +#include "gl/system/gl_system.h" +#include "files.h" +#include "m_swap.h" +#include "v_video.h" +#include "gl/gl_functions.h" +#include "vectors.h" +#include "gl/system/gl_interface.h" +#include "gl/system/gl_framebuffer.h" +#include "gl/system/gl_cvars.h" +#include "gl/shaders/gl_shadowmapshader.h" + +void FShadowMapShader::Bind() +{ + if (!mShader) + { + mShader.Compile(FShaderProgram::Vertex, "shaders/glsl/screenquad.vp", "", 430); + mShader.Compile(FShaderProgram::Fragment, "shaders/glsl/shadowmap.fp", "", 430); + mShader.SetFragDataLocation(0, "FragColor"); + mShader.Link("shaders/glsl/shadowmap"); + mShader.SetAttribLocation(0, "PositionInProjection"); + } + mShader.Bind(); +} diff --git a/src/gl/shaders/gl_shadowmapshader.h b/src/gl/shaders/gl_shadowmapshader.h new file mode 100644 index 0000000000..7d01f9974a --- /dev/null +++ b/src/gl/shaders/gl_shadowmapshader.h @@ -0,0 +1,15 @@ +#ifndef __GL_SHADOWMAPSHADER_H +#define __GL_SHADOWMAPSHADER_H + +#include "gl_shaderprogram.h" + +class FShadowMapShader +{ +public: + void Bind(); + +private: + FShaderProgram mShader; +}; + +#endif \ No newline at end of file diff --git a/src/gl/system/gl_cvars.h b/src/gl/system/gl_cvars.h index 3b425d2613..b7122b01c3 100644 --- a/src/gl/system/gl_cvars.h +++ b/src/gl/system/gl_cvars.h @@ -26,6 +26,7 @@ EXTERN_CVAR (Bool, gl_attachedlights); EXTERN_CVAR (Bool, gl_lights_checkside); EXTERN_CVAR (Bool, gl_light_sprites); EXTERN_CVAR (Bool, gl_light_particles); +EXTERN_CVAR (Bool, gl_light_shadowmap); EXTERN_CVAR(Int, gl_fogmode) EXTERN_CVAR(Int, gl_lightmode) diff --git a/wadsrc/static/language.enu b/wadsrc/static/language.enu index bf56bb61fc..0e0e9831d0 100644 --- a/wadsrc/static/language.enu +++ b/wadsrc/static/language.enu @@ -2675,7 +2675,7 @@ GLLIGHTMNU_LIGHTDEFS = "Enable light definitions"; GLLIGHTMNU_CLIPLIGHTS = "Clip lights"; GLLIGHTMNU_LIGHTSPRITES = "Lights affect sprites"; GLLIGHTMNU_LIGHTPARTICLES = "Lights affect particles"; -GLLIGHTMNU_LIGHTMATH = "Light quality"; +GLLIGHTMNU_LIGHTSHADOWMAP = "Light shadowmaps"; // OpenGL Preferences GLPREFMNU_TITLE = "OPENGL PREFERENCES"; diff --git a/wadsrc/static/menudef.zz b/wadsrc/static/menudef.zz index 84ddb94485..054c5ac67a 100644 --- a/wadsrc/static/menudef.zz +++ b/wadsrc/static/menudef.zz @@ -226,6 +226,7 @@ OptionMenu "GLLightOptions" Option "$GLLIGHTMNU_CLIPLIGHTS", gl_lights_checkside, "YesNo" Option "$GLLIGHTMNU_LIGHTSPRITES", gl_light_sprites, "YesNo" Option "$GLLIGHTMNU_LIGHTPARTICLES", gl_light_particles, "YesNo" + Option "$GLLIGHTMNU_LIGHTSHADOWMAP", gl_light_shadowmap, "YesNo" } OptionMenu "GLPrefOptions" diff --git a/wadsrc/static/shaders/glsl/main.fp b/wadsrc/static/shaders/glsl/main.fp index a6bc6bbd7f..9a682f3868 100644 --- a/wadsrc/static/shaders/glsl/main.fp +++ b/wadsrc/static/shaders/glsl/main.fp @@ -26,6 +26,7 @@ out vec4 FragNormal; uniform sampler2D tex; +uniform sampler2D ShadowMap; vec4 Process(vec4 color); vec4 ProcessTexel(); @@ -132,6 +133,74 @@ float R_DoomLightingEquation(float light) return lightscale; } +//=========================================================================== +// +// Check if light is in shadow according to its 1D shadow map +// +//=========================================================================== + +#ifdef SUPPORTS_SHADOWMAPS + +float sampleShadowmap(vec2 dir, float v) +{ + float u; + if (abs(dir.x) > abs(dir.y)) + { + if (dir.x >= 0.0) + u = dir.y / dir.x * 0.125 + (0.25 + 0.125); + else + u = dir.y / dir.x * 0.125 + (0.75 + 0.125); + } + else + { + if (dir.y >= 0.0) + u = dir.x / dir.y * 0.125 + 0.125; + else + u = dir.x / dir.y * 0.125 + (0.50 + 0.125); + } + float dist2 = dot(dir, dir); + return texture(ShadowMap, vec2(u, v)).x > dist2 ? 1.0 : 0.0; +} + +//=========================================================================== +// +// Check if light is in shadow using Percentage Closer Filtering (PCF) +// +//=========================================================================== + +#define PCF_FILTER_STEP_COUNT 3 +#define PCF_COUNT (PCF_FILTER_STEP_COUNT * 2 + 1) + +float shadowmapAttenuation(vec4 lightpos, float shadowIndex) +{ + if (shadowIndex >= 1024.0) + return 1.0; // No shadowmap available for this light + + float v = (shadowIndex + 0.5) / 1024.0; + + vec2 ray = pixelpos.xz - lightpos.xz; + float length = length(ray); + if (length < 3.0) + return 1.0; + + vec2 dir = ray / length; + + ray -= dir * 2.0; // margin + dir = dir * min(length / 50.0, 1.0); // avoid sampling behind light + + vec2 normal = vec2(-dir.y, dir.x); + vec2 bias = dir * 10.0; + + float sum = 0.0; + for (float x = -PCF_FILTER_STEP_COUNT; x <= PCF_FILTER_STEP_COUNT; x++) + { + sum += sampleShadowmap(ray + normal * x - bias * abs(x), v); + } + return sum / PCF_COUNT; +} + +#endif + //=========================================================================== // // Standard lambertian diffuse light calculation @@ -151,10 +220,14 @@ float diffuseContribution(vec3 lightDirection, vec3 normal) // //=========================================================================== -float pointLightAttenuation(vec4 lightpos, float attenuate) +float pointLightAttenuation(vec4 lightpos, float lightcolorA) { float attenuation = max(lightpos.w - distance(pixelpos.xyz, lightpos.xyz),0.0) / lightpos.w; - if (attenuate == 0.0) +#ifdef SUPPORTS_SHADOWMAPS + float shadowIndex = abs(lightcolorA) - 1.0; + attenuation *= shadowmapAttenuation(lightpos, shadowIndex); +#endif + if (lightcolorA >= 0.0) // Sign bit is the attenuated light flag { return attenuation; } diff --git a/wadsrc/static/shaders/glsl/shadowmap.fp b/wadsrc/static/shaders/glsl/shadowmap.fp new file mode 100644 index 0000000000..194da954ef --- /dev/null +++ b/wadsrc/static/shaders/glsl/shadowmap.fp @@ -0,0 +1,162 @@ + +in vec2 TexCoord; +out vec4 FragColor; + +struct GPUNode +{ + vec2 aabb_min; + vec2 aabb_max; + int left; + int right; + int line_index; + int padding; +}; + +struct GPULine +{ + vec2 pos; + vec2 delta; +}; + +layout(std430, binding = 2) buffer LightNodes +{ + GPUNode nodes[]; +}; + +layout(std430, binding = 3) buffer LightLines +{ + GPULine lines[]; +}; + +layout(std430, binding = 4) buffer LightList +{ + vec4 lights[]; +}; + +bool overlapRayAABB(vec2 ray_start2d, vec2 ray_end2d, vec2 aabb_min2d, vec2 aabb_max2d) +{ + // To do: simplify test to use a 2D test + vec3 ray_start = vec3(ray_start2d, 0.0); + vec3 ray_end = vec3(ray_end2d, 0.0); + vec3 aabb_min = vec3(aabb_min2d, -1.0); + vec3 aabb_max = vec3(aabb_max2d, 1.0); + + vec3 c = (ray_start + ray_end) * 0.5f; + vec3 w = ray_end - c; + vec3 h = (aabb_max - aabb_min) * 0.5f; // aabb.extents(); + + c -= (aabb_max + aabb_min) * 0.5f; // aabb.center(); + + vec3 v = abs(w); + + if (abs(c.x) > v.x + h.x || abs(c.y) > v.y + h.y || abs(c.z) > v.z + h.z) + return false; // disjoint; + + if (abs(c.y * w.z - c.z * w.y) > h.y * v.z + h.z * v.y || + abs(c.x * w.z - c.z * w.x) > h.x * v.z + h.z * v.x || + abs(c.x * w.y - c.y * w.x) > h.x * v.y + h.y * v.x) + return false; // disjoint; + + return true; // overlap; +} + +float intersectRayLine(vec2 ray_start, vec2 ray_end, int line_index, vec2 raydelta, float rayd, float raydist2) +{ + const float epsilon = 0.0000001; + GPULine line = lines[line_index]; + + vec2 raynormal = vec2(raydelta.y, -raydelta.x); + + float den = dot(raynormal, line.delta); + if (abs(den) > epsilon) + { + float t_line = (rayd - dot(raynormal, line.pos)) / den; + if (t_line >= 0.0 && t_line <= 1.0) + { + vec2 linehitdelta = line.pos + line.delta * t_line - ray_start; + float t = dot(raydelta, linehitdelta) / raydist2; + return t > 0.0 ? t : 1.0; + } + } + + return 1.0; +} + +bool isLeaf(int node_index) +{ + return nodes[node_index].line_index != -1; +} + +float rayTest(vec2 ray_start, vec2 ray_end) +{ + vec2 raydelta = ray_end - ray_start; + float raydist2 = dot(raydelta, raydelta); + vec2 raynormal = vec2(raydelta.y, -raydelta.x); + float rayd = dot(raynormal, ray_start); + if (raydist2 < 1.0) + return 1.0; + + float t = 1.0; + + int stack[16]; + int stack_pos = 1; + stack[0] = nodes.length() - 1; + while (stack_pos > 0) + { + int node_index = stack[stack_pos - 1]; + + if (!overlapRayAABB(ray_start, ray_end, nodes[node_index].aabb_min, nodes[node_index].aabb_max)) + { + stack_pos--; + } + else if (isLeaf(node_index)) + { + t = min(intersectRayLine(ray_start, ray_end, nodes[node_index].line_index, raydelta, rayd, raydist2), t); + stack_pos--; + } + else if (stack_pos == 16) + { + stack_pos--; // stack overflow + } + else + { + stack[stack_pos - 1] = nodes[node_index].left; + stack[stack_pos] = nodes[node_index].right; + stack_pos++; + } + } + + return t; +} + +void main() +{ + int lightIndex = int(gl_FragCoord.y); + + vec4 light = lights[lightIndex]; + float radius = light.w; + vec2 lightpos = light.xy; + + if (radius > 0.0) + { + vec2 pixelpos; + switch (int(gl_FragCoord.x) / 256) + { + case 0: pixelpos = vec2((gl_FragCoord.x - 128.0) / 128.0, 1.0); break; + case 1: pixelpos = vec2(1.0, (gl_FragCoord.x - 384.0) / 128.0); break; + case 2: pixelpos = vec2(-(gl_FragCoord.x - 640.0) / 128.0, -1.0); break; + case 3: pixelpos = vec2(-1.0, -(gl_FragCoord.x - 896.0) / 128.0); break; + } + pixelpos = lightpos + pixelpos * radius; + + float t = rayTest(lightpos, pixelpos); + vec2 delta = (pixelpos - lightpos) * t; + float dist2 = dot(delta, delta); + + FragColor = vec4(dist2, 0.0, 0.0, 1.0); + } + else + { + FragColor = vec4(1.0, 0.0, 0.0, 1.0); + } +}