1
0
mirror of https://github.com/rwengine/openrw.git synced 2024-09-15 15:02:34 +02:00

Improve graphics performance

+ Add Framebuffer rendering to store data
+ Re-implement water using projected grid aproach
This commit is contained in:
Daniel Evans 2015-03-28 13:42:29 +00:00
parent 2985a70354
commit 30e059a0b6
8 changed files with 406 additions and 144 deletions

View File

@ -11,6 +11,7 @@
#include <render/OpenGLRenderer.hpp>
#include "MapRenderer.hpp"
#include "TextRenderer.hpp"
#include "WaterRenderer.hpp"
class Model;
class ModelFrame;
@ -77,10 +78,16 @@ class GameRenderer
/** Camera values passed to renderWorld() */
ViewCamera _camera;
GLuint framebufferName;
GLuint fbTextures[2];
GLuint fbRenderBuffers[1];
Renderer::ShaderProgram* postProg;
public:
GameRenderer(GameWorld*);
~GameRenderer();
/** Number of issued draw calls */
size_t rendered;
@ -92,7 +99,6 @@ public:
/** @todo Clean up all these shader program and location variables */
Renderer::ShaderProgram* worldProg;
Renderer::ShaderProgram* skyProg;
Renderer::ShaderProgram* waterProg;
Renderer::ShaderProgram* particleProg;
GLuint ssRectProgram;
@ -181,7 +187,10 @@ public:
return renderer;
}
void setViewport(int w, int h);
MapRenderer map;
WaterRenderer water;
TextRenderer text;
};

View File

@ -2,15 +2,26 @@
#ifndef _GAMESHADERS_HPP_
#define _GAMESHADERS_HPP_
#define SHADER_VF(Name) \
struct Name {\
static const char* VertexShader;\
static const char* FragmentShader;\
};
/**
* @brief collection of shaders to make managing them a little easier.
*/
namespace GameShaders {
struct WaterHQ {
static const char* VertexShader;
static const char* FragmentShader;
};
/**
* High Quality Projected-Grid water shader
*/
SHADER_VF(WaterHQ)
/**
* Simple 3D masking shader
*/
SHADER_VF(Mask3D)
struct Sky {
static const char* VertexShader;
@ -37,6 +48,8 @@ struct ScreenSpaceRect {
static const char* FragmentShader;
};
SHADER_VF(DefaultPostProcess);
}
#endif

View File

@ -79,6 +79,8 @@ public:
const AttributeList& getDataAttributes() const
{ return attributes; }
AttributeList& getDataAttributes()
{ return attributes; }
};
#endif

View File

@ -0,0 +1,48 @@
#pragma once
#include <engine/RWTypes.hpp>
#include <render/OpenGLRenderer.hpp>
class GameRenderer;
class GameWorld;
/**
* Implements the rendering routines for drawing the sea water.
*/
class WaterRenderer
{
public:
WaterRenderer(GameRenderer* renderer);
~WaterRenderer();
/**
* Creates the required data for rendering the water. Accepts
* two arrays. waterHeights stores the real world heights which
* are indexed into by the array tiles for each water tile.
*
* This data is used to create the internal stencil mask for clipping
* the water rendering.
*/
void setWaterTable(float* waterHeights, unsigned int nHeights, uint8_t* tiles, unsigned int nTiles);
void setDataTexture(GLuint fbBinding, GLuint dataTexture);
/**
* Render the water using the currently active render state
*/
void render(GameRenderer* renderer, GameWorld* world);
private:
Renderer::ShaderProgram* waterProg;
Renderer::ShaderProgram* maskProg;
DrawBuffer maskDraw;
GeometryBuffer maskGeom;
std::vector<int> maskSizes;
DrawBuffer gridDraw;
GeometryBuffer gridGeom;
GLuint fbOutput;
GLuint dataTexture;
};

View File

@ -37,20 +37,6 @@ struct WaterVertex {
float x, y;
};
std::vector<WaterVertex> waterLQVerts = {
{1.0f, 1.0f},
{0.0f, 1.0f},
{1.0f,-0.0f},
{0.0f,-0.0f}
};
std::vector<WaterVertex> waterHQVerts;
GeometryBuffer waterLQBuffer;
DrawBuffer waterLQDraw;
GeometryBuffer waterHQBuffer;
DrawBuffer waterHQDraw;
/// @todo collapse all of these into "VertPNC" etc.
struct ParticleVert {
static const AttributeList vertex_attributes() {
@ -81,7 +67,7 @@ DrawBuffer ssRectDraw;
GameRenderer::GameRenderer(GameWorld* engine)
: engine(engine), renderer(new OpenGLRenderer), _renderAlpha(0.f),
map(engine, renderer), text(engine, this)
map(engine, renderer), water(this), text(engine, this)
{
engine->logger.info("Renderer", renderer->getIDString());
@ -102,49 +88,44 @@ GameRenderer::GameRenderer(GameWorld* engine)
renderer->setProgramBlockBinding(particleProg, "ObjectData", 2);
skyProg = renderer->createShader(
GameShaders::Sky::VertexShader,
GameShaders::Sky::FragmentShader);
GameShaders::Sky::VertexShader,
GameShaders::Sky::FragmentShader);
renderer->setProgramBlockBinding(skyProg, "SceneData", 1);
waterProg = renderer->createShader(
GameShaders::WaterHQ::VertexShader,
GameShaders::WaterHQ::FragmentShader);
renderer->setUniformTexture(waterProg, "texture", 0);
renderer->setProgramBlockBinding(waterProg, "SceneData", 1);
renderer->setProgramBlockBinding(waterProg, "ObjectData", 2);
postProg = renderer->createShader(
GameShaders::DefaultPostProcess::VertexShader,
GameShaders::DefaultPostProcess::FragmentShader);
glGenVertexArrays( 1, &vao );
glGenFramebuffers(1, &framebufferName);
glBindFramebuffer(GL_FRAMEBUFFER, framebufferName);
glGenTextures(2, fbTextures);
glBindTexture(GL_TEXTURE_2D, fbTextures[0]);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 128, 128, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glBindTexture(GL_TEXTURE_2D, fbTextures[1]);
glTexImage2D(GL_TEXTURE_2D, 0, GL_R16F, 128, 128, 0, GL_RED, GL_FLOAT, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// Upload water plane
waterLQBuffer.uploadVertices(waterLQVerts);
waterLQDraw.addGeometry(&waterLQBuffer);
waterLQDraw.setFaceType(GL_TRIANGLE_STRIP);
// Generate HQ water geometry
int waterverts = 5;
float vertStep = 1.f/waterverts;
for(int x = 0; x < waterverts; ++x) {
float xB = vertStep * x;
for(int y = 0; y < waterverts; ++y) {
float yB = vertStep * y;
waterHQVerts.push_back({xB + vertStep, yB + vertStep});
waterHQVerts.push_back({xB, yB + vertStep});
waterHQVerts.push_back({xB + vertStep, yB });
waterHQVerts.push_back({xB + vertStep, yB });
waterHQVerts.push_back({xB, yB + vertStep});
waterHQVerts.push_back({xB, yB });
}
}
waterHQBuffer.uploadVertices(waterHQVerts);
waterHQDraw.addGeometry(&waterHQBuffer);
waterHQDraw.setFaceType(GL_TRIANGLES);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, fbTextures[0], 0);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, fbTextures[1], 0);
// Give water renderer the data texture
water.setDataTexture(1, fbTextures[1]);
glGenRenderbuffers(1, fbRenderBuffers);
glBindRenderbuffer(GL_RENDERBUFFER, fbRenderBuffers[0]);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 128, 128);
glFramebufferRenderbuffer(
GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, fbRenderBuffers[0]
);
// Create the skydome
size_t segments = skydomeSegments, rows = skydomeRows;
@ -199,6 +180,7 @@ GameRenderer::GameRenderer(GameWorld* engine)
ssRectGeom.uploadVertices(sspaceRect);
ssRectDraw.addGeometry(&ssRectGeom);
ssRectDraw.setFaceType(GL_TRIANGLE_STRIP);
ssRectProgram = compileProgram(GameShaders::ScreenSpaceRect::VertexShader,
GameShaders::ScreenSpaceRect::FragmentShader);
@ -233,6 +215,11 @@ GameRenderer::GameRenderer(GameWorld* engine)
cylinderBuffer.setFaceType(GL_TRIANGLES);
}
GameRenderer::~GameRenderer()
{
glDeleteFramebuffers(1, &framebufferName);
}
float mix(uint8_t a, uint8_t b, float num)
{
return a+(b-a)*num;
@ -248,6 +235,8 @@ void GameRenderer::renderWorld(const ViewCamera &camera, float alpha)
// Set the viewport
const glm::ivec2& vp = getRenderer()->getViewport();
glViewport(0, 0, vp.x, vp.y);
glBindFramebuffer(GL_FRAMEBUFFER, framebufferName);
glClear(GL_DEPTH_BUFFER_BIT);
glBindVertexArray( vao );
@ -394,76 +383,7 @@ void GameRenderer::renderWorld(const ViewCamera &camera, float alpha)
}
glDepthMask(GL_TRUE);
// Draw the water.
renderer->useProgram( waterProg );
float blockLQSize = WATER_WORLD_SIZE/WATER_LQ_DATA_SIZE;
float blockHQSize = WATER_WORLD_SIZE/WATER_HQ_DATA_SIZE;
glm::vec2 waterOffset { -WATER_WORLD_SIZE/2.f, -WATER_WORLD_SIZE/2.f };
auto waterTex = engine->gameData.findTexture("water_old");
glActiveTexture(GL_TEXTURE0);
auto camposFlat = glm::vec2(camera.position);
Renderer::DrawParameters wdp;
wdp.start = 0;
wdp.count = waterHQVerts.size();
wdp.texture = waterTex->getName();
renderer->useProgram(waterProg);
renderer->setSceneParameters(sceneParams);
// Draw High detail water
renderer->setUniform(waterProg, "size", blockHQSize);
renderer->setUniform(waterProg, "time", engine->gameTime);
renderer->setUniform(waterProg, "waveParams", glm::vec2(WATER_SCALE, WATER_HEIGHT));
for( int x = 0; x < WATER_HQ_DATA_SIZE; x++ ) {
for( int y = 0; y < WATER_HQ_DATA_SIZE; y++ ) {
auto waterWS = waterOffset + glm::vec2(blockHQSize) * glm::vec2(x, y);
auto cullWS = waterWS + (blockHQSize / 2.f);
// Check that this is the right time to draw the HQ water
if( glm::distance(camposFlat, cullWS) - blockHQSize >= WATER_HQ_DISTANCE ) continue;
int i = (x*WATER_HQ_DATA_SIZE) + y;
int hI = engine->gameData.realWater[i];
if( hI >= NO_WATER_INDEX ) continue;
float h = engine->gameData.waterHeights[hI];
glm::mat4 m;
m = glm::translate(m, glm::vec3(waterWS, h));
renderer->drawArrays(m, &waterHQDraw, wdp);
}
}
wdp.count = waterLQVerts.size();
renderer->setUniform(waterProg, "size", blockLQSize);
renderer->setUniform(waterProg, "waveParams", glm::vec2(0.f));
for( int x = 0; x < WATER_LQ_DATA_SIZE; x++ ) {
for( int y = 0; y < WATER_LQ_DATA_SIZE; y++ ) {
auto waterWS = waterOffset + glm::vec2(blockLQSize) * glm::vec2(x, y);
auto cullWS = waterWS + (blockLQSize / 2.f);
// Check that this is the right time to draw the LQ
if( glm::distance(camposFlat, cullWS) - blockHQSize/4.f < WATER_HQ_DISTANCE ) continue;
if( glm::distance(camposFlat, cullWS) - blockLQSize/2.f > camera.frustum.far ) continue;
int i = (x*WATER_LQ_DATA_SIZE) + y;
int hI = engine->gameData.visibleWater[i];
if( hI >= NO_WATER_INDEX ) continue;
float h = engine->gameData.waterHeights[hI];
glm::mat4 m;
m = glm::translate(m, glm::vec3(waterWS, h));
renderer->drawArrays(m, &waterLQDraw, wdp);
}
}
water.render(this, engine);
glBindVertexArray( vao );
@ -531,6 +451,18 @@ void GameRenderer::renderWorld(const ViewCamera &camera, float alpha)
if( (engine->state.isCinematic || engine->state.currentCutscene ) && splashTexName == 0 ) {
renderLetterbox();
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
renderer->useProgram(postProg);
Renderer::DrawParameters wdp;
wdp.start = 0;
wdp.count = ssRectGeom.getCount();
wdp.texture = fbTextures[0];
renderer->drawArrays(glm::mat4(), &ssRectDraw, wdp);
glUseProgram(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
@ -1205,3 +1137,19 @@ void GameRenderer::renderLetterbox()
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}
void GameRenderer::setViewport(int w, int h)
{
auto& lastViewport = renderer->getViewport();
if( lastViewport.x != w || lastViewport.y != h)
{
renderer->setViewport({w, h});
glBindTexture(GL_TEXTURE_2D, fbTextures[0]);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, w, h, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
glBindTexture(GL_TEXTURE_2D, fbTextures[1]);
glTexImage2D(GL_TEXTURE_2D, 0, GL_R16F, w, h, 0, GL_RED, GL_FLOAT, NULL);
glBindRenderbuffer(GL_RENDERBUFFER, fbRenderBuffers[0]);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, w, h);
}
}

View File

@ -21,26 +21,43 @@ layout(std140) uniform SceneData {
float fogEnd;
};
layout(std140) uniform ObjectData {
mat4 model;
vec4 colour;
float diffusefac;
float ambientfac;
float visibility;
};
uniform float size;
uniform float time;
uniform vec2 waveParams;
uniform sampler2D data;
vec3 waterNormal = vec3(0.0, 0.0, 1.0);
vec3 planeIntercept( vec3 start, vec3 dir, float height )
{
float dist = (height - dot(waterNormal, start)) / dot(dir, waterNormal);
if( dist < 0.0 )
{
return start + dir * dist;
}
else
{
// uh oh
return vec3(0.0);
}
}
void main()
{
mat4 MVP = projection * view;
vec4 vp = model * vec4(position * size, 0.0, 1.0);
vp.z = (1.0+sin(time + (vp.x + vp.y) * waveParams.x)) * waveParams.y;
TexCoords = position * 2.0;
gl_Position = MVP * vp;
TexCoords = position * vec2(0.5,0.5) + vec2(0.5);
mat4 vp = projection * view;
mat4 projector = inverse(vp);
mat3 rot = mat3(view);
vec3 ray = vec3(-position.x, -position.y, projection[0][0] ) * rot;
float plane = texture2D( data, TexCoords ).r;
vec3 ws = planeIntercept( campos.xyz, ray, plane );
ws.z = ws.z + (-1.0+(sin(time + (ws.x + ws.y) * waveParams.x)) * waveParams.y);
TexCoords = ws.xy / 5.0;
gl_Position = vp * vec4(ws, 1.0);
})";
const char* WaterHQ::FragmentShader = R"(
@ -49,11 +66,46 @@ in vec3 Normal;
in vec2 TexCoords;
uniform sampler2D texture;
out vec4 outColour;
in vec3 test;
void main() {
vec4 c = texture2D(texture, TexCoords);
outColour = c;
})";
const char* Mask3D::VertexShader = R"(
#version 130
#extension GL_ARB_explicit_attrib_location : enable
#extension GL_ARB_uniform_buffer_object : enable
layout(location = 0) in vec3 position;
layout(std140) uniform SceneData {
mat4 projection;
mat4 view;
vec4 ambient;
vec4 dynamic;
vec4 fogColor;
vec4 campos;
float fogStart;
float fogEnd;
};
out vec3 pp;
void main()
{
pp = position;
gl_Position = projection * view * vec4(position, 1.0);
})";
const char* Mask3D::FragmentShader = R"(
#version 130
in vec3 pp;
out vec4 outColour;
void main() {
outColour = vec4(pp.z, 0.0, 0.0, 1.0);
})";
const char* Sky::VertexShader = R"(
#version 130
#extension GL_ARB_explicit_attrib_location : enable
@ -260,4 +312,33 @@ void main()
// Set colour to 0, 0, 0, 1 for textured mode.
outColour = vec4(colour.rgb + c.rgb, colour.a);
})";
}
const char* DefaultPostProcess::VertexShader = R"(
#version 130
#extension GL_ARB_explicit_attrib_location : enable
#extension GL_ARB_uniform_buffer_object : enable
layout(location = 0) in vec2 position;
out vec2 TexCoords;
void main()
{
TexCoords = position * vec2(0.5,0.5) + vec2(0.5);
gl_Position = vec4(position, 0.0, 1.0);
})";
const char* DefaultPostProcess::FragmentShader = R"(
#version 130
in vec2 TexCoords;
uniform sampler2D colour;
uniform sampler2D data;
out vec4 outColour;
void main()
{
vec4 c = texture2D(colour, TexCoords);
outColour = c;
})";
}

View File

@ -0,0 +1,159 @@
#include <render/WaterRenderer.hpp>
#include <render/GameRenderer.hpp>
#include <render/GameShaders.hpp>
#include <engine/GameWorld.hpp>
#include <glm/glm.hpp>
WaterRenderer::WaterRenderer(GameRenderer* renderer)
: waterProg(nullptr)
{
maskDraw.setFaceType(GL_TRIANGLES);
gridDraw.setFaceType(GL_TRIANGLES);
waterProg = renderer->getRenderer()->createShader(
GameShaders::WaterHQ::VertexShader, GameShaders::WaterHQ::FragmentShader
);
maskProg = renderer->getRenderer()->createShader(
GameShaders::Mask3D::VertexShader, GameShaders::Mask3D::FragmentShader
);
renderer->getRenderer()->setProgramBlockBinding(waterProg, "SceneData", 1);
renderer->getRenderer()->setProgramBlockBinding(maskProg, "SceneData", 1);
renderer->getRenderer()->setUniformTexture(waterProg, "data", 1);
// Generate grid mesh
int gridres = 60;
std::vector<glm::vec2> grid;
float gridresinv = 1.f / (gridres*0.5f);
glm::vec2 b(-1.f,-1.f);
for( int x = 0; x < gridres; x++ )
{
for( int y = 0; y < gridres; y++ )
{
glm::vec2 tMin( b + glm::vec2(x,y) * gridresinv );
glm::vec2 tMax( b + glm::vec2(x+1,y+1) * gridresinv );
// Build geometry
grid.push_back(glm::vec2(tMax.x, tMax.y));
grid.push_back(glm::vec2(tMax.x, tMin.y));
grid.push_back(glm::vec2(tMin.x, tMin.y));
grid.push_back(glm::vec2(tMin.x, tMin.y));
grid.push_back(glm::vec2(tMin.x, tMax.y));
grid.push_back(glm::vec2(tMax.x, tMax.y));
}
}
gridGeom.uploadVertices(grid.size(), sizeof(glm::vec2)*grid.size(), grid.data());
gridGeom.getDataAttributes().push_back(
{ATRS_Position, 2, 0, 0, GL_FLOAT}
);
gridDraw.addGeometry(&gridGeom);
}
WaterRenderer::~WaterRenderer()
{
}
void WaterRenderer::setWaterTable(float* waterHeights, unsigned int nHeights, uint8_t* tiles, unsigned int nTiles)
{
// Determine the dimensions of the input tiles
int edgeNum = sqrt(nTiles);
float tileSize = WATER_WORLD_SIZE/edgeNum;
glm::vec2 wO { -WATER_WORLD_SIZE/2.f, -WATER_WORLD_SIZE/2.f };
std::vector<glm::vec3> vertexData;
for( int x = 0; x < edgeNum; x++ )
{
int xi = x * WATER_HQ_DATA_SIZE;
for( int y = 0; y < edgeNum; y++ )
{
// Tiles with the magic value contain no water.
if( tiles[xi + y] >= NO_WATER_INDEX ) continue;
float h = waterHeights[tiles[xi + y]];
float hMax = h + WATER_HEIGHT;
glm::vec2 tMin( wO + glm::vec2(x,y) * tileSize );
glm::vec2 tMax( wO + glm::vec2(x+1,y+1) * tileSize );
// Build geometry
vertexData.push_back(glm::vec3(tMax.x, tMax.y, hMax));
vertexData.push_back(glm::vec3(tMax.x, tMin.y, hMax));
vertexData.push_back(glm::vec3(tMin.x, tMin.y, hMax));
vertexData.push_back(glm::vec3(tMin.x, tMin.y, hMax));
vertexData.push_back(glm::vec3(tMin.x, tMax.y, hMax));
vertexData.push_back(glm::vec3(tMax.x, tMax.y, hMax));
}
}
maskGeom.uploadVertices(vertexData.size(), sizeof(glm::vec3)*vertexData.size(), vertexData.data());
maskGeom.getDataAttributes().push_back(
{ATRS_Position, 3, 0, 0, GL_FLOAT}
);
maskDraw.addGeometry(&maskGeom);
}
void WaterRenderer::setDataTexture(GLuint fbBinding, GLuint dataTex)
{
fbOutput = fbBinding;
dataTexture = dataTex;
}
void WaterRenderer::render(GameRenderer* renderer, GameWorld* world)
{
auto r = renderer->getRenderer();
auto waterTex = world->gameData.findTexture("water_old");
Renderer::DrawParameters wdp;
wdp.start = 0;
wdp.count = maskGeom.getCount();
wdp.texture = 0;
glm::mat4 m(1.0);
glEnable(GL_STENCIL_TEST);
glDisable(GL_DEPTH_TEST);
glStencilFunc(GL_ALWAYS, 1, 0xFF);
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
glStencilMask(0xFF);
GLenum buffers[] = {GL_COLOR_ATTACHMENT1};
glDrawBuffers(1, buffers);
glClear(GL_STENCIL_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
r->useProgram( maskProg );
r->drawArrays(m, &maskDraw, wdp);
glStencilFunc(GL_EQUAL, 1, 0xFF);
glStencilMask(0x00);
glEnable(GL_DEPTH_TEST);
r->useProgram( waterProg );
buffers[0] = GL_COLOR_ATTACHMENT0;
glDrawBuffers(1, buffers);
r->setUniform(waterProg, "time", world->gameTime);
r->setUniform(waterProg, "waveParams", glm::vec2(WATER_SCALE, WATER_HEIGHT));
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, dataTexture);
glActiveTexture(GL_TEXTURE0);
wdp.count = gridGeom.getCount();
wdp.texture = waterTex->getName();
r->drawArrays(m, &gridDraw, wdp);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, 0);
glActiveTexture(GL_TEXTURE0);
glDisable(GL_STENCIL_TEST);
}

View File

@ -93,6 +93,8 @@ RWGame::RWGame(const std::string& gamepath, int argc, char* argv[])
}
StateManager::get().enter(new LoadingState(this));
getRenderer()->water.setWaterTable(engine->gameData.waterHeights, 48, engine->gameData.realWater, 128*128);
engine->logger.info("Game", "Started");
}
@ -247,7 +249,7 @@ void RWGame::tick(float dt)
void RWGame::render(float alpha, float time)
{
auto size = getWindow().getSize();
renderer->getRenderer()->setViewport({size.x, size.y});
renderer->setViewport(size.x, size.y);
ViewCamera viewCam;
if( engine->state.currentCutscene != nullptr && engine->state.cutsceneStartTime >= 0.f )