We got a skybox
This commit is contained in:
@@ -25,8 +25,10 @@ set(SRC_FILES
|
|||||||
|
|
||||||
"src/Graphics/Resources/GPUImage.cpp"
|
"src/Graphics/Resources/GPUImage.cpp"
|
||||||
"src/Graphics/Resources/NBuffer.cpp"
|
"src/Graphics/Resources/NBuffer.cpp"
|
||||||
|
"src/Graphics/Resources/Cubemap.cpp"
|
||||||
|
|
||||||
"src/Graphics/Pipelines/MeshPipeline.cpp"
|
"src/Graphics/Pipelines/MeshPipeline.cpp"
|
||||||
|
"src/Graphics/Pipelines/SkyboxPipeline.cpp"
|
||||||
|
|
||||||
"src/Input/InputManager.cpp"
|
"src/Input/InputManager.cpp"
|
||||||
|
|
||||||
|
|||||||
38
destrum/assets_src/shaders/cubemap.frag
Normal file
38
destrum/assets_src/shaders/cubemap.frag
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
#version 450
|
||||||
|
#extension GL_GOOGLE_include_directive : require
|
||||||
|
#include "bindless.glsl"
|
||||||
|
|
||||||
|
|
||||||
|
layout(location = 0) in vec3 localPos;
|
||||||
|
|
||||||
|
// Input equirectangular HDR texture
|
||||||
|
|
||||||
|
layout(location = 0) out vec4 outColor;
|
||||||
|
|
||||||
|
const float PI = 3.14159265359;
|
||||||
|
|
||||||
|
layout(push_constant) uniform PushConstants {
|
||||||
|
mat4 view;
|
||||||
|
mat4 proj;
|
||||||
|
uint skyboxId;
|
||||||
|
} pcs;
|
||||||
|
|
||||||
|
vec2 sampleSphericalMap(vec3 v) {
|
||||||
|
// Convert direction to spherical coordinates
|
||||||
|
vec2 uv = vec2(atan(v.z, v.x), asin(v.y));
|
||||||
|
uv /= vec2(2.0 * PI, PI);
|
||||||
|
uv += 0.5;
|
||||||
|
return uv;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// Normalize direction vector
|
||||||
|
vec3 dir = normalize(localPos);
|
||||||
|
|
||||||
|
// Sample from equirectangular texture
|
||||||
|
vec2 uv = sampleSphericalMap(dir);
|
||||||
|
vec4 color = sampleTexture2DNearest(pcs.skyboxId, uv);
|
||||||
|
|
||||||
|
|
||||||
|
outColor = color;
|
||||||
|
}
|
||||||
42
destrum/assets_src/shaders/cubemap.vert
Normal file
42
destrum/assets_src/shaders/cubemap.vert
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
#version 450
|
||||||
|
|
||||||
|
// Hardcoded cube vertices (36 vertices for 12 triangles)
|
||||||
|
const vec3 positions[36] = vec3[](
|
||||||
|
// Front face
|
||||||
|
vec3(-1.0, -1.0, 1.0), vec3( 1.0, -1.0, 1.0), vec3( 1.0, 1.0, 1.0),
|
||||||
|
vec3( 1.0, 1.0, 1.0), vec3(-1.0, 1.0, 1.0), vec3(-1.0, -1.0, 1.0),
|
||||||
|
// Back face
|
||||||
|
vec3(-1.0, -1.0, -1.0), vec3(-1.0, 1.0, -1.0), vec3( 1.0, 1.0, -1.0),
|
||||||
|
vec3( 1.0, 1.0, -1.0), vec3( 1.0, -1.0, -1.0), vec3(-1.0, -1.0, -1.0),
|
||||||
|
// Top face
|
||||||
|
vec3(-1.0, 1.0, -1.0), vec3(-1.0, 1.0, 1.0), vec3( 1.0, 1.0, 1.0),
|
||||||
|
vec3( 1.0, 1.0, 1.0), vec3( 1.0, 1.0, -1.0), vec3(-1.0, 1.0, -1.0),
|
||||||
|
// Bottom face
|
||||||
|
vec3(-1.0, -1.0, -1.0), vec3( 1.0, -1.0, -1.0), vec3( 1.0, -1.0, 1.0),
|
||||||
|
vec3( 1.0, -1.0, 1.0), vec3(-1.0, -1.0, 1.0), vec3(-1.0, -1.0, -1.0),
|
||||||
|
// Right face
|
||||||
|
vec3( 1.0, -1.0, -1.0), vec3( 1.0, 1.0, -1.0), vec3( 1.0, 1.0, 1.0),
|
||||||
|
vec3( 1.0, 1.0, 1.0), vec3( 1.0, -1.0, 1.0), vec3( 1.0, -1.0, -1.0),
|
||||||
|
// Left face
|
||||||
|
vec3(-1.0, -1.0, -1.0), vec3(-1.0, -1.0, 1.0), vec3(-1.0, 1.0, 1.0),
|
||||||
|
vec3(-1.0, 1.0, 1.0), vec3(-1.0, 1.0, -1.0), vec3(-1.0, -1.0, -1.0)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Hardcoded UVs aren't needed for cube map rendering
|
||||||
|
layout(location = 0) out vec3 localPos;
|
||||||
|
|
||||||
|
// Push constants for view and projection matrices
|
||||||
|
layout(push_constant) uniform PushConstants {
|
||||||
|
mat4 view;
|
||||||
|
mat4 proj;
|
||||||
|
uint skyboxId;
|
||||||
|
} pc;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// Get hardcoded vertex position
|
||||||
|
vec3 pos = positions[gl_VertexIndex];
|
||||||
|
localPos = pos; // Pass to fragment shader
|
||||||
|
|
||||||
|
// Apply view and projection matrices
|
||||||
|
gl_Position = pc.proj * pc.view * vec4(pos, 1.0);
|
||||||
|
}
|
||||||
10
destrum/assets_src/shaders/fullscreen_triangle.vert
Normal file
10
destrum/assets_src/shaders/fullscreen_triangle.vert
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#version 460
|
||||||
|
|
||||||
|
layout (location = 0) out vec2 outUV;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
outUV = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2);
|
||||||
|
// gl_Position = vec4(outUV * 2.0f + -1.0f, 0.0f, 1.0f);
|
||||||
|
gl_Position = vec4(outUV * 2.0 - 1.0, 1.0, 1.0);
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@ void main()
|
|||||||
{
|
{
|
||||||
MaterialData material = pcs.sceneData.materials.data[pcs.materialID];
|
MaterialData material = pcs.sceneData.materials.data[pcs.materialID];
|
||||||
|
|
||||||
vec4 diffuse = sampleTexture2DLinear(material.diffuseTex, inUV);
|
vec4 diffuse = sampleTexture2DLinear(material.diffuseTex, inUV) * material.baseColor;
|
||||||
outFragColor = diffuse;
|
outFragColor = diffuse;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
35
destrum/assets_src/shaders/skybox.frag
Normal file
35
destrum/assets_src/shaders/skybox.frag
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
#version 450
|
||||||
|
#extension GL_GOOGLE_include_directive : require
|
||||||
|
#extension GL_EXT_nonuniform_qualifier : enable
|
||||||
|
|
||||||
|
#include "bindless.glsl"
|
||||||
|
|
||||||
|
layout(location = 0) in vec2 uv; // from fullscreen triangle: 0..2 range
|
||||||
|
layout(location = 0) out vec4 outColor;
|
||||||
|
|
||||||
|
layout(push_constant) uniform SkyboxPC {
|
||||||
|
mat4 invViewProj; // inverse(Proj * View) (your current setup)
|
||||||
|
vec4 cameraPos; // xyz = camera world position
|
||||||
|
uint skyboxTextureId; // index into textureCubes[]
|
||||||
|
} pcs;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
// Fullscreen-triangle trick gives uv in [0..2]. Convert to [0..1].
|
||||||
|
vec2 uv01 = uv * 0.5;
|
||||||
|
|
||||||
|
// Build an NDC point on the far plane.
|
||||||
|
// Vulkan NDC is x,y in [-1..1], z in [0..1]. Using z=1 means "far".
|
||||||
|
vec4 ndc = vec4(uv01 * 2.0 - 1.0, 1.0, 1.0);
|
||||||
|
|
||||||
|
// Unproject to world space
|
||||||
|
vec4 world = pcs.invViewProj * ndc;
|
||||||
|
vec3 worldPos = world.xyz / world.w;
|
||||||
|
|
||||||
|
// Direction from camera through this pixel
|
||||||
|
vec3 dir = normalize(worldPos - pcs.cameraPos.xyz);
|
||||||
|
|
||||||
|
// Sample cubemap directly
|
||||||
|
outColor = sampleTextureCubeLinear(pcs.skyboxTextureId, dir);
|
||||||
|
// outColor = sampleTextureCubeNearest(pcs.skyboxTextureId, dir);
|
||||||
|
}
|
||||||
BIN
destrum/assets_src/textures/skybox.jpg
Normal file
BIN
destrum/assets_src/textures/skybox.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 281 KiB |
BIN
destrum/assets_src/textures/test-skybox.png
Normal file
BIN
destrum/assets_src/textures/test-skybox.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
@@ -21,6 +21,7 @@ public:
|
|||||||
void Randomize(uint32_t seed);
|
void Randomize(uint32_t seed);
|
||||||
|
|
||||||
void Update() override;
|
void Update() override;
|
||||||
|
void Start() override;
|
||||||
|
|
||||||
// optional setters
|
// optional setters
|
||||||
void SetRadius(float r) { m_Radius = r; }
|
void SetRadius(float r) { m_Radius = r; }
|
||||||
@@ -37,12 +38,18 @@ private:
|
|||||||
float m_OrbitSpeed = 1.0f; // rad/sec
|
float m_OrbitSpeed = 1.0f; // rad/sec
|
||||||
float m_OrbitAngle = 0.0f; // current angle
|
float m_OrbitAngle = 0.0f; // current angle
|
||||||
float m_OrbitPhase = 0.0f; // starting offset
|
float m_OrbitPhase = 0.0f; // starting offset
|
||||||
|
float m_GrowPhase = 0.0f;
|
||||||
glm::vec3 m_U{1,0,0}; // orbit basis axis 1
|
glm::vec3 m_U{1,0,0}; // orbit basis axis 1
|
||||||
glm::vec3 m_V{0,0,1}; // orbit basis axis 2
|
glm::vec3 m_V{0,0,1}; // orbit basis axis 2
|
||||||
|
|
||||||
|
glm::vec3 m_BaseScale{1.0f};
|
||||||
|
float m_GrowSpeed = 1.0f; // rad/sec
|
||||||
|
|
||||||
// self spin
|
// self spin
|
||||||
glm::vec3 m_SpinAxis{0,1,0};
|
glm::vec3 m_SpinAxis{0,1,0};
|
||||||
float m_SpinSpeed = 2.0f; // rad/sec
|
float m_SpinSpeed = 2.0f; // rad/sec
|
||||||
|
|
||||||
|
MaterialID m_MaterialID{0};
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif //ORBITANDSPIN_H
|
#endif //ORBITANDSPIN_H
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ public:
|
|||||||
|
|
||||||
VkCommandBuffer beginFrame();
|
VkCommandBuffer beginFrame();
|
||||||
[[nodiscard]] bool needsSwapchainRecreate() const { return swapchain.isDirty(); }
|
[[nodiscard]] bool needsSwapchainRecreate() const { return swapchain.isDirty(); }
|
||||||
|
VulkanImmediateExecutor& GetImmediateExecuter();
|
||||||
|
|
||||||
|
|
||||||
struct EndFrameProps {
|
struct EndFrameProps {
|
||||||
@@ -126,6 +127,19 @@ private:
|
|||||||
|
|
||||||
ImageCache imageCache;
|
ImageCache imageCache;
|
||||||
|
|
||||||
|
static uint32_t BytesPerTexel(VkFormat fmt) {
|
||||||
|
switch (fmt) {
|
||||||
|
case VK_FORMAT_R8_UNORM: return 1;
|
||||||
|
case VK_FORMAT_R8G8B8A8_UNORM: return 4;
|
||||||
|
case VK_FORMAT_B8G8R8A8_SRGB: return 4;
|
||||||
|
case VK_FORMAT_R16G16B16A16_SFLOAT: return 8;
|
||||||
|
case VK_FORMAT_R32G32B32A32_SFLOAT: return 16;
|
||||||
|
case VK_FORMAT_R8G8B8A8_SRGB: return 4;
|
||||||
|
default:
|
||||||
|
throw std::runtime_error("BytesPerTexel: unsupported format");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,16 @@ public:
|
|||||||
|
|
||||||
void setErrorImageId(ImageID id) { errorImageId = id; }
|
void setErrorImageId(ImageID id) { errorImageId = id; }
|
||||||
|
|
||||||
|
static uint32_t BytesPerTexel(VkFormat fmt) {
|
||||||
|
switch (fmt) {
|
||||||
|
case VK_FORMAT_R32G32B32A32_SFLOAT: return 16;
|
||||||
|
case VK_FORMAT_R16G16B16A16_SFLOAT: return 8;
|
||||||
|
case VK_FORMAT_R8G8B8A8_UNORM: return 4;
|
||||||
|
// add formats you use
|
||||||
|
default: throw std::runtime_error("BytesPerTexel: unsupported format");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::vector<GPUImage> images;
|
std::vector<GPUImage> images;
|
||||||
GfxDevice& gfxDevice;
|
GfxDevice& gfxDevice;
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ public:
|
|||||||
const GPUBuffer& getMaterialDataBuffer() const { return materialDataBuffer; }
|
const GPUBuffer& getMaterialDataBuffer() const { return materialDataBuffer; }
|
||||||
VkDeviceAddress getMaterialDataBufferAddress() const { return materialDataBuffer.address; }
|
VkDeviceAddress getMaterialDataBufferAddress() const { return materialDataBuffer.address; }
|
||||||
|
|
||||||
|
|
||||||
|
Material& getMaterialMutable(MaterialID id);
|
||||||
|
void updateMaterialGPU(GfxDevice& gfxDevice, MaterialID id);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::vector<Material> materials;
|
std::vector<Material> materials;
|
||||||
|
|
||||||
|
|||||||
43
destrum/include/destrum/Graphics/Pipelines/SkyboxPipeline.h
Normal file
43
destrum/include/destrum/Graphics/Pipelines/SkyboxPipeline.h
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
#ifndef SKYBOXPIPELINE_H
|
||||||
|
#define SKYBOXPIPELINE_H
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <vulkan/vulkan.h>
|
||||||
|
#include <glm/mat4x4.hpp>
|
||||||
|
#include <glm/vec4.hpp>
|
||||||
|
#include <destrum/Graphics/ids.h>
|
||||||
|
#include <destrum/Graphics/Pipeline.h>
|
||||||
|
#include <destrum/Graphics/Camera.h>
|
||||||
|
|
||||||
|
class SkyboxPipeline final {
|
||||||
|
public:
|
||||||
|
|
||||||
|
SkyboxPipeline();
|
||||||
|
~SkyboxPipeline();
|
||||||
|
|
||||||
|
void init(
|
||||||
|
GfxDevice& gfxDevice,
|
||||||
|
VkFormat drawImageFormat,
|
||||||
|
VkFormat depthImageFormat
|
||||||
|
);
|
||||||
|
void cleanup(VkDevice device);
|
||||||
|
|
||||||
|
void draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera& camera);
|
||||||
|
|
||||||
|
void setSkyboxImage(const ImageID skyboxId);
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
VkPipelineLayout pipelineLayout;
|
||||||
|
std::unique_ptr<Pipeline> pipeline;
|
||||||
|
ImageID skyboxTextureId{NULL_IMAGE_ID};
|
||||||
|
|
||||||
|
|
||||||
|
struct SkyboxPushConstants {
|
||||||
|
glm::mat4 invViewProj;
|
||||||
|
glm::vec4 cameraPos;
|
||||||
|
std::uint32_t skyboxTextureId;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif //SKYBOXPIPELINE_H
|
||||||
@@ -13,6 +13,8 @@
|
|||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
|
#include "Pipelines/SkyboxPipeline.h"
|
||||||
|
|
||||||
class GameRenderer {
|
class GameRenderer {
|
||||||
public:
|
public:
|
||||||
struct SceneData {
|
struct SceneData {
|
||||||
@@ -39,6 +41,12 @@ public:
|
|||||||
createDrawImage(gfxDevice, newSize, false);
|
createDrawImage(gfxDevice, newSize, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Material& getMaterialMutable(MaterialID id);
|
||||||
|
void updateMaterialGPU(MaterialID id);
|
||||||
|
|
||||||
|
void setSkyboxTexture(ImageID skyboxImageId);
|
||||||
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void createDrawImage(GfxDevice& gfxDevice, const glm::ivec2& drawImageSize, bool firstCreate);
|
void createDrawImage(GfxDevice& gfxDevice, const glm::ivec2& drawImageSize, bool firstCreate);
|
||||||
|
|
||||||
@@ -79,6 +87,7 @@ private:
|
|||||||
|
|
||||||
|
|
||||||
std::unique_ptr<MeshPipeline> meshPipeline;
|
std::unique_ptr<MeshPipeline> meshPipeline;
|
||||||
|
std::unique_ptr<SkyboxPipeline> skyboxPipeline;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif //RENDERER_H
|
#endif //RENDERER_H
|
||||||
|
|||||||
66
destrum/include/destrum/Graphics/Resources/Cubemap.h
Normal file
66
destrum/include/destrum/Graphics/Resources/Cubemap.h
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
#ifndef CUBEMAP_H
|
||||||
|
#define CUBEMAP_H
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
#include <glm/ext/matrix_transform.hpp>
|
||||||
|
#include <vulkan/vulkan.h>
|
||||||
|
|
||||||
|
#include <destrum/Graphics/ids.h>
|
||||||
|
|
||||||
|
#include "destrum/Graphics/Pipeline.h"
|
||||||
|
|
||||||
|
|
||||||
|
class CubeMap {
|
||||||
|
public:
|
||||||
|
explicit CubeMap();
|
||||||
|
~CubeMap();
|
||||||
|
|
||||||
|
void LoadCubeMap(const std::filesystem::path &directoryPath);
|
||||||
|
void RenderToCubemap(ImageID inputImage, VkImage outputImage, std::array<VkImageView, 6> faceViews, uint32_t size);
|
||||||
|
|
||||||
|
void InitCubemapPipeline(const std::string& vertPath, const std::string& fragPath);
|
||||||
|
void CreateCubeMap();
|
||||||
|
ImageID GetCubeMapImageID();
|
||||||
|
|
||||||
|
private:
|
||||||
|
const std::array<glm::mat4, 6> viewMatrices = {
|
||||||
|
// POSITIVE_X
|
||||||
|
glm::lookAt(glm::vec3(0.0f), glm::vec3(1.0f, 0.0f, 0.0f), glm::vec3(0.0f, -1.0f, 0.0f)),
|
||||||
|
// NEGATIVE_X
|
||||||
|
glm::lookAt(glm::vec3(0.0f), glm::vec3(-1.0f, 0.0f, 0.0f), glm::vec3(0.0f, -1.0f, 0.0f)),
|
||||||
|
// POSITIVE_Y
|
||||||
|
glm::lookAt(glm::vec3(0.0f), glm::vec3(0.0f, 1.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f)),
|
||||||
|
// NEGATIVE_Y
|
||||||
|
glm::lookAt(glm::vec3(0.0f), glm::vec3(0.0f, -1.0f, 0.0f), glm::vec3(0.0f, 0.0f, -1.0f)),
|
||||||
|
// POSITIVE_Z
|
||||||
|
glm::lookAt(glm::vec3(0.0f), glm::vec3(0.0f, 0.0f, 1.0f), glm::vec3(0.0f, -1.0f, 0.0f)),
|
||||||
|
// NEGATIVE_Z
|
||||||
|
glm::lookAt(glm::vec3(0.0f), glm::vec3(0.0f, 0.0f, -1.0f), glm::vec3(0.0f, -1.0f, 0.0f))
|
||||||
|
};
|
||||||
|
|
||||||
|
struct alignas(16) PC {
|
||||||
|
glm::mat4 viewMtx; // 64
|
||||||
|
glm::mat4 projMtx; // 64
|
||||||
|
std::uint32_t inputImageId; // 4
|
||||||
|
};
|
||||||
|
|
||||||
|
glm::mat4 m_projection{};
|
||||||
|
|
||||||
|
ImageID m_hdrImage{};
|
||||||
|
|
||||||
|
uint32_t m_cubeMapSize = 1024; // Default size for cube map
|
||||||
|
VkImageView m_skyboxView;
|
||||||
|
|
||||||
|
VkPipelineLayout m_cubemapPipelineLayout = VK_NULL_HANDLE;
|
||||||
|
std::unique_ptr<Pipeline> m_cubemapPipeline;
|
||||||
|
|
||||||
|
std::string m_cubemapVert;
|
||||||
|
std::string m_cubemapFrag;
|
||||||
|
|
||||||
|
|
||||||
|
ImageID m_cubemapImageID;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif //CUBEMAP_H
|
||||||
40
destrum/include/destrum/Util/GameState.h
Normal file
40
destrum/include/destrum/Util/GameState.h
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
#ifndef GAMESTATE_H
|
||||||
|
#define GAMESTATE_H
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
#include <destrum/Singleton.h>
|
||||||
|
|
||||||
|
|
||||||
|
class GfxDevice;
|
||||||
|
class GameRenderer;
|
||||||
|
|
||||||
|
class GameState final: public Singleton<GameState> {
|
||||||
|
public:
|
||||||
|
friend class Singleton<GameState>;
|
||||||
|
|
||||||
|
void SetGfxDevice(GfxDevice* device) { m_gfxDevice = device; }
|
||||||
|
GfxDevice& Gfx() {
|
||||||
|
assert(m_gfxDevice && "GfxDevice not registered yet!");
|
||||||
|
return *m_gfxDevice;
|
||||||
|
}
|
||||||
|
const GfxDevice& Gfx() const {
|
||||||
|
assert(m_gfxDevice && "GfxDevice not registered yet!");
|
||||||
|
return *m_gfxDevice;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetRenderer(GameRenderer* renderer) { m_renderer = renderer; }
|
||||||
|
GameRenderer& Renderer() {
|
||||||
|
assert(m_renderer && "Renderer not registered yet!");
|
||||||
|
return *m_renderer;
|
||||||
|
}
|
||||||
|
const GameRenderer& Renderer() const {
|
||||||
|
assert(m_renderer && "Renderer not registered yet!");
|
||||||
|
return *m_renderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
GfxDevice* m_gfxDevice{nullptr};
|
||||||
|
GameRenderer* m_renderer{nullptr};
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif //GAMESTATE_H
|
||||||
@@ -4,7 +4,10 @@
|
|||||||
#include <random>
|
#include <random>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
|
#include "destrum/ObjectModel/GameObject.h"
|
||||||
#include "destrum/ObjectModel/Transform.h"
|
#include "destrum/ObjectModel/Transform.h"
|
||||||
|
#include "destrum/Components/MeshRendererComponent.h"
|
||||||
|
#include "destrum/Util/GameState.h"
|
||||||
|
|
||||||
static glm::vec3 RandomUnitVector(std::mt19937& rng)
|
static glm::vec3 RandomUnitVector(std::mt19937& rng)
|
||||||
{
|
{
|
||||||
@@ -48,11 +51,6 @@ void OrbitAndSpin::BuildOrbitBasis()
|
|||||||
|
|
||||||
void OrbitAndSpin::Update()
|
void OrbitAndSpin::Update()
|
||||||
{
|
{
|
||||||
// If your engine provides dt via a global/time service, use that instead.
|
|
||||||
// Since your Spinner takes dt indirectly, I'm assuming Component::Update()
|
|
||||||
// is called once per frame and you can access dt somewhere globally.
|
|
||||||
//
|
|
||||||
// If you CAN pass dt into Update, change signature to Update(float dt).
|
|
||||||
float dt = 1.0f / 60.0f;
|
float dt = 1.0f / 60.0f;
|
||||||
|
|
||||||
// orbit
|
// orbit
|
||||||
@@ -60,10 +58,37 @@ void OrbitAndSpin::Update()
|
|||||||
float a = m_OrbitAngle + m_OrbitPhase;
|
float a = m_OrbitAngle + m_OrbitPhase;
|
||||||
|
|
||||||
glm::vec3 offset = (m_U * std::cos(a) + m_V * std::sin(a)) * m_Radius;
|
glm::vec3 offset = (m_U * std::cos(a) + m_V * std::sin(a)) * m_Radius;
|
||||||
|
|
||||||
|
// IMPORTANT: if SetWorldPosition resets TRS in your engine, this will wipe scale unless you set it again after.
|
||||||
GetTransform().SetWorldPosition(m_Center + offset);
|
GetTransform().SetWorldPosition(m_Center + offset);
|
||||||
|
|
||||||
// self spin (local rotation)
|
// spin
|
||||||
glm::quat dq = glm::angleAxis(m_SpinSpeed * dt, glm::normalize(m_SpinAxis));
|
glm::quat dq = glm::angleAxis(m_SpinSpeed * dt, glm::normalize(m_SpinAxis));
|
||||||
auto current = GetTransform().GetLocalRotation(); // adapt to your API
|
auto current = GetTransform().GetLocalRotation();
|
||||||
GetTransform().SetLocalRotation(glm::normalize(dq * current));
|
GetTransform().SetLocalRotation(glm::normalize(dq * current));
|
||||||
|
|
||||||
|
// grow (always positive)
|
||||||
|
m_GrowPhase += m_GrowSpeed * dt;
|
||||||
|
|
||||||
|
float t = 0.5f * (std::sin(m_GrowPhase) + 1.0f); // 0..1
|
||||||
|
float s = 1.50 + t * 0.70f; // 0.05..0.15 (pick what you want)
|
||||||
|
|
||||||
|
GetTransform().SetLocalScale(glm::vec3(std::sin(m_GrowPhase)));
|
||||||
|
|
||||||
|
// material color
|
||||||
|
// auto& mat = GameState::GetInstance().Renderer().getMaterialMutable(m_MaterialID);
|
||||||
|
// mat.baseColor = glm::vec3(
|
||||||
|
// 0.5f + 0.5f * std::sin(m_OrbitAngle * 2.0f),
|
||||||
|
// 0.5f + 0.5f * std::sin(m_OrbitAngle * 3.0f + 2.0f),
|
||||||
|
// 0.5f + 0.5f * std::sin(m_OrbitAngle * 4.0f + 4.0f)
|
||||||
|
// );
|
||||||
|
// GameState::GetInstance().Renderer().updateMaterialGPU(m_MaterialID);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OrbitAndSpin::Start() {
|
||||||
|
auto meshComp = this->GetGameObject()->GetComponent<MeshRendererComponent>();
|
||||||
|
m_MaterialID = meshComp->GetMaterialID();
|
||||||
|
|
||||||
|
m_BaseScale = GetTransform().GetLocalScale(); // <-- important
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,8 +70,8 @@ void Camera::Update(float deltaTime) {
|
|||||||
if (input.IsKeyDown(SDL_SCANCODE_S)) move -= m_forward;
|
if (input.IsKeyDown(SDL_SCANCODE_S)) move -= m_forward;
|
||||||
if (input.IsKeyDown(SDL_SCANCODE_D)) move += m_right;
|
if (input.IsKeyDown(SDL_SCANCODE_D)) move += m_right;
|
||||||
if (input.IsKeyDown(SDL_SCANCODE_A)) move -= m_right;
|
if (input.IsKeyDown(SDL_SCANCODE_A)) move -= m_right;
|
||||||
if (input.IsKeyDown(SDL_SCANCODE_Q)) move += glm::vec3(0, 1, 0); // Absolute Up
|
if (input.IsKeyDown(SDL_SCANCODE_E)) move += glm::vec3(0, 1, 0); // Absolute Up
|
||||||
if (input.IsKeyDown(SDL_SCANCODE_E)) move -= glm::vec3(0, 1, 0); // Absolute Down
|
if (input.IsKeyDown(SDL_SCANCODE_Q)) move -= glm::vec3(0, 1, 0); // Absolute Down
|
||||||
|
|
||||||
if (glm::length2(move) > 0.0f) {
|
if (glm::length2(move) > 0.0f) {
|
||||||
m_position += glm::normalize(move) * (moveSpeed * deltaTime);
|
m_position += glm::normalize(move) * (moveSpeed * deltaTime);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
#include <destrum/Graphics/Init.h>
|
#include <destrum/Graphics/Init.h>
|
||||||
|
|
||||||
#include "destrum/Graphics/imageLoader.h"
|
#include "destrum/Graphics/imageLoader.h"
|
||||||
|
#include "destrum/Util/GameState.h"
|
||||||
#include "spdlog/spdlog.h"
|
#include "spdlog/spdlog.h"
|
||||||
|
|
||||||
GfxDevice::GfxDevice(): imageCache(*this) {
|
GfxDevice::GfxDevice(): imageCache(*this) {
|
||||||
@@ -141,7 +142,7 @@ void GfxDevice::init(SDL_Window* window, const std::string& appName, bool vSync)
|
|||||||
// "white texture",
|
// "white texture",
|
||||||
// &pixel);
|
// &pixel);
|
||||||
// }
|
// }
|
||||||
|
GameState::GetInstance().SetGfxDevice(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GfxDevice::recreateSwapchain(int width, int height) {
|
void GfxDevice::recreateSwapchain(int width, int height) {
|
||||||
@@ -164,6 +165,10 @@ VkCommandBuffer GfxDevice::beginFrame() {
|
|||||||
return cmd;
|
return cmd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VulkanImmediateExecutor& GfxDevice::GetImmediateExecuter() {
|
||||||
|
return executor;
|
||||||
|
}
|
||||||
|
|
||||||
void GfxDevice::endFrame(VkCommandBuffer cmd, const GPUImage& drawImage, const EndFrameProps& props) {
|
void GfxDevice::endFrame(VkCommandBuffer cmd, const GPUImage& drawImage, const EndFrameProps& props) {
|
||||||
// get swapchain image
|
// get swapchain image
|
||||||
const auto [swapchainImage, swapchainImageIndex] = swapchain.acquireNextImage(getCurrentFrameIndex());
|
const auto [swapchainImage, swapchainImageIndex] = swapchain.acquireNextImage(getCurrentFrameIndex());
|
||||||
@@ -426,7 +431,7 @@ GPUImage GfxDevice::createImageRaw(
|
|||||||
|
|
||||||
if (createInfo.isCubemap) {
|
if (createInfo.isCubemap) {
|
||||||
assert(createInfo.numLayers % 6 == 0);
|
assert(createInfo.numLayers % 6 == 0);
|
||||||
assert(!createInfo.mipMap);
|
// assert(!createInfo.mipMap);
|
||||||
assert((createInfo.flags & VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT) != 0);
|
assert((createInfo.flags & VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT) != 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -503,16 +508,14 @@ GPUImage GfxDevice::createImageRaw(
|
|||||||
|
|
||||||
void GfxDevice::uploadImageData(const GPUImage& image, void* pixelData, std::uint32_t layer) const
|
void GfxDevice::uploadImageData(const GPUImage& image, void* pixelData, std::uint32_t layer) const
|
||||||
{
|
{
|
||||||
int numChannels = 4;
|
VkDeviceSize dataSize =
|
||||||
if (image.format == VK_FORMAT_R8_UNORM) {
|
VkDeviceSize(image.extent.depth) *
|
||||||
// FIXME: support more types
|
image.extent.width *
|
||||||
numChannels = 1;
|
image.extent.height *
|
||||||
}
|
BytesPerTexel(image.format);
|
||||||
const auto dataSize =
|
|
||||||
image.extent.depth * image.extent.width * image.extent.height * numChannels;
|
|
||||||
|
|
||||||
const auto uploadBuffer = createBuffer(dataSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT);
|
auto uploadBuffer = createBuffer(dataSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT);
|
||||||
memcpy(uploadBuffer.info.pMappedData, pixelData, dataSize);
|
memcpy(uploadBuffer.info.pMappedData, pixelData, size_t(dataSize));
|
||||||
|
|
||||||
executor.immediateSubmit([&](VkCommandBuffer cmd) {
|
executor.immediateSubmit([&](VkCommandBuffer cmd) {
|
||||||
assert(
|
assert(
|
||||||
|
|||||||
@@ -75,3 +75,33 @@ MaterialID MaterialCache::getPlaceholderMaterialId() const
|
|||||||
assert(placeholderMaterialId != NULL_MATERIAL_ID && "MaterialCache::init not called");
|
assert(placeholderMaterialId != NULL_MATERIAL_ID && "MaterialCache::init not called");
|
||||||
return placeholderMaterialId;
|
return placeholderMaterialId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Material& MaterialCache::getMaterialMutable(MaterialID id) {
|
||||||
|
assert(id < materials.size());
|
||||||
|
return materials.at(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MaterialCache::updateMaterialGPU(GfxDevice& gfxDevice, MaterialID id)
|
||||||
|
{
|
||||||
|
assert(id < materials.size());
|
||||||
|
assert(materialDataBuffer.info.pMappedData && "materialDataBuffer must be mapped");
|
||||||
|
|
||||||
|
const auto getTextureOrElse = [](ImageID imageId, ImageID placeholder) {
|
||||||
|
return imageId != NULL_IMAGE_ID ? imageId : placeholder;
|
||||||
|
};
|
||||||
|
|
||||||
|
Material& material = materials[id];
|
||||||
|
|
||||||
|
MaterialData* data = reinterpret_cast<MaterialData*>(materialDataBuffer.info.pMappedData);
|
||||||
|
|
||||||
|
const ImageID whiteTextureID = gfxDevice.getWhiteTextureID();
|
||||||
|
|
||||||
|
data[id] = MaterialData{
|
||||||
|
.baseColor = glm::vec4(material.baseColor, 1.0f),
|
||||||
|
.metalRoughnessEmissive = glm::vec4(material.metallicFactor, material.roughnessFactor, material.emissiveFactor, 0.f),
|
||||||
|
.diffuseTex = getTextureOrElse(material.diffuseTexture, whiteTextureID),
|
||||||
|
.normalTex = whiteTextureID, // if you have this field
|
||||||
|
.metallicRoughnessTex = whiteTextureID,
|
||||||
|
.emissiveTex = whiteTextureID,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#include <destrum/Graphics/Pipelines/MeshPipeline.h>
|
#include <destrum/Graphics/Pipelines/MeshPipeline.h>
|
||||||
#include <destrum/FS/AssetFS.h>
|
#include <destrum/FS/AssetFS.h>
|
||||||
|
|
||||||
MeshPipeline::MeshPipeline() {
|
MeshPipeline::MeshPipeline(): m_pipelineLayout{nullptr} {
|
||||||
}
|
}
|
||||||
|
|
||||||
MeshPipeline::~MeshPipeline() {
|
MeshPipeline::~MeshPipeline() {
|
||||||
|
|||||||
90
destrum/src/Graphics/Pipelines/SkyboxPipeline.cpp
Normal file
90
destrum/src/Graphics/Pipelines/SkyboxPipeline.cpp
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
#include <destrum/Graphics/Pipelines/SkyboxPipeline.h>
|
||||||
|
|
||||||
|
#include <destrum/FS/AssetFS.h>
|
||||||
|
|
||||||
|
#include "spdlog/spdlog.h"
|
||||||
|
|
||||||
|
SkyboxPipeline::SkyboxPipeline(): pipelineLayout{nullptr} {
|
||||||
|
}
|
||||||
|
|
||||||
|
SkyboxPipeline::~SkyboxPipeline() {
|
||||||
|
}
|
||||||
|
|
||||||
|
void SkyboxPipeline::init(GfxDevice& gfxDevice, VkFormat drawImageFormat, VkFormat depthImageFormat) {
|
||||||
|
|
||||||
|
const auto vertexShader = AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/fullscreen_triangle.vert");
|
||||||
|
const auto fragShader = AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/skybox.frag");
|
||||||
|
|
||||||
|
constexpr auto bufferRange = VkPushConstantRange{
|
||||||
|
.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT,
|
||||||
|
.offset = 0,
|
||||||
|
.size = sizeof(SkyboxPushConstants),
|
||||||
|
};
|
||||||
|
constexpr auto pushConstantRanges = std::array{bufferRange};
|
||||||
|
const auto layouts = std::array{gfxDevice.getBindlessDescSetLayout()};
|
||||||
|
|
||||||
|
VkPipelineLayoutCreateInfo pipelineLayoutInfo{};
|
||||||
|
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
|
||||||
|
pipelineLayoutInfo.setLayoutCount = static_cast<uint32_t>(layouts.size());
|
||||||
|
pipelineLayoutInfo.pSetLayouts = layouts.data();
|
||||||
|
|
||||||
|
pipelineLayoutInfo.pushConstantRangeCount = static_cast<uint32_t>(pushConstantRanges.size());
|
||||||
|
pipelineLayoutInfo.pPushConstantRanges = pushConstantRanges.data();
|
||||||
|
|
||||||
|
if (vkCreatePipelineLayout(gfxDevice.getDevice().device, &pipelineLayoutInfo, nullptr, &pipelineLayout) != VK_SUCCESS) {
|
||||||
|
throw std::runtime_error("Could not make pipleine layout");
|
||||||
|
}
|
||||||
|
|
||||||
|
PipelineConfigInfo pipelineConfig{};
|
||||||
|
Pipeline::DefaultPipelineConfigInfo(pipelineConfig);
|
||||||
|
pipelineConfig.name = "skybox pipeline";
|
||||||
|
pipelineConfig.pipelineLayout = pipelineLayout;
|
||||||
|
|
||||||
|
pipelineConfig.vertexAttributeDescriptions = {};
|
||||||
|
pipelineConfig.vertexBindingDescriptions = {};
|
||||||
|
|
||||||
|
pipelineConfig.colorAttachments = { drawImageFormat };
|
||||||
|
pipelineConfig.depthAttachment = depthImageFormat;
|
||||||
|
|
||||||
|
pipelineConfig.rasterizationInfo.cullMode = VK_CULL_MODE_NONE;
|
||||||
|
|
||||||
|
pipelineConfig.depthStencilInfo.depthTestEnable = VK_TRUE;
|
||||||
|
pipelineConfig.depthStencilInfo.depthWriteEnable = VK_FALSE;
|
||||||
|
pipelineConfig.depthStencilInfo.depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL;
|
||||||
|
|
||||||
|
pipelineConfig.rasterizationInfo.cullMode = VK_CULL_MODE_NONE;
|
||||||
|
|
||||||
|
pipeline = std::make_unique<Pipeline>(
|
||||||
|
gfxDevice,
|
||||||
|
vertexShader.string(),
|
||||||
|
fragShader.string(),
|
||||||
|
pipelineConfig
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SkyboxPipeline::cleanup(VkDevice device) {
|
||||||
|
pipeline.reset();
|
||||||
|
vkDestroyPipelineLayout(device, pipelineLayout, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SkyboxPipeline::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera& camera) {
|
||||||
|
if (skyboxTextureId == NULL_IMAGE_ID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pipeline->bind(cmd);
|
||||||
|
gfxDevice.bindBindlessDescSet(cmd, pipelineLayout);
|
||||||
|
|
||||||
|
const auto pcs = SkyboxPushConstants{
|
||||||
|
.invViewProj = glm::inverse(camera.GetViewProjectionMatrix()),
|
||||||
|
.cameraPos = glm::vec4{camera.GetPosition(), 1.f},
|
||||||
|
.skyboxTextureId = static_cast<std::uint32_t>(skyboxTextureId),
|
||||||
|
};
|
||||||
|
vkCmdPushConstants(cmd, pipelineLayout, VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(SkyboxPushConstants), &pcs);
|
||||||
|
|
||||||
|
vkCmdDraw(cmd, 3, 1, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SkyboxPipeline::setSkyboxImage(const ImageID skyboxId) {
|
||||||
|
skyboxTextureId = skyboxId;
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <destrum/Graphics/Util.h>
|
#include <destrum/Graphics/Util.h>
|
||||||
|
|
||||||
|
#include "destrum/Util/GameState.h"
|
||||||
#include "spdlog/spdlog.h"
|
#include "spdlog/spdlog.h"
|
||||||
|
|
||||||
GameRenderer::GameRenderer(MeshCache& meshCache, MaterialCache& matCache): meshCache{meshCache}, materialCache{matCache} {
|
GameRenderer::GameRenderer(MeshCache& meshCache, MaterialCache& matCache): meshCache{meshCache}, materialCache{matCache} {
|
||||||
@@ -19,6 +20,11 @@ void GameRenderer::init(GfxDevice& gfxDevice, const glm::ivec2& drawImageSize) {
|
|||||||
meshPipeline = std::make_unique<MeshPipeline>();
|
meshPipeline = std::make_unique<MeshPipeline>();
|
||||||
meshPipeline->init(gfxDevice, drawImageFormat, depthImageFormat);
|
meshPipeline->init(gfxDevice, drawImageFormat, depthImageFormat);
|
||||||
|
|
||||||
|
skyboxPipeline = std::make_unique<SkyboxPipeline>();
|
||||||
|
skyboxPipeline->init(gfxDevice, drawImageFormat, depthImageFormat);
|
||||||
|
|
||||||
|
|
||||||
|
GameState::GetInstance().SetRenderer(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameRenderer::beginDrawing(GfxDevice& gfxDevice) {
|
void GameRenderer::beginDrawing(GfxDevice& gfxDevice) {
|
||||||
@@ -80,6 +86,9 @@ void GameRenderer::draw(VkCommandBuffer cmd, GfxDevice& gfxDevice, const Camera&
|
|||||||
meshDrawCommands,
|
meshDrawCommands,
|
||||||
sortedMeshDrawCommands);
|
sortedMeshDrawCommands);
|
||||||
|
|
||||||
|
skyboxPipeline->draw(cmd, gfxDevice, camera);
|
||||||
|
|
||||||
|
|
||||||
vkCmdEndRendering(cmd);
|
vkCmdEndRendering(cmd);
|
||||||
// vkutil::cmdEndLabel(cmd);
|
// vkutil::cmdEndLabel(cmd);
|
||||||
|
|
||||||
@@ -106,6 +115,21 @@ const GPUImage& GameRenderer::getDrawImage(const GfxDevice& gfx_device) const {
|
|||||||
return gfx_device.getImage(drawImageId);
|
return gfx_device.getImage(drawImageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Material& GameRenderer::getMaterialMutable(MaterialID id) {
|
||||||
|
assert(id != NULL_MATERIAL_ID);
|
||||||
|
return materialCache.getMaterialMutable(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameRenderer::updateMaterialGPU(MaterialID id) {
|
||||||
|
assert(id != NULL_MATERIAL_ID);
|
||||||
|
materialCache.updateMaterialGPU(GameState::GetInstance().Gfx(), id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameRenderer::setSkyboxTexture(ImageID skyboxImageId) {
|
||||||
|
spdlog::debug("Set skybox texture to image id {}", skyboxImageId);
|
||||||
|
skyboxPipeline->setSkyboxImage(skyboxImageId);
|
||||||
|
}
|
||||||
|
|
||||||
void GameRenderer::createDrawImage(GfxDevice& gfxDevice,
|
void GameRenderer::createDrawImage(GfxDevice& gfxDevice,
|
||||||
const glm::ivec2& drawImageSize,
|
const glm::ivec2& drawImageSize,
|
||||||
bool firstCreate)
|
bool firstCreate)
|
||||||
|
|||||||
286
destrum/src/Graphics/Resources/Cubemap.cpp
Normal file
286
destrum/src/Graphics/Resources/Cubemap.cpp
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
#include <destrum/Graphics/Resources/Cubemap.h>
|
||||||
|
|
||||||
|
#include "destrum/FS/AssetFS.h"
|
||||||
|
#include "destrum/Graphics/GfxDevice.h"
|
||||||
|
#include "destrum/Graphics/Pipeline.h"
|
||||||
|
#include "destrum/Util/GameState.h"
|
||||||
|
#include <destrum/Graphics/Util.h>
|
||||||
|
|
||||||
|
#include "glm/ext/matrix_clip_space.hpp"
|
||||||
|
#include "spdlog/spdlog.h"
|
||||||
|
|
||||||
|
|
||||||
|
CubeMap::CubeMap() {
|
||||||
|
m_projection = glm::perspective(glm::radians(90.0f), 1.0f, 0.1f, 10.0f);;
|
||||||
|
}
|
||||||
|
|
||||||
|
CubeMap::~CubeMap() {
|
||||||
|
auto& gfx = GameState::GetInstance().Gfx();
|
||||||
|
VkDevice device = gfx.getDevice();
|
||||||
|
|
||||||
|
if (m_skyboxView) {
|
||||||
|
vkDestroyImageView(device, m_skyboxView, nullptr);
|
||||||
|
m_skyboxView = VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_cubemapPipelineLayout) {
|
||||||
|
vkDestroyPipelineLayout(device, m_cubemapPipelineLayout, nullptr);
|
||||||
|
m_cubemapPipelineLayout = VK_NULL_HANDLE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CubeMap::LoadCubeMap(const std::filesystem::path& directoryPath) {
|
||||||
|
m_hdrImage = GameState::GetInstance().Gfx().loadImageFromFile(directoryPath, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CubeMap::RenderToCubemap(ImageID inputImage,
|
||||||
|
VkImage outputImage,
|
||||||
|
std::array<VkImageView, 6> faceViews,
|
||||||
|
uint32_t size)
|
||||||
|
{
|
||||||
|
// Ensure pipeline exists
|
||||||
|
if (!m_cubemapPipeline || m_cubemapPipelineLayout == VK_NULL_HANDLE) {
|
||||||
|
throw std::runtime_error("Cubemap pipeline not initialized. Call InitCubemapPipeline first.");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& gfx = GameState::GetInstance().Gfx();
|
||||||
|
|
||||||
|
gfx.GetImmediateExecuter().immediateSubmit([&](VkCommandBuffer cmd) {
|
||||||
|
|
||||||
|
VkImageMemoryBarrier barrier{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER };
|
||||||
|
barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||||
|
barrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
||||||
|
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||||
|
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||||
|
barrier.image = outputImage;
|
||||||
|
barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||||
|
barrier.subresourceRange.baseMipLevel = 0;
|
||||||
|
barrier.subresourceRange.levelCount = 1;
|
||||||
|
barrier.subresourceRange.baseArrayLayer = 0;
|
||||||
|
barrier.subresourceRange.layerCount = 6;
|
||||||
|
barrier.srcAccessMask = 0;
|
||||||
|
barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
|
||||||
|
|
||||||
|
vkCmdPipelineBarrier(
|
||||||
|
cmd,
|
||||||
|
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
|
||||||
|
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
|
||||||
|
0,
|
||||||
|
0, nullptr,
|
||||||
|
0, nullptr,
|
||||||
|
1, &barrier
|
||||||
|
);
|
||||||
|
|
||||||
|
VkViewport viewport{};
|
||||||
|
viewport.x = 0.0f;
|
||||||
|
viewport.y = 0.0f;
|
||||||
|
viewport.width = static_cast<float>(size);
|
||||||
|
viewport.height = static_cast<float>(size);
|
||||||
|
viewport.minDepth = 0.0f;
|
||||||
|
viewport.maxDepth = 1.0f;
|
||||||
|
|
||||||
|
VkRect2D scissor{};
|
||||||
|
scissor.offset = {0, 0};
|
||||||
|
scissor.extent = {size, size};
|
||||||
|
|
||||||
|
for (uint32_t face = 0; face < 6; ++face) {
|
||||||
|
PC pc{};
|
||||||
|
pc.viewMtx = viewMatrices[face];
|
||||||
|
pc.projMtx = m_projection;
|
||||||
|
pc.inputImageId = m_hdrImage;
|
||||||
|
|
||||||
|
VkRenderingAttachmentInfoKHR colorAttachment{ VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO_KHR };
|
||||||
|
colorAttachment.imageView = faceViews[face];
|
||||||
|
colorAttachment.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
||||||
|
colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
|
||||||
|
colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
|
||||||
|
colorAttachment.clearValue.color = {0.f, 0.f, 0.f, 1.f};
|
||||||
|
|
||||||
|
VkRenderingInfoKHR renderingInfo{ VK_STRUCTURE_TYPE_RENDERING_INFO_KHR };
|
||||||
|
renderingInfo.renderArea.offset = {0, 0};
|
||||||
|
renderingInfo.renderArea.extent = {size, size};
|
||||||
|
renderingInfo.layerCount = 1;
|
||||||
|
renderingInfo.colorAttachmentCount = 1;
|
||||||
|
renderingInfo.pColorAttachments = &colorAttachment;
|
||||||
|
|
||||||
|
vkCmdBeginRendering(cmd, &renderingInfo);
|
||||||
|
|
||||||
|
vkCmdSetViewport(cmd, 0, 1, &viewport);
|
||||||
|
vkCmdSetScissor(cmd, 0, 1, &scissor);
|
||||||
|
|
||||||
|
m_cubemapPipeline->bind(cmd);
|
||||||
|
gfx.bindBindlessDescSet(cmd, m_cubemapPipelineLayout);
|
||||||
|
|
||||||
|
vkCmdPushConstants(
|
||||||
|
cmd,
|
||||||
|
m_cubemapPipelineLayout,
|
||||||
|
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
|
||||||
|
0,
|
||||||
|
sizeof(PC),
|
||||||
|
&pc
|
||||||
|
);
|
||||||
|
|
||||||
|
vkCmdDraw(cmd, 36, 1, 0, 0);
|
||||||
|
|
||||||
|
vkCmdEndRendering(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transition to shader read
|
||||||
|
barrier.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
||||||
|
barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||||
|
barrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
|
||||||
|
barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
||||||
|
|
||||||
|
vkCmdPipelineBarrier(
|
||||||
|
cmd,
|
||||||
|
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
|
||||||
|
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
|
||||||
|
0,
|
||||||
|
0, nullptr,
|
||||||
|
0, nullptr,
|
||||||
|
1, &barrier
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void CubeMap::CreateCubeMap() {
|
||||||
|
const uint32_t mipLevels = static_cast<uint32_t>(std::floor(std::log2(m_cubeMapSize))) + 1;
|
||||||
|
|
||||||
|
VkImageCreateInfo imageInfo{};
|
||||||
|
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
|
||||||
|
imageInfo.imageType = VK_IMAGE_TYPE_2D;
|
||||||
|
imageInfo.extent.height = m_cubeMapSize;
|
||||||
|
imageInfo.extent.width = m_cubeMapSize;
|
||||||
|
imageInfo.extent.depth = 1;
|
||||||
|
imageInfo.mipLevels = mipLevels;
|
||||||
|
imageInfo.arrayLayers = 6; // 6 faces for cubemap
|
||||||
|
imageInfo.format = VK_FORMAT_R32G32B32A32_SFLOAT;
|
||||||
|
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||||
|
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||||
|
imageInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
|
||||||
|
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||||
|
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||||
|
imageInfo.flags = VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT; // Create a cubemap
|
||||||
|
|
||||||
|
VmaAllocationCreateInfo allocInfo{};
|
||||||
|
allocInfo.usage = VMA_MEMORY_USAGE_GPU_ONLY;
|
||||||
|
|
||||||
|
auto& device = GameState::GetInstance().Gfx();
|
||||||
|
GPUImage cubeMapID = device.createImageRaw({
|
||||||
|
.format = VK_FORMAT_R32G32B32A32_SFLOAT,
|
||||||
|
.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT,
|
||||||
|
.flags = VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT,
|
||||||
|
.extent =
|
||||||
|
VkExtent3D{
|
||||||
|
.width = m_cubeMapSize,
|
||||||
|
.height = m_cubeMapSize,
|
||||||
|
.depth = 1,
|
||||||
|
},
|
||||||
|
.numLayers = 6,
|
||||||
|
.mipMap = true,
|
||||||
|
.isCubemap = true
|
||||||
|
});
|
||||||
|
|
||||||
|
// if (vmaCreateImage(device.getAllocator(), &imageInfo, &allocInfo, &cubeMapID.image, &cubeMapID.allocation, nullptr) != VK_SUCCESS) {
|
||||||
|
// throw std::runtime_error("Failed to create image with VMA!");
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
std::array<VkImageView, 6> faceViews{};
|
||||||
|
for (uint32_t face = 0; face < 6; ++face) {
|
||||||
|
VkImageViewCreateInfo viewInfo{};
|
||||||
|
viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
|
||||||
|
viewInfo.image = cubeMapID.image;
|
||||||
|
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||||
|
viewInfo.format = VK_FORMAT_R32G32B32A32_SFLOAT;
|
||||||
|
viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||||
|
viewInfo.subresourceRange.baseMipLevel = 0;
|
||||||
|
viewInfo.subresourceRange.levelCount = 1;
|
||||||
|
viewInfo.subresourceRange.baseArrayLayer = face;
|
||||||
|
viewInfo.subresourceRange.layerCount = 1;
|
||||||
|
|
||||||
|
if (vkCreateImageView(device.getDevice(), &viewInfo, nullptr, &faceViews[face]) != VK_SUCCESS) {
|
||||||
|
throw std::runtime_error("Failed to create image view!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VkImageViewCreateInfo viewInfo{};
|
||||||
|
viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
|
||||||
|
viewInfo.image = cubeMapID.image;
|
||||||
|
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_CUBE;
|
||||||
|
viewInfo.format = VK_FORMAT_R32G32B32A32_SFLOAT;
|
||||||
|
viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||||
|
viewInfo.subresourceRange.baseMipLevel = 0;
|
||||||
|
viewInfo.subresourceRange.levelCount = 1;
|
||||||
|
viewInfo.subresourceRange.baseArrayLayer = 0;
|
||||||
|
viewInfo.subresourceRange.layerCount = 6;
|
||||||
|
|
||||||
|
|
||||||
|
if (vkCreateImageView(device.getDevice(), &viewInfo, nullptr, &m_skyboxView) != VK_SUCCESS) {
|
||||||
|
throw std::runtime_error("Failed to create image view!");
|
||||||
|
}
|
||||||
|
const auto vertPath = AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/cubemap.vert");
|
||||||
|
const auto fragPath = AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/cubemap.frag");
|
||||||
|
spdlog::info("hdriImage id = {}", m_hdrImage);
|
||||||
|
RenderToCubemap(m_hdrImage, cubeMapID.image, faceViews, m_cubeMapSize);
|
||||||
|
|
||||||
|
m_cubemapImageID = GameState::GetInstance().Gfx().addImageToCache(cubeMapID);
|
||||||
|
|
||||||
|
for (auto v : faceViews) {
|
||||||
|
vkDestroyImageView(device.getDevice(), v, nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageID CubeMap::GetCubeMapImageID() {
|
||||||
|
return m_cubemapImageID;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CubeMap::InitCubemapPipeline(const std::string& vertPath, const std::string& fragPath)
|
||||||
|
{
|
||||||
|
auto& gfx = GameState::GetInstance().Gfx();
|
||||||
|
VkDevice device = gfx.getDevice();
|
||||||
|
|
||||||
|
if (m_cubemapPipeline) return; // already created
|
||||||
|
|
||||||
|
// Save paths if you want
|
||||||
|
m_cubemapVert = vertPath;
|
||||||
|
m_cubemapFrag = fragPath;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
VkPushConstantRange pushConstantRange{};
|
||||||
|
pushConstantRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
|
||||||
|
pushConstantRange.offset = 0;
|
||||||
|
pushConstantRange.size = sizeof(PC);
|
||||||
|
|
||||||
|
const auto layouts = std::array{ gfx.getBindlessDescSetLayout() };
|
||||||
|
|
||||||
|
VkPipelineLayoutCreateInfo pipelineLayoutInfo{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO };
|
||||||
|
pipelineLayoutInfo.setLayoutCount = static_cast<uint32_t>(layouts.size());
|
||||||
|
pipelineLayoutInfo.pSetLayouts = layouts.data();
|
||||||
|
pipelineLayoutInfo.pushConstantRangeCount = 1;
|
||||||
|
pipelineLayoutInfo.pPushConstantRanges = &pushConstantRange;
|
||||||
|
|
||||||
|
if (vkCreatePipelineLayout(device, &pipelineLayoutInfo, nullptr, &m_cubemapPipelineLayout) != VK_SUCCESS) {
|
||||||
|
throw std::runtime_error("Failed to create cubemap pipeline layout!");
|
||||||
|
}
|
||||||
|
|
||||||
|
PipelineConfigInfo pipelineConfig{};
|
||||||
|
Pipeline::DefaultPipelineConfigInfo(pipelineConfig);
|
||||||
|
|
||||||
|
pipelineConfig.vertexAttributeDescriptions = {};
|
||||||
|
pipelineConfig.vertexBindingDescriptions = {};
|
||||||
|
pipelineConfig.pipelineLayout = m_cubemapPipelineLayout;
|
||||||
|
pipelineConfig.colorAttachments = { VK_FORMAT_R32G32B32A32_SFLOAT }; // must match cubemap image view format
|
||||||
|
pipelineConfig.depthAttachment = VK_FORMAT_UNDEFINED;
|
||||||
|
pipelineConfig.depthStencilInfo.depthTestEnable = VK_FALSE;
|
||||||
|
pipelineConfig.depthStencilInfo.depthWriteEnable = VK_FALSE;
|
||||||
|
pipelineConfig.rasterizationInfo.cullMode = VK_CULL_MODE_NONE;
|
||||||
|
|
||||||
|
m_cubemapPipeline = std::make_unique<Pipeline>(
|
||||||
|
gfx,
|
||||||
|
vertPath,
|
||||||
|
fragPath,
|
||||||
|
pipelineConfig
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -47,3 +47,5 @@ add_custom_target(_internal_cook_game_assets ALL
|
|||||||
DEPENDS TheChef
|
DEPENDS TheChef
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
#include <destrum/App.h>
|
#include <destrum/App.h>
|
||||||
#include <destrum/Scene/SceneManager.h>
|
#include <destrum/Scene/SceneManager.h>
|
||||||
|
|
||||||
|
#include "destrum/Graphics/Resources/Cubemap.h"
|
||||||
|
|
||||||
class LightKeeper final : public App {
|
class LightKeeper final : public App {
|
||||||
public:
|
public:
|
||||||
LightKeeper();
|
LightKeeper();
|
||||||
@@ -24,6 +26,8 @@ private:
|
|||||||
CPUMesh testMesh{};
|
CPUMesh testMesh{};
|
||||||
MeshID testMeshID;
|
MeshID testMeshID;
|
||||||
MaterialID testMaterialID;
|
MaterialID testMaterialID;
|
||||||
|
|
||||||
|
std::unique_ptr<CubeMap> skyboxCubemap;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif //LIGHTKEEPER_H
|
#endif //LIGHTKEEPER_H
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ void LightKeeper::customInit() {
|
|||||||
});
|
});
|
||||||
spdlog::info("Test material created with id: {}", testMaterialID);
|
spdlog::info("Test material created with id: {}", testMaterialID);
|
||||||
|
|
||||||
|
renderer.setSkyboxTexture(testimgID);
|
||||||
|
|
||||||
camera.SetRotation(glm::radians(glm::vec2(90.f, 0.f)));
|
camera.SetRotation(glm::radians(glm::vec2(90.f, 0.f)));
|
||||||
|
|
||||||
auto& scene = SceneManager::GetInstance().CreateScene("Main");
|
auto& scene = SceneManager::GetInstance().CreateScene("Main");
|
||||||
@@ -63,31 +65,46 @@ void LightKeeper::customInit() {
|
|||||||
globeRoot->AddComponent<Spinner>(glm::vec3(0, 1, 0), 1.0f); // spin around Y, rad/sec
|
globeRoot->AddComponent<Spinner>(glm::vec3(0, 1, 0), 1.0f); // spin around Y, rad/sec
|
||||||
scene.Add(globeRoot);
|
scene.Add(globeRoot);
|
||||||
|
|
||||||
const int count = 100;
|
// const int count = 100;
|
||||||
const float radius = 5.0f;
|
// const float radius = 5.0f;
|
||||||
|
//
|
||||||
const float orbitRadius = 5.0f;
|
// const float orbitRadius = 5.0f;
|
||||||
|
//
|
||||||
for (int i = 0; i < count; ++i) {
|
// for (int i = 0; i < count; ++i) {
|
||||||
auto childCube = std::make_shared<GameObject>(fmt::format("ChildCube{}", i));
|
// auto childCube = std::make_shared<GameObject>(fmt::format("ChildCube{}", i));
|
||||||
|
//
|
||||||
auto childMeshComp = childCube->AddComponent<MeshRendererComponent>();
|
// auto childMeshComp = childCube->AddComponent<MeshRendererComponent>();
|
||||||
childMeshComp->SetMeshID(testMeshID);
|
// childMeshComp->SetMeshID(testMeshID);
|
||||||
childMeshComp->SetMaterialID(testMaterialID);
|
// childMeshComp->SetMaterialID(testMaterialID);
|
||||||
|
//
|
||||||
childCube->GetTransform().SetWorldScale(glm::vec3(0.1f));
|
// childCube->GetTransform().SetWorldScale(glm::vec3(0.1f));
|
||||||
|
//
|
||||||
// Add orbit + self spin
|
// // Add orbit + self spin
|
||||||
auto orbit = childCube->AddComponent<OrbitAndSpin>(orbitRadius, glm::vec3(0.0f));
|
// auto orbit = childCube->AddComponent<OrbitAndSpin>(orbitRadius, glm::vec3(0.0f));
|
||||||
orbit->Randomize(1337u + (uint32_t)i); // stable random per index
|
// orbit->Randomize(1337u + (uint32_t)i); // stable random per index
|
||||||
|
//
|
||||||
scene.Add(childCube);
|
// scene.Add(childCube);
|
||||||
}
|
// }
|
||||||
|
|
||||||
// testCube->AddComponent<Rotator>(10, 5);
|
// testCube->AddComponent<Rotator>(10, 5);
|
||||||
|
|
||||||
|
|
||||||
scene.Add(testCube);
|
scene.Add(testCube);
|
||||||
|
|
||||||
|
// const auto skyboxID = AssetFS::GetInstance().GetFullPath("engine://textures/skybox.jpg");
|
||||||
|
const auto skyboxID = AssetFS::GetInstance().GetFullPath("engine://textures/test-skybox.png");
|
||||||
|
auto skyboxIDs = gfxDevice.loadImageFromFile(testimgpath);
|
||||||
|
|
||||||
|
const auto vertShaderPath = AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/cubemap.vert");
|
||||||
|
const auto fragShaderPath = AssetFS::GetInstance().GetCookedPathForFile("engine://shaders/cubemap.frag");
|
||||||
|
|
||||||
|
skyboxCubemap = std::make_unique<CubeMap>();
|
||||||
|
skyboxCubemap->LoadCubeMap(skyboxID.generic_string());
|
||||||
|
skyboxCubemap->InitCubemapPipeline(vertShaderPath.generic_string(), fragShaderPath.generic_string());
|
||||||
|
skyboxCubemap->CreateCubeMap();
|
||||||
|
|
||||||
|
renderer.setSkyboxTexture(skyboxCubemap->GetCubeMapImageID());
|
||||||
|
// skyboxCubemap->CreateCubeMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
void LightKeeper::customUpdate(float dt) {
|
void LightKeeper::customUpdate(float dt) {
|
||||||
|
|||||||
Reference in New Issue
Block a user