mirror of
https://github.com/ZDoom/Raze.git
synced 2024-11-15 00:42:08 +00:00
- adapted GZDoom's portal framework.
Not tested yet and somewhat stripped down, as the portals this needs to support are a lot less complex - plus some of the issues in Doom do not apply here.
This commit is contained in:
parent
09a9e14feb
commit
9fe462d358
4 changed files with 998 additions and 16 deletions
|
@ -1080,6 +1080,7 @@ set (PCH_SOURCES
|
|||
core/rendering/scene/hw_drawlist.cpp
|
||||
core/rendering/scene/hw_drawinfo.cpp
|
||||
core/rendering/scene/hw_bunchdrawer.cpp
|
||||
core/rendering/scene/hw_portal.cpp
|
||||
|
||||
core/console/c_notifybuffer.cpp
|
||||
core/console/d_event.cpp
|
||||
|
|
|
@ -47,6 +47,11 @@ inline double WallStartY(const walltype* wallnum)
|
|||
return wallnum->y * (1 / -16.);
|
||||
}
|
||||
|
||||
inline DVector2 WallStart(const walltype* wallnum)
|
||||
{
|
||||
return { WallStartX(wallnum), WallStartY(wallnum) };
|
||||
}
|
||||
|
||||
inline double WallEndX(const walltype* wallnum)
|
||||
{
|
||||
return wall[wallnum->point2].x * (1 / 16.);
|
||||
|
@ -57,6 +62,16 @@ inline double WallEndY(const walltype* wallnum)
|
|||
return wall[wallnum->point2].y * (1 / -16.);
|
||||
}
|
||||
|
||||
inline DVector2 WallEnd(const walltype* wallnum)
|
||||
{
|
||||
return { WallEndX(wallnum), WallEndY(wallnum) };
|
||||
}
|
||||
|
||||
inline DVector2 WallDelta(const walltype* wallnum)
|
||||
{
|
||||
return WallEnd(wallnum) - WallStart(wallnum);
|
||||
}
|
||||
|
||||
inline double SpriteX(int wallnum)
|
||||
{
|
||||
return sprite[wallnum].x * (1 / 16.);
|
||||
|
@ -72,6 +87,11 @@ inline double PointOnLineSide(double x, double y, double linex, double liney, do
|
|||
return (x - linex) * deltay - (y - liney) * deltax;
|
||||
}
|
||||
|
||||
inline double PointOnLineSide(const DVector2 &pos, const walltype *line)
|
||||
{
|
||||
return (pos.X - WallStartX(line)) * WallDelta(line).Y - (pos.Y - WallStartY(line)) * WallDelta(line).X;
|
||||
}
|
||||
|
||||
inline int sectorofwall(int wallNum)
|
||||
{
|
||||
if ((unsigned)wallNum < (unsigned)numwalls) return wall[wallNum].sector;
|
||||
|
|
956
source/core/rendering/scene/hw_portal.cpp
Normal file
956
source/core/rendering/scene/hw_portal.cpp
Normal file
|
@ -0,0 +1,956 @@
|
|||
//
|
||||
//---------------------------------------------------------------------------
|
||||
//
|
||||
// Copyright(C) 2004-2018 Christoph Oelckers
|
||||
// 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/
|
||||
//
|
||||
//--------------------------------------------------------------------------
|
||||
//
|
||||
/*
|
||||
** hw_portal.cpp
|
||||
** portal maintenance classes for skyboxes, horizons etc. (API independent parts)
|
||||
**
|
||||
*/
|
||||
|
||||
#include "c_dispatch.h"
|
||||
#include "hw_portal.h"
|
||||
#include "hw_clipper.h"
|
||||
#include "hw_renderstate.h"
|
||||
#include "flatvertices.h"
|
||||
#include "hw_clock.h"
|
||||
#include "texturemanager.h"
|
||||
|
||||
EXTERN_CVAR(Int, r_mirror_recursions)
|
||||
EXTERN_CVAR(Bool, gl_portals)
|
||||
|
||||
enum
|
||||
{
|
||||
plane_floor,
|
||||
plane_ceiling
|
||||
};
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
//
|
||||
// StartFrame
|
||||
//
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
void FPortalSceneState::StartFrame()
|
||||
{
|
||||
if (renderdepth == 0)
|
||||
{
|
||||
inskybox = false;
|
||||
screen->instack[plane_floor] = screen->instack[plane_ceiling] = 0;
|
||||
}
|
||||
renderdepth++;
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
//
|
||||
// printing portal info
|
||||
//
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
static bool gl_portalinfo;
|
||||
|
||||
CCMD(gl_portalinfo)
|
||||
{
|
||||
gl_portalinfo = true;
|
||||
}
|
||||
|
||||
static FString indent;
|
||||
FPortalSceneState portalState;
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
//
|
||||
// EndFrame
|
||||
//
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
void FPortalSceneState::EndFrame(HWDrawInfo *di, FRenderState &state)
|
||||
{
|
||||
HWPortal * p;
|
||||
|
||||
if (gl_portalinfo)
|
||||
{
|
||||
Printf("%s%d portals, depth = %d\n%s{\n", indent.GetChars(), di->Portals.Size(), renderdepth, indent.GetChars());
|
||||
indent += " ";
|
||||
}
|
||||
|
||||
while (di->Portals.Pop(p) && p)
|
||||
{
|
||||
if (gl_portalinfo)
|
||||
{
|
||||
Printf("%sProcessing %s, depth = %d\n", indent.GetChars(), p->GetName(), renderdepth);
|
||||
}
|
||||
if (p->lines.Size() > 0)
|
||||
{
|
||||
RenderPortal(p, state, true, di);
|
||||
}
|
||||
delete p;
|
||||
}
|
||||
renderdepth--;
|
||||
|
||||
if (gl_portalinfo)
|
||||
{
|
||||
indent.Truncate(long(indent.Len()-2));
|
||||
Printf("%s}\n", indent.GetChars());
|
||||
if (indent.Len() == 0) gl_portalinfo = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
//
|
||||
// Renders one sky portal without a stencil.
|
||||
// In more complex scenes using a stencil for skies can severely stall
|
||||
// the GPU and there's rarely more than one sky visible at a time.
|
||||
//
|
||||
//-----------------------------------------------------------------------------
|
||||
bool FPortalSceneState::RenderFirstSkyPortal(int recursion, HWDrawInfo *outer_di, FRenderState &state)
|
||||
{
|
||||
HWPortal * p;
|
||||
HWPortal * best = nullptr;
|
||||
unsigned bestindex = 0;
|
||||
|
||||
// Find the one with the highest amount of lines.
|
||||
// Normally this is also the one that saves the largest amount
|
||||
// of time by drawing it before the scene itself.
|
||||
auto &portals = outer_di->Portals;
|
||||
for (int i = portals.Size() - 1; i >= 0; --i)
|
||||
{
|
||||
p = portals[i];
|
||||
if (p->lines.Size() > 0 && p->IsSky())
|
||||
{
|
||||
// Cannot clear the depth buffer inside a portal recursion
|
||||
if (recursion && p->NeedDepthBuffer()) continue;
|
||||
|
||||
if (!best || p->lines.Size() > best->lines.Size())
|
||||
{
|
||||
best = p;
|
||||
bestindex = i;
|
||||
}
|
||||
|
||||
// If the portal area contains the current camera viewpoint, let's always use it because it's likely to give the largest area.
|
||||
if (p->boundingBox.contains(outer_di->Viewpoint.Pos))
|
||||
{
|
||||
best = p;
|
||||
bestindex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (best)
|
||||
{
|
||||
portals.Delete(bestindex);
|
||||
RenderPortal(best, state, false, outer_di);
|
||||
delete best;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
void FPortalSceneState::RenderPortal(HWPortal *p, FRenderState &state, bool usestencil, HWDrawInfo *outer_di)
|
||||
{
|
||||
if (gl_portals) outer_di->RenderPortal(p, state, usestencil);
|
||||
}
|
||||
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
//
|
||||
// DrawPortalStencil
|
||||
//
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
void HWPortal::DrawPortalStencil(FRenderState &state, int pass)
|
||||
{
|
||||
if (mPrimIndices.Size() == 0)
|
||||
{
|
||||
mPrimIndices.Resize(2 * lines.Size());
|
||||
|
||||
for (unsigned int i = 0; i < lines.Size(); i++)
|
||||
{
|
||||
mPrimIndices[i * 2] = lines[i].vertindex;
|
||||
mPrimIndices[i * 2 + 1] = lines[i].vertcount;
|
||||
}
|
||||
|
||||
if (NeedCap() && lines.Size() > 1 && planesused != 0)
|
||||
{
|
||||
screen->mVertexData->Map();
|
||||
if (planesused & (1 << plane_floor))
|
||||
{
|
||||
auto verts = screen->mVertexData->AllocVertices(4);
|
||||
auto ptr = verts.first;
|
||||
ptr[0].Set((float)boundingBox.left, -32767.f, (float)boundingBox.top, 0, 0);
|
||||
ptr[1].Set((float)boundingBox.right, -32767.f, (float)boundingBox.top, 0, 0);
|
||||
ptr[2].Set((float)boundingBox.left, -32767.f, (float)boundingBox.bottom, 0, 0);
|
||||
ptr[3].Set((float)boundingBox.right, -32767.f, (float)boundingBox.bottom, 0, 0);
|
||||
mBottomCap = verts.second;
|
||||
}
|
||||
if (planesused & (1 << plane_ceiling))
|
||||
{
|
||||
auto verts = screen->mVertexData->AllocVertices(4);
|
||||
auto ptr = verts.first;
|
||||
ptr[0].Set((float)boundingBox.left, 32767.f, (float)boundingBox.top, 0, 0);
|
||||
ptr[1].Set((float)boundingBox.right, 32767.f, (float)boundingBox.top, 0, 0);
|
||||
ptr[2].Set((float)boundingBox.left, 32767.f, (float)boundingBox.bottom, 0, 0);
|
||||
ptr[3].Set((float)boundingBox.right, 32767.f, (float)boundingBox.bottom, 0, 0);
|
||||
mTopCap = verts.second;
|
||||
}
|
||||
screen->mVertexData->Unmap();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for (unsigned int i = 0; i < mPrimIndices.Size(); i += 2)
|
||||
{
|
||||
state.Draw(DT_TriangleFan, mPrimIndices[i], mPrimIndices[i + 1], i == 0);
|
||||
}
|
||||
if (NeedCap() && lines.Size() > 1)
|
||||
{
|
||||
// The cap's depth handling needs special treatment so that it won't block further portal caps.
|
||||
if (pass == STP_DepthRestore) state.SetDepthRange(1, 1);
|
||||
|
||||
if (mBottomCap != ~0u)
|
||||
{
|
||||
state.Draw(DT_TriangleStrip, mBottomCap, 4, false);
|
||||
}
|
||||
if (mTopCap != ~0u)
|
||||
{
|
||||
state.Draw(DT_TriangleStrip, mTopCap, 4, false);
|
||||
}
|
||||
|
||||
if (pass == STP_DepthRestore) state.SetDepthRange(0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
//
|
||||
// Start
|
||||
//
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
void HWPortal::SetupStencil(HWDrawInfo *di, FRenderState &state, bool usestencil)
|
||||
{
|
||||
Clocker c(PortalAll);
|
||||
|
||||
rendered_portals++;
|
||||
|
||||
if (usestencil)
|
||||
{
|
||||
// Create stencil
|
||||
state.SetStencil(0, SOP_Increment); // create stencil, increment stencil of valid pixels
|
||||
state.SetColorMask(false);
|
||||
state.SetEffect(EFF_STENCIL);
|
||||
state.EnableTexture(false);
|
||||
state.ResetColor();
|
||||
state.SetDepthFunc(DF_Less);
|
||||
|
||||
if (NeedDepthBuffer())
|
||||
{
|
||||
state.SetDepthMask(false); // don't write to Z-buffer!
|
||||
|
||||
DrawPortalStencil(state, STP_Stencil);
|
||||
|
||||
// Clear Z-buffer
|
||||
state.SetStencil(1, SOP_Keep); // draw sky into stencil. This stage doesn't modify the stencil.
|
||||
state.SetDepthMask(true); // enable z-buffer again
|
||||
state.SetDepthRange(1, 1);
|
||||
state.SetDepthFunc(DF_Always);
|
||||
DrawPortalStencil(state, STP_DepthClear);
|
||||
|
||||
// set normal drawing mode
|
||||
state.EnableTexture(true);
|
||||
state.SetDepthRange(0, 1);
|
||||
state.SetDepthFunc(DF_Less);
|
||||
state.SetColorMask(true);
|
||||
state.SetEffect(EFF_NONE);
|
||||
}
|
||||
else
|
||||
{
|
||||
// No z-buffer is needed therefore we can skip all the complicated stuff that is involved
|
||||
// Note: We must draw the stencil with z-write enabled here because there is no second pass!
|
||||
|
||||
state.SetDepthMask(true);
|
||||
DrawPortalStencil(state, STP_AllInOne);
|
||||
state.SetStencil(1, SOP_Keep); // draw sky into stencil. This stage doesn't modify the stencil.
|
||||
state.EnableTexture(true);
|
||||
state.SetColorMask(true);
|
||||
state.SetEffect(EFF_NONE);
|
||||
state.EnableDepthTest(false);
|
||||
state.SetDepthMask(false); // don't write to Z-buffer!
|
||||
}
|
||||
screen->stencilValue++;
|
||||
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!NeedDepthBuffer())
|
||||
{
|
||||
state.SetDepthMask(false);
|
||||
state.EnableDepthTest(false);
|
||||
}
|
||||
}
|
||||
|
||||
// save viewpoint
|
||||
//savedvisibility = di->Viewpoint.
|
||||
}
|
||||
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
//
|
||||
// End
|
||||
//
|
||||
//-----------------------------------------------------------------------------
|
||||
void HWPortal::RemoveStencil(HWDrawInfo *di, FRenderState &state, bool usestencil)
|
||||
{
|
||||
Clocker c(PortalAll);
|
||||
bool needdepth = NeedDepthBuffer();
|
||||
|
||||
// Restore the old view
|
||||
auto &vp = di->Viewpoint;
|
||||
//if (vp.camera != nullptr) vp.camera->renderflags = (vp.camera->renderflags & ~RF_MAYBEINVISIBLE) | savedvisibility;
|
||||
|
||||
if (usestencil)
|
||||
{
|
||||
|
||||
state.SetColorMask(false); // no graphics
|
||||
state.SetEffect(EFF_NONE);
|
||||
state.ResetColor();
|
||||
state.EnableTexture(false);
|
||||
|
||||
if (needdepth)
|
||||
{
|
||||
// first step: reset the depth buffer to max. depth
|
||||
state.SetDepthRange(1, 1); // always
|
||||
state.SetDepthFunc(DF_Always); // write the farthest depth value
|
||||
DrawPortalStencil(state, STP_DepthClear);
|
||||
}
|
||||
else
|
||||
{
|
||||
state.EnableDepthTest(true);
|
||||
}
|
||||
|
||||
// second step: restore the depth buffer to the previous values and reset the stencil
|
||||
state.SetDepthFunc(DF_LEqual);
|
||||
state.SetDepthRange(0, 1);
|
||||
state.SetStencil(0, SOP_Decrement);
|
||||
DrawPortalStencil(state, STP_DepthRestore);
|
||||
state.SetDepthFunc(DF_Less);
|
||||
|
||||
|
||||
state.EnableTexture(true);
|
||||
state.SetEffect(EFF_NONE);
|
||||
state.SetColorMask(true);
|
||||
screen->stencilValue--;
|
||||
|
||||
// restore old stencil op.
|
||||
state.SetStencil(0, SOP_Keep);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (needdepth)
|
||||
{
|
||||
state.Clear(CT_Depth);
|
||||
}
|
||||
else
|
||||
{
|
||||
state.EnableDepthTest(true);
|
||||
state.SetDepthMask(true);
|
||||
}
|
||||
|
||||
// This draws a valid z-buffer into the stencil's contents to ensure it
|
||||
// doesn't get overwritten by the level's geometry.
|
||||
|
||||
state.ResetColor();
|
||||
state.SetDepthFunc(DF_LEqual);
|
||||
state.SetDepthRange(0, 1);
|
||||
state.SetColorMask(0, 0, 0, 1); // mark portal in alpha channel but don't touch color
|
||||
state.SetEffect(EFF_STENCIL);
|
||||
state.EnableTexture(false);
|
||||
state.SetRenderStyle(STYLE_Source);
|
||||
DrawPortalStencil(state, STP_DepthRestore);
|
||||
state.SetEffect(EFF_NONE);
|
||||
state.EnableTexture(true);
|
||||
state.SetColorMask(true);
|
||||
state.SetDepthFunc(DF_Less);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
//
|
||||
//
|
||||
//
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
void HWScenePortalBase::ClearClipper(HWDrawInfo *di, Clipper *clipper)
|
||||
{
|
||||
auto outer_di = di->outer;
|
||||
// This requires maximum precision, so convert everything to double.
|
||||
DAngle angleOffset = deltaangle(DAngle(outer_di->Viewpoint.HWAngles.Yaw.Degrees), DAngle(di->Viewpoint.HWAngles.Yaw.Degrees));
|
||||
|
||||
clipper->Clear();
|
||||
|
||||
// Set the clipper to the minimal visible area
|
||||
clipper->SafeAddClipRange(0, 0xffffffff);
|
||||
for (unsigned int i = 0; i < lines.Size(); i++)
|
||||
{
|
||||
DAngle startAngle = (DVector2(lines[i].glseg.x2, lines[i].glseg.y2) - outer_di->Viewpoint.Pos).Angle() + angleOffset;
|
||||
DAngle endAngle = (DVector2(lines[i].glseg.x1, lines[i].glseg.y1) - outer_di->Viewpoint.Pos).Angle() + angleOffset;
|
||||
|
||||
if (deltaangle(endAngle, startAngle) < 0)
|
||||
{
|
||||
clipper->SafeRemoveClipRangeRealAngles(startAngle.BAMs(), endAngle.BAMs());
|
||||
}
|
||||
}
|
||||
|
||||
// and finally clip it to the visible area
|
||||
angle_t a1 = di->FrustumAngle();
|
||||
if (a1 < ANGLE_180) clipper->SafeAddClipRangeRealAngles(di->Viewpoint.HWAngles.Yaw.BAMs() + a1, di->Viewpoint.HWAngles.Yaw.BAMs() - a1);
|
||||
|
||||
// lock the parts that have just been clipped out.
|
||||
//clipper->SetSilhouette();
|
||||
}
|
||||
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
//-----------------------------------------------------------------------------
|
||||
//
|
||||
//
|
||||
// Common code for line to line and mirror portals
|
||||
//
|
||||
//
|
||||
//-----------------------------------------------------------------------------
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
inline int P_GetLineSide(const DVector2& pos, const walltype* line)
|
||||
{
|
||||
auto delta = WallDelta(line);
|
||||
double v = (pos.Y - WallStartY(line) * delta.X + WallStartX(line) - pos.X) * delta.Y;
|
||||
return v < -1. / 65536. ? -1 : v > 1. / 65536 ? 1 : 0;
|
||||
}
|
||||
|
||||
bool P_ClipLineToPortal(walltype* line, walltype* portal, DVector2 view)
|
||||
{
|
||||
int behind1 = P_GetLineSide(WallStart(line), portal);
|
||||
int behind2 = P_GetLineSide(WallEnd(line), portal);
|
||||
|
||||
if (behind1 == 0 && behind2 == 0)
|
||||
{
|
||||
// The line is parallel to the portal and cannot possibly be visible.
|
||||
return true;
|
||||
}
|
||||
// If one point lies on the same straight line than the portal, the other vertex will determine sidedness alone.
|
||||
else if (behind2 == 0) behind2 = behind1;
|
||||
else if (behind1 == 0) behind1 = behind2;
|
||||
|
||||
if (behind1 > 0 && behind2 > 0)
|
||||
{
|
||||
// The line is behind the portal, i.e. between viewer and portal line, and must be rejected
|
||||
return true;
|
||||
}
|
||||
else if (behind1 < 0 && behind2 < 0)
|
||||
{
|
||||
// The line is in front of the portal, i.e. the portal is between viewer and line. This line must not be rejected
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// The line intersects with the portal straight, so we need to do another check to see how both ends of the portal lie in relation to the viewer.
|
||||
int viewside = P_GetLineSide(view, line);
|
||||
int p1side = P_GetLineSide(WallStart(portal), line);
|
||||
int p2side = P_GetLineSide(WallEnd(portal), line);
|
||||
// Do the same handling of points on the portal straight as above.
|
||||
if (p1side == 0) p1side = p2side;
|
||||
else if (p2side == 0) p2side = p1side;
|
||||
// If the portal is on the other side of the line than the viewpoint, there is no possibility to see this line inside the portal.
|
||||
return (p1side == p2side && viewside != p1side);
|
||||
}
|
||||
}
|
||||
|
||||
int HWLinePortal::ClipSeg(walltype *seg, const DVector3 &viewpos)
|
||||
{
|
||||
return P_ClipLineToPortal(seg, line, viewpos) ? PClip_InFront : PClip_Inside;
|
||||
}
|
||||
|
||||
int HWLinePortal::ClipSector(sectortype *sub)
|
||||
{
|
||||
// this seg is completely behind the mirror
|
||||
for (unsigned int i = 0; i<sub->wallnum; i++)
|
||||
{
|
||||
if (PointOnLineSide(WallStart(&wall[sub->wallptr]), line) == 0) return PClip_Inside;
|
||||
}
|
||||
return PClip_InFront;
|
||||
}
|
||||
|
||||
int HWLinePortal::ClipPoint(const DVector2 &pos)
|
||||
{
|
||||
if (PointOnLineSide(pos, line))
|
||||
{
|
||||
return PClip_InFront;
|
||||
}
|
||||
return PClip_Inside;
|
||||
}
|
||||
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
//-----------------------------------------------------------------------------
|
||||
//
|
||||
//
|
||||
// Mirror Portal
|
||||
//
|
||||
//
|
||||
//-----------------------------------------------------------------------------
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
//
|
||||
//
|
||||
//
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
bool HWMirrorPortal::Setup(HWDrawInfo *di, FRenderState &rstate, Clipper *clipper)
|
||||
{
|
||||
auto state = mState;
|
||||
if (state->renderdepth > r_mirror_recursions)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
auto &vp = di->Viewpoint;
|
||||
|
||||
di->mClipPortal = this;
|
||||
DAngle StartAngle = vp.HWAngles.Yaw.Degrees;
|
||||
DVector3 StartPos = vp.Pos;
|
||||
|
||||
auto pos1 = WallStart(line);
|
||||
auto pos2 = WallEnd(line);
|
||||
|
||||
// the player is always visible in a mirror.
|
||||
//vp.showviewer = true;
|
||||
// Reflect the current view behind the mirror.
|
||||
if (pos2.X == pos1.X)
|
||||
{
|
||||
// vertical mirror
|
||||
vp.Pos.X = 2 * pos1.X - StartPos.X;
|
||||
|
||||
// Compensation for reendering inaccuracies
|
||||
if (StartPos.X < pos1.X) vp.Pos.X -= 0.1;
|
||||
else vp.Pos.X += 0.1;
|
||||
}
|
||||
else if (pos2.Y == pos1.Y)
|
||||
{
|
||||
// horizontal mirror
|
||||
vp.Pos.Y = 2 * pos1.Y - StartPos.Y;
|
||||
|
||||
// Compensation for reendering inaccuracies
|
||||
if (StartPos.Y < pos1.Y) vp.Pos.Y -= 0.1;
|
||||
else vp.Pos.Y += 0.1;
|
||||
}
|
||||
else
|
||||
{
|
||||
// any mirror--use floats to avoid integer overflow.
|
||||
// Use doubles to avoid losing precision which is very important here.
|
||||
|
||||
double dx = pos2.X - pos1.X;
|
||||
double dy = pos2.Y - pos1.Y;
|
||||
double x1 = pos1.X;
|
||||
double y1 = pos1.Y;
|
||||
double x = StartPos.X;
|
||||
double y = StartPos.Y;
|
||||
|
||||
// the above two cases catch len == 0
|
||||
double r = ((x - x1)*dx + (y - y1)*dy) / (dx*dx + dy * dy);
|
||||
|
||||
vp.Pos.X = (x1 + r * dx) * 2 - x;
|
||||
vp.Pos.Y = (y1 + r * dy) * 2 - y;
|
||||
|
||||
// Compensation for reendering inaccuracies
|
||||
FVector2 v(-dx, dy);
|
||||
v.MakeUnit();
|
||||
|
||||
vp.Pos.X += v[1] * state->renderdepth / 2;
|
||||
vp.Pos.Y += v[0] * state->renderdepth / 2;
|
||||
}
|
||||
vp.HWAngles.Yaw = WallDelta(line).Angle().Degrees * 2. - StartAngle.Degrees;
|
||||
|
||||
//vp.ViewActor = nullptr;
|
||||
|
||||
state->MirrorFlag++;
|
||||
di->SetClipLine(line);
|
||||
di->SetupView(rstate, vp.Pos.X, vp.Pos.Y, vp.Pos.Z, !!(state->MirrorFlag & 1), !!(state->PlaneMirrorFlag & 1));
|
||||
|
||||
clipper->Clear();
|
||||
|
||||
angle_t af = di->FrustumAngle();
|
||||
if (af < ANGLE_180) clipper->SafeAddClipRangeRealAngles(vp.HWAngles.Yaw.BAMs() + af, vp.HWAngles.Yaw.BAMs() - af);
|
||||
|
||||
clipper->SafeAddClipRange(pos1, pos2);
|
||||
return true;
|
||||
}
|
||||
|
||||
void HWMirrorPortal::Shutdown(HWDrawInfo *di, FRenderState &rstate)
|
||||
{
|
||||
mState->MirrorFlag--;
|
||||
}
|
||||
|
||||
const char *HWMirrorPortal::GetName() { return "Mirror"; }
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
//-----------------------------------------------------------------------------
|
||||
//
|
||||
//
|
||||
// Line to line Portal
|
||||
//
|
||||
//
|
||||
//-----------------------------------------------------------------------------
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
//
|
||||
//
|
||||
//
|
||||
//-----------------------------------------------------------------------------
|
||||
bool HWLineToLinePortal::Setup(HWDrawInfo *di, FRenderState &rstate, Clipper *clipper)
|
||||
{
|
||||
// TODO: Handle recursion more intelligently
|
||||
auto &state = mState;
|
||||
if (state->renderdepth>r_mirror_recursions)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
auto &vp = di->Viewpoint;
|
||||
di->mClipPortal = this;
|
||||
|
||||
auto srccenter = (WallStart(origin) + WallEnd(origin)) / 2;
|
||||
auto destcenter = (WallStart(line) + WallEnd(line)) / 2;
|
||||
DVector2 npos = vp.Pos - srccenter + destcenter;
|
||||
|
||||
//vp.ViewActor = nullptr;
|
||||
di->SetClipLine(line);
|
||||
di->SetupView(rstate, vp.Pos.X, vp.Pos.Y, vp.Pos.Z, !!(state->MirrorFlag & 1), !!(state->PlaneMirrorFlag & 1));
|
||||
|
||||
ClearClipper(di, clipper);
|
||||
return true;
|
||||
}
|
||||
|
||||
const char *HWLineToLinePortal::GetName() { return "LineToLine"; }
|
||||
|
||||
|
||||
#if 0 // currently none of the games has any support for this. Maybe later.
|
||||
//-----------------------------------------------------------------------------
|
||||
//-----------------------------------------------------------------------------
|
||||
//
|
||||
//
|
||||
// Skybox Portal
|
||||
//
|
||||
//
|
||||
//-----------------------------------------------------------------------------
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
//
|
||||
// GLSkyboxPortal::DrawContents
|
||||
//
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
bool HWSkyboxPortal::Setup(HWDrawInfo *di, FRenderState &rstate, Clipper *clipper)
|
||||
{
|
||||
auto state = mState;
|
||||
old_pm = state->PlaneMirrorMode;
|
||||
|
||||
if (mState->skyboxrecursion >= 3)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
auto &vp = di->Viewpoint;
|
||||
|
||||
state->skyboxrecursion++;
|
||||
state->PlaneMirrorMode = 0;
|
||||
state->inskybox = true;
|
||||
|
||||
AActor *origin = portal->mSkybox;
|
||||
portal->mFlags |= PORTSF_INSKYBOX;
|
||||
vp.extralight = 0;
|
||||
|
||||
oldclamp = rstate.SetDepthClamp(false);
|
||||
vp.Pos = origin->InterpolatedPosition(vp.TicFrac);
|
||||
vp.ActorPos = origin->Pos();
|
||||
vp.Angles.Yaw += (origin->PrevAngles.Yaw + deltaangle(origin->PrevAngles.Yaw, origin->Angles.Yaw) * vp.TicFrac);
|
||||
|
||||
// Don't let the viewpoint be too close to a floor or ceiling
|
||||
double floorh = origin->Sector->floorplane.ZatPoint(origin->Pos());
|
||||
double ceilh = origin->Sector->ceilingplane.ZatPoint(origin->Pos());
|
||||
if (vp.Pos.Z < floorh + 4) vp.Pos.Z = floorh + 4;
|
||||
if (vp.Pos.Z > ceilh - 4) vp.Pos.Z = ceilh - 4;
|
||||
|
||||
vp.ViewActor = origin;
|
||||
|
||||
di->SetupView(rstate, vp.Pos.X, vp.Pos.Y, vp.Pos.Z, !!(state->MirrorFlag & 1), !!(state->PlaneMirrorFlag & 1));
|
||||
di->SetViewArea();
|
||||
ClearClipper(di, clipper);
|
||||
di->UpdateCurrentMapSection();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void HWSkyboxPortal::Shutdown(HWDrawInfo *di, FRenderState &rstate)
|
||||
{
|
||||
rstate.SetDepthClamp(oldclamp);
|
||||
|
||||
auto state = mState;
|
||||
portal->mFlags &= ~PORTSF_INSKYBOX;
|
||||
state->inskybox = false;
|
||||
state->skyboxrecursion--;
|
||||
state->PlaneMirrorMode = old_pm;
|
||||
}
|
||||
|
||||
const char *HWSkyboxPortal::GetName() { return "Skybox"; }
|
||||
bool HWSkyboxPortal::AllowSSAO() { return false; } // [MK] sector skyboxes don't allow SSAO by default
|
||||
#endif
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
//-----------------------------------------------------------------------------
|
||||
//
|
||||
//
|
||||
// Sector stack Portal
|
||||
//
|
||||
//
|
||||
//-----------------------------------------------------------------------------
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
//
|
||||
// GLSectorStackPortal::DrawContents
|
||||
//
|
||||
//-----------------------------------------------------------------------------
|
||||
bool HWSectorStackPortal::Setup(HWDrawInfo *di, FRenderState &rstate, Clipper *clipper)
|
||||
{
|
||||
auto state = mState;
|
||||
auto portal = origin;
|
||||
auto &vp = di->Viewpoint;
|
||||
|
||||
vp.Pos += DVector3(portal->dx / 16., portal->dy / -16., portal->dz / -256.);
|
||||
//vp.ActorPos += origin->mDisplacement;
|
||||
//vp.ViewActor = nullptr;
|
||||
|
||||
// avoid recursions!
|
||||
screen->instack[origin->type == PORTAL_SECTOR_CEILING ? 1 : 0]++;
|
||||
|
||||
di->SetupView(rstate, vp.Pos.X, vp.Pos.Y, vp.Pos.Z, !!(state->MirrorFlag & 1), !!(state->PlaneMirrorFlag & 1));
|
||||
//SetupCoverage(di);
|
||||
ClearClipper(di, clipper);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void HWSectorStackPortal::Shutdown(HWDrawInfo *di, FRenderState &rstate)
|
||||
{
|
||||
screen->instack[origin->type == PORTAL_SECTOR_CEILING ? 1 : 0]--;
|
||||
}
|
||||
|
||||
const char *HWSectorStackPortal::GetName() { return "Sectorstack"; }
|
||||
|
||||
#if 0
|
||||
//-----------------------------------------------------------------------------
|
||||
//-----------------------------------------------------------------------------
|
||||
//
|
||||
//
|
||||
// Plane Mirror Portal
|
||||
//
|
||||
//
|
||||
//-----------------------------------------------------------------------------
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
//
|
||||
// GLPlaneMirrorPortal::DrawContents
|
||||
//
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
bool HWPlaneMirrorPortal::Setup(HWDrawInfo *di, FRenderState &rstate, Clipper *clipper)
|
||||
{
|
||||
auto state = mState;
|
||||
if (state->renderdepth > r_mirror_recursions)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// A plane mirror needs to flip the portal exclusion logic because inside the mirror, up is down and down is up.
|
||||
std::swap(screen->instack[plane_floor], screen->instack[plane_ceiling]);
|
||||
|
||||
auto &vp = di->Viewpoint;
|
||||
old_pm = state->PlaneMirrorMode;
|
||||
|
||||
// the player is always visible in a mirror.
|
||||
vp.showviewer = true;
|
||||
|
||||
double planez = origin->ZatPoint(vp.Pos);
|
||||
vp.Pos.Z = 2 * planez - vp.Pos.Z;
|
||||
vp.ViewActor = nullptr;
|
||||
state->PlaneMirrorMode = origin->fC() < 0 ? -1 : 1;
|
||||
|
||||
state->PlaneMirrorFlag++;
|
||||
di->SetClipHeight(planez, state->PlaneMirrorMode < 0 ? -1.f : 1.f);
|
||||
di->SetupView(rstate, vp.Pos.X, vp.Pos.Y, vp.Pos.Z, !!(state->MirrorFlag & 1), !!(state->PlaneMirrorFlag & 1));
|
||||
ClearClipper(di, clipper);
|
||||
|
||||
di->UpdateCurrentMapSection();
|
||||
return true;
|
||||
}
|
||||
|
||||
void HWPlaneMirrorPortal::Shutdown(HWDrawInfo *di, FRenderState &rstate)
|
||||
{
|
||||
auto state = mState;
|
||||
state->PlaneMirrorFlag--;
|
||||
state->PlaneMirrorMode = old_pm;
|
||||
std::swap(screen->instack[plane_floor], screen->instack[plane_ceiling]);
|
||||
}
|
||||
|
||||
const char *HWPlaneMirrorPortal::GetName() { return origin->fC() < 0? "Planemirror ceiling" : "Planemirror floor"; }
|
||||
#endif
|
||||
|
||||
#if 0
|
||||
//-----------------------------------------------------------------------------
|
||||
//-----------------------------------------------------------------------------
|
||||
//
|
||||
//
|
||||
// Horizon Portal
|
||||
//
|
||||
//
|
||||
//-----------------------------------------------------------------------------
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
HWHorizonPortal::HWHorizonPortal(FPortalSceneState *s, HWHorizonInfo * pt, FRenderViewpoint &vp, bool local)
|
||||
: HWPortal(s, local)
|
||||
{
|
||||
origin = pt;
|
||||
|
||||
// create the vertex data for this horizon portal.
|
||||
HWSectorPlane * sp = &origin->plane;
|
||||
const float vx = vp.Pos.X;
|
||||
const float vy = vp.Pos.Y;
|
||||
const float vz = vp.Pos.Z;
|
||||
const float z = sp->Texheight;
|
||||
const float tz = (z - vz);
|
||||
|
||||
// Draw to some far away boundary
|
||||
// This is not drawn as larger strips because it causes visual glitches.
|
||||
auto verts = screen->mVertexData->AllocVertices(1024 + 10);
|
||||
auto ptr = verts.first;
|
||||
for (int xx = -32768; xx < 32768; xx += 4096)
|
||||
{
|
||||
float x = xx + vx;
|
||||
for (int yy = -32768; yy < 32768; yy += 4096)
|
||||
{
|
||||
float y = yy + vy;
|
||||
ptr->Set(x, z, y, x / 64, -y / 64);
|
||||
ptr++;
|
||||
ptr->Set(x + 4096, z, y, x / 64 + 64, -y / 64);
|
||||
ptr++;
|
||||
ptr->Set(x, z, y + 4096, x / 64, -y / 64 - 64);
|
||||
ptr++;
|
||||
ptr->Set(x + 4096, z, y + 4096, x / 64 + 64, -y / 64 - 64);
|
||||
ptr++;
|
||||
}
|
||||
}
|
||||
|
||||
// fill the gap between the polygon and the true horizon
|
||||
// Since I can't draw into infinity there can always be a
|
||||
// small gap
|
||||
ptr->Set(-32768 + vx, z, -32768 + vy, 512.f, 0);
|
||||
ptr++;
|
||||
ptr->Set(-32768 + vx, vz, -32768 + vy, 512.f, tz);
|
||||
ptr++;
|
||||
ptr->Set(-32768 + vx, z, 32768 + vy, -512.f, 0);
|
||||
ptr++;
|
||||
ptr->Set(-32768 + vx, vz, 32768 + vy, -512.f, tz);
|
||||
ptr++;
|
||||
ptr->Set(32768 + vx, z, 32768 + vy, 512.f, 0);
|
||||
ptr++;
|
||||
ptr->Set(32768 + vx, vz, 32768 + vy, 512.f, tz);
|
||||
ptr++;
|
||||
ptr->Set(32768 + vx, z, -32768 + vy, -512.f, 0);
|
||||
ptr++;
|
||||
ptr->Set(32768 + vx, vz, -32768 + vy, -512.f, tz);
|
||||
ptr++;
|
||||
ptr->Set(-32768 + vx, z, -32768 + vy, 512.f, 0);
|
||||
ptr++;
|
||||
ptr->Set(-32768 + vx, vz, -32768 + vy, 512.f, tz);
|
||||
ptr++;
|
||||
|
||||
voffset = verts.second;
|
||||
vcount = 1024;
|
||||
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
//
|
||||
// HWHorizonPortal::DrawContents
|
||||
//
|
||||
//-----------------------------------------------------------------------------
|
||||
void HWHorizonPortal::DrawContents(HWDrawInfo *di, FRenderState &state)
|
||||
{
|
||||
Clocker c(PortalAll);
|
||||
|
||||
HWSectorPlane * sp = &origin->plane;
|
||||
auto &vp = di->Viewpoint;
|
||||
|
||||
auto texture = TexMan.GetGameTexture(sp->texture, true);
|
||||
|
||||
if (!texture || !texture->isValid())
|
||||
{
|
||||
state.ClearScreen();
|
||||
return;
|
||||
}
|
||||
di->SetCameraPos(vp.Pos);
|
||||
|
||||
|
||||
if (texture->isFullbright())
|
||||
{
|
||||
// glowing textures are always drawn full bright without color
|
||||
di->SetColor(state, 255, 0, false, origin->colormap, 1.f);
|
||||
di->SetFog(state, 255, 0, false, &origin->colormap, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
int rel = getExtraLight();
|
||||
di->SetColor(state, origin->lightlevel, rel, di->isFullbrightScene(), origin->colormap, 1.0f);
|
||||
di->SetFog(state, origin->lightlevel, rel, di->isFullbrightScene(), &origin->colormap, false);
|
||||
}
|
||||
|
||||
|
||||
state.SetMaterial(texture, UF_Texture, 0, CLAMP_NONE, 0, -1);
|
||||
state.SetObjectColor(origin->specialcolor);
|
||||
|
||||
SetPlaneTextureRotation(state, sp, texture);
|
||||
state.AlphaFunc(Alpha_GEqual, 0.f);
|
||||
state.SetRenderStyle(STYLE_Source);
|
||||
|
||||
for (unsigned i = 0; i < vcount; i += 4)
|
||||
{
|
||||
state.Draw(DT_TriangleStrip, voffset + i, 4, true);// i == 0);
|
||||
}
|
||||
state.Draw(DT_TriangleStrip, voffset + vcount, 10, false);
|
||||
|
||||
state.EnableTextureMatrix(false);
|
||||
}
|
||||
#endif
|
|
@ -5,6 +5,7 @@
|
|||
#include "hw_drawstructs.h"
|
||||
#include "hw_renderstate.h"
|
||||
#include "hw_material.h"
|
||||
#include "render.h"
|
||||
|
||||
class FSkyBox;
|
||||
|
||||
|
@ -143,7 +144,7 @@ public:
|
|||
}
|
||||
virtual ~HWPortal() {}
|
||||
virtual int ClipSeg(walltype *seg, const DVector3 &viewpos) { return PClip_Inside; }
|
||||
virtual int ClipSubsector(sectortype *sub) { return PClip_Inside; }
|
||||
virtual int ClipSector(sectortype *sub) { return PClip_Inside; }
|
||||
virtual int ClipPoint(const DVector2 &pos) { return PClip_Inside; }
|
||||
virtual walltype *ClipLine() { return nullptr; }
|
||||
virtual void * GetSource() const = 0; // GetSource MUST be implemented!
|
||||
|
@ -229,19 +230,22 @@ public:
|
|||
|
||||
struct HWLinePortal : public HWScenePortalBase
|
||||
{
|
||||
protected:
|
||||
// this must be the same as at the start of line_t, so that we can pass in this structure directly to P_ClipLineToPortal.
|
||||
walltype* line;
|
||||
/*
|
||||
vec2_t *v1, *v2; // vertices, from v1 to v2
|
||||
DVector2 delta; // precalculated v2 - v1 for side checking
|
||||
|
||||
angle_t angv1, angv2; // for quick comparisons with a line or subsector
|
||||
*/
|
||||
|
||||
HWLinePortal(FPortalSceneState *state, walltype *line) : HWScenePortalBase(state)
|
||||
{
|
||||
this->line = line;
|
||||
v1 = &line->pos;
|
||||
v2 = &wall[line->point2].pos;
|
||||
CalcDelta();
|
||||
//v1 = &line->pos;
|
||||
//v2 = &wall[line->point2].pos;
|
||||
//CalcDelta();
|
||||
}
|
||||
|
||||
void CalcDelta()
|
||||
|
@ -250,7 +254,7 @@ struct HWLinePortal : public HWScenePortalBase
|
|||
}
|
||||
|
||||
int ClipSeg(walltype *seg, const DVector3 &viewpos) override;
|
||||
int ClipSubsector(sectortype *sub) override;
|
||||
int ClipSector(sectortype *sub) override;
|
||||
int ClipPoint(const DVector2 &pos);
|
||||
bool NeedCap() override { return false; }
|
||||
};
|
||||
|
@ -275,22 +279,23 @@ public:
|
|||
|
||||
struct HWLineToLinePortal : public HWLinePortal
|
||||
{
|
||||
walltype* origin;
|
||||
protected:
|
||||
bool Setup(HWDrawInfo *di, FRenderState &rstate, Clipper *clipper) override;
|
||||
virtual void * GetSource() const override { return line; }
|
||||
virtual const char *GetName() override;
|
||||
virtual walltype *ClipLine() override { return line; }
|
||||
virtual void RenderAttached(HWDrawInfo *di) override;
|
||||
|
||||
public:
|
||||
|
||||
HWLineToLinePortal(FPortalSceneState *state, walltype *ll)
|
||||
: HWLinePortal(state, ll)
|
||||
HWLineToLinePortal(FPortalSceneState *state, walltype* from, walltype *to)
|
||||
: HWLinePortal(state, to), origin(from)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
#if 0
|
||||
struct HWSkyboxPortal : public HWScenePortalBase
|
||||
{
|
||||
bool oldclamp;
|
||||
|
@ -314,6 +319,7 @@ public:
|
|||
}
|
||||
|
||||
};
|
||||
#endif
|
||||
|
||||
|
||||
struct HWSectorStackPortal : public HWScenePortalBase
|
||||
|
@ -325,22 +331,19 @@ protected:
|
|||
virtual void * GetSource() const { return origin; }
|
||||
virtual bool IsSky() { return true; } // although this isn't a real sky it can be handled as one.
|
||||
virtual const char *GetName();
|
||||
FSectorPortalGroup *origin;
|
||||
PortalDesc *origin;
|
||||
|
||||
public:
|
||||
|
||||
HWSectorStackPortal(FPortalSceneState *state, FSectorPortalGroup *pt) : HWScenePortalBase(state)
|
||||
HWSectorStackPortal(FPortalSceneState *state, PortalDesc *pt) : HWScenePortalBase(state)
|
||||
{
|
||||
origin = pt;
|
||||
}
|
||||
void SetupCoverage(HWDrawInfo *di);
|
||||
void AddSector(sectortype *sub)
|
||||
{
|
||||
sectors.Push(sub);
|
||||
}
|
||||
//void SetupCoverage(HWDrawInfo *di);
|
||||
|
||||
};
|
||||
|
||||
#if 0
|
||||
struct HWPlaneMirrorPortal : public HWScenePortalBase
|
||||
{
|
||||
int old_pm;
|
||||
|
@ -359,8 +362,9 @@ public:
|
|||
}
|
||||
|
||||
};
|
||||
#endif
|
||||
|
||||
|
||||
#if 0
|
||||
struct HWHorizonPortal : public HWPortal
|
||||
{
|
||||
HWHorizonInfo * origin;
|
||||
|
@ -379,6 +383,7 @@ public:
|
|||
|
||||
HWHorizonPortal(FPortalSceneState *state, HWHorizonInfo * pt, FRenderViewpoint &vp, bool local = false);
|
||||
};
|
||||
#endif
|
||||
|
||||
struct HWSkyPortal : public HWPortal
|
||||
{
|
||||
|
|
Loading…
Reference in a new issue