- moved gl_spritelight out of gl/. This required a few more changes:

* split gl_shadowmap.cpp into a GL dependent and an API independent part.
* gl_drawinfo must be kept around for the HUD sprite because it connects the renderer with the hardware indpendent part of the engine.
This commit is contained in:
Christoph Oelckers 2018-04-29 11:00:34 +02:00
parent 819ea8f937
commit 634b3cf413
12 changed files with 256 additions and 193 deletions

View file

@ -825,7 +825,6 @@ set( FASTMATH_SOURCES
gl/scene/gl_swscene.cpp
gl/scene/gl_portal.cpp
gl/scene/gl_walls_draw.cpp
gl/scene/gl_spritelight.cpp
gl_load/gl_load.c
hwrenderer/dynlights/hw_dynlightdata.cpp
hwrenderer/scene/hw_fakeflat.cpp
@ -835,6 +834,7 @@ set( FASTMATH_SOURCES
hwrenderer/scene/hw_renderhacks.cpp
hwrenderer/scene/hw_sky.cpp
hwrenderer/scene/hw_sprites.cpp
hwrenderer/scene/hw_spritelight.cpp
hwrenderer/scene/hw_walls.cpp
hwrenderer/scene/hw_walls_vertex.cpp
r_data/models/models.cpp
@ -1059,6 +1059,7 @@ set (PCH_SOURCES
gl/textures/gl_samplers.cpp
hwrenderer/data/flatvertices.cpp
hwrenderer/dynlights/hw_aabbtree.cpp
hwrenderer/dynlights/hw_shadowmap.cpp
hwrenderer/scene/hw_skydome.cpp
hwrenderer/textures/hw_material.cpp
hwrenderer/textures/hw_precache.cpp

View file

@ -1,6 +1,6 @@
//
//---------------------------------------------------------------------------
// 1D dynamic shadow maps
// 1D dynamic shadow maps (OpenGL dependent part)
// Copyright(C) 2017 Magnus Norddahl
// All rights reserved.
//
@ -32,77 +32,6 @@
#include "hwrenderer/dynlights/hw_dynlightdata.h"
#include "stats.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.
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.
*/
namespace
{
cycle_t UpdateCycles;
int LightsProcessed;
int LightsShadowmapped;
}
ADD_STAT(shadowmap)
{
FString out;
out.Format("upload=%04.2f ms lights=%d shadowmapped=%d", UpdateCycles.TimeMS(), LightsProcessed, LightsShadowmapped);
return out;
}
CUSTOM_CVAR(Int, gl_shadowmap_quality, 512, CVAR_ARCHIVE | CVAR_GLOBALCONFIG)
{
switch (self)
{
case 128:
case 256:
case 512:
case 1024:
break;
default:
self = 128;
break;
}
}
CUSTOM_CVAR (Bool, gl_light_shadowmap, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG)
{
if (!self)
{
// Unset any residual shadow map indices in the light actors.
TThinkerIterator<ADynamicLight> it(STAT_DLIGHT);
while (auto light = it.Next())
{
light->mShadowmapIndex = 1024;
}
}
}
void FShadowMap::Update()
{
UpdateCycles.Reset();
@ -145,52 +74,9 @@ void FShadowMap::Update()
UpdateCycles.Unclock();
}
bool FShadowMap::ShadowTest(ADynamicLight *light, const DVector3 &pos)
{
if (light->shadowmapped && light->radius > 0.0 && 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);
}
void FShadowMap::UploadLights()
{
if (mLights.Size() != 1024 * 4) mLights.Resize(1024 * 4);
int lightindex = 0;
// Todo: this should go through the blockmap in a spiral pattern around the player so that closer lights are preferred.
TThinkerIterator<ADynamicLight> it(STAT_DLIGHT);
while (auto light = it.Next())
{
LightsProcessed++;
if (light->shadowmapped && lightindex < 1024 * 4)
{
LightsShadowmapped++;
light->mShadowmapIndex = lightindex >> 2;
mLights[lightindex] = light->X();
mLights[lightindex+1] = light->Y();
mLights[lightindex+2] = light->Z();
mLights[lightindex+3] = light->GetRadius();
lightindex += 4;
}
else
{
light->mShadowmapIndex = 1024;
}
}
for (; lightindex < 1024 * 4; lightindex++)
{
mLights[lightindex] = 0;
}
CollectLights();
if (mLightList == 0)
glGenBuffers(1, (GLuint*)&mLightList);
@ -204,28 +90,21 @@ void FShadowMap::UploadLights()
void FShadowMap::UploadAABBTree()
{
// Just comparing the level info is not enough. If two MAPINFO-less levels get played after each other,
// they can both refer to the same default level info.
if (level.info != mLastLevel && (level.nodes.Size() != mLastNumNodes || level.segs.Size() != mLastNumSegs))
Clear();
if (!ValidateAABBTree())
{
int oldBinding = 0;
glGetIntegerv(GL_SHADER_STORAGE_BUFFER_BINDING, &oldBinding);
if (mAABBTree)
return;
glGenBuffers(1, (GLuint*)&mNodesBuffer);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, mNodesBuffer);
glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(hwrenderer::AABBTreeNode) * mAABBTree->nodes.Size(), &mAABBTree->nodes[0], GL_STATIC_DRAW);
mAABBTree.reset(new hwrenderer::LevelAABBTree());
glGenBuffers(1, (GLuint*)&mLinesBuffer);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, mLinesBuffer);
glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(hwrenderer::AABBTreeLine) * mAABBTree->lines.Size(), &mAABBTree->lines[0], GL_STATIC_DRAW);
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(hwrenderer::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(hwrenderer::AABBTreeLine) * mAABBTree->lines.Size(), &mAABBTree->lines[0], GL_STATIC_DRAW);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, oldBinding);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, oldBinding);
}
}
void FShadowMap::Clear()
@ -247,10 +126,4 @@ void FShadowMap::Clear()
glDeleteBuffers(1, (GLuint*)&mLinesBuffer);
mLinesBuffer = 0;
}
mAABBTree.reset();
mLastLevel = level.info;
mLastNumNodes = level.nodes.Size();
mLastNumSegs = level.segs.Size();
}

View file

@ -1,30 +1,18 @@
#pragma once
#include "hwrenderer/dynlights/hw_aabbtree.h"
#include "tarray.h"
#include <memory>
#include "hwrenderer/dynlights/hw_shadowmap.h"
class ADynamicLight;
struct level_info_t;
class FShadowMap
class FShadowMap : public IShadowMap
{
public:
FShadowMap() { }
~FShadowMap() { Clear(); }
// Release resources
void Clear();
void Clear() override;
// Update shadow map texture
void Update();
// 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;
void Update() override;
private:
// Upload the AABB-tree to the GPU
@ -36,21 +24,7 @@ private:
// 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<float> mLights;
// 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
level_info_t *mLastLevel = nullptr;
unsigned mLastNumNodes = 0;
unsigned mLastNumSegs = 0;
// AABB-tree of the level, used for ray tests
std::unique_ptr<hwrenderer::LevelAABBTree> mAABBTree;
FShadowMap(const FShadowMap &) = delete;
FShadowMap &operator=(FShadowMap &) = delete;
};

View file

@ -256,6 +256,7 @@ void GLSceneDrawer::CreateScene()
gl_drawinfo->mAngles = GLRenderer->mAngles;
gl_drawinfo->mViewVector = GLRenderer->mViewVector;
gl_drawinfo->mViewActor = GLRenderer->mViewActor;
gl_drawinfo->mShadowMap = &GLRenderer->mShadowMap;
RenderBSPNode (level.HeadNode());
if (GLRenderer->mCurrentPortal != NULL) GLRenderer->mCurrentPortal->RenderAttached();
@ -581,7 +582,6 @@ void GLSceneDrawer::DrawEndScene2D(sector_t * viewsector)
void GLSceneDrawer::ProcessScene(bool toscreen)
{
FDrawInfo::StartDrawInfo(this);
iter_dlightf = iter_dlight = draw_dlight = draw_dlightf = 0;
GLPortal::BeginScene();
@ -590,7 +590,6 @@ void GLSceneDrawer::ProcessScene(bool toscreen)
CurrentMapSections.Zero();
CurrentMapSections.Set(mapsection);
DrawScene(toscreen ? DM_MAINVIEW : DM_OFFSCREEN);
FDrawInfo::EndDrawInfo();
}
@ -714,8 +713,11 @@ sector_t * GLSceneDrawer::RenderViewpoint (AActor * camera, GL_IRECT * bounds, f
SetViewMatrix(r_viewpoint.Pos.X, r_viewpoint.Pos.Y, r_viewpoint.Pos.Z, false, false);
gl_RenderState.ApplyMatrices();
FDrawInfo::StartDrawInfo(this);
ProcessScene(toscreen);
if (mainview && toscreen) EndDrawScene(lviewsector); // do not call this for camera textures.
FDrawInfo::EndDrawInfo();
if (mainview && FGLRenderBuffers::IsEnabled())
{
GLRenderer->PostProcessScene(FixedColormap, [&]() { if (gl_bloom && FixedColormap == CM_DEFAULT) DrawEndScene2D(lviewsector); });

View file

@ -69,7 +69,7 @@ int gl_SetDynModelLight(AActor *self, int dynlightindex)
if (gl.legacyMode)
{
float out[3];
hw_GetDynSpriteLight(self, nullptr, out);
gl_drawinfo->GetDynSpriteLight(self, nullptr, out);
gl_RenderState.SetDynLight(out[0], out[1], out[2]);
return -1;
}
@ -198,7 +198,7 @@ void FDrawInfo::DrawSprite(GLSprite *sprite, int pass)
else
{
float out[3];
hw_GetDynSpriteLight(gl_light_sprites ? sprite->actor : nullptr, gl_light_particles ? sprite->particle : nullptr, out);
GetDynSpriteLight(gl_light_sprites ? sprite->actor : nullptr, gl_light_particles ? sprite->particle : nullptr, out);
gl_RenderState.SetDynLight(out[0], out[1], out[2]);
}
}

View file

@ -491,7 +491,7 @@ void FDrawInfo::DrawDecal(GLDecal *gldecal)
double x, y;
float out[3];
decal->GetXY(seg->sidedef, x, y);
hw_GetDynSpriteLight(nullptr, x, y, gldecal->zcenter, wall->sub, out);
GetDynSpriteLight(nullptr, x, y, gldecal->zcenter, wall->sub, out);
gl_RenderState.SetDynLight(out[0], out[1], out[2]);
}

View file

@ -457,7 +457,7 @@ void GLSceneDrawer::DrawPlayerSprites(sector_t * viewsector, bool hudModelStep)
else
{
float out[3];
hw_GetDynSpriteLight(playermo, nullptr, out);
gl_drawinfo->GetDynSpriteLight(playermo, nullptr, out);
gl_RenderState.SetDynLight(out[0], out[1], out[2]);
}
}

View file

@ -0,0 +1,162 @@
//
//---------------------------------------------------------------------------
// 1D dynamic shadow maps (API independent part)
// 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 "hwrenderer/dynlights/hw_shadowmap.h"
#include "hwrenderer/utility/hw_cvars.h"
#include "hwrenderer/dynlights/hw_dynlightdata.h"
#include "stats.h"
#include "g_levellocals.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.
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.
*/
cycle_t IShadowMap::UpdateCycles;
int IShadowMap::LightsProcessed;
int IShadowMap::LightsShadowmapped;
ADD_STAT(shadowmap)
{
FString out;
out.Format("upload=%04.2f ms lights=%d shadowmapped=%d", IShadowMap::UpdateCycles.TimeMS(), IShadowMap::LightsProcessed, IShadowMap::LightsShadowmapped);
return out;
}
CUSTOM_CVAR(Int, gl_shadowmap_quality, 512, CVAR_ARCHIVE | CVAR_GLOBALCONFIG)
{
switch (self)
{
case 128:
case 256:
case 512:
case 1024:
break;
default:
self = 128;
break;
}
}
CUSTOM_CVAR (Bool, gl_light_shadowmap, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG)
{
if (!self)
{
// Unset any residual shadow map indices in the light actors.
TThinkerIterator<ADynamicLight> it(STAT_DLIGHT);
while (auto light = it.Next())
{
light->mShadowmapIndex = 1024;
}
}
}
bool IShadowMap::ShadowTest(ADynamicLight *light, const DVector3 &pos)
{
if (light->shadowmapped && light->radius > 0.0 && IsEnabled() && mAABBTree)
return mAABBTree->RayTest(light->Pos(), pos) >= 1.0f;
else
return true;
}
bool IShadowMap::IsEnabled() const
{
return gl_light_shadowmap && (screen->hwcaps & RFL_SHADER_STORAGE_BUFFER);
}
void IShadowMap::CollectLights()
{
if (mLights.Size() != 1024 * 4) mLights.Resize(1024 * 4);
int lightindex = 0;
// Todo: this should go through the blockmap in a spiral pattern around the player so that closer lights are preferred.
TThinkerIterator<ADynamicLight> it(STAT_DLIGHT);
while (auto light = it.Next())
{
LightsProcessed++;
if (light->shadowmapped && light->IsActive() && lightindex < 1024 * 4)
{
LightsShadowmapped++;
light->mShadowmapIndex = lightindex >> 2;
mLights[lightindex] = (float)light->X();
mLights[lightindex+1] = (float)light->Y();
mLights[lightindex+2] = (float)light->Z();
mLights[lightindex+3] = light->GetRadius();
lightindex += 4;
}
else
{
light->mShadowmapIndex = 1024;
}
}
for (; lightindex < 1024 * 4; lightindex++)
{
mLights[lightindex] = 0;
}
}
bool IShadowMap::ValidateAABBTree()
{
// Just comparing the level info is not enough. If two MAPINFO-less levels get played after each other,
// they can both refer to the same default level info.
if (level.info != mLastLevel && (level.nodes.Size() != mLastNumNodes || level.segs.Size() != mLastNumSegs))
{
mAABBTree.reset();
mLastLevel = level.info;
mLastNumNodes = level.nodes.Size();
mLastNumSegs = level.segs.Size();
}
if (mAABBTree)
return true;
mAABBTree.reset(new hwrenderer::LevelAABBTree());
return false;
}

View file

@ -0,0 +1,51 @@
#pragma once
#include "hw_aabbtree.h"
#include "tarray.h"
#include "stats.h"
#include <memory>
class ADynamicLight;
struct level_info_t;
class IShadowMap
{
public:
IShadowMap() { }
virtual ~IShadowMap() { }
// Release resources
virtual void Clear() = 0;
// Update shadow map texture
virtual void Update() = 0;
// 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;
static cycle_t UpdateCycles;
static int LightsProcessed;
static int LightsShadowmapped;
protected:
void CollectLights();
bool ValidateAABBTree();
// Working buffer for creating the list of lights. Stored here to avoid allocating memory each frame
TArray<float> mLights;
// Used to detect when a level change requires the AABB tree to be regenerated
level_info_t *mLastLevel = nullptr;
unsigned mLastNumNodes = 0;
unsigned mLastNumSegs = 0;
// AABB-tree of the level, used for ray tests
std::unique_ptr<hwrenderer::LevelAABBTree> mAABBTree;
IShadowMap(const IShadowMap &) = delete;
IShadowMap &operator=(IShadowMap &) = delete;
};

View file

@ -10,6 +10,8 @@ class GLWall;
class GLFlat;
class GLSprite;
struct GLDecal;
class IShadowMap;
struct particle_t;
//==========================================================================
//
@ -78,6 +80,7 @@ struct HWDrawInfo
FRotator mAngles;
FVector2 mViewVector;
AActor *mViewActor;
IShadowMap *mShadowMap;
TArray<MissingTextureInfo> MissingUpperTextures;
TArray<MissingTextureInfo> MissingLowerTextures;
@ -137,6 +140,10 @@ public:
void AddOtherFloorPlane(int sector, gl_subsectorrendernode * node);
void AddOtherCeilingPlane(int sector, gl_subsectorrendernode * node);
void GetDynSpriteLight(AActor *self, float x, float y, float z, subsector_t * subsec, float *out);
void GetDynSpriteLight(AActor *thing, particle_t *particle, float *out);
virtual void FloodUpperGap(seg_t * seg) = 0;
virtual void FloodLowerGap(seg_t * seg) = 0;
virtual void ProcessLowerMinisegs(TArray<seg_t *> &lowersegs) = 0;

View file

@ -432,8 +432,6 @@ inline float Dist2(float x1,float y1,float x2,float y2)
}
bool hw_SetPlaneTextureRotation(const GLSectorPlane * secplane, FMaterial * gltexture, VSMatrix &mat);
void hw_GetDynSpriteLight(AActor *self, float x, float y, float z, subsector_t *subsec, float *out);
void hw_GetDynSpriteLight(AActor *actor, particle_t *particle, float *out);
void hw_GetDynModelLight(AActor *self, FDynLightData &modellightdata);
extern const float LARGE_VALUE;

View file

@ -33,13 +33,8 @@
#include "g_levellocals.h"
#include "actorinlines.h"
#include "hwrenderer/dynlights/hw_dynlightdata.h"
#include "gl/renderer/gl_renderer.h"
#include "gl/renderer/gl_lightdata.h"
#include "gl/scene/gl_drawinfo.h"
#include "gl/shaders/gl_shader.h"
#include "gl/dynlights/gl_lightbuffer.h"
#include "hwrenderer/dynlights/hw_shadowmap.h"
#include "hwrenderer/scene/hw_drawinfo.h"
template<class T>
T smoothstep(const T edge0, const T edge1, const T x)
@ -54,7 +49,7 @@ T smoothstep(const T edge0, const T edge1, const T x)
//
//==========================================================================
void hw_GetDynSpriteLight(AActor *self, float x, float y, float z, subsector_t * subsec, float *out)
void HWDrawInfo::GetDynSpriteLight(AActor *self, float x, float y, float z, subsector_t * subsec, float *out)
{
ADynamicLight *light;
float frac, lr, lg, lb;
@ -109,7 +104,7 @@ void hw_GetDynSpriteLight(AActor *self, float x, float y, float z, subsector_t *
frac *= (float)smoothstep(light->SpotOuterAngle.Cos(), light->SpotInnerAngle.Cos(), cosDir);
}
if (frac > 0 && GLRenderer->mShadowMap.ShadowTest(light, { x, y, z }))
if (frac > 0 && (!light->shadowmapped || mShadowMap->ShadowTest(light, { x, y, z })))
{
lr = light->GetRed() / 255.0f;
lg = light->GetGreen() / 255.0f;
@ -133,15 +128,15 @@ void hw_GetDynSpriteLight(AActor *self, float x, float y, float z, subsector_t *
}
}
void hw_GetDynSpriteLight(AActor *thing, particle_t *particle, float *out)
void HWDrawInfo::GetDynSpriteLight(AActor *thing, particle_t *particle, float *out)
{
if (thing != NULL)
{
hw_GetDynSpriteLight(thing, thing->X(), thing->Y(), thing->Center(), thing->subsector, out);
GetDynSpriteLight(thing, thing->X(), thing->Y(), thing->Center(), thing->subsector, out);
}
else if (particle != NULL)
{
hw_GetDynSpriteLight(NULL, particle->Pos.X, particle->Pos.Y, particle->Pos.Z, particle->subsector, out);
GetDynSpriteLight(NULL, particle->Pos.X, particle->Pos.Y, particle->Pos.Z, particle->subsector, out);
}
}