Browse Source

Render+Scene: Implement uniformbuffer objects

master
Riyyi 4 months ago
parent
commit
b5f3cae5ba
  1. 5
      assets/glsl/batch-3d.vert
  2. 5
      assets/glsl/batch-quad.vert
  3. 22
      src/inferno/application.cpp
  4. 12
      src/inferno/asset/shader.cpp
  5. 4
      src/inferno/asset/shader.h
  6. 62
      src/inferno/render/buffer.cpp
  7. 25
      src/inferno/render/buffer.h
  8. 14
      src/inferno/render/renderer.cpp
  9. 4
      src/inferno/render/renderer.h
  10. 224
      src/inferno/render/uniformbuffer.cpp
  11. 90
      src/inferno/render/uniformbuffer.h
  12. 10
      src/inferno/scene/scene.cpp

5
assets/glsl/batch-3d.vert

@ -9,7 +9,10 @@ out vec3 v_normal;
out vec2 v_textureCoordinates;
out flat uint v_textureIndex;
uniform mat4 u_projectionView;
layout(std140, binding = 0) uniform Camera
{
mat4 u_projectionView;
};
void main()
{

5
assets/glsl/batch-quad.vert

@ -9,7 +9,10 @@ out vec4 v_color;
out vec2 v_textureCoordinates;
out flat uint v_textureIndex;
uniform mat4 u_projectionView;
layout(std140, binding = 0) uniform Camera
{
mat4 u_projectionView;
};
void main()
{

22
src/inferno/application.cpp

@ -23,6 +23,7 @@
#include "inferno/render/buffer.h"
#include "inferno/render/context.h"
#include "inferno/render/framebuffer.h"
#include "inferno/render/uniformbuffer.h"
// #include "inferno/render/gltf.h"
#include "inferno/asset/shader.h"
#include "inferno/asset/texture.h"
@ -67,6 +68,13 @@ Application::Application()
.clearBit = GL_COLOR_BUFFER_BIT,
});
Uniformbuffer::the().setLayout(
"Camera", 0,
{
{ BufferElementType::Mat4, "u_projectionView" },
});
Uniformbuffer::the().create("Camera");
m_scene = std::make_shared<Scene>();
m_scene->initialize();
@ -90,6 +98,7 @@ Application::Application()
Application::~Application()
{
m_scene->destroy();
Uniformbuffer::destroy();
RendererFont::destroy();
Renderer2D::destroy();
@ -189,20 +198,9 @@ int Application::run()
render();
std::pair<glm::mat4, glm::mat4> projectionView = m_scene->cameraProjectionView();
RendererCubemap::the().beginScene(projectionView.first, projectionView.second); // camera, lights, environment
Renderer3D::the().beginScene(projectionView.first, projectionView.second); // camera, lights, environment
Renderer2D::the().beginScene(projectionView.first, projectionView.second); // camera, lights, environment
RendererFont::the().beginScene(projectionView.first, projectionView.second); // camera, lights, environment
m_scene->render();
// RendererCharacter::the().drawCharacter(character, f->texture());
RendererCubemap::the().endScene();
Renderer3D::the().endScene();
Renderer2D::the().endScene();
RendererFont::the().endScene();
m_framebuffer->unbind();
// ---------------------------------
@ -214,7 +212,7 @@ int Application::run()
RenderCommand::clearBit(m_screenFramebuffer->clearBit());
Renderer2D::the().setEnableDepthBuffer(false);
Renderer2D::the().beginScene(matIdentity, matIdentity);
Uniformbuffer::the().setFloat("Camera", "u_projectionView", matIdentity);
Renderer2D::the().drawQuad(transformIdentity, vectorOne, m_framebuffer->texture(0));
Renderer2D::the().endScene();
Renderer2D::the().setEnableDepthBuffer(true);

12
src/inferno/asset/shader.cpp

@ -50,16 +50,18 @@ Shader::~Shader()
}
}
int32_t Shader::findUniformLocation(std::string_view name)
// -----------------------------------------
uint32_t Shader::findUniformLocation(std::string_view name)
{
// Cache uniform locations, prevent going to the GPU every call
if (m_uniformLocation.find(name) != m_uniformLocation.end()) {
return m_uniformLocation[name];
if (m_uniformLocations.find(name) != m_uniformLocations.end()) {
return m_uniformLocations[name];
}
int32_t location = glGetUniformLocation(m_id, name.data());
VERIFY(location != -1, "Shader could not find uniform '{}'", name);
m_uniformLocation[name] = location;
m_uniformLocations[name] = static_cast<uint32_t>(location);
return location;
}
@ -128,6 +130,8 @@ void Shader::unbind() const
glUseProgram(0);
}
// -----------------------------------------
uint32_t Shader::compileShader(int32_t type, const char* source) const
{
// Create new shader

4
src/inferno/asset/shader.h

@ -21,7 +21,7 @@ public:
// Factory function
static std::shared_ptr<Shader> create(std::string_view path);
int32_t findUniformLocation(std::string_view name);
uint32_t findUniformLocation(std::string_view name);
void setInt(std::string_view name, int value);
void setInt(std::string_view name, int* values, uint32_t count);
@ -53,7 +53,7 @@ private:
private:
uint32_t m_id { 0 };
std::unordered_map<std::string_view, int32_t> m_uniformLocation;
std::unordered_map<std::string_view, uint32_t> m_uniformLocations;
};
// -----------------------------------------

62
src/inferno/render/buffer.cpp

@ -4,6 +4,12 @@
* SPDX-License-Identifier: MIT
*/
#include <cstddef> // size_t
#include <cstdint> // int32_t, uint8_t, uint32_t
#include <memory> // std::shared_ptr
#include <string>
#include <utility> // std::pair
#include "glad/glad.h"
#include "ruc/meta/assert.h"
@ -61,10 +67,10 @@ uint32_t BufferElement::getTypeSize(const BufferElementType type)
case BufferElementType::Vec3:
case BufferElementType::Vec4:
return sizeof(float) * getTypeCount(type);
case BufferElementType::VecDouble:
case BufferElementType::VecDouble2:
case BufferElementType::VecDouble3:
case BufferElementType::VecDouble4:
case BufferElementType::Double:
case BufferElementType::Vec2Double:
case BufferElementType::Vec3Double:
case BufferElementType::Vec4Double:
return sizeof(double) * getTypeCount(type);
case BufferElementType::Mat2:
case BufferElementType::Mat3:
@ -89,25 +95,25 @@ uint32_t BufferElement::getTypeCount(const BufferElementType type)
case BufferElementType::Int:
case BufferElementType::Uint:
case BufferElementType::Float:
case BufferElementType::VecDouble:
case BufferElementType::Double:
return 1;
case BufferElementType::Bool2:
case BufferElementType::Int2:
case BufferElementType::Uint2:
case BufferElementType::Vec2:
case BufferElementType::VecDouble2:
case BufferElementType::Vec2Double:
return 2;
case BufferElementType::Bool3:
case BufferElementType::Int3:
case BufferElementType::Uint3:
case BufferElementType::Vec3:
case BufferElementType::VecDouble3:
case BufferElementType::Vec3Double:
return 3;
case BufferElementType::Bool4:
case BufferElementType::Int4:
case BufferElementType::Uint4:
case BufferElementType::Vec4:
case BufferElementType::VecDouble4:
case BufferElementType::Vec4Double:
return 4;
case BufferElementType::Mat2:
return 2 * 2;
@ -152,10 +158,10 @@ uint32_t BufferElement::getTypeGL(const BufferElementType type)
case BufferElementType::Vec3:
case BufferElementType::Vec4:
return GL_FLOAT;
case BufferElementType::VecDouble:
case BufferElementType::VecDouble2:
case BufferElementType::VecDouble3:
case BufferElementType::VecDouble4:
case BufferElementType::Double:
case BufferElementType::Vec2Double:
case BufferElementType::Vec3Double:
case BufferElementType::Vec4Double:
return GL_DOUBLE;
case BufferElementType::Mat2:
case BufferElementType::Mat3:
@ -184,7 +190,7 @@ void BufferLayout::calculateOffsetsAndStride()
m_stride = 0;
for (auto& element : m_elements) {
element.setOffset(m_stride);
m_stride += element.getSize();
m_stride += element.size();
}
}
@ -202,7 +208,7 @@ VertexBuffer::VertexBuffer(size_t size, float* vertices)
bind();
// Upload data to the GPU
glBufferData(GL_ARRAY_BUFFER, size, vertices, GL_STATIC_DRAW);
glBufferData(GL_ARRAY_BUFFER, size, vertices, GL_DYNAMIC_DRAW);
unbind();
}
@ -242,7 +248,7 @@ IndexBuffer::IndexBuffer(uint32_t* indices, size_t size)
bind();
// Upload data to the GPU
glBufferData(GL_ELEMENT_ARRAY_BUFFER, size, indices, GL_STATIC_DRAW);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, size, indices, GL_DYNAMIC_DRAW);
unbind();
}
@ -298,7 +304,7 @@ void VertexArray::unbind() const
void VertexArray::addVertexBuffer(std::shared_ptr<VertexBuffer> vertexBuffer)
{
const auto& layout = vertexBuffer->getLayout();
VERIFY(layout.getElements().size(), "VertexBuffer has no layout");
VERIFY(layout.elements().size(), "VertexBuffer has no layout");
bind();
vertexBuffer->bind();
@ -306,7 +312,7 @@ void VertexArray::addVertexBuffer(std::shared_ptr<VertexBuffer> vertexBuffer)
uint32_t index = 0;
for (const auto& element : layout) {
glEnableVertexAttribArray(index);
switch (element.getType()) {
switch (element.type()) {
case BufferElementType::None:
break;
case BufferElementType::Int:
@ -321,8 +327,8 @@ void VertexArray::addVertexBuffer(std::shared_ptr<VertexBuffer> vertexBuffer)
index,
element.getTypeCount(),
element.getTypeGL(),
layout.getStride(),
reinterpret_cast<const void*>(element.getOffset()));
layout.stride(),
reinterpret_cast<const void*>(element.offset()));
break;
}
case BufferElementType::Bool:
@ -340,15 +346,15 @@ void VertexArray::addVertexBuffer(std::shared_ptr<VertexBuffer> vertexBuffer)
index,
element.getTypeCount(),
element.getTypeGL(),
element.getNormalized() ? GL_TRUE : GL_FALSE,
layout.getStride(),
reinterpret_cast<const void*>(element.getOffset()));
element.normalized() ? GL_TRUE : GL_FALSE,
layout.stride(),
reinterpret_cast<const void*>(element.offset()));
break;
}
case BufferElementType::VecDouble:
case BufferElementType::VecDouble2:
case BufferElementType::VecDouble3:
case BufferElementType::VecDouble4:
case BufferElementType::Double:
case BufferElementType::Vec2Double:
case BufferElementType::Vec3Double:
case BufferElementType::Vec4Double:
case BufferElementType::MatDouble2:
case BufferElementType::MatDouble3:
case BufferElementType::MatDouble4: {
@ -356,8 +362,8 @@ void VertexArray::addVertexBuffer(std::shared_ptr<VertexBuffer> vertexBuffer)
index,
element.getTypeCount(),
element.getTypeGL(),
layout.getStride(),
reinterpret_cast<const void*>(element.getOffset()));
layout.stride(),
reinterpret_cast<const void*>(element.offset()));
break;
}
default:

25
src/inferno/render/buffer.h

@ -7,10 +7,11 @@
#pragma once
#include <cstddef> // size_t
#include <cstdint> // int32_t, uint32_t
#include <cstdint> // int32_t, uint8_t, uint32_t
#include <memory> // std::shared_ptr
#include <string> // std::string
#include <vector> // std::vector
#include <string>
#include <utility> // std::pair
#include <vector>
namespace Inferno {
@ -22,7 +23,7 @@ enum class BufferElementType {
Int, Int2, Int3, Int4, // ivec
Uint, Uint2, Uint3, Uint4, // uvec
Float, Vec2, Vec3, Vec4, // vec
VecDouble, VecDouble2, VecDouble3, VecDouble4, // dvec
Double, Vec2Double, Vec3Double, Vec4Double, // dvec
Mat2, Mat3, Mat4, // mat
MatDouble2, MatDouble3, MatDouble4, // dmat
};
@ -43,11 +44,11 @@ public:
static uint32_t getTypeCount(const BufferElementType type);
static uint32_t getTypeGL(const BufferElementType type);
BufferElementType getType() const { return m_type; }
std::string getName() const { return m_name; }
uint32_t getSize() const { return m_size; }
uint32_t getOffset() const { return m_offset; }
bool getNormalized() const { return m_normalized; }
BufferElementType type() const { return m_type; }
std::string name() const { return m_name; }
uint32_t size() const { return m_size; }
uint32_t offset() const { return m_offset; }
bool normalized() const { return m_normalized; }
void setType(const BufferElementType& type) { m_type = type; }
void setName(const std::string& name) { m_name = name; }
@ -72,8 +73,8 @@ public:
BufferLayout(const std::initializer_list<BufferElement>& elements);
~BufferLayout() = default;
const std::vector<BufferElement>& getElements() const { return m_elements; }
uint32_t getStride() const { return m_stride; }
const std::vector<BufferElement>& elements() const { return m_elements; }
uint32_t stride() const { return m_stride; }
// Iterators
std::vector<BufferElement>::iterator begin() { return m_elements.begin(); }
@ -105,7 +106,7 @@ public:
const BufferLayout& getLayout() const { return m_layout; }
inline void setLayout(const BufferLayout& layout) { m_layout = layout; }
void setLayout(const BufferLayout& layout) { m_layout = layout; }
private:
uint32_t m_id { 0 };

14
src/inferno/render/renderer.cpp

@ -228,13 +228,6 @@ Renderer2D::Renderer2D(s)
ruc::info("Renderer2D initialized");
}
void Renderer2D::beginScene(glm::mat4 cameraProjection, glm::mat4 cameraView)
{
m_shader->bind();
m_shader->setFloat("u_projectionView", cameraProjection * cameraView);
m_shader->unbind();
}
void Renderer2D::drawQuad(const TransformComponent& transform, glm::vec4 color)
{
drawQuad(transform, color, nullptr);
@ -536,13 +529,6 @@ void Renderer3D::drawModel(std::span<const Vertex> vertices, std::span<const uin
m_elementIndex += elements.size();
}
void Renderer3D::beginScene(glm::mat4 cameraProjection, glm::mat4 cameraView)
{
m_shader->bind();
m_shader->setFloat("u_projectionView", cameraProjection * cameraView);
m_shader->unbind();
}
void Renderer3D::createElementBuffer()
{
// ---------------------------------

4
src/inferno/render/renderer.h

@ -132,8 +132,6 @@ public:
using Singleton<Renderer2D>::destroy;
virtual void beginScene(glm::mat4 cameraProjection, glm::mat4 cameraView) override;
void drawQuad(const TransformComponent& transform, glm::vec4 color);
void drawQuad(const TransformComponent& transform, glm::mat4 color);
void drawQuad(const TransformComponent& transform, glm::vec4 color, std::shared_ptr<Texture> texture);
@ -199,8 +197,6 @@ public:
using Singleton<Renderer3D>::destroy;
virtual void beginScene(glm::mat4 cameraProjection, glm::mat4 cameraView) override;
void drawModel(std::span<const Vertex> vertices, std::span<const uint32_t> indices, const TransformComponent& transform, std::shared_ptr<Texture> texture);
private:

224
src/inferno/render/uniformbuffer.cpp

@ -0,0 +1,224 @@
/*
* Copyright (C) 2024 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#include <cstddef> // size_t
#include <cstdint> // int32_t, uint8_t
#include <string_view>
#include "glad/glad.h"
#include "glm/gtc/type_ptr.hpp" // glm::value_ptr
#include "ruc/meta/assert.h"
#include "inferno/render/buffer.h"
#include "inferno/render/uniformbuffer.h"
namespace Inferno {
Uniformbuffer::Uniformbuffer(s)
{
// Get maximum uniformbuffer bindings the GPU supports
int32_t maxBindingPoints = 0;
glGetIntegerv(GL_MAX_UNIFORM_BUFFER_BINDINGS, &maxBindingPoints);
m_maxBindingPoints = static_cast<uint8_t>(maxBindingPoints);
}
Uniformbuffer::~Uniformbuffer()
{
for (const auto& [_, block] : m_blocks) {
glDeleteBuffers(1, &block.id);
}
m_blocks.clear();
}
// -----------------------------------------
void Uniformbuffer::setLayout(std::string_view blockName, uint8_t bindingPoint, const BufferLayout& layout)
{
VERIFY(bindingPoint < m_maxBindingPoints, "{} < {}", bindingPoint, m_maxBindingPoints);
if (!exists(blockName)) {
m_blocks[blockName] = {};
}
UniformbufferBlock& block = m_blocks[blockName];
// Example block layout:
// - mat3
// - float
// - vec2
// - vec2
// - float
// Chunks, 4 slots, 4 bytes per slot
// [x][x][x][ ] #1
// [x][x][x][ ] #2
// [x][x][x][ ] #3
// [x][ ][x][x] #4
// [x][x][x][ ] #5
size_t chunk = 0;
uint8_t offset = 0;
for (auto it = layout.begin(); it != layout.end(); ++it) {
BufferElementType type = it->type();
const std::string& name = it->name();
// Calculate offset
switch (type) {
// Scalar 1
case BufferElementType::Bool:
case BufferElementType::Int:
case BufferElementType::Uint:
case BufferElementType::Float: {
// Offset
block.uniformLocations[name] = (chunk * 16) + (offset * 4);
// Jump
offset += 1;
break;
}
// Scalar 2
case BufferElementType::Bool2:
case BufferElementType::Int2:
case BufferElementType::Uint2:
case BufferElementType::Vec2: {
// Add padding
if (offset == 1) {
offset++;
}
if (offset == 3) {
offset = 0;
chunk++;
}
// Offset
block.uniformLocations[name] = (chunk * 16) + (offset * 4);
// Jump
offset += 2;
break;
}
// Scalar 3
case BufferElementType::Bool3:
case BufferElementType::Int3:
case BufferElementType::Uint3:
case BufferElementType::Vec3: {
// Add padding
if (offset != 0) {
offset = 0;
chunk++;
}
// Offset
block.uniformLocations[name] = (chunk * 16) + (offset * 4);
// Jump
offset += 3;
break;
}
// Scalar 4
case BufferElementType::Bool4:
case BufferElementType::Int4:
case BufferElementType::Uint4:
case BufferElementType::Vec4: {
// Add padding
if (offset != 0) {
offset = 0;
chunk++;
}
// Offset
block.uniformLocations[name] = (chunk * 16) + (offset * 4);
// Jump
offset += 4;
break;
}
// Array types
case BufferElementType::Mat2:
case BufferElementType::Mat3:
case BufferElementType::Mat4: {
// Add padding
if (offset != 0) {
offset = 0;
chunk++;
}
// Offset
block.uniformLocations[name] = (chunk * 16) + (offset * 4);
// Additional rows
if (type == BufferElementType::Mat2) {
chunk += 1;
}
else if (type == BufferElementType::Mat3) {
chunk += 2;
}
else {
chunk += 3;
}
// Jump
offset += 4;
break;
}
// TODO: Implement these types
case BufferElementType::Double:
case BufferElementType::Vec2Double:
case BufferElementType::Vec3Double:
case BufferElementType::Vec4Double:
case BufferElementType::MatDouble2:
case BufferElementType::MatDouble3:
case BufferElementType::MatDouble4:
VERIFY_NOT_REACHED();
case BufferElementType::None:
VERIFY_NOT_REACHED();
};
// Overflow slots to next chunk
if (offset > 3) {
offset = 0;
chunk++;
}
}
// Pad the end of the buffer
if (offset != 0) {
offset = 0;
chunk++;
}
block.size = chunk * 16;
}
void Uniformbuffer::create(std::string_view blockName)
{
VERIFY(exists(blockName), "uniformbuffer block doesnt exist");
UniformbufferBlock& block = m_blocks[blockName];
if (block.id != 0) {
glDeleteBuffers(1, &block.id);
}
// Allocate buffer
block.id = UINT_MAX;
glGenBuffers(1, &block.id);
glBindBuffer(GL_UNIFORM_BUFFER, block.id);
glBufferData(GL_UNIFORM_BUFFER, block.size, NULL, GL_DYNAMIC_DRAW);
glBindBuffer(GL_UNIFORM_BUFFER, 0);
// Bind buffer to binding point
glBindBufferBase(GL_UNIFORM_BUFFER, block.bindingPoint, block.id);
}
void Uniformbuffer::setFloat(std::string_view blockName, std::string_view member, glm::mat4 matrix)
{
VERIFY(exists(blockName), "uniformbuffer block doesnt exist");
const UniformbufferBlock& block = m_blocks[blockName];
VERIFY(block.uniformLocations.find(member.data()) != block.uniformLocations.end(),
"uniformbuffer block member doesnt exist");
// Note: Uniformbuffers are bound to a binding point for the entire pipeline,
// it remains accessible without needing to rebind it every time
glBufferSubData(GL_UNIFORM_BUFFER, block.uniformLocations.at(member.data()), sizeof(glm::mat4), glm::value_ptr(matrix));
}
} // namespace Inferno

90
src/inferno/render/uniformbuffer.h

@ -0,0 +1,90 @@
/*
* Copyright (C) 2024 Riyyi
*
* SPDX-License-Identifier: MIT
*/
#include <cstdint> // uint8_t, uint32_t
#include <string>
#include <string_view>
#include <unordered_map>
#include "glm/ext/matrix_float4x4.hpp" // glm::mat4
#include "ruc/singleton.h"
#include "inferno/render/buffer.h"
namespace Inferno {
struct UniformbufferBlock {
uint32_t id { 0 };
uint32_t size { 0 };
uint8_t bindingPoint { 0 };
std::unordered_map<std::string, uint32_t> uniformLocations {};
};
class Uniformbuffer final : public ruc::Singleton<Uniformbuffer> { // Uniform Buffer Object, UBO
public:
Uniformbuffer(s);
~Uniformbuffer();
void setLayout(std::string_view blockName, uint8_t bindingPoint, const BufferLayout& layout);
void create(std::string_view blockName);
void setFloat(std::string_view blockName, std::string_view member, glm::mat4 matrix);
bool exists(std::string_view blockName) const { return m_blocks.find(blockName) != m_blocks.end(); }
private:
uint8_t m_maxBindingPoints { 0 };
std::unordered_map<std::string_view, UniformbufferBlock> m_blocks;
};
} // namespace Inferno
#if 0
// -----------------------------------------
// Example usage:
Uniformbuffer::the().setLayout(
"ExampleBlock", 0,
{
{ BufferElementType::Mat3, "a" },
{ BufferElementType::Float, "b" },
{ BufferElementType::Vec2, "c" },
{ BufferElementType::Vec2, "d" },
{ BufferElementType::Float, "e" },
});
Uniformbuffer::the().create("ExampleBlock");
#endif
// -----------------------------------------
// Memory alignment of uniform blocks using std140
//
// Main points:
// - Memory is organized into chunks.
// - A block is at least the size of 1 chunk.
// - One chunk has 4 slots, 4 bytes per slot.
// - Can't fit? Move to next chunk.
//
// The rules:
// 1. Scalar (bool, int, uint, float) takes up 1 slot, can appear after anything
// 2. Vec2 takes up 2 slots, in first or last half of a chunk
// 3. Vec3 takes up 3 slots, only at the start of a chunk
// 4. Everything else:
// - Take up maxumum room
// - As often as needed
// - Add padding as needed
// 5. Mat3 (or any matrix) are treated like arrays
// 6. Each member of *any* array gets its own chunk
// TODO: How do double types work?
// -----------------------------------------
// References:
// - https://learnopengl.com/Advanced-OpenGL/Advanced-GLSL
// - https://www.khronos.org/opengl/wiki/Interface_Block_(GLSL)#Memory_layout
// - https://www.oreilly.com/library/view/opengl-programming-guide/9780132748445/app09lev1sec2.html (The std140 Layout Rules)
// - https://www.youtube.com/watch?v=JPvbRko9lBg (WebGL 2: Uniform Buffer Objects)

10
src/inferno/scene/scene.cpp

@ -25,6 +25,7 @@
#include "inferno/component/tagcomponent.h"
#include "inferno/component/textareacomponent.h"
#include "inferno/component/transformcomponent.h"
#include "inferno/render/uniformbuffer.h"
#include "inferno/scene/scene.h"
#include "inferno/script/nativescript.h"
#include "inferno/system/camerasystem.h"
@ -83,8 +84,17 @@ void Scene::update(float deltaTime)
void Scene::render()
{
auto [projection, view] = CameraSystem::the().projectionView();
Uniformbuffer::the().setFloat("Camera", "u_projectionView", projection * view);
RendererCubemap::the().beginScene(projection, view); // camera, lights, environment
RenderSystem::the().render();
TextAreaSystem::the().render();
RendererCubemap::the().endScene();
Renderer3D::the().endScene();
Renderer2D::the().endScene();
RendererFont::the().endScene();
}
void Scene::destroy()

Loading…
Cancel
Save