mirror of
https://github.com/ZDoom/gzdoom.git
synced 2024-11-29 07:12:36 +00:00
- add dynamic lights to softpoly and software renderer models
This commit is contained in:
parent
61f379c88f
commit
5464d2a577
13 changed files with 283 additions and 65 deletions
|
@ -34,6 +34,7 @@
|
||||||
#include "hwrenderer/dynlights/hw_dynlightdata.h"
|
#include "hwrenderer/dynlights/hw_dynlightdata.h"
|
||||||
#include "hwrenderer/dynlights/hw_shadowmap.h"
|
#include "hwrenderer/dynlights/hw_shadowmap.h"
|
||||||
#include "hwrenderer/scene/hw_drawinfo.h"
|
#include "hwrenderer/scene/hw_drawinfo.h"
|
||||||
|
#include "r_data/models/models.h"
|
||||||
|
|
||||||
template<class T>
|
template<class T>
|
||||||
T smoothstep(const T edge0, const T edge1, const T x)
|
T smoothstep(const T edge0, const T edge1, const T x)
|
||||||
|
@ -138,49 +139,6 @@ void HWDrawInfo::GetDynSpriteLight(AActor *thing, particle_t *particle, float *o
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if circle potentially intersects with node AABB
|
|
||||||
static bool CheckBBoxCircle(float *bbox, float x, float y, float radiusSquared)
|
|
||||||
{
|
|
||||||
float centerX = (bbox[BOXRIGHT] + bbox[BOXLEFT]) * 0.5f;
|
|
||||||
float centerY = (bbox[BOXBOTTOM] + bbox[BOXTOP]) * 0.5f;
|
|
||||||
float extentX = (bbox[BOXRIGHT] - bbox[BOXLEFT]) * 0.5f;
|
|
||||||
float extentY = (bbox[BOXBOTTOM] - bbox[BOXTOP]) * 0.5f;
|
|
||||||
float aabbRadiusSquared = extentX * extentX + extentY * extentY;
|
|
||||||
x -= centerX;
|
|
||||||
y -= centerY;
|
|
||||||
float dist = x * x + y * y;
|
|
||||||
return dist <= radiusSquared + aabbRadiusSquared;
|
|
||||||
}
|
|
||||||
|
|
||||||
template<typename Callback>
|
|
||||||
void BSPNodeWalkCircle(void *node, float x, float y, float radiusSquared, const Callback &callback)
|
|
||||||
{
|
|
||||||
while (!((size_t)node & 1))
|
|
||||||
{
|
|
||||||
node_t *bsp = (node_t *)node;
|
|
||||||
|
|
||||||
if (CheckBBoxCircle(bsp->bbox[0], x, y, radiusSquared))
|
|
||||||
BSPNodeWalkCircle(bsp->children[0], x, y, radiusSquared, callback);
|
|
||||||
|
|
||||||
if (!CheckBBoxCircle(bsp->bbox[1], x, y, radiusSquared))
|
|
||||||
return;
|
|
||||||
|
|
||||||
node = bsp->children[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
subsector_t *sub = (subsector_t *)((uint8_t *)node - 1);
|
|
||||||
callback(sub);
|
|
||||||
}
|
|
||||||
|
|
||||||
template<typename Callback>
|
|
||||||
void BSPWalkCircle(float x, float y, float radiusSquared, const Callback &callback)
|
|
||||||
{
|
|
||||||
if (level.nodes.Size() == 0)
|
|
||||||
callback(&level.subsectors[0]);
|
|
||||||
else
|
|
||||||
BSPNodeWalkCircle(level.HeadNode(), x, y, radiusSquared, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
// static so that we build up a reserve (memory allocations stop)
|
// static so that we build up a reserve (memory allocations stop)
|
||||||
// For multithread processing each worker thread needs its own copy, though.
|
// For multithread processing each worker thread needs its own copy, though.
|
||||||
static thread_local TArray<ADynamicLight*> addedLightsArray;
|
static thread_local TArray<ADynamicLight*> addedLightsArray;
|
||||||
|
|
|
@ -77,9 +77,9 @@ void PolyTriangleDrawer::SetViewport(const DrawerCommandQueuePtr &queue, int x,
|
||||||
queue->Push<PolySetViewportCommand>(viewport_x, viewport_y, viewport_width, viewport_height, dest, dest_width, dest_height, dest_pitch, dest_bgra);
|
queue->Push<PolySetViewportCommand>(viewport_x, viewport_y, viewport_width, viewport_height, dest, dest_width, dest_height, dest_pitch, dest_bgra);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PolyTriangleDrawer::SetTransform(const DrawerCommandQueuePtr &queue, const Mat4f *objectToClip)
|
void PolyTriangleDrawer::SetTransform(const DrawerCommandQueuePtr &queue, const Mat4f *objectToClip, const Mat4f *objectToWorld)
|
||||||
{
|
{
|
||||||
queue->Push<PolySetTransformCommand>(objectToClip);
|
queue->Push<PolySetTransformCommand>(objectToClip, objectToWorld);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PolyTriangleDrawer::SetCullCCW(const DrawerCommandQueuePtr &queue, bool ccw)
|
void PolyTriangleDrawer::SetCullCCW(const DrawerCommandQueuePtr &queue, bool ccw)
|
||||||
|
@ -114,9 +114,10 @@ void PolyTriangleThreadData::SetViewport(int x, int y, int width, int height, ui
|
||||||
weaponScene = false;
|
weaponScene = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void PolyTriangleThreadData::SetTransform(const Mat4f *newObjectToClip)
|
void PolyTriangleThreadData::SetTransform(const Mat4f *newObjectToClip, const Mat4f *newObjectToWorld)
|
||||||
{
|
{
|
||||||
objectToClip = newObjectToClip;
|
objectToClip = newObjectToClip;
|
||||||
|
objectToWorld = newObjectToWorld;
|
||||||
}
|
}
|
||||||
|
|
||||||
void PolyTriangleThreadData::DrawElements(const PolyDrawArgs &drawargs)
|
void PolyTriangleThreadData::DrawElements(const PolyDrawArgs &drawargs)
|
||||||
|
@ -235,18 +236,30 @@ void PolyTriangleThreadData::DrawArrays(const PolyDrawArgs &drawargs)
|
||||||
ShadedTriVertex PolyTriangleThreadData::ShadeVertex(const PolyDrawArgs &drawargs, const TriVertex &v)
|
ShadedTriVertex PolyTriangleThreadData::ShadeVertex(const PolyDrawArgs &drawargs, const TriVertex &v)
|
||||||
{
|
{
|
||||||
// Apply transform to get clip coordinates:
|
// Apply transform to get clip coordinates:
|
||||||
Vec4f position = (*objectToClip) * Vec4f(v.x, v.y, v.z, v.w);
|
Vec4f objpos = Vec4f(v.x, v.y, v.z, v.w);
|
||||||
|
Vec4f clippos = (*objectToClip) * objpos;
|
||||||
|
|
||||||
ShadedTriVertex sv;
|
ShadedTriVertex sv;
|
||||||
sv.x = position.X;
|
sv.x = clippos.X;
|
||||||
sv.y = position.Y;
|
sv.y = clippos.Y;
|
||||||
sv.z = position.Z;
|
sv.z = clippos.Z;
|
||||||
sv.w = position.W;
|
sv.w = clippos.W;
|
||||||
sv.u = v.u;
|
sv.u = v.u;
|
||||||
sv.v = v.v;
|
sv.v = v.v;
|
||||||
|
|
||||||
|
if (!objectToWorld) // Identity matrix
|
||||||
|
{
|
||||||
sv.worldX = v.x;
|
sv.worldX = v.x;
|
||||||
sv.worldY = v.y;
|
sv.worldY = v.y;
|
||||||
sv.worldZ = v.z;
|
sv.worldZ = v.z;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Vec4f worldpos = (*objectToWorld) * objpos;
|
||||||
|
sv.worldX = worldpos.X;
|
||||||
|
sv.worldY = worldpos.Y;
|
||||||
|
sv.worldZ = worldpos.Z;
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate gl_ClipDistance[i]
|
// Calculate gl_ClipDistance[i]
|
||||||
for (int i = 0; i < 3; i++)
|
for (int i = 0; i < 3; i++)
|
||||||
|
@ -576,13 +589,13 @@ PolyTriangleThreadData *PolyTriangleThreadData::Get(DrawerThread *thread)
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
PolySetTransformCommand::PolySetTransformCommand(const Mat4f *objectToClip) : objectToClip(objectToClip)
|
PolySetTransformCommand::PolySetTransformCommand(const Mat4f *objectToClip, const Mat4f *objectToWorld) : objectToClip(objectToClip), objectToWorld(objectToWorld)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
void PolySetTransformCommand::Execute(DrawerThread *thread)
|
void PolySetTransformCommand::Execute(DrawerThread *thread)
|
||||||
{
|
{
|
||||||
PolyTriangleThreadData::Get(thread)->SetTransform(objectToClip);
|
PolyTriangleThreadData::Get(thread)->SetTransform(objectToClip, objectToWorld);
|
||||||
}
|
}
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
|
@ -37,7 +37,7 @@ public:
|
||||||
static void SetCullCCW(const DrawerCommandQueuePtr &queue, bool ccw);
|
static void SetCullCCW(const DrawerCommandQueuePtr &queue, bool ccw);
|
||||||
static void SetTwoSided(const DrawerCommandQueuePtr &queue, bool twosided);
|
static void SetTwoSided(const DrawerCommandQueuePtr &queue, bool twosided);
|
||||||
static void SetWeaponScene(const DrawerCommandQueuePtr &queue, bool enable);
|
static void SetWeaponScene(const DrawerCommandQueuePtr &queue, bool enable);
|
||||||
static void SetTransform(const DrawerCommandQueuePtr &queue, const Mat4f *objectToClip);
|
static void SetTransform(const DrawerCommandQueuePtr &queue, const Mat4f *objectToClip, const Mat4f *objectToWorld);
|
||||||
|
|
||||||
static bool IsBgra();
|
static bool IsBgra();
|
||||||
};
|
};
|
||||||
|
@ -48,7 +48,7 @@ public:
|
||||||
PolyTriangleThreadData(int32_t core, int32_t num_cores) : core(core), num_cores(num_cores) { }
|
PolyTriangleThreadData(int32_t core, int32_t num_cores) : core(core), num_cores(num_cores) { }
|
||||||
|
|
||||||
void SetViewport(int x, int y, int width, int height, uint8_t *dest, int dest_width, int dest_height, int dest_pitch, bool dest_bgra);
|
void SetViewport(int x, int y, int width, int height, uint8_t *dest, int dest_width, int dest_height, int dest_pitch, bool dest_bgra);
|
||||||
void SetTransform(const Mat4f *objectToClip);
|
void SetTransform(const Mat4f *objectToClip, const Mat4f *objectToWorld);
|
||||||
void SetCullCCW(bool value) { ccw = value; }
|
void SetCullCCW(bool value) { ccw = value; }
|
||||||
void SetTwoSided(bool value) { twosided = value; }
|
void SetTwoSided(bool value) { twosided = value; }
|
||||||
void SetWeaponScene(bool value) { weaponScene = value; }
|
void SetWeaponScene(bool value) { weaponScene = value; }
|
||||||
|
@ -88,6 +88,7 @@ private:
|
||||||
bool twosided = false;
|
bool twosided = false;
|
||||||
bool weaponScene = false;
|
bool weaponScene = false;
|
||||||
const Mat4f *objectToClip = nullptr;
|
const Mat4f *objectToClip = nullptr;
|
||||||
|
const Mat4f *objectToWorld = nullptr;
|
||||||
|
|
||||||
enum { max_additional_vertices = 16 };
|
enum { max_additional_vertices = 16 };
|
||||||
};
|
};
|
||||||
|
@ -95,13 +96,14 @@ private:
|
||||||
class PolySetTransformCommand : public DrawerCommand
|
class PolySetTransformCommand : public DrawerCommand
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
PolySetTransformCommand(const Mat4f *objectToClip);
|
PolySetTransformCommand(const Mat4f *objectToClip, const Mat4f *objectToWorld);
|
||||||
|
|
||||||
void Execute(DrawerThread *thread) override;
|
void Execute(DrawerThread *thread) override;
|
||||||
FString DebugInfo() override { return "PolySetTransform"; }
|
FString DebugInfo() override { return "PolySetTransform"; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const Mat4f *objectToClip;
|
const Mat4f *objectToClip;
|
||||||
|
const Mat4f *objectToWorld;
|
||||||
};
|
};
|
||||||
|
|
||||||
class PolySetCullCCWCommand : public DrawerCommand
|
class PolySetCullCCWCommand : public DrawerCommand
|
||||||
|
|
|
@ -480,6 +480,29 @@ void DrawSpanOpt32(int y, int x0, int x1, const TriDrawTriangleArgs *args)
|
||||||
worldnormalZ = args->uniforms->Normal().Z;
|
worldnormalZ = args->uniforms->Normal().Z;
|
||||||
dynlightcolor = args->uniforms->DynLightColor();
|
dynlightcolor = args->uniforms->DynLightColor();
|
||||||
|
|
||||||
|
// The normal vector cannot be uniform when drawing models. Calculate and use the face normal:
|
||||||
|
if (worldnormalX == 0.0f && worldnormalY == 0.0f && worldnormalZ == 0.0f)
|
||||||
|
{
|
||||||
|
float dx1 = args->v2->worldX - args->v1->worldX;
|
||||||
|
float dy1 = args->v2->worldY - args->v1->worldY;
|
||||||
|
float dz1 = args->v2->worldZ - args->v1->worldZ;
|
||||||
|
float dx2 = args->v3->worldX - args->v1->worldX;
|
||||||
|
float dy2 = args->v3->worldY - args->v1->worldY;
|
||||||
|
float dz2 = args->v3->worldZ - args->v1->worldZ;
|
||||||
|
worldnormalX = dy1 * dz2 - dz1 * dy2;
|
||||||
|
worldnormalY = dz1 * dx2 - dx1 * dz2;
|
||||||
|
worldnormalZ = dx1 * dy2 - dy1 * dx2;
|
||||||
|
float lensqr = worldnormalX * worldnormalX + worldnormalY * worldnormalY + worldnormalZ * worldnormalZ;
|
||||||
|
#ifndef NO_SSE
|
||||||
|
float rcplen = _mm_cvtss_f32(_mm_rsqrt_ss(_mm_set_ss(lensqr)));
|
||||||
|
#else
|
||||||
|
float rcplen = 1.0f / sqrt(lensqr);
|
||||||
|
#endif
|
||||||
|
worldnormalX *= rcplen;
|
||||||
|
worldnormalY *= rcplen;
|
||||||
|
worldnormalZ *= rcplen;
|
||||||
|
}
|
||||||
|
|
||||||
int affineOffset = x0 / 16 * 16 - x0;
|
int affineOffset = x0 / 16 * 16 - x0;
|
||||||
float posLightW = posW + stepW * affineOffset;
|
float posLightW = posW + stepW * affineOffset;
|
||||||
posWorldX = posWorldX + stepWorldX * affineOffset;
|
posWorldX = posWorldX + stepWorldX * affineOffset;
|
||||||
|
@ -1067,6 +1090,29 @@ void DrawSpanOpt8(int y, int x0, int x1, const TriDrawTriangleArgs *args)
|
||||||
worldnormalZ = args->uniforms->Normal().Z;
|
worldnormalZ = args->uniforms->Normal().Z;
|
||||||
dynlightcolor = args->uniforms->DynLightColor();
|
dynlightcolor = args->uniforms->DynLightColor();
|
||||||
|
|
||||||
|
// The normal vector cannot be uniform when drawing models. Calculate and use the face normal:
|
||||||
|
if (worldnormalX == 0.0f && worldnormalY == 0.0f && worldnormalZ == 0.0f)
|
||||||
|
{
|
||||||
|
float dx1 = args->v2->worldX - args->v1->worldX;
|
||||||
|
float dy1 = args->v2->worldY - args->v1->worldY;
|
||||||
|
float dz1 = args->v2->worldZ - args->v1->worldZ;
|
||||||
|
float dx2 = args->v3->worldX - args->v1->worldX;
|
||||||
|
float dy2 = args->v3->worldY - args->v1->worldY;
|
||||||
|
float dz2 = args->v3->worldZ - args->v1->worldZ;
|
||||||
|
worldnormalX = dy1 * dz2 - dz1 * dy2;
|
||||||
|
worldnormalY = dz1 * dx2 - dx1 * dz2;
|
||||||
|
worldnormalZ = dx1 * dy2 - dy1 * dx2;
|
||||||
|
float lensqr = worldnormalX * worldnormalX + worldnormalY * worldnormalY + worldnormalZ * worldnormalZ;
|
||||||
|
#ifndef NO_SSE
|
||||||
|
float rcplen = _mm_cvtss_f32(_mm_rsqrt_ss(_mm_set_ss(lensqr)));
|
||||||
|
#else
|
||||||
|
float rcplen = 1.0f / sqrt(lensqr);
|
||||||
|
#endif
|
||||||
|
worldnormalX *= rcplen;
|
||||||
|
worldnormalY *= rcplen;
|
||||||
|
worldnormalZ *= rcplen;
|
||||||
|
}
|
||||||
|
|
||||||
int affineOffset = x0 / 16 * 16 - x0;
|
int affineOffset = x0 / 16 * 16 - x0;
|
||||||
float posLightW = posW + stepW * affineOffset;
|
float posLightW = posW + stepW * affineOffset;
|
||||||
posWorldX = posWorldX + stepWorldX * affineOffset;
|
posWorldX = posWorldX + stepWorldX * affineOffset;
|
||||||
|
|
|
@ -32,6 +32,7 @@ class RenderMemory;
|
||||||
class PolyTranslucentObject;
|
class PolyTranslucentObject;
|
||||||
class PolyDrawSectorPortal;
|
class PolyDrawSectorPortal;
|
||||||
class PolyDrawLinePortal;
|
class PolyDrawLinePortal;
|
||||||
|
class ADynamicLight;
|
||||||
|
|
||||||
class PolyRenderThread
|
class PolyRenderThread
|
||||||
{
|
{
|
||||||
|
@ -53,6 +54,8 @@ public:
|
||||||
std::vector<std::unique_ptr<PolyDrawSectorPortal>> SectorPortals;
|
std::vector<std::unique_ptr<PolyDrawSectorPortal>> SectorPortals;
|
||||||
std::vector<std::unique_ptr<PolyDrawLinePortal>> LinePortals;
|
std::vector<std::unique_ptr<PolyDrawLinePortal>> LinePortals;
|
||||||
|
|
||||||
|
TArray<ADynamicLight*> AddedLightsArray;
|
||||||
|
|
||||||
// Make sure texture can accessed safely
|
// Make sure texture can accessed safely
|
||||||
void PrepareTexture(FTexture *texture, FRenderStyle style);
|
void PrepareTexture(FTexture *texture, FRenderStyle style);
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,7 @@
|
||||||
void PolyRenderModel(PolyRenderThread *thread, const Mat4f &worldToClip, uint32_t stencilValue, float x, float y, float z, FSpriteModelFrame *smf, AActor *actor)
|
void PolyRenderModel(PolyRenderThread *thread, const Mat4f &worldToClip, uint32_t stencilValue, float x, float y, float z, FSpriteModelFrame *smf, AActor *actor)
|
||||||
{
|
{
|
||||||
PolyModelRenderer renderer(thread, worldToClip, stencilValue);
|
PolyModelRenderer renderer(thread, worldToClip, stencilValue);
|
||||||
|
renderer.AddLights(actor);
|
||||||
renderer.RenderModel(x, y, z, smf, actor);
|
renderer.RenderModel(x, y, z, smf, actor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,6 +52,70 @@ PolyModelRenderer::PolyModelRenderer(PolyRenderThread *thread, const Mat4f &worl
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PolyModelRenderer::AddLights(AActor *actor)
|
||||||
|
{
|
||||||
|
if (gl_lights && actor)
|
||||||
|
{
|
||||||
|
auto &addedLights = Thread->AddedLightsArray;
|
||||||
|
|
||||||
|
addedLights.Clear();
|
||||||
|
|
||||||
|
float x = (float)actor->X();
|
||||||
|
float y = (float)actor->Y();
|
||||||
|
float z = (float)actor->Center();
|
||||||
|
float radiusSquared = (float)(actor->renderradius * actor->renderradius);
|
||||||
|
|
||||||
|
BSPWalkCircle(x, y, radiusSquared, [&](subsector_t *subsector) // Iterate through all subsectors potentially touched by actor
|
||||||
|
{
|
||||||
|
FLightNode * node = subsector->lighthead;
|
||||||
|
while (node) // check all lights touching a subsector
|
||||||
|
{
|
||||||
|
ADynamicLight *light = node->lightsource;
|
||||||
|
if (light->visibletoplayer && !(light->flags2&MF2_DORMANT) && (!(light->lightflags&LF_DONTLIGHTSELF) || light->target != actor) && !(light->lightflags&LF_DONTLIGHTACTORS))
|
||||||
|
{
|
||||||
|
int group = subsector->sector->PortalGroup;
|
||||||
|
DVector3 pos = light->PosRelative(group);
|
||||||
|
float radius = (float)(light->GetRadius() + actor->renderradius);
|
||||||
|
double dx = pos.X - x;
|
||||||
|
double dy = pos.Y - y;
|
||||||
|
double dz = pos.Z - z;
|
||||||
|
double distSquared = dx * dx + dy * dy + dz * dz;
|
||||||
|
if (distSquared < radius * radius) // Light and actor touches
|
||||||
|
{
|
||||||
|
if (std::find(addedLights.begin(), addedLights.end(), light) == addedLights.end()) // Check if we already added this light from a different subsector
|
||||||
|
{
|
||||||
|
addedLights.Push(light);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
node = node->nextLight;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
NumLights = addedLights.Size();
|
||||||
|
Lights = Thread->FrameMemory->AllocMemory<PolyLight>(NumLights);
|
||||||
|
for (int i = 0; i < NumLights; i++)
|
||||||
|
{
|
||||||
|
ADynamicLight *lightsource = addedLights[i];
|
||||||
|
|
||||||
|
bool is_point_light = (lightsource->lightflags & LF_ATTENUATE) != 0;
|
||||||
|
|
||||||
|
uint32_t red = lightsource->GetRed();
|
||||||
|
uint32_t green = lightsource->GetGreen();
|
||||||
|
uint32_t blue = lightsource->GetBlue();
|
||||||
|
|
||||||
|
PolyLight &light = Lights[i];
|
||||||
|
light.x = (float)lightsource->X();
|
||||||
|
light.y = (float)lightsource->Y();
|
||||||
|
light.z = (float)lightsource->Z();
|
||||||
|
light.radius = 256.0f / lightsource->GetRadius();
|
||||||
|
light.color = (red << 16) | (green << 8) | blue;
|
||||||
|
if (is_point_light)
|
||||||
|
light.radius = -light.radius;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void PolyModelRenderer::BeginDrawModel(AActor *actor, FSpriteModelFrame *smf, const VSMatrix &objectToWorldMatrix, bool mirrored)
|
void PolyModelRenderer::BeginDrawModel(AActor *actor, FSpriteModelFrame *smf, const VSMatrix &objectToWorldMatrix, bool mirrored)
|
||||||
{
|
{
|
||||||
ModelActor = actor;
|
ModelActor = actor;
|
||||||
|
@ -139,8 +204,9 @@ void PolyModelRenderer::SetTransform()
|
||||||
swapYZ.Matrix[1 + 2 * 4] = 1.0f;
|
swapYZ.Matrix[1 + 2 * 4] = 1.0f;
|
||||||
swapYZ.Matrix[2 + 1 * 4] = 1.0f;
|
swapYZ.Matrix[2 + 1 * 4] = 1.0f;
|
||||||
swapYZ.Matrix[3 + 3 * 4] = 1.0f;
|
swapYZ.Matrix[3 + 3 * 4] = 1.0f;
|
||||||
|
ObjectToWorld = swapYZ * ObjectToWorld;
|
||||||
|
|
||||||
PolyTriangleDrawer::SetTransform(Thread->DrawQueue, Thread->FrameMemory->NewObject<Mat4f>(WorldToClip * swapYZ * ObjectToWorld));
|
PolyTriangleDrawer::SetTransform(Thread->DrawQueue, Thread->FrameMemory->NewObject<Mat4f>(WorldToClip * ObjectToWorld), Thread->FrameMemory->NewObject<Mat4f>(ObjectToWorld));
|
||||||
}
|
}
|
||||||
|
|
||||||
void PolyModelRenderer::DrawArrays(int start, int count)
|
void PolyModelRenderer::DrawArrays(int start, int count)
|
||||||
|
@ -156,6 +222,7 @@ void PolyModelRenderer::DrawArrays(int start, int count)
|
||||||
|
|
||||||
PolyDrawArgs args;
|
PolyDrawArgs args;
|
||||||
args.SetLight(GetColorTable(sector->Colormap, sector->SpecialColors[sector_t::sprites], true), lightlevel, PolyRenderer::Instance()->Light.SpriteGlobVis(foggy), fullbrightSprite);
|
args.SetLight(GetColorTable(sector->Colormap, sector->SpecialColors[sector_t::sprites], true), lightlevel, PolyRenderer::Instance()->Light.SpriteGlobVis(foggy), fullbrightSprite);
|
||||||
|
args.SetLights(Lights, NumLights);
|
||||||
args.SetStencilTestValue(StencilValue);
|
args.SetStencilTestValue(StencilValue);
|
||||||
args.SetClipPlane(0, PolyClipPlane());
|
args.SetClipPlane(0, PolyClipPlane());
|
||||||
args.SetStyle(ModelActor->RenderStyle, ModelActor->Alpha, ModelActor->fillcolor, ModelActor->Translation, SkinTexture, fullbrightSprite);
|
args.SetStyle(ModelActor->RenderStyle, ModelActor->Alpha, ModelActor->fillcolor, ModelActor->Translation, SkinTexture, fullbrightSprite);
|
||||||
|
@ -178,6 +245,7 @@ void PolyModelRenderer::DrawElements(int numIndices, size_t offset)
|
||||||
|
|
||||||
PolyDrawArgs args;
|
PolyDrawArgs args;
|
||||||
args.SetLight(GetColorTable(sector->Colormap, sector->SpecialColors[sector_t::sprites], true), lightlevel, PolyRenderer::Instance()->Light.SpriteGlobVis(foggy), fullbrightSprite);
|
args.SetLight(GetColorTable(sector->Colormap, sector->SpecialColors[sector_t::sprites], true), lightlevel, PolyRenderer::Instance()->Light.SpriteGlobVis(foggy), fullbrightSprite);
|
||||||
|
args.SetLights(Lights, NumLights);
|
||||||
args.SetStencilTestValue(StencilValue);
|
args.SetStencilTestValue(StencilValue);
|
||||||
args.SetClipPlane(0, PolyClipPlane());
|
args.SetClipPlane(0, PolyClipPlane());
|
||||||
args.SetStyle(ModelActor->RenderStyle, ModelActor->Alpha, ModelActor->fillcolor, ModelActor->Translation, SkinTexture, fullbrightSprite);
|
args.SetStyle(ModelActor->RenderStyle, ModelActor->Alpha, ModelActor->fillcolor, ModelActor->Translation, SkinTexture, fullbrightSprite);
|
||||||
|
|
|
@ -34,6 +34,8 @@ class PolyModelRenderer : public FModelRenderer
|
||||||
public:
|
public:
|
||||||
PolyModelRenderer(PolyRenderThread *thread, const Mat4f &worldToClip, uint32_t stencilValue);
|
PolyModelRenderer(PolyRenderThread *thread, const Mat4f &worldToClip, uint32_t stencilValue);
|
||||||
|
|
||||||
|
void AddLights(AActor *actor);
|
||||||
|
|
||||||
ModelRendererType GetType() const override { return PolyModelRendererType; }
|
ModelRendererType GetType() const override { return PolyModelRendererType; }
|
||||||
|
|
||||||
void BeginDrawModel(AActor *actor, FSpriteModelFrame *smf, const VSMatrix &objectToWorldMatrix, bool mirrored) override;
|
void BeginDrawModel(AActor *actor, FSpriteModelFrame *smf, const VSMatrix &objectToWorldMatrix, bool mirrored) override;
|
||||||
|
@ -61,6 +63,8 @@ public:
|
||||||
unsigned int *IndexBuffer = nullptr;
|
unsigned int *IndexBuffer = nullptr;
|
||||||
TriVertex *VertexBuffer = nullptr;
|
TriVertex *VertexBuffer = nullptr;
|
||||||
float InterpolationFactor = 0.0;
|
float InterpolationFactor = 0.0;
|
||||||
|
PolyLight *Lights = nullptr;
|
||||||
|
int NumLights = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
class PolyModelVertexBuffer : public IModelVertexBuffer
|
class PolyModelVertexBuffer : public IModelVertexBuffer
|
||||||
|
|
|
@ -104,7 +104,7 @@ void RenderPolyScene::RenderSectors()
|
||||||
PolyRenderer::Instance()->Threads.RenderThreadSlices(totalcount, [&](PolyRenderThread *thread)
|
PolyRenderer::Instance()->Threads.RenderThreadSlices(totalcount, [&](PolyRenderThread *thread)
|
||||||
{
|
{
|
||||||
PolyTriangleDrawer::SetCullCCW(thread->DrawQueue, !CurrentViewpoint->Mirror);
|
PolyTriangleDrawer::SetCullCCW(thread->DrawQueue, !CurrentViewpoint->Mirror);
|
||||||
PolyTriangleDrawer::SetTransform(thread->DrawQueue, thread->FrameMemory->NewObject<Mat4f>(CurrentViewpoint->WorldToClip));
|
PolyTriangleDrawer::SetTransform(thread->DrawQueue, thread->FrameMemory->NewObject<Mat4f>(CurrentViewpoint->WorldToClip), nullptr);
|
||||||
|
|
||||||
if (thread != mainthread)
|
if (thread != mainthread)
|
||||||
{
|
{
|
||||||
|
@ -336,7 +336,7 @@ void RenderPolyScene::RenderPortals()
|
||||||
|
|
||||||
Mat4f *transform = thread->FrameMemory->NewObject<Mat4f>(CurrentViewpoint->WorldToClip);
|
Mat4f *transform = thread->FrameMemory->NewObject<Mat4f>(CurrentViewpoint->WorldToClip);
|
||||||
PolyTriangleDrawer::SetCullCCW(thread->DrawQueue, !CurrentViewpoint->Mirror);
|
PolyTriangleDrawer::SetCullCCW(thread->DrawQueue, !CurrentViewpoint->Mirror);
|
||||||
PolyTriangleDrawer::SetTransform(thread->DrawQueue, transform);
|
PolyTriangleDrawer::SetTransform(thread->DrawQueue, transform, nullptr);
|
||||||
|
|
||||||
PolyDrawArgs args;
|
PolyDrawArgs args;
|
||||||
args.SetWriteColor(!enterPortals);
|
args.SetWriteColor(!enterPortals);
|
||||||
|
@ -379,7 +379,7 @@ void RenderPolyScene::RenderTranslucent()
|
||||||
|
|
||||||
Mat4f *transform = thread->FrameMemory->NewObject<Mat4f>(CurrentViewpoint->WorldToClip);
|
Mat4f *transform = thread->FrameMemory->NewObject<Mat4f>(CurrentViewpoint->WorldToClip);
|
||||||
PolyTriangleDrawer::SetCullCCW(thread->DrawQueue, !CurrentViewpoint->Mirror);
|
PolyTriangleDrawer::SetCullCCW(thread->DrawQueue, !CurrentViewpoint->Mirror);
|
||||||
PolyTriangleDrawer::SetTransform(thread->DrawQueue, transform);
|
PolyTriangleDrawer::SetTransform(thread->DrawQueue, transform, nullptr);
|
||||||
|
|
||||||
PolyMaskedCycles.Clock();
|
PolyMaskedCycles.Clock();
|
||||||
|
|
||||||
|
|
|
@ -83,7 +83,7 @@ void PolySkyDome::Render(PolyRenderThread *thread, const Mat4f &worldToView, con
|
||||||
|
|
||||||
int rc = mRows + 1;
|
int rc = mRows + 1;
|
||||||
|
|
||||||
PolyTriangleDrawer::SetTransform(thread->DrawQueue, thread->FrameMemory->NewObject<Mat4f>(worldToClip * objectToWorld));
|
PolyTriangleDrawer::SetTransform(thread->DrawQueue, thread->FrameMemory->NewObject<Mat4f>(worldToClip * objectToWorld), nullptr);
|
||||||
|
|
||||||
PolyDrawArgs args;
|
PolyDrawArgs args;
|
||||||
args.SetLight(&NormalLight, 255, PolyRenderer::Instance()->Light.WallGlobVis(false), true);
|
args.SetLight(&NormalLight, 255, PolyRenderer::Instance()->Light.WallGlobVis(false), true);
|
||||||
|
|
|
@ -499,4 +499,49 @@ public:
|
||||||
|
|
||||||
extern DeletingModelArray Models;
|
extern DeletingModelArray Models;
|
||||||
|
|
||||||
|
// Check if circle potentially intersects with node AABB
|
||||||
|
inline bool CheckBBoxCircle(float *bbox, float x, float y, float radiusSquared)
|
||||||
|
{
|
||||||
|
float centerX = (bbox[BOXRIGHT] + bbox[BOXLEFT]) * 0.5f;
|
||||||
|
float centerY = (bbox[BOXBOTTOM] + bbox[BOXTOP]) * 0.5f;
|
||||||
|
float extentX = (bbox[BOXRIGHT] - bbox[BOXLEFT]) * 0.5f;
|
||||||
|
float extentY = (bbox[BOXBOTTOM] - bbox[BOXTOP]) * 0.5f;
|
||||||
|
float aabbRadiusSquared = extentX * extentX + extentY * extentY;
|
||||||
|
x -= centerX;
|
||||||
|
y -= centerY;
|
||||||
|
float dist = x * x + y * y;
|
||||||
|
return dist <= radiusSquared + aabbRadiusSquared;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function for BSPWalkCircle
|
||||||
|
template<typename Callback>
|
||||||
|
void BSPNodeWalkCircle(void *node, float x, float y, float radiusSquared, const Callback &callback)
|
||||||
|
{
|
||||||
|
while (!((size_t)node & 1))
|
||||||
|
{
|
||||||
|
node_t *bsp = (node_t *)node;
|
||||||
|
|
||||||
|
if (CheckBBoxCircle(bsp->bbox[0], x, y, radiusSquared))
|
||||||
|
BSPNodeWalkCircle(bsp->children[0], x, y, radiusSquared, callback);
|
||||||
|
|
||||||
|
if (!CheckBBoxCircle(bsp->bbox[1], x, y, radiusSquared))
|
||||||
|
return;
|
||||||
|
|
||||||
|
node = bsp->children[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
subsector_t *sub = (subsector_t *)((uint8_t *)node - 1);
|
||||||
|
callback(sub);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search BSP for subsectors within the given radius and call callback(subsector) for each found
|
||||||
|
template<typename Callback>
|
||||||
|
void BSPWalkCircle(float x, float y, float radiusSquared, const Callback &callback)
|
||||||
|
{
|
||||||
|
if (level.nodes.Size() == 0)
|
||||||
|
callback(&level.subsectors[0]);
|
||||||
|
else
|
||||||
|
BSPNodeWalkCircle(level.HeadNode(), x, y, radiusSquared, callback);
|
||||||
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
class DrawerCommandQueue;
|
class DrawerCommandQueue;
|
||||||
typedef std::shared_ptr<DrawerCommandQueue> DrawerCommandQueuePtr;
|
typedef std::shared_ptr<DrawerCommandQueue> DrawerCommandQueuePtr;
|
||||||
class RenderMemory;
|
class RenderMemory;
|
||||||
|
class ADynamicLight;
|
||||||
|
|
||||||
EXTERN_CVAR(Bool, r_models);
|
EXTERN_CVAR(Bool, r_models);
|
||||||
|
|
||||||
|
@ -75,6 +76,8 @@ namespace swrenderer
|
||||||
std::unique_ptr<LightVisibility> Light;
|
std::unique_ptr<LightVisibility> Light;
|
||||||
DrawerCommandQueuePtr DrawQueue;
|
DrawerCommandQueuePtr DrawQueue;
|
||||||
|
|
||||||
|
TArray<ADynamicLight*> AddedLightsArray;
|
||||||
|
|
||||||
std::thread thread;
|
std::thread thread;
|
||||||
|
|
||||||
// VisibleSprite working buffers
|
// VisibleSprite working buffers
|
||||||
|
|
|
@ -75,6 +75,7 @@ namespace swrenderer
|
||||||
void RenderModel::Render(RenderThread *thread, short *cliptop, short *clipbottom, int minZ, int maxZ, Fake3DTranslucent clip3DFloor)
|
void RenderModel::Render(RenderThread *thread, short *cliptop, short *clipbottom, int minZ, int maxZ, Fake3DTranslucent clip3DFloor)
|
||||||
{
|
{
|
||||||
SWModelRenderer renderer(thread, clip3DFloor, &WorldToClip, MirrorWorldToClip);
|
SWModelRenderer renderer(thread, clip3DFloor, &WorldToClip, MirrorWorldToClip);
|
||||||
|
renderer.AddLights(actor);
|
||||||
renderer.RenderModel(x, y, z, smf, actor);
|
renderer.RenderModel(x, y, z, smf, actor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,6 +94,70 @@ namespace swrenderer
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SWModelRenderer::AddLights(AActor *actor)
|
||||||
|
{
|
||||||
|
if (gl_lights && actor)
|
||||||
|
{
|
||||||
|
auto &addedLights = Thread->AddedLightsArray;
|
||||||
|
|
||||||
|
addedLights.Clear();
|
||||||
|
|
||||||
|
float x = (float)actor->X();
|
||||||
|
float y = (float)actor->Y();
|
||||||
|
float z = (float)actor->Center();
|
||||||
|
float radiusSquared = (float)(actor->renderradius * actor->renderradius);
|
||||||
|
|
||||||
|
BSPWalkCircle(x, y, radiusSquared, [&](subsector_t *subsector) // Iterate through all subsectors potentially touched by actor
|
||||||
|
{
|
||||||
|
FLightNode * node = subsector->lighthead;
|
||||||
|
while (node) // check all lights touching a subsector
|
||||||
|
{
|
||||||
|
ADynamicLight *light = node->lightsource;
|
||||||
|
if (light->visibletoplayer && !(light->flags2&MF2_DORMANT) && (!(light->lightflags&LF_DONTLIGHTSELF) || light->target != actor) && !(light->lightflags&LF_DONTLIGHTACTORS))
|
||||||
|
{
|
||||||
|
int group = subsector->sector->PortalGroup;
|
||||||
|
DVector3 pos = light->PosRelative(group);
|
||||||
|
float radius = (float)(light->GetRadius() + actor->renderradius);
|
||||||
|
double dx = pos.X - x;
|
||||||
|
double dy = pos.Y - y;
|
||||||
|
double dz = pos.Z - z;
|
||||||
|
double distSquared = dx * dx + dy * dy + dz * dz;
|
||||||
|
if (distSquared < radius * radius) // Light and actor touches
|
||||||
|
{
|
||||||
|
if (std::find(addedLights.begin(), addedLights.end(), light) == addedLights.end()) // Check if we already added this light from a different subsector
|
||||||
|
{
|
||||||
|
addedLights.Push(light);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
node = node->nextLight;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
NumLights = addedLights.Size();
|
||||||
|
Lights = Thread->FrameMemory->AllocMemory<PolyLight>(NumLights);
|
||||||
|
for (int i = 0; i < NumLights; i++)
|
||||||
|
{
|
||||||
|
ADynamicLight *lightsource = addedLights[i];
|
||||||
|
|
||||||
|
bool is_point_light = (lightsource->lightflags & LF_ATTENUATE) != 0;
|
||||||
|
|
||||||
|
uint32_t red = lightsource->GetRed();
|
||||||
|
uint32_t green = lightsource->GetGreen();
|
||||||
|
uint32_t blue = lightsource->GetBlue();
|
||||||
|
|
||||||
|
PolyLight &light = Lights[i];
|
||||||
|
light.x = (float)lightsource->X();
|
||||||
|
light.y = (float)lightsource->Y();
|
||||||
|
light.z = (float)lightsource->Z();
|
||||||
|
light.radius = 256.0f / lightsource->GetRadius();
|
||||||
|
light.color = (red << 16) | (green << 8) | blue;
|
||||||
|
if (is_point_light)
|
||||||
|
light.radius = -light.radius;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void SWModelRenderer::BeginDrawModel(AActor *actor, FSpriteModelFrame *smf, const VSMatrix &objectToWorldMatrix, bool mirrored)
|
void SWModelRenderer::BeginDrawModel(AActor *actor, FSpriteModelFrame *smf, const VSMatrix &objectToWorldMatrix, bool mirrored)
|
||||||
{
|
{
|
||||||
ModelActor = actor;
|
ModelActor = actor;
|
||||||
|
@ -235,8 +300,9 @@ namespace swrenderer
|
||||||
swapYZ.Matrix[1 + 2 * 4] = 1.0f;
|
swapYZ.Matrix[1 + 2 * 4] = 1.0f;
|
||||||
swapYZ.Matrix[2 + 1 * 4] = 1.0f;
|
swapYZ.Matrix[2 + 1 * 4] = 1.0f;
|
||||||
swapYZ.Matrix[3 + 3 * 4] = 1.0f;
|
swapYZ.Matrix[3 + 3 * 4] = 1.0f;
|
||||||
|
ObjectToWorld = swapYZ * ObjectToWorld;
|
||||||
|
|
||||||
PolyTriangleDrawer::SetTransform(Thread->DrawQueue, Thread->FrameMemory->NewObject<Mat4f>((*WorldToClip) * swapYZ * ObjectToWorld));
|
PolyTriangleDrawer::SetTransform(Thread->DrawQueue, Thread->FrameMemory->NewObject<Mat4f>((*WorldToClip) * ObjectToWorld), Thread->FrameMemory->NewObject<Mat4f>(ObjectToWorld));
|
||||||
}
|
}
|
||||||
|
|
||||||
void SWModelRenderer::DrawArrays(int start, int count)
|
void SWModelRenderer::DrawArrays(int start, int count)
|
||||||
|
@ -252,6 +318,8 @@ namespace swrenderer
|
||||||
|
|
||||||
PolyDrawArgs args;
|
PolyDrawArgs args;
|
||||||
args.SetLight(GetColorTable(sector->Colormap, sector->SpecialColors[sector_t::sprites], true), lightlevel, Thread->Light->SpriteGlobVis(foggy), fullbrightSprite);
|
args.SetLight(GetColorTable(sector->Colormap, sector->SpecialColors[sector_t::sprites], true), lightlevel, Thread->Light->SpriteGlobVis(foggy), fullbrightSprite);
|
||||||
|
args.SetLights(Lights, NumLights);
|
||||||
|
args.SetNormal(FVector3(0.0f, 0.0f, 0.0f));
|
||||||
args.SetStyle(ModelActor->RenderStyle, ModelActor->Alpha, ModelActor->fillcolor, ModelActor->Translation, SkinTexture, fullbrightSprite);
|
args.SetStyle(ModelActor->RenderStyle, ModelActor->Alpha, ModelActor->fillcolor, ModelActor->Translation, SkinTexture, fullbrightSprite);
|
||||||
args.SetDepthTest(true);
|
args.SetDepthTest(true);
|
||||||
args.SetWriteDepth(true);
|
args.SetWriteDepth(true);
|
||||||
|
@ -276,6 +344,8 @@ namespace swrenderer
|
||||||
|
|
||||||
PolyDrawArgs args;
|
PolyDrawArgs args;
|
||||||
args.SetLight(GetColorTable(sector->Colormap, sector->SpecialColors[sector_t::sprites], true), lightlevel, Thread->Light->SpriteGlobVis(foggy), fullbrightSprite);
|
args.SetLight(GetColorTable(sector->Colormap, sector->SpecialColors[sector_t::sprites], true), lightlevel, Thread->Light->SpriteGlobVis(foggy), fullbrightSprite);
|
||||||
|
args.SetLights(Lights, NumLights);
|
||||||
|
args.SetNormal(FVector3(0.0f, 0.0f, 0.0f));
|
||||||
args.SetStyle(ModelActor->RenderStyle, ModelActor->Alpha, ModelActor->fillcolor, ModelActor->Translation, SkinTexture, fullbrightSprite);
|
args.SetStyle(ModelActor->RenderStyle, ModelActor->Alpha, ModelActor->fillcolor, ModelActor->Translation, SkinTexture, fullbrightSprite);
|
||||||
args.SetDepthTest(true);
|
args.SetDepthTest(true);
|
||||||
args.SetWriteDepth(true);
|
args.SetWriteDepth(true);
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
#include "swrenderer/r_renderthread.h"
|
#include "swrenderer/r_renderthread.h"
|
||||||
#include "swrenderer/things/r_visiblesprite.h"
|
#include "swrenderer/things/r_visiblesprite.h"
|
||||||
|
|
||||||
|
struct PolyLight;
|
||||||
|
|
||||||
namespace swrenderer
|
namespace swrenderer
|
||||||
{
|
{
|
||||||
void RenderHUDModel(RenderThread *thread, DPSprite *psp, float ofsx, float ofsy);
|
void RenderHUDModel(RenderThread *thread, DPSprite *psp, float ofsx, float ofsy);
|
||||||
|
@ -56,6 +58,8 @@ namespace swrenderer
|
||||||
public:
|
public:
|
||||||
SWModelRenderer(RenderThread *thread, Fake3DTranslucent clip3DFloor, Mat4f *worldToClip, bool mirrorWorldToClip);
|
SWModelRenderer(RenderThread *thread, Fake3DTranslucent clip3DFloor, Mat4f *worldToClip, bool mirrorWorldToClip);
|
||||||
|
|
||||||
|
void AddLights(AActor *actor);
|
||||||
|
|
||||||
ModelRendererType GetType() const override { return SWModelRendererType; }
|
ModelRendererType GetType() const override { return SWModelRendererType; }
|
||||||
|
|
||||||
void BeginDrawModel(AActor *actor, FSpriteModelFrame *smf, const VSMatrix &objectToWorldMatrix, bool mirrored) override;
|
void BeginDrawModel(AActor *actor, FSpriteModelFrame *smf, const VSMatrix &objectToWorldMatrix, bool mirrored) override;
|
||||||
|
@ -85,6 +89,8 @@ namespace swrenderer
|
||||||
float InterpolationFactor = 0.0;
|
float InterpolationFactor = 0.0;
|
||||||
Mat4f *WorldToClip = nullptr;
|
Mat4f *WorldToClip = nullptr;
|
||||||
bool MirrorWorldToClip = false;
|
bool MirrorWorldToClip = false;
|
||||||
|
PolyLight *Lights = nullptr;
|
||||||
|
int NumLights = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
class SWModelVertexBuffer : public IModelVertexBuffer
|
class SWModelVertexBuffer : public IModelVertexBuffer
|
||||||
|
|
Loading…
Reference in a new issue