diff --git a/.gitignore b/.gitignore index 79923009..afbe4636 100644 --- a/.gitignore +++ b/.gitignore @@ -7,9 +7,13 @@ # Build /build/ -/scripts/local_*.sh /projects/ -/resources/ + +# Resources +/resources/fonts/ +/resources/icons/ +/resources/meshes/ +/resources/textures/ # Visual Studio /.vs diff --git a/CMakeLists.txt b/CMakeLists.txt index 8110db13..5ed2aa0d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.14) -project(atta VERSION 0.3.10 LANGUAGES CXX C) +project(atta VERSION 0.3.11 LANGUAGES CXX C) option(ATTA_BUILD_TESTS "Set to ON to build also the test executables" ON) option(ATTA_BUILD_DOCS "Build the documentation" OFF) diff --git a/examples/tutorials/material-resource/material-resource.atta b/examples/tutorials/material-resource/material-resource.atta index 52ec5caf..f7ca4859 100644 --- a/examples/tutorials/material-resource/material-resource.atta +++ b/examples/tutorials/material-resource/material-resource.atta @@ -1,5 +1,5 @@ [project] -attaVersion = "0.3.7" +attaVersion = "0.3.10" name = "material-resource" [config] @@ -166,3 +166,8 @@ transform.position = vec3(0.000000, 0.000000, 2.548024) transform.orientation = quat(0.000000, 0.000000, 0.000000, 1.000000) transform.scale = vec3(1.000000, 1.000000, 1.000000) +[node] +id = 9 +environmentLight.sid = "environment/suburban_soccer_park_2k.hdr" +name.name = "Environment light" + diff --git a/resources/shaders/common/depth.asl b/resources/shaders/common/depth.asl new file mode 100644 index 00000000..5b3dd184 --- /dev/null +++ b/resources/shaders/common/depth.asl @@ -0,0 +1,18 @@ +perFrame mat4 uView; +perFrame mat4 uProjection; +perDraw mat4 uModel; +perDraw vec4 uColor; +// uniform sampler2D uDepthMap; + +perVertex vec3 vUV; + +vec4 vertex(vec3 iVertex, vec3 iNormal, vec3 iUV) { + vUV = iUV; + return uProjection * uView * uModel * vec4(iVertex, 1.0f); +} + +vec4 fragment() { + // float depthValue = texture(depthMap, vUV).r; + // return vec4(vec3(depthValue), 1.0f); + return vec4(0, 0, 0, 1); +} diff --git a/resources/shaders/common/drawerLine.asl b/resources/shaders/common/drawerLine.asl new file mode 100644 index 00000000..1c96d768 --- /dev/null +++ b/resources/shaders/common/drawerLine.asl @@ -0,0 +1,12 @@ +perFrame mat4 uView; +perFrame mat4 uProjection; + +perVertex vec4 vColor; + +vec4 vertex(vec3 iPos, vec4 iColor) { + vec4 pos = uProjection * uView * vec4(iPos, 1.0); + vColor = iColor; + return vec4(pos.xyz, pos.w); +} + +void fragment(out vec4 color) { color = vColor; } diff --git a/resources/shaders/common/drawerPoint.asl b/resources/shaders/common/drawerPoint.asl new file mode 100644 index 00000000..7c11932e --- /dev/null +++ b/resources/shaders/common/drawerPoint.asl @@ -0,0 +1,14 @@ +perFrame mat4 uView; +perFrame mat4 uProjection; + +perVertex vec4 vColor; + +vec4 vertex(vec3 iPos, vec4 iColor) { + vec4 pos = uProjection * uView * vec4(iPos, 1.0); + vColor = iColor; + gl_PointSize = 5.0; + return pos; +} + +void fragment(out vec4 color) { color = vColor; } + diff --git a/resources/shaders/common/grid.asl b/resources/shaders/common/grid.asl new file mode 100644 index 00000000..2a5939fb --- /dev/null +++ b/resources/shaders/common/grid.asl @@ -0,0 +1,28 @@ +perFrame mat4 uView; +perFrame mat4 uProjection; +perFrame vec3 uCamPos; + +perVertex vec4 vColor; +perVertex vec3 vFragPos; + +vec4 vertex(vec3 iPos, vec4 iColor, float iWidth) { + vec4 pos = uProjection * uView * vec4(iPos, 1.0); + vColor = iColor; + vFragPos = iPos; + return vec4(pos.xyz, pos.w); +} + +void fragment(out vec4 color) { + float dist = length(uCamPos - vFragPos) + 0.0001; + color = vColor; + + // Parameters for controlling the fade effect + float fadeStart = 20.0; // Distance where the fading starts + float fadeEnd = 50.0; // Distance where the grid is fully faded/transparent + + // Clamped ratio between where fading starts and where it ends + float fadeRatio = clamp((dist - fadeStart) / (fadeEnd - fadeStart), 0.0, 1.0); + + // Apply fade falloff + color.a *= 1.0 - fadeRatio; +} diff --git a/resources/shaders/common/selected.asl b/resources/shaders/common/selected.asl new file mode 100644 index 00000000..50f974d5 --- /dev/null +++ b/resources/shaders/common/selected.asl @@ -0,0 +1,8 @@ +perFrame mat4 uView; +perFrame mat4 uProjection; +perDraw mat4 uModel; +perDraw vec4 uColor; + +vec4 vertex(vec3 iVertex, vec3 iNormal, vec3 iUV) { return uProjection * uView * uModel * vec4(iVertex, 1.0f); } + +vec4 fragment() { return uColor; } diff --git a/resources/shaders/common/skybox.asl b/resources/shaders/common/skybox.asl new file mode 100644 index 00000000..ccd31034 --- /dev/null +++ b/resources/shaders/common/skybox.asl @@ -0,0 +1,20 @@ +perFrame mat4 uProjection; +perFrame mat4 uView; +perFrame samplerCube uEnvironmentMap; + +perVertex vec3 vTexCoords; + +vec4 vertex(vec3 iVertex, vec3 iNormal, vec2 iUV) { + vTexCoords = iVertex; + // Add small dependency between vertex and normal/uv to avoid compilation error + vTexCoords += iNormal / 10000.0f + vec3(iUV,0) / 10000.0f; + + // Remove translation from the view matrix (so the skybox remains centered) + mat4 rotView = mat4(mat3(uView)); + return uProjection * rotView * vec4(iVertex, 1.0); +} + +void fragment(out vec4 color) { + // Sample the cubemap using the normalized direction + color = texture(uEnvironmentMap, normalize(vTexCoords)); +} diff --git a/resources/shaders/compute/brdf.asl b/resources/shaders/compute/brdf.asl new file mode 100644 index 00000000..f196c8fc --- /dev/null +++ b/resources/shaders/compute/brdf.asl @@ -0,0 +1,92 @@ +perVertex vec2 uv; + +vec4 vertex(vec2 iVertex, vec2 iUV) { + uv = iUV; + return vec4(iVertex, 0.0, 1.0); +} + +const float PI = 3.14159265359; + +float RadicalInverse_VdC(uint bits) { + bits = (bits << 16u) | (bits >> 16u); + bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u); + bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u); + bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u); + bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u); + return float(bits) * 2.3283064365386963e-10; +} + +vec2 Hammersley(uint i, uint N) { + return vec2(float(i) / float(N), RadicalInverse_VdC(i)); +} + +vec3 ImportanceSampleGGX(vec2 Xi, vec3 N, float roughness) { + float a = roughness * roughness; + + float phi = 2.0 * PI * Xi.x; + float cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (a * a - 1.0) * Xi.y)); + float sinTheta = sqrt(1.0 - cosTheta * cosTheta); + + vec3 H; + H.x = cos(phi) * sinTheta; + H.y = sin(phi) * sinTheta; + H.z = cosTheta; + + vec3 up = abs(N.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0); + vec3 tangent = normalize(cross(up, N)); + vec3 bitangent = cross(N, tangent); + + return normalize(tangent * H.x + bitangent * H.y + N * H.z); +} + +float GeometrySchlickGGX(float NdotV, float roughness) { + float a = roughness; + float k = (a * a) / 2.0; + return NdotV / (NdotV * (1.0 - k) + k); +} + +float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness) { + float NdotV = max(dot(N, V), 0.0); + float NdotL = max(dot(N, L), 0.0); + return GeometrySchlickGGX(NdotV, roughness) * GeometrySchlickGGX(NdotL, roughness); +} + +vec2 IntegrateBRDF(float NdotV, float roughness) { + vec3 V; + V.x = sqrt(1.0 - NdotV * NdotV); + V.y = 0.0; + V.z = NdotV; + + float A = 0.0; + float B = 0.0; + + vec3 N = vec3(0.0, 0.0, 1.0); + + const uint SAMPLE_COUNT = 1024u; + for (uint i = 0u; i < SAMPLE_COUNT; ++i) { + vec2 Xi = Hammersley(i, SAMPLE_COUNT); + vec3 H = ImportanceSampleGGX(Xi, N, roughness); + vec3 L = normalize(2.0 * dot(V, H) * H - V); + + float NdotL = max(L.z, 0.0); + float NdotH = max(H.z, 0.0); + float VdotH = max(dot(V, H), 0.0); + + if (NdotL > 0.0) { + float G = GeometrySmith(N, V, L, roughness); + float G_Vis = (G * VdotH) / (NdotH * NdotV); + float Fc = pow(1.0 - VdotH, 5.0); + + A += (1.0 - Fc) * G_Vis; + B += Fc * G_Vis; + } + } + A /= float(SAMPLE_COUNT); + B /= float(SAMPLE_COUNT); + return vec2(A, B); +} + +void fragment(out vec4 color) { + vec2 integratedBRDF = IntegrateBRDF(uv.x, uv.y); + color = vec4(integratedBRDF, 0.0, 1.0); +} diff --git a/resources/shaders/compute/entityClick.asl b/resources/shaders/compute/entityClick.asl new file mode 100644 index 00000000..6282f4ab --- /dev/null +++ b/resources/shaders/compute/entityClick.asl @@ -0,0 +1,8 @@ +perFrame mat4 projection; +perFrame mat4 view; +perDraw mat4 model; +perDraw int entityId; + +vec4 vertex(vec3 inPos, vec3 inNormal, vec2 inUV) { return projection * view * model * vec4(inPos, 1.0f); } + +void fragment(out int eid) { eid = entityId; } diff --git a/resources/shaders/compute/equiToCubemap.asl b/resources/shaders/compute/equiToCubemap.asl new file mode 100644 index 00000000..115ce2ad --- /dev/null +++ b/resources/shaders/compute/equiToCubemap.asl @@ -0,0 +1,26 @@ +perFrame mat4 uProjection; +perFrame mat4 uView; +perFrame sampler2D uEquirectangularMap; + +perVertex vec3 localPos; + +vec4 vertex(vec3 iPos, vec3 iNormal, vec2 iUV) { + localPos = iPos; + // Add small dependency between vertex and normal/uv to avoid compilation error + localPos += iNormal / 10000.0f + vec3(iUV,0) / 10000.0f; + return uProjection * uView * vec4(localPos, 1.0); +} + +const vec2 invAtan = vec2(0.1591, 0.3183); +vec2 sampleSphericalMap(vec3 v) { + // Z-up convention: azimuth in XY plane, elevation along Z axis + vec2 uv = vec2(atan(v.y, v.x), asin(v.z)); + uv *= invAtan; + uv += 0.5; + return uv; +} + +void fragment(out vec4 color) { + vec2 uv = sampleSphericalMap(normalize(localPos)); + color = vec4(texture(uEquirectangularMap, uv).rgb, 1.0); +} diff --git a/resources/shaders/compute/irradiance.asl b/resources/shaders/compute/irradiance.asl new file mode 100644 index 00000000..a660600e --- /dev/null +++ b/resources/shaders/compute/irradiance.asl @@ -0,0 +1,38 @@ +perFrame mat4 uProjection; +perFrame mat4 uView; +perFrame samplerCube uEnvironmentMap; + +perVertex vec3 localPos; + +vec4 vertex(vec3 iPos, vec3 iNormal, vec2 iUV) { + localPos = iPos; + localPos += iNormal / 10000.0f + vec3(iUV, 0) / 10000.0f; + return uProjection * uView * vec4(localPos, 1.0); +} + +const float PI = 3.14159265359; + +void fragment(out vec4 color) { + vec3 N = normalize(localPos); + + vec3 irradiance = vec3(0.0); + + vec3 up = vec3(0.0, 1.0, 0.0); + vec3 right = normalize(cross(up, N)); + up = normalize(cross(N, right)); + + float sampleDelta = 0.025; + float nrSamples = 0.0; + for (float phi = 0.0; phi < 2.0 * PI; phi += sampleDelta) { + for (float theta = 0.0; theta < 0.5 * PI; theta += sampleDelta) { + vec3 tangentSample = vec3(sin(theta) * cos(phi), sin(theta) * sin(phi), cos(theta)); + vec3 sampleVec = tangentSample.x * right + tangentSample.y * up + tangentSample.z * N; + + irradiance += texture(uEnvironmentMap, sampleVec).rgb * cos(theta) * sin(theta); + nrSamples++; + } + } + irradiance = PI * irradiance * (1.0 / float(nrSamples)); + + color = vec4(irradiance, 1.0); +} diff --git a/resources/shaders/compute/prefilter.asl b/resources/shaders/compute/prefilter.asl new file mode 100644 index 00000000..149a24bf --- /dev/null +++ b/resources/shaders/compute/prefilter.asl @@ -0,0 +1,70 @@ +perFrame mat4 uProjection; +perFrame mat4 uView; +perFrame samplerCube uEnvironmentMap; +perFrame float uRoughness; + +perVertex vec3 localPos; + +vec4 vertex(vec3 iPos, vec3 iNormal, vec2 iUV) { + localPos = iPos; + localPos += iNormal / 10000.0f + vec3(iUV, 0) / 10000.0f; + return uProjection * uView * vec4(localPos, 1.0); +} + +const float PI = 3.14159265359; + +float RadicalInverse_VdC(uint bits) { + bits = (bits << 16u) | (bits >> 16u); + bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u); + bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u); + bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u); + bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u); + return float(bits) * 2.3283064365386963e-10; +} + +vec2 Hammersley(uint i, uint N) { + return vec2(float(i) / float(N), RadicalInverse_VdC(i)); +} + +vec3 ImportanceSampleGGX(vec2 Xi, vec3 N, float roughness) { + float a = roughness * roughness; + + float phi = 2.0 * PI * Xi.x; + float cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (a * a - 1.0) * Xi.y)); + float sinTheta = sqrt(1.0 - cosTheta * cosTheta); + + vec3 H; + H.x = cos(phi) * sinTheta; + H.y = sin(phi) * sinTheta; + H.z = cosTheta; + + vec3 up = abs(N.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0); + vec3 tangent = normalize(cross(up, N)); + vec3 bitangent = cross(N, tangent); + + return normalize(tangent * H.x + bitangent * H.y + N * H.z); +} + +void fragment(out vec4 color) { + vec3 N = normalize(localPos); + vec3 R = N; + vec3 V = R; + + const uint SAMPLE_COUNT = 1024u; + float totalWeight = 0.0; + vec3 prefilteredColor = vec3(0.0); + for (uint i = 0u; i < SAMPLE_COUNT; ++i) { + vec2 Xi = Hammersley(i, SAMPLE_COUNT); + vec3 H = ImportanceSampleGGX(Xi, N, uRoughness); + vec3 L = normalize(2.0 * dot(V, H) * H - V); + + float NdotL = max(dot(N, L), 0.0); + if (NdotL > 0.0) { + prefilteredColor += texture(uEnvironmentMap, L).rgb * NdotL; + totalWeight += NdotL; + } + } + prefilteredColor = prefilteredColor / totalWeight; + + color = vec4(prefilteredColor, 1.0); +} diff --git a/resources/shaders/drawer/line.asl b/resources/shaders/drawer/line.asl new file mode 100644 index 00000000..1c96d768 --- /dev/null +++ b/resources/shaders/drawer/line.asl @@ -0,0 +1,12 @@ +perFrame mat4 uView; +perFrame mat4 uProjection; + +perVertex vec4 vColor; + +vec4 vertex(vec3 iPos, vec4 iColor) { + vec4 pos = uProjection * uView * vec4(iPos, 1.0); + vColor = iColor; + return vec4(pos.xyz, pos.w); +} + +void fragment(out vec4 color) { color = vColor; } diff --git a/resources/shaders/drawer/point.asl b/resources/shaders/drawer/point.asl new file mode 100644 index 00000000..7c11932e --- /dev/null +++ b/resources/shaders/drawer/point.asl @@ -0,0 +1,14 @@ +perFrame mat4 uView; +perFrame mat4 uProjection; + +perVertex vec4 vColor; + +vec4 vertex(vec3 iPos, vec4 iColor) { + vec4 pos = uProjection * uView * vec4(iPos, 1.0); + vColor = iColor; + gl_PointSize = 5.0; + return pos; +} + +void fragment(out vec4 color) { color = vColor; } + diff --git a/resources/shaders/fastRenderer/fastRenderer.asl b/resources/shaders/fastRenderer/fastRenderer.asl new file mode 100644 index 00000000..d4679a14 --- /dev/null +++ b/resources/shaders/fastRenderer/fastRenderer.asl @@ -0,0 +1,24 @@ +perFrame mat4 uView; +perFrame mat4 uProjection; +perDraw mat4 uModel; +perDraw vec3 uAlbedo; +perDraw sampler2D uAlbedoTexture; + +perVertex vec2 vUV; + +vec4 vertex(vec3 iVertex, vec3 iNormal, vec2 iUV) { + vUV = iUV; + float dummyNormal = iNormal.x / 10000.0f + 1.0f;// To avoid being optimized out + return uProjection * uView * uModel * vec4(iVertex, 1.0f) * dummyNormal; +} + +void fragment(out vec4 color) { + if (uAlbedo.x < 0.0f) { + color = texture(uAlbedoTexture, vUV).rgba; + if (color.a < 0.1) + discard; + return; + } + + color = vec4(uAlbedo, 1.0f); +} diff --git a/resources/shaders/grid/grid.asl b/resources/shaders/grid/grid.asl new file mode 100644 index 00000000..2a5939fb --- /dev/null +++ b/resources/shaders/grid/grid.asl @@ -0,0 +1,28 @@ +perFrame mat4 uView; +perFrame mat4 uProjection; +perFrame vec3 uCamPos; + +perVertex vec4 vColor; +perVertex vec3 vFragPos; + +vec4 vertex(vec3 iPos, vec4 iColor, float iWidth) { + vec4 pos = uProjection * uView * vec4(iPos, 1.0); + vColor = iColor; + vFragPos = iPos; + return vec4(pos.xyz, pos.w); +} + +void fragment(out vec4 color) { + float dist = length(uCamPos - vFragPos) + 0.0001; + color = vColor; + + // Parameters for controlling the fade effect + float fadeStart = 20.0; // Distance where the fading starts + float fadeEnd = 50.0; // Distance where the grid is fully faded/transparent + + // Clamped ratio between where fading starts and where it ends + float fadeRatio = clamp((dist - fadeStart) / (fadeEnd - fadeStart), 0.0, 1.0); + + // Apply fade falloff + color.a *= 1.0 - fadeRatio; +} diff --git a/resources/shaders/pbrRenderer/omniShadow.asl b/resources/shaders/pbrRenderer/omniShadow.asl new file mode 100644 index 00000000..b5611b61 --- /dev/null +++ b/resources/shaders/pbrRenderer/omniShadow.asl @@ -0,0 +1,32 @@ +perPass mat4 model; +perPass vec3 lightPos; +perPass float far_plane; +perPass mat4 shadowMatrices[6]; + +perVertex vec4 FragPos; + +void vertex(in vec3 inPosition, in vec3 inNormal, in vec2 inTexCoord, out vec4 position) { position = model * vec4(inPosition, 1.0); } + +void geometry(in(triangles), out(triangle_strip, max_vertices = 18)) { + for (int face = 0; face < 6; face++) { + gl_Layer = face; // built-in variable that specifies to which face we render. + for (int i = 0; i < 3; i++) // for each triangle vertex + { + FragPos = gl_in[i].gl_Position; + gl_Position = shadowMatrices[face] * FragPos; + EmitVertex(); + } + EndPrimitive(); + } +} + +void fragment(out vec4 color, out float depth) { + // get distance between fragment and light source + float lightDistance = length(FragPos.xyz - lightPos); + + // map to [0;1] range by dividing by far_plane + lightDistance = lightDistance / far_plane; + + // write this as modified depth + depth = lightDistance; +} diff --git a/resources/shaders/pbrRenderer/pbrRenderer.asl b/resources/shaders/pbrRenderer/pbrRenderer.asl new file mode 100644 index 00000000..6eda2c7a --- /dev/null +++ b/resources/shaders/pbrRenderer/pbrRenderer.asl @@ -0,0 +1,256 @@ +struct Material { + float metallic; + float roughness; + float ao; + vec3 albedo; +}; +struct PointLight { + vec3 position; + vec3 intensity; +}; +struct DirectionalLight { + vec3 direction; + vec3 intensity; +}; +const float PI = 3.14159265359; + +//----- Per Frame -----// +perFrame mat4 projection; +perFrame mat4 view; +perFrame mat4 directionalLightMatrix; +perFrame vec3 camPos; +perFrame float p0;// Dummy + +perFrame int numPointLights; +perFrame int numDirectionalLights; +perFrame int numEnvironmentLights; + +// Point light +#define MAX_NUM_POINT_LIGHTS 10 +perFrame PointLight pointLights[MAX_NUM_POINT_LIGHTS]; + +// Directional light - Shadow map +perFrame DirectionalLight directionalLight; +perFrame sampler2D directionalShadowMap; + +// Environment light - IBL +perFrame mat3 environmentLightOri; +perFrame samplerCube prefilterMap; +perFrame samplerCube irradianceMap; +perFrame sampler2D brdfLUT; + +//----- Per Draw -----// +perDraw mat4 model; +perDraw mat4 invModel; +perDraw Material material; +perDraw sampler2D albedoTexture; +perDraw sampler2D metallicTexture; +perDraw sampler2D roughnessTexture; +perDraw sampler2D aoTexture; +perDraw sampler2D normalTexture; + +perVertex vec3 worldPos; +perVertex vec4 posDirectionalLightSpace; +perVertex vec3 normal; +perVertex vec2 texCoord; + +//----- Vertex Shader -----// +vec4 vertex(vec3 inPosition, vec3 inNormal, vec2 inTexCoord) { + vec4 coord = model * vec4(inPosition, 1.0f); + vec4 coordImage = view * coord; + + // float z = coordImage.z; + // coordImage /= z; + // float r = sqrt(coordImage.x*coordImage.x + coordImage.y*coordImage.y); + // float L = 1 + r*0.103689 + r*r*0.00487908 + r*r*r*0.00116894 + r*r*r*r*0.000941614; + // coordImage.x *= L; + // coordImage.y *= L; + // coordImage *= z; + + worldPos = coord.xyz; + posDirectionalLightSpace = directionalLightMatrix * vec4(worldPos, 1.0f); + normal = mat3(transpose(invModel)) * inNormal; + texCoord = inTexCoord; + + return projection * coordImage; +} + +//----- Fragment Shader -----// +vec3 calcLightContribution(vec3 L, vec3 N, vec3 V, vec3 radiance, vec3 albedo, float metallic, float roughness, vec3 F0); +float directionalShadowCalculation(vec4 fragPosLightSpace, vec3 normal, vec3 lightDir); +float distributionGGX(vec3 N, vec3 H, float roughness); // Distribution of microsurface normals +float geometrySmith(vec3 N, vec3 V, vec3 L, float roughness); // Microsurface shadowing +vec3 fresnelSchlick(float cosTheta, vec3 F0); // Reflect-refract ratio +vec3 fresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness); + +void fragment(out vec4 outColor) { + // Calculate material parameters + vec3 albedo = material.albedo; + if (albedo.x < 0.0f) + albedo = pow(texture(albedoTexture, texCoord).rgb, vec3(2.2)); + + float metallic = material.metallic; + if (metallic < 0.0f) + metallic = texture(metallicTexture, texCoord).r; + + float roughness = material.roughness; + if (roughness < 0.0f) + roughness = texture(roughnessTexture, texCoord).r; + + float ao = material.ao; + if (ao < 0.0f) + ao = texture(aoTexture, texCoord).r; + + // Calculate vectors + vec3 N = normalize(normal); + vec3 V = normalize(camPos - worldPos); + + // Fresnel coefficient + vec3 F0 = vec3(0.04); + F0 = mix(F0, albedo, metallic); + + vec3 Lo = vec3(0.0); + //----- Point lights -----// + for (int i = 0; i < numPointLights; i++) { + // Compute light direction + vec3 L = normalize(pointLights[i].position - worldPos); + + // Compute radiance + float dist = length(pointLights[i].position - worldPos); + float attenuation = 1.0 / (dist * dist); + vec3 radiance = pointLights[i].intensity * attenuation; + + float shadow = 0.0f; + Lo += calcLightContribution(L, N, V, radiance, albedo, metallic, roughness, F0) * (1.0f - shadow); + } + + //----- Directional light -----// + float shadow = 0.0f; + if (numDirectionalLights == 1) { + vec3 L = -directionalLight.direction; + vec3 radiance = directionalLight.intensity; + + shadow = directionalShadowCalculation(posDirectionalLightSpace, N, L); + Lo += calcLightContribution(L, N, V, radiance, albedo, metallic, roughness, F0) * (1.0f - shadow); + } + + //----- Environment light -----// + vec3 ambient = vec3(0.0f); + if (numEnvironmentLights == 1) { + // Diffuse IBL + vec3 F = fresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness); + vec3 kS = F; + vec3 kD = 1.0 - kS; + kD *= 1.0 - metallic; + vec3 irradiance = texture(irradianceMap, environmentLightOri * N).rgb; + vec3 diffuse = irradiance * albedo; + + // Specular IBL + const float MAX_REFLECTION_LOD = 4.0; + vec3 R = reflect(-V, N); + vec3 prefilteredColor = textureLod(prefilterMap, environmentLightOri * R, roughness * MAX_REFLECTION_LOD).rgb; + vec2 brdf = texture(brdfLUT, vec2(max(dot(environmentLightOri * N, V), 0.0), roughness)).rg; + vec3 specular = prefilteredColor * (F * brdf.x + brdf.y); + + ambient = (kD * diffuse + specular) * ao; + // Partially darken ambient in shadowed areas (indirect light is reduced but not eliminated) + ambient *= (1.0 - shadow * 0.5); + } else if (numEnvironmentLights == 0) { + ambient = vec3(0.03) * albedo * ao * (1.0 - shadow * 0.5); + } + + vec3 color = ambient + Lo; + + // HDR tonemapping + color = color / (color + vec3(1.0)); + // Gamma correction + color = pow(color, vec3(1.0 / 2.2)); + + outColor = vec4(color, 1.0f); +} + +vec3 calcLightContribution(vec3 L, vec3 N, vec3 V, vec3 radiance, vec3 albedo, float metallic, float roughness, vec3 F0) { + vec3 H = normalize(V + L); + + // Cook-torrance BRDF + float D = distributionGGX(N, H, roughness); + float G = geometrySmith(N, V, L, roughness); + vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0); + + vec3 num = D * G * F; + float denom = 4.0f * max(dot(N, V), 0.0) * max(dot(N, L), 0.0) + 0.0001; + vec3 specular = num / denom; + + vec3 kS = F; // Reflection/specular fraction + vec3 kD = vec3(1.0) - kS; // Refraction/diffuse fraction + kD *= 1.0 - metallic; + + // Add outgoing radiance to Lo + float NdotL = max(dot(N, L), 0.0); + return (kD * albedo / PI + specular) * radiance * NdotL; +} + +float directionalShadowCalculation(vec4 fragPosLightSpace, vec3 normal, vec3 lightDir) { + // Perspective divide (no-op for orthographic, but correct for general use) + vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w; + // Remap XY from [-1,1] to [0,1] UV range; Z is already in [0,1] (Vulkan NDC) + projCoords.xy = projCoords.xy * 0.5 + 0.5; + // Bias to reduce shadow acne (scale by surface angle to light) + float bias = max(0.01 * (1.0 - dot(normal, lightDir)), 0.0001); + float shadow = 0.0; + vec2 texelSize = 1.0 / vec2(textureSize(directionalShadowMap, 0)); + // 5x5 PCF kernel + for (int x = -2; x <= 2; ++x) { + for (int y = -2; y <= 2; ++y) { + float pcfDepth = texture(directionalShadowMap, projCoords.xy + vec2(x, y) * texelSize).r; + shadow += projCoords.z - bias > pcfDepth ? 1.0 : 0.0; + } + } + shadow /= 25.0; + + // No shadow outside the light frustum + if (projCoords.z > 1.0) + shadow = 0.0; + + return shadow; +} + +float distributionGGX(vec3 N, vec3 H, float roughness) { + // Trowbridge-Reitz GGX normal distribution function + float a = roughness * roughness; + float a2 = a * a; + float NdotH = max(dot(N, H), 0.0); + float NdotH2 = NdotH * NdotH; + + float num = a2; + float denom = (NdotH2 * (a2 - 1.0) + 1.0); + denom = PI * denom * denom; + + return num / denom; +} + +float geometrySchlickGGX(float NdotV, float roughness) { + // Compute k for direct lighting + float r = (roughness + 1.0); + float k = (r * r) / 8.0; + + float num = NdotV; + float denom = NdotV * (1.0 - k) + k; + return num / denom; +} + +float geometrySmith(vec3 N, vec3 V, vec3 L, float roughness) { + // Smith's method to take into account both view direction and light direction + float NdotV = max(dot(N, V), 0.0); + float NdotL = max(dot(N, L), 0.0); + float ggx2 = geometrySchlickGGX(NdotV, roughness); + float ggx1 = geometrySchlickGGX(NdotL, roughness); + + return ggx1 * ggx2; +} + +vec3 fresnelSchlick(float cosTheta, vec3 F0) { return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0); } + +vec3 fresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness) { + return F0 + (max(vec3(1.0 - roughness), F0) - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0); +} diff --git a/resources/shaders/pbrRenderer/shadow.asl b/resources/shaders/pbrRenderer/shadow.asl new file mode 100644 index 00000000..8d8d9c74 --- /dev/null +++ b/resources/shaders/pbrRenderer/shadow.asl @@ -0,0 +1,18 @@ +perFrame mat4 lightSpaceMatrix; +perDraw mat4 model; + +vec4 vertex(vec3 iPos, vec3 iNormal, vec2 iUV) { + vec4 pos = lightSpaceMatrix * model * vec4(iPos, 1.0); +#ifdef ATTA_OPENGL + // atta's orthographic outputs Z in [0,1] (Vulkan-style). + // OpenGL expects Z_ndc in [-1,1] and remaps to depth as d=(z+1)/2. + // Remap so the depth buffer stores the same [0,1] range that + // pbrRenderer.asl reads back via posDirectionalLightSpace.z. + pos.z = pos.z * 2.0 - 1.0; +#endif + return pos; +} + +// Depth-only pass: no color output needed. +// gl_FragCoord.z is automatically written to the depth buffer by the rasterizer. +void fragment() {} diff --git a/resources/shaders/phongRenderer/phongRenderer.asl b/resources/shaders/phongRenderer/phongRenderer.asl new file mode 100644 index 00000000..f09d4bec --- /dev/null +++ b/resources/shaders/phongRenderer/phongRenderer.asl @@ -0,0 +1,135 @@ +struct Material { + vec3 albedo; + float pad0; + float metallic; + float roughness; + float ao; +}; +struct PointLight { + vec3 position; + float pad0; + vec3 intensity; + float pad1; +}; +struct DirectionalLight { + vec3 direction; + float pad0; + vec3 intensity; + float pad1; +}; + +perFrame mat4 uView; +perFrame mat4 uProjection; +perFrame vec3 uViewPos; +perFrame float pad0; +perFrame vec3 uAmbientColor; +perFrame float pad1; +perFrame float uAmbientStrength; +perFrame float uDiffuseStrength; +perFrame float uSpecularStrength; +perFrame bool uHasDirectionalLight; +perFrame DirectionalLight uDirectionalLight; + +#define MAX_POINT_LIGHTS 10 +perFrame int uNumPointLights; +perFrame PointLight uPointLights[MAX_POINT_LIGHTS]; + +perDraw mat4 uModel; +perDraw mat4 uInvModel; +perDraw Material uMaterial; +perDraw sampler2D uAlbedoTexture; +perDraw sampler2D uMetallicTexture; +perDraw sampler2D uRoughnessTexture; +perDraw sampler2D uAoTexture; + +perVertex vec3 vFragPos; +perVertex vec3 vNormal; +perVertex vec2 vUV; + +vec3 calcDirectionalLight(DirectionalLight light, vec3 normal, vec3 viewDir); +vec3 calcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir); + +vec4 vertex(vec3 iVertex, vec3 iNormal, vec2 iUV) { + vec4 coord = uModel * vec4(iVertex, 1.0f); + vFragPos = coord.xyz; + vNormal = mat3(transpose(uInvModel)) * iNormal; + vUV = iUV; + return uProjection * uView * coord; +} + +vec3 albedo; +float metallic; +float roughness; +float ao; +float alpha; +void fragment(out vec4 color) { + // Initialize material + albedo = uMaterial.albedo; + alpha = 1.0f; + if (albedo.x < 0.0f) { + albedo = texture(uAlbedoTexture, vUV).rgb; + alpha = texture(uAlbedoTexture, vUV).a; + } + + if (alpha < 0.1) + discard; + + metallic = uMaterial.metallic; + if (metallic < 0.0f) + metallic = texture(uMetallicTexture, vUV).r; + + roughness = uMaterial.roughness; + if (roughness < 0.0f) + roughness = texture(uRoughnessTexture, vUV).r; + + ao = uMaterial.ao; + if (ao < 0.0f) + ao = texture(uAoTexture, vUV).r; + + vec3 result = vec3(0.0f); + + // Directional light + vec3 norm = normalize(vNormal); + vec3 viewDir = normalize(uViewPos - vFragPos); + if (uHasDirectionalLight) + result += calcDirectionalLight(uDirectionalLight, norm, viewDir); + + // Point light + if (uNumPointLights > 0 && uNumPointLights <= MAX_POINT_LIGHTS) + for (int i = 0; i < uNumPointLights; i++) + result += calcPointLight(uPointLights[i], norm, vFragPos, viewDir); + + result += uAmbientColor * uAmbientStrength * albedo; + result *= ao; + + color = vec4(result, 1.0f); +} + +vec3 calcDirectionalLight(DirectionalLight light, vec3 normal, vec3 viewDir) { + vec3 lightDir = normalize(-light.direction); + // diffuse shading + float diff = max(dot(normal, lightDir), 0.0); + // specular shading + vec3 reflectDir = reflect(-lightDir, normal); + float spec = pow(max(dot(viewDir, reflectDir), 0.0), 3.0f); + // combine results + vec3 diffuse = albedo * uDiffuseStrength * diff; + vec3 specular = vec3(uSpecularStrength * spec * metallic); + return light.intensity * (diffuse + specular); +} + +vec3 calcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir) { + vec3 lightDir = normalize(light.position - fragPos); + // diffuse shading + float diff = max(dot(normal, lightDir), 0.0); + // specular shading + vec3 reflectDir = reflect(-lightDir, normal); + float spec = pow(max(dot(viewDir, reflectDir), 0.0), 5.0f); + // attenuation + float dist = length(light.position - fragPos); + float attenuation = 1.0 / (2.0f + 2.0f * dist + 2.0 * (dist * dist)); + // combine results + vec3 diffuse = albedo * uDiffuseStrength * diff; + vec3 specular = vec3(uSpecularStrength * spec * metallic); + return light.intensity * attenuation * (diffuse + specular); +} diff --git a/src/atta/component/components/environmentLight.cpp b/src/atta/component/components/environmentLight.cpp index d7eb3f71..c1b06519 100644 --- a/src/atta/component/components/environmentLight.cpp +++ b/src/atta/component/components/environmentLight.cpp @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: 2020-2026 Breno Cunha Queiroz #include +#include +#include namespace atta::component { @@ -16,8 +18,13 @@ ComponentDescription& TypedComponentRegistry::getDescription() } EnvironmentLight::EnvironmentLight() { - if (TypedComponentRegistry::description->attributeDescriptions[0].options.size()) - sid = StringId(*(TypedComponentRegistry::description->attributeDescriptions[0].options.begin())); + // Set first .hdr image as default image for environment lights + for (StringId resourceSid : resource::getResources()) { + if (resourceSid.getString().find(".hdr") != std::string::npos) { + sid = resourceSid; + break; + } + } } } // namespace atta::component diff --git a/src/atta/event/events/uiCameraComponent.h b/src/atta/event/events/uiCameraComponent.h index b0408fae..76b9872c 100644 --- a/src/atta/event/events/uiCameraComponent.h +++ b/src/atta/event/events/uiCameraComponent.h @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: 2020-2026 Breno Cunha Queiroz #pragma once -#include #include +#include namespace atta::event { diff --git a/src/atta/graphics/CMakeLists.txt b/src/atta/graphics/CMakeLists.txt index 2778444b..59442110 100644 --- a/src/atta/graphics/CMakeLists.txt +++ b/src/atta/graphics/CMakeLists.txt @@ -17,11 +17,16 @@ set(ATTA_GRAPHICS_MODULE_SOURCE cameras/perspectiveCamera.cpp cameras/orthographicCamera.cpp + compute/brdfLut.cpp compute/entityClick.cpp + compute/equiToCubemap.cpp + compute/irradiance.cpp + compute/prefilter.cpp renderers/common/drawerPipeline.cpp renderers/common/gridPipeline.cpp renderers/common/selectedPipeline.cpp + renderers/common/skyboxPipeline.cpp renderers/fastRenderer.cpp renderers/pbrRenderer.cpp renderers/phongRenderer.cpp diff --git a/src/atta/graphics/apis/openGL/framebuffer.cpp b/src/atta/graphics/apis/openGL/framebuffer.cpp index fea8ece0..b99c8026 100644 --- a/src/atta/graphics/apis/openGL/framebuffer.cpp +++ b/src/atta/graphics/apis/openGL/framebuffer.cpp @@ -54,23 +54,16 @@ void Framebuffer::bindAttachments() { std::shared_ptr image = std::dynamic_pointer_cast(_images[i]); bool isColor = ((int)i != _depthAttachmentIndex) && ((int)i != _stencilAttachmentIndex); if ((int)i == _depthAttachmentIndex) { - if (!image->isCubemap()) - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, image->getHandle(), 0); - else - glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, image->getHandle(), 0); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, image->getHandle(), 0); } if ((int)i == _stencilAttachmentIndex) { - if (!image->isCubemap()) - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_TEXTURE_2D, image->getHandle(), 0); - else - glFramebufferTexture(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, image->getHandle(), 0); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_TEXTURE_2D, image->getHandle(), 0); } - if (isColor) { - if (!image->isCubemap()) - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + colorIndex, GL_TEXTURE_2D, image->getHandle(), 0); + if (image->isCubemap()) + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + colorIndex, GL_TEXTURE_CUBE_MAP_POSITIVE_X, image->getHandle(), 0); else - glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + colorIndex, image->getHandle(), 0); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + colorIndex, GL_TEXTURE_2D, image->getHandle(), 0); colorIndex++; } } @@ -120,9 +113,10 @@ void Framebuffer::resize(uint32_t width, uint32_t height, bool forceRecreate) { glBindFramebuffer(GL_FRAMEBUFFER, _id); //----- Update attachment images -----// - // Resize attachments + // Only resize framebuffer-owned images (not pre-created ones passed via attachment.image) for (unsigned i = 0; i < _attachments.size(); i++) - _images[i]->resize(width, height, forceRecreate); + if (!_attachments[i].image) + _images[i]->resize(width, height, forceRecreate); // Bind attachments bindAttachments(); @@ -135,7 +129,38 @@ void Framebuffer::resize(uint32_t width, uint32_t height, bool forceRecreate) { glBindFramebuffer(GL_FRAMEBUFFER, 0); } -//------------------------------------------------// +void Framebuffer::setLayer(uint32_t layer) { + if (_images.size() != 1 || _colorAttachmentIndex == -1) { + LOG_ERROR( + "gfx::gl::Framebuffer", + "Error when setting layer for framebuffer [w]$0[]. It is only possible to set layer for a framebuffer with one cubemap color attachment", + _debugName); + return; + } + // Bind framebuffer + glBindFramebuffer(GL_FRAMEBUFFER, _id); + + // Update layer + std::shared_ptr image = std::dynamic_pointer_cast(_images[0]); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_POSITIVE_X + layer, image->getHandle(), 0); + + // Unbind framebuffer + glBindFramebuffer(GL_FRAMEBUFFER, 0); +} + +void Framebuffer::setLayerAndMip(uint32_t layer, uint32_t mipLevel) { + if (_images.size() != 1 || _colorAttachmentIndex == -1) { + LOG_ERROR("gfx::gl::Framebuffer", + "Error when setting layer+mip for framebuffer [w]$0[]. It is only possible to set layer+mip for a framebuffer with one cubemap " + "color attachment", + _debugName); + return; + } + glBindFramebuffer(GL_FRAMEBUFFER, _id); + std::shared_ptr image = std::dynamic_pointer_cast(_images[0]); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_POSITIVE_X + layer, image->getHandle(), mipLevel); + glBindFramebuffer(GL_FRAMEBUFFER, 0); +} //---------- Atta to OpenGL conversions ----------// //------------------------------------------------// GLenum Framebuffer::convertDepthAttachmentType(Image::Format format) { diff --git a/src/atta/graphics/apis/openGL/framebuffer.h b/src/atta/graphics/apis/openGL/framebuffer.h index 035f312b..1e6a1eb3 100644 --- a/src/atta/graphics/apis/openGL/framebuffer.h +++ b/src/atta/graphics/apis/openGL/framebuffer.h @@ -17,6 +17,9 @@ class Framebuffer final : public gfx::Framebuffer { void resize(uint32_t width, uint32_t height, bool forceRecreate = false) override; + void setLayer(uint32_t layer) override; + void setLayerAndMip(uint32_t layer, uint32_t mipLevel) override; + static GLenum convertDepthAttachmentType(Image::Format format); private: diff --git a/src/atta/graphics/apis/openGL/image.cpp b/src/atta/graphics/apis/openGL/image.cpp index 65352832..b93b5526 100644 --- a/src/atta/graphics/apis/openGL/image.cpp +++ b/src/atta/graphics/apis/openGL/image.cpp @@ -13,16 +13,29 @@ Image::~Image() { } void Image::write(uint8_t* data) { - if (!_isCubemap) { - GLenum dataType = Image::convertDataType(_format); - GLenum internalFormat = Image::convertInternalFormat(_format); - GLenum format = Image::convertFormat(_format); + GLenum dataType = Image::convertDataType(_format); + GLenum internalFormat = Image::convertInternalFormat(_format); + GLenum format = Image::convertFormat(_format); + if (!_isCubemap) { glBindTexture(GL_TEXTURE_2D, _id); glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, _width, _height, format, dataType, data); + if (_mipLevels > 1) + glGenerateMipmap(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, 0); - } else - LOG_WARN("gfx::gl::Image", "Writing to cubemap image is not implemented yet. Image debug name: [w]$0[]", _debugName); + } else { + glBindTexture(GL_TEXTURE_CUBE_MAP, _id); + + // Calculate the size (in bytes) of one face + uint32_t faceSize = _width * _height * Image::getPixelSize(_format); + + // Update each of the six faces + for (unsigned int i = 0; i < 6; i++) + glTexSubImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, 0, 0, _width, _height, format, dataType, data + (i * faceSize)); + if (_mipLevels > 1) + glGenerateMipmap(GL_TEXTURE_CUBE_MAP); + glBindTexture(GL_TEXTURE_CUBE_MAP, 0); + } } std::vector Image::read(vec2i offset, vec2i size) { @@ -73,7 +86,6 @@ void Image::resize(uint32_t width, uint32_t height, bool forceRecreate) { for (unsigned int i = 0; i < 6; i++) glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, internalFormat, _width, _height, 0, format, dataType, _data); - // glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_DEPTH_COMPONENT, _width, _height, 0, GL_DEPTH_COMPONENT, GL_FLOAT, nullptr); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, wrapMode); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, wrapMode); @@ -84,6 +96,10 @@ void Image::resize(uint32_t width, uint32_t height, bool forceRecreate) { if (_mipLevels > 1) glGenerateMipmap(GL_TEXTURE_CUBE_MAP); } + + // Update the texture data if _data is set + if (_data) + write(_data); } void Image::setFramebufferRead(std::function(vec2i, vec2i)> framebufferRead) { _framebufferRead = framebufferRead; } diff --git a/src/atta/graphics/apis/openGL/image.h b/src/atta/graphics/apis/openGL/image.h index 792bd2b9..343e30e3 100644 --- a/src/atta/graphics/apis/openGL/image.h +++ b/src/atta/graphics/apis/openGL/image.h @@ -21,6 +21,7 @@ class Image final : public gfx::Image { void write(uint8_t* data) override; std::vector read(vec2i offset = {0, 0}, vec2i size = {0, 0}) override; void resize(uint32_t width, uint32_t height, bool forceRecreate = false) override; + void prepareForSampling() override {} // No-op: OpenGL has no explicit layout transitions OpenGLId getHandle() const { return _id; } diff --git a/src/atta/graphics/apis/openGL/openGLAPI.cpp b/src/atta/graphics/apis/openGL/openGLAPI.cpp index 9ca863f2..4a9bf64f 100644 --- a/src/atta/graphics/apis/openGL/openGLAPI.cpp +++ b/src/atta/graphics/apis/openGL/openGLAPI.cpp @@ -68,6 +68,12 @@ void OpenGLAPI::startUp() { nullptr); #endif // ATTA_DEBUG_BUILD + // Enable cubemap seamless if supported (desktop OpenGL only, not available in WebGL/GLES) +#ifndef ATTA_OS_WEB + if (_apiVersion >= 320) + glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS); +#endif + glEnable(GL_DEPTH_TEST); } diff --git a/src/atta/graphics/apis/openGL/pipeline.cpp b/src/atta/graphics/apis/openGL/pipeline.cpp index 09777230..8fcd173a 100644 --- a/src/atta/graphics/apis/openGL/pipeline.cpp +++ b/src/atta/graphics/apis/openGL/pipeline.cpp @@ -15,9 +15,25 @@ Pipeline::Pipeline(const Pipeline::CreateInfo& info) : gfx::Pipeline(info) { Pipeline::~Pipeline() {} -void Pipeline::begin() { _shader->bind(); } +void Pipeline::begin() { + // Disable depth testing + if (!_enableDepthTest) { + glDisable(GL_DEPTH_TEST); + glDepthMask(GL_FALSE); + } + + _shader->bind(); +} -void Pipeline::end() { _shader->unbind(); } +void Pipeline::end() { + _shader->unbind(); + + // Restore depth testing + if (!_enableDepthTest) { + glEnable(GL_DEPTH_TEST); + glDepthMask(GL_TRUE); + } +} void Pipeline::resize(uint32_t width, uint32_t height) { _renderPass->getFramebuffer()->resize(width, height); } @@ -29,24 +45,6 @@ void Pipeline::renderMesh(StringId meshSid, size_t numVertices) { LOG_WARN("gfx::gl::Pipeline", "Could not render mesh [w]$0[], mesh not found", meshSid); } -void Pipeline::renderQuad() { - renderMesh("atta::gfx::quad"); - if (_renderPass->getFramebuffer()->hasDepthAttachment()) - glClear(GL_DEPTH_BUFFER_BIT); -} - -void Pipeline::renderQuad3() { - renderMesh("atta::gfx::quad3"); - if (_renderPass->getFramebuffer()->hasDepthAttachment()) - glClear(GL_DEPTH_BUFFER_BIT); -} - -void Pipeline::renderCube() { - renderMesh("atta::gfx::cube"); - if (_renderPass->getFramebuffer()->hasDepthAttachment()) - glClear(GL_DEPTH_BUFFER_BIT); -} - void* Pipeline::getImGuiTexture() const { return reinterpret_cast(std::static_pointer_cast(_renderPass->getFramebuffer()->getImage(0))->getImGuiImage()); } diff --git a/src/atta/graphics/apis/openGL/pipeline.h b/src/atta/graphics/apis/openGL/pipeline.h index 837c5ab6..d4963419 100644 --- a/src/atta/graphics/apis/openGL/pipeline.h +++ b/src/atta/graphics/apis/openGL/pipeline.h @@ -18,9 +18,6 @@ class Pipeline final : public gfx::Pipeline { void resize(uint32_t width, uint32_t height) override; void renderMesh(StringId meshSid, size_t numVertices = 0) override; - void renderQuad() override; - void renderQuad3() override; - void renderCube() override; void* getImGuiTexture() const override; diff --git a/src/atta/graphics/apis/openGL/shader.cpp b/src/atta/graphics/apis/openGL/shader.cpp index a5a71514..8e3ccca6 100644 --- a/src/atta/graphics/apis/openGL/shader.cpp +++ b/src/atta/graphics/apis/openGL/shader.cpp @@ -33,11 +33,13 @@ std::string Shader::generateApiCode(ShaderType type, std::string iCode) { apiCode = "#version 330 core\n"; } else if (openGLVersion >= 300) { apiCode = "#version 300 es\n" - "precision mediump float;\n"; + "precision highp float;\n" + "precision highp int;\n"; } else { LOG_ERROR("gfx::gl::Shader", "Compiling shaders for OpenGL version [w]$0[] is not supported", openGLVersion); return ""; } + apiCode += "#define ATTA_OPENGL 1\n"; apiCode += iCode; // Replace perFrame/perDraw @@ -70,6 +72,11 @@ std::string Shader::generateApiCode(ShaderType type, std::string iCode) { // Move to the next match start = match.suffix().first; } + + // Strip f/F suffix from float literals for GLSL ES (e.g. 1.0f -> 1.0) + if (openGLVersion < 330) + apiCode = std::regex_replace(apiCode, std::regex(R"((\d+\.\d*)[fF]\b)"), "$1"); + // LOG_DEBUG("gfx::gl::Shader", "Generated API code for shader [g]$1[]\n[w]$0[]", apiCode, _file.string()); return apiCode; @@ -190,7 +197,11 @@ void Shader::setImage(const char* name, std::shared_ptr inImage) { // Activate texture unit glActiveTexture(GL_TEXTURE0 + imgUnit); - glBindTexture(GL_TEXTURE_2D, image->getHandle()); + // Bind the texture with the proper target + if (image->isCubemap()) + glBindTexture(GL_TEXTURE_CUBE_MAP, image->getHandle()); + else + glBindTexture(GL_TEXTURE_2D, image->getHandle()); } void Shader::setCubemap(const char* name, std::shared_ptr inImage) { diff --git a/src/atta/graphics/apis/vulkan/framebuffer.cpp b/src/atta/graphics/apis/vulkan/framebuffer.cpp index 78673ea4..ed73d06d 100644 --- a/src/atta/graphics/apis/vulkan/framebuffer.cpp +++ b/src/atta/graphics/apis/vulkan/framebuffer.cpp @@ -20,8 +20,15 @@ void Framebuffer::bind(bool clear) { } } void Framebuffer::unbind() { - for (size_t i = 0; i < _images.size(); i++) - std::dynamic_pointer_cast(_images[i])->setLayout(VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); + for (size_t i = 0; i < _images.size(); i++) { + auto vkImage = std::dynamic_pointer_cast(_images[i]); + VkImageLayout layout; + if (gfx::Image::isDepthFormat(vkImage->getSupportedFormat()) || gfx::Image::isStencilFormat(vkImage->getSupportedFormat())) + layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + else + layout = _isSwapchain ? VK_IMAGE_LAYOUT_PRESENT_SRC_KHR : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + vkImage->setLayout(layout); + } } void Framebuffer::resize(uint32_t width, uint32_t height, bool forceRecreate) { @@ -47,6 +54,93 @@ void Framebuffer::resize(uint32_t width, uint32_t height, bool forceRecreate) { } } +void Framebuffer::setLayer(uint32_t layer) { + // For now, support only framebuffers with a single attachment that is a cubemap + if (_images.size() != 1) { + LOG_ERROR("gfx::vk::Framebuffer", "setLayer is only supported for framebuffers with one attachment, not for framebuffer [w]$0[]", _debugName); + return; + } + + // Get the image and ensure it's a cubemap + std::shared_ptr image = _images[0]; + std::shared_ptr vkImage = std::dynamic_pointer_cast(image); + if (!vkImage->isCubemap()) { + LOG_ERROR("gfx::vk::Framebuffer", "setLayer called on a non-cubemap image for framebuffer [w]$0[]", _debugName); + return; + } + + // Destroy the existing framebuffer if it exists + if (_framebuffer != VK_NULL_HANDLE) { + vkDestroyFramebuffer(_device->getHandle(), _framebuffer, nullptr); + _framebuffer = VK_NULL_HANDLE; + } + + // Retrieve the image view for the desired cubemap face (layer) + VkImageView faceView = vkImage->getCubemapImageViewHandle(layer); + if (faceView == VK_NULL_HANDLE) { + LOG_ERROR("gfx::vk::Framebuffer", "Failed to get cubemap image view for layer %d", layer); + return; + } + + // Create a new framebuffer with this per-face view + std::vector attachments = {faceView}; + + VkFramebufferCreateInfo fbInfo{}; + fbInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; + fbInfo.renderPass = _renderPass->getHandle(); + fbInfo.attachmentCount = static_cast(attachments.size()); + fbInfo.pAttachments = attachments.data(); + fbInfo.width = _width; + fbInfo.height = _height; + fbInfo.layers = 1; + + if (vkCreateFramebuffer(_device->getHandle(), &fbInfo, nullptr, &_framebuffer) != VK_SUCCESS) { + LOG_ERROR("gfx::vk::Framebuffer", "Failed to create framebuffer for cubemap layer %d", layer); + return; + } +} + +void Framebuffer::setLayerAndMip(uint32_t layer, uint32_t mipLevel) { + if (_images.size() != 1) { + LOG_ERROR("gfx::vk::Framebuffer", "setLayerAndMip is only supported for framebuffers with one attachment, not for framebuffer [w]$0[]", + _debugName); + return; + } + + std::shared_ptr vkImage = std::dynamic_pointer_cast(_images[0]); + if (!vkImage->isCubemap()) { + LOG_ERROR("gfx::vk::Framebuffer", "setLayerAndMip called on a non-cubemap image for framebuffer [w]$0[]", _debugName); + return; + } + + if (_framebuffer != VK_NULL_HANDLE) { + vkDestroyFramebuffer(_device->getHandle(), _framebuffer, nullptr); + _framebuffer = VK_NULL_HANDLE; + } + + VkImageView faceView = vkImage->getCubemapFaceMipImageViewHandle(layer, mipLevel); + if (faceView == VK_NULL_HANDLE) { + LOG_ERROR("gfx::vk::Framebuffer", "Failed to get cubemap face+mip image view for layer $0 mip $1", layer, mipLevel); + return; + } + + std::vector attachments = {faceView}; + + VkFramebufferCreateInfo fbInfo{}; + fbInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; + fbInfo.renderPass = _renderPass->getHandle(); + fbInfo.attachmentCount = static_cast(attachments.size()); + fbInfo.pAttachments = attachments.data(); + fbInfo.width = _width; + fbInfo.height = _height; + fbInfo.layers = 1; + + if (vkCreateFramebuffer(_device->getHandle(), &fbInfo, nullptr, &_framebuffer) != VK_SUCCESS) { + LOG_ERROR("gfx::vk::Framebuffer", "Failed to create framebuffer for cubemap layer $0 mip $1", layer, mipLevel); + return; + } +} + VkFramebuffer Framebuffer::getHandle() const { return _framebuffer; } std::shared_ptr Framebuffer::getDevice() const { return _device; } std::shared_ptr Framebuffer::getRenderPass() const { return _renderPass; } @@ -58,10 +152,17 @@ void Framebuffer::create(std::shared_ptr renderPass) { if (_framebuffer != VK_NULL_HANDLE) vkDestroyFramebuffer(_device->getHandle(), _framebuffer, nullptr); - // Get image views + // Get image views (framebuffer attachments must have levelCount == 1) std::vector attachments; - for (std::shared_ptr image : _images) - attachments.push_back(std::dynamic_pointer_cast(image)->getImageViewHandle()); + for (std::shared_ptr image : _images) { + auto vkImage = std::dynamic_pointer_cast(image); + // Cubemap images: use face 0 mip 0 as a valid single-mip placeholder. + // setLayer() or setLayerAndMip() will replace it with the correct view before rendering. + if (vkImage->isCubemap()) + attachments.push_back(vkImage->getCubemapFaceMipImageViewHandle(0, 0)); + else + attachments.push_back(vkImage->getImageViewHandle()); + } // Create framebuffer VkFramebufferCreateInfo framebufferInfo{}; diff --git a/src/atta/graphics/apis/vulkan/framebuffer.h b/src/atta/graphics/apis/vulkan/framebuffer.h index dd05022e..30b144fe 100644 --- a/src/atta/graphics/apis/vulkan/framebuffer.h +++ b/src/atta/graphics/apis/vulkan/framebuffer.h @@ -21,6 +21,9 @@ class Framebuffer final : public gfx::Framebuffer { void resize(uint32_t width, uint32_t height, bool forceRecreate = false) override; + void setLayer(uint32_t layer) override; + void setLayerAndMip(uint32_t layer, uint32_t mipLevel) override; + VkFramebuffer getHandle() const; std::shared_ptr getDevice() const; std::shared_ptr getRenderPass() const; diff --git a/src/atta/graphics/apis/vulkan/image.cpp b/src/atta/graphics/apis/vulkan/image.cpp index 30b7637f..eadf496f 100644 --- a/src/atta/graphics/apis/vulkan/image.cpp +++ b/src/atta/graphics/apis/vulkan/image.cpp @@ -7,7 +7,6 @@ #include // TODO should not have UI code here -#include "imgui.h" #include namespace atta::graphics::vk { @@ -23,65 +22,96 @@ Image::Image(const gfx::Image::CreateInfo& info, std::shared_ptr device, : gfx::Image(info), _image(image), _imageView(VK_NULL_HANDLE), _sampler(VK_NULL_HANDLE), _memory(VK_NULL_HANDLE), _imGuiDescriptorSet(VK_NULL_HANDLE), _device(device), _destroyImage(false) { _supportedFormat = supportedFormat(_format); - createImageView(); + createImageViews(); } Image::~Image() { destroy(); } void Image::write(uint8_t* data) { + // Determine number of faces: 6 for cubemaps, 1 for 2D textures + uint32_t numFaces = _isCubemap ? 6 : 1; + uint8_t* finalData = data; - uint32_t finalSize = _width * _height * Image::getPixelSize(_format); + uint32_t finalSize = _width * _height * Image::getPixelSize(_format) * numFaces; + + // If the chosen format is not supported by the GPU, convert data to _supportedFormat if (_format != _supportedFormat) { - uint32_t co = Image::getNumChannels(_format); // Num channels original - uint32_t cs = Image::getNumChannels(_supportedFormat); // Num channels supported - // Convert data from _format to _supportedFormat - if (_supportedFormat == Image::Format::RGBA) { - _supportedData.resize(_width * _height * cs); - for (size_t y = 0; y < _height; y++) - for (size_t x = 0; x < _width; x++) { - size_t idx = x + y * _width; - size_t idxo = idx * co; - size_t idxs = idx * cs; - for (size_t c = 0; c < cs; c++) - _supportedData[idxs + c] = c < co ? data[idxo + c] : 255; - } - } else if (_supportedFormat == Image::Format::RGBA32F) { - _supportedData.resize(_width * _height * cs * sizeof(float)); - float* oDataF = (float*)data; - float* sDataF = (float*)_supportedData.data(); - for (size_t y = 0; y < _height; y++) + // Get channel counts for original and supported formats + uint32_t co = Image::getNumChannels(_format); // Number of channels in original format + uint32_t cs = Image::getNumChannels(_supportedFormat); // Number of channels in supported format + + // Resize _supportedData to store the converted data + _supportedData.resize(_width * _height * Image::getPixelSize(_supportedFormat) * numFaces); + + // Process each face separately + for (uint32_t face = 0; face < numFaces; face++) { + // Loop over each pixel in the face + for (size_t y = 0; y < _height; y++) { for (size_t x = 0; x < _width; x++) { - size_t idx = x + y * _width; - size_t idxo = idx * co; - size_t idxs = idx * cs; - for (size_t c = 0; c < cs; c++) - sDataF[idxs + c] = c < co ? oDataF[idxo + c] : 1.0f; + size_t pixelIdx = x + y * _width + face * _width * _height; + + // Compute source and destination indices for this pixel + size_t srcIdx = pixelIdx * Image::getPixelSize(_format); + size_t dstIdx = pixelIdx * Image::getPixelSize(_supportedFormat); + + // If converting to RGBA (8-bit) format + if (_format == Image::Format::RGB && _supportedFormat == Image::Format::RGBA) { + for (size_t c = 0; c < cs; c++) + _supportedData[dstIdx + c] = (c < co ? data[srcIdx + c] : 255); + } + // If converting to RGBA32F (float) format + else if (_format == Image::Format::RGB32F && _supportedFormat == Image::Format::RGBA32F) { + float* oDataF = reinterpret_cast(&data[srcIdx]); + float* sDataF = reinterpret_cast(&_supportedData[dstIdx]); + for (size_t c = 0; c < cs; c++) + sDataF[c] = (c < co ? oDataF[c] : 1.0f); + } else { + LOG_ERROR("gfx::vk::Image", "Unknown format conversion when writing image. Image will not be written"); + return; + } } - } else { - LOG_ERROR("gfx::vk::Image", "Unknown format conversion when writing image. Image will not be written"); - return; + } } finalData = _supportedData.data(); - finalSize = _supportedData.size(); + finalSize = (uint32_t)_supportedData.size(); } - // Copy data to GPU + // Copy finalData to GPU using a staging buffer std::shared_ptr stagingBuffer = std::make_shared(finalData, finalSize); VkCommandBuffer commandBuffer = common::getCommandPool()->beginSingleTimeCommands(); { transitionLayout(commandBuffer, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); - VkBufferImageCopy region{}; - region.bufferOffset = 0; - region.bufferRowLength = 0; - region.bufferImageHeight = 0; - region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - region.imageSubresource.mipLevel = 0; - region.imageSubresource.baseArrayLayer = 0; - region.imageSubresource.layerCount = 1; - region.imageOffset = {0, 0, 0}; - region.imageExtent = {_width, _height, 1}; - vkCmdCopyBufferToImage(commandBuffer, stagingBuffer->getHandle(), _image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); + if (!_isCubemap) { + VkBufferImageCopy region{}; + region.bufferOffset = 0; + region.bufferRowLength = 0; + region.bufferImageHeight = 0; + region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + region.imageSubresource.mipLevel = 0; + region.imageSubresource.baseArrayLayer = 0; + region.imageSubresource.layerCount = 1; + region.imageOffset = {0, 0, 0}; + region.imageExtent = {_width, _height, 1}; + vkCmdCopyBufferToImage(commandBuffer, stagingBuffer->getHandle(), _image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); + } else { + // Prepare an array of 6 regions, one for each cubemap face + std::array regions{}; + uint32_t supportedPixelSize = Image::getPixelSize(_supportedFormat); + for (uint32_t i = 0; i < 6; i++) { + regions[i].bufferOffset = i * (_width * _height * supportedPixelSize); + regions[i].bufferRowLength = 0; + regions[i].bufferImageHeight = 0; + regions[i].imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + regions[i].imageSubresource.mipLevel = 0; + regions[i].imageSubresource.baseArrayLayer = i; + regions[i].imageSubresource.layerCount = 1; + regions[i].imageOffset = {0, 0, 0}; + regions[i].imageExtent = {_width, _height, 1}; + } + vkCmdCopyBufferToImage(commandBuffer, stagingBuffer->getHandle(), _image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + static_cast(regions.size()), regions.data()); + } transitionLayout(commandBuffer, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); } common::getCommandPool()->endSingleTimeCommands(commandBuffer); @@ -137,8 +167,8 @@ void Image::resize(uint32_t width, uint32_t height, bool forceRecreate) { // Create new handles createImage(); allocMemory(); - createImageView(); - if (isColorFormat(_supportedFormat)) + createImageViews(); + if (isColorFormat(_supportedFormat) || isDepthFormat(_supportedFormat)) createSampler(); // Transfer data if specified @@ -146,6 +176,12 @@ void Image::resize(uint32_t width, uint32_t height, bool forceRecreate) { write(_data); } +void Image::prepareForSampling() { + VkCommandBuffer commandBuffer = common::getCommandPool()->beginSingleTimeCommands(); + transitionLayout(commandBuffer, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + common::getCommandPool()->endSingleTimeCommands(commandBuffer); +} + void* Image::getImGuiImage() { // Make sure image is readable VkCommandBuffer commandBuffer = common::getCommandPool()->beginSingleTimeCommands(); @@ -162,6 +198,40 @@ VkImage Image::getImageHandle() const { return _image; } VkImageView Image::getImageViewHandle() const { return _imageView; } +VkImageView Image::getCubemapImageViewHandle(uint32_t layer) const { + if (layer >= _cubemapImageViews.size()) { + LOG_ERROR("gfx::vk::Image", "getCubemapImageViewHandle with layer index out of bounds. Image debug name: [w]$0[]", _debugName); + return VK_NULL_HANDLE; + } + return _cubemapImageViews[layer]; +} + +VkImageView Image::getCubemapFaceMipImageViewHandle(uint32_t layer, uint32_t mipLevel) { + uint32_t key = layer * _mipLevels + mipLevel; + auto it = _cubemapFaceMipImageViews.find(key); + if (it != _cubemapFaceMipImageViews.end()) + return it->second; + + VkImageViewCreateInfo info{}; + info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + info.image = _image; + info.viewType = VK_IMAGE_VIEW_TYPE_2D; + info.format = convertFormat(_supportedFormat); + info.subresourceRange.aspectMask = convertAspectFlags(_supportedFormat); + info.subresourceRange.baseMipLevel = mipLevel; + info.subresourceRange.levelCount = 1; + info.subresourceRange.baseArrayLayer = layer; + info.subresourceRange.layerCount = 1; + + VkImageView view; + if (vkCreateImageView(_device->getHandle(), &info, nullptr, &view) != VK_SUCCESS) { + LOG_ERROR("gfx::vk::Image", "Failed to create cubemap face+mip image view (layer=$0 mip=$1) for [w]$2[]", layer, mipLevel, _debugName); + return VK_NULL_HANDLE; + } + _cubemapFaceMipImageViews[key] = view; + return view; +} + VkSampler Image::getSamplerHandle() const { return _sampler; } std::shared_ptr Image::getDevice() const { return _device; } @@ -298,6 +368,7 @@ VkImageAspectFlags Image::convertAspectFlags(Image::Format format) { return VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT; } ASSERT(false, "Could not convert atta format to vulkan aspect flags. Unknown image format"); + return VK_IMAGE_ASPECT_COLOR_BIT; } void Image::createImage() { @@ -305,8 +376,15 @@ void Image::createImage() { info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; info.imageType = VK_IMAGE_TYPE_2D; info.extent = {_width, _height, 1}; - info.mipLevels = 1; - info.arrayLayers = 1; + info.mipLevels = _mipLevels; + // If this is a cubemap, use 6 layers, else 1. + if (_isCubemap) { + info.arrayLayers = 6; + info.flags = VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT; + } else { + info.arrayLayers = 1; + info.flags = 0; + } info.format = convertFormat(_supportedFormat); info.tiling = VK_IMAGE_TILING_OPTIMAL; info.initialLayout = _layout = VK_IMAGE_LAYOUT_UNDEFINED; @@ -314,45 +392,62 @@ void Image::createImage() { info.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; else if (isDepthFormat(_supportedFormat)) - info.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT; + info.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; info.samples = VK_SAMPLE_COUNT_1_BIT; - info.flags = 0; if (vkCreateImage(_device->getHandle(), &info, nullptr, &_image) != VK_SUCCESS) LOG_ERROR("gfx::vk::Image", "Failed to create image"); } -void Image::createImageView() { +VkImageView Image::createImageView(uint32_t layer, uint32_t layerCount) { VkImageViewCreateInfo info{}; info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; info.image = _image; info.format = convertFormat(_supportedFormat); + info.subresourceRange.aspectMask = convertAspectFlags(_supportedFormat); + info.subresourceRange.baseMipLevel = 0; + info.subresourceRange.levelCount = _mipLevels; + info.subresourceRange.baseArrayLayer = layer; + info.viewType = layerCount == 6 ? VK_IMAGE_VIEW_TYPE_CUBE : VK_IMAGE_VIEW_TYPE_2D; + info.subresourceRange.layerCount = layerCount; + + VkImageView imageView; + if (vkCreateImageView(_device->getHandle(), &info, nullptr, &imageView) != VK_SUCCESS) + LOG_ERROR("gfx::vk::Image", "Failed to create image view for image [w]$0[] (layer $0 layerCount $1)", _debugName, layer, layerCount); + return imageView; +} + +void Image::createImageViews() { if (_isCubemap) { - info.viewType = VK_IMAGE_VIEW_TYPE_CUBE; - info.subresourceRange.layerCount = 6; + _imageView = createImageView(0, 6); + for (uint32_t i = 0; i < 6; i++) + _cubemapImageViews[i] = createImageView(i, 1); } else { - info.viewType = VK_IMAGE_VIEW_TYPE_2D; - info.subresourceRange.aspectMask = convertAspectFlags(_supportedFormat); - info.subresourceRange.baseMipLevel = 0; - info.subresourceRange.levelCount = _mipLevels; - info.subresourceRange.baseArrayLayer = 0; - info.subresourceRange.layerCount = 1; + _imageView = createImageView(); } - - if (vkCreateImageView(_device->getHandle(), &info, nullptr, &_imageView) != VK_SUCCESS) - LOG_ERROR("gfx::vk::Image", "Failed to create image view"); } void Image::createSampler() { + VkSamplerAddressMode addressMode; + switch (_samplerWrap) { + case Wrap::CLAMP: + addressMode = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + break; + case Wrap::REPEAT: + default: + addressMode = VK_SAMPLER_ADDRESS_MODE_REPEAT; + break; + } + VkSamplerCreateInfo info{}; info.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; // Mag/min filters info.magFilter = VK_FILTER_LINEAR; info.minFilter = VK_FILTER_LINEAR; // Address mode - info.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT; - info.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT; - info.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT; + info.addressModeU = addressMode; + info.addressModeV = addressMode; + info.addressModeW = addressMode; info.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK; // Anisotropy info.anisotropyEnable = VK_TRUE; @@ -368,7 +463,7 @@ void Image::createSampler() { info.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; info.mipLodBias = 0.0f; info.minLod = 0.0f; - info.maxLod = 0.0f; + info.maxLod = static_cast(_mipLevels - 1); if (vkCreateSampler(_device->getHandle(), &info, nullptr, &_sampler) != VK_SUCCESS) LOG_ERROR("gfx::vk::Image", "Failed to create texture sampler"); @@ -390,12 +485,18 @@ void Image::allocMemory() { void Image::destroy() { vkDeviceWaitIdle(common::getDevice()->getHandle()); if (_imGuiDescriptorSet != VK_NULL_HANDLE) { - // XXX We would need to make sure this happens UI module is shut down, for now it is OK to not remove the texture because it is freed when the - // UI descroptor pool is freed. This will fail if a lot of images are created and destroyed while the UI is running + // XXX We would need to make sure this happens UI module is shut down, for now it is OK to not remove the texture because it is freed when + // the UI descroptor pool is freed. This will fail if a lot of images are created and destroyed while the UI is running // ImGui_ImplVulkan_RemoveTexture(_imGuiDescriptorSet); } if (_imageView != VK_NULL_HANDLE) vkDestroyImageView(_device->getHandle(), _imageView, nullptr); + for (const VkImageView& imageView : _cubemapImageViews) + if (imageView != VK_NULL_HANDLE) + vkDestroyImageView(_device->getHandle(), imageView, nullptr); + for (auto& [key, imageView] : _cubemapFaceMipImageViews) + if (imageView != VK_NULL_HANDLE) + vkDestroyImageView(_device->getHandle(), imageView, nullptr); if (_sampler != VK_NULL_HANDLE) vkDestroySampler(_device->getHandle(), _sampler, nullptr); if (_image != VK_NULL_HANDLE && _destroyImage) @@ -426,6 +527,14 @@ void populateLayoutStage(VkImageLayout layout, VkPipelineStageFlags* stage, VkAc *access = 0; *stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; break; + case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL: + *access = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + *stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + break; + case VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL: + *access = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + *stage = VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT; + break; case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL: *access = VK_ACCESS_SHADER_READ_BIT; *stage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; @@ -455,11 +564,11 @@ void Image::transitionLayout(VkCommandBuffer commandBuffer, VkImageLayout newLay barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.image = _image; - barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + barrier.subresourceRange.aspectMask = convertAspectFlags(_supportedFormat); barrier.subresourceRange.baseMipLevel = 0; - barrier.subresourceRange.levelCount = 1; + barrier.subresourceRange.levelCount = _mipLevels; barrier.subresourceRange.baseArrayLayer = 0; - barrier.subresourceRange.layerCount = 1; + barrier.subresourceRange.layerCount = _isCubemap ? 6 : 1; VkPipelineStageFlags sourceStage; VkPipelineStageFlags destinationStage; diff --git a/src/atta/graphics/apis/vulkan/image.h b/src/atta/graphics/apis/vulkan/image.h index 57cfe1f4..8f4d3e30 100644 --- a/src/atta/graphics/apis/vulkan/image.h +++ b/src/atta/graphics/apis/vulkan/image.h @@ -6,6 +6,7 @@ #include #include #include +#include namespace atta::graphics::vk { @@ -37,10 +38,13 @@ class Image final : public gfx::Image { void write(uint8_t* data) override; std::vector read(vec2i offset = {0, 0}, vec2i size = {0, 0}) override; void resize(uint32_t width, uint32_t height, bool forceRecreate = false) override; + void prepareForSampling() override; void* getImGuiImage() override; VkImage getImageHandle() const; VkImageView getImageViewHandle() const; + VkImageView getCubemapImageViewHandle(uint32_t layer) const; + VkImageView getCubemapFaceMipImageViewHandle(uint32_t layer, uint32_t mipLevel); VkSampler getSamplerHandle() const; std::shared_ptr getDevice() const; @@ -54,6 +58,14 @@ class Image final : public gfx::Image { */ void setLayout(VkImageLayout layout); + /** + * @brief Transition image layout + * + * Transition image layout to a new layout. A pipeline barrier will be created to perform the transition. This can be used to ensure + * the image is in the correct layout to be bind to a shader. + */ + void transitionLayout(VkCommandBuffer commandBuffer, VkImageLayout newLayout); + /** * @brief Convert format to supported vulkan format * @@ -76,12 +88,12 @@ class Image final : public gfx::Image { private: void createImage(); - void createImageView(); + VkImageView createImageView(uint32_t layer = 0, uint32_t layerCount = 1); + void createImageViews(); void createSampler(); void allocMemory(); void destroy(); uint32_t findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties); - void transitionLayout(VkCommandBuffer commandBuffer, VkImageLayout newLayout); /** * @brief Supported format by the GPU @@ -92,7 +104,9 @@ class Image final : public gfx::Image { std::vector _supportedData; ///< Converted _data to _supportedFormat VkImage _image; - VkImageView _imageView; + VkImageView _imageView; // Image view for the whole image (all layers) + std::array _cubemapImageViews{}; // Image view for each face of a cubemap image + std::unordered_map _cubemapFaceMipImageViews; // Image view for each face+mip of a cubemap image VkSampler _sampler; VkDeviceMemory _memory; VkImageLayout _layout; diff --git a/src/atta/graphics/apis/vulkan/pipeline.cpp b/src/atta/graphics/apis/vulkan/pipeline.cpp index 0abe93da..ece36470 100644 --- a/src/atta/graphics/apis/vulkan/pipeline.cpp +++ b/src/atta/graphics/apis/vulkan/pipeline.cpp @@ -11,6 +11,7 @@ namespace atta::graphics::vk { Pipeline::Pipeline(const gfx::Pipeline::CreateInfo& info) : gfx::Pipeline(info), _device(common::getDevice()) { // Create framebuffer + // TODO do not create framebuffer if already created for this render pass _framebuffer = std::dynamic_pointer_cast(_renderPass->getFramebuffer()); _framebuffer->create(std::dynamic_pointer_cast(_renderPass)); @@ -63,11 +64,11 @@ Pipeline::Pipeline(const gfx::Pipeline::CreateInfo& info) : gfx::Pipeline(info), // Depth stencil VkPipelineDepthStencilStateCreateInfo depthStencil{}; + depthStencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO; depthStencil.depthTestEnable = VK_FALSE; depthStencil.depthWriteEnable = VK_FALSE; depthStencil.stencilTestEnable = VK_FALSE; - if (_framebuffer->hasDepthAttachment()) { - depthStencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO; + if (_framebuffer->hasDepthAttachment() && _enableDepthTest) { depthStencil.depthTestEnable = VK_TRUE; depthStencil.depthWriteEnable = VK_TRUE; depthStencil.depthCompareOp = VK_COMPARE_OP_LESS; @@ -85,21 +86,24 @@ Pipeline::Pipeline(const gfx::Pipeline::CreateInfo& info) : gfx::Pipeline(info), VkPipelineColorBlendAttachmentState colorBlendAttachment{}; colorBlendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; colorBlendAttachment.blendEnable = VK_FALSE; - if (gfx::Image::getNumChannels(_framebuffer->getImage(0)->getFormat()) == 4) { - colorBlendAttachment.blendEnable = VK_TRUE; - colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; - colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; - colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD; - colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; - colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE; - colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD; + if (_framebuffer->hasColorAttachment()) { + int colorIdx = _framebuffer->getColorAttachmentIndex(); + if (gfx::Image::getNumChannels(_framebuffer->getImage(colorIdx)->getFormat()) == 4) { + colorBlendAttachment.blendEnable = VK_TRUE; + colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; + colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD; + colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; + colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE; + colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD; + } } VkPipelineColorBlendStateCreateInfo colorBlending{}; colorBlending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; colorBlending.logicOpEnable = VK_FALSE; - colorBlending.attachmentCount = 1; - colorBlending.pAttachments = &colorBlendAttachment; + colorBlending.attachmentCount = _framebuffer->hasColorAttachment() ? 1 : 0; + colorBlending.pAttachments = _framebuffer->hasColorAttachment() ? &colorBlendAttachment : nullptr; // Dynamic state std::vector dynamicStates = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; @@ -124,11 +128,9 @@ Pipeline::Pipeline(const gfx::Pipeline::CreateInfo& info) : gfx::Pipeline(info), std::vector bindings; for (const auto& element : _shader->getPerFrameImageLayout().getElements()) { uint32_t bindingIdx = bindings.size(); - if (element.type == BufferLayout::Element::Type::SAMPLER_2D) + if (element.type == BufferLayout::Element::Type::SAMPLER_2D || element.type == BufferLayout::Element::Type::SAMPLER_CUBE) bindings.push_back( {bindingIdx, 1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT}); - else if (element.type == BufferLayout::Element::Type::SAMPLER_CUBE) - bindings.push_back({bindingIdx, 1, VK_DESCRIPTOR_TYPE_SAMPLER, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT}); } if (bindings.size()) { _perFrameImageDescriptorSetLayout = std::make_shared(bindings); @@ -141,11 +143,9 @@ Pipeline::Pipeline(const gfx::Pipeline::CreateInfo& info) : gfx::Pipeline(info), std::vector bindings; for (const auto& element : _shader->getPerDrawImageLayout().getElements()) { uint32_t bindingIdx = bindings.size(); - if (element.type == BufferLayout::Element::Type::SAMPLER_2D) + if (element.type == BufferLayout::Element::Type::SAMPLER_2D || element.type == BufferLayout::Element::Type::SAMPLER_CUBE) bindings.push_back( {bindingIdx, 1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT}); - else if (element.type == BufferLayout::Element::Type::SAMPLER_CUBE) - bindings.push_back({bindingIdx, 1, VK_DESCRIPTOR_TYPE_SAMPLER, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT}); } if (bindings.size()) { _perDrawImageDescriptorSetLayout = std::make_shared(bindings); @@ -164,12 +164,12 @@ Pipeline::Pipeline(const gfx::Pipeline::CreateInfo& info) : gfx::Pipeline(info), const uint32_t maxSets = 254; // Maximum number of image groups std::vector poolSizes = { {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1}, // Up to 1 uniform buffer - {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 8 * maxSets}, // Up to 8 sampler2D perDraw/perFrame - {VK_DESCRIPTOR_TYPE_SAMPLER, 8 * maxSets}, // Up to 8 samplerCube perDraw/perFrame + {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 8 * maxSets}, // Up to 8 sampler2D/samplerCube perDraw/perFrame }; _descriptorPool = std::make_shared(poolSizes, maxSets); _descriptorSets = std::make_shared(_descriptorPool, _uniformDescriptorSetLayout, _pipelineLayout, 1); - _descriptorSets->update(0, std::dynamic_pointer_cast(_shader)->getUniformBuffer()); + if (auto uniformBuffer = std::dynamic_pointer_cast(_shader)->getUniformBuffer()) + _descriptorSets->update(0, uniformBuffer); // Create default image group descriptor sets (pink images) if (createDefaultImageGroupPerFrame) @@ -274,9 +274,6 @@ void Pipeline::renderMesh(StringId meshSid, size_t numVertices) { } else LOG_WARN("gfx::vk::Pipeline", "Could not render mesh [w]$0[], mesh not found", meshSid); } -void Pipeline::renderQuad() { renderMesh("atta::gfx::quad"); } -void Pipeline::renderQuad3() { renderMesh("atta::gfx::quad3"); } -void Pipeline::renderCube() { renderMesh("atta::gfx::cube"); } void* Pipeline::getImGuiTexture() const { return reinterpret_cast(std::static_pointer_cast(_framebuffer->getImage(0))->getImGuiImage()); @@ -333,13 +330,23 @@ void Pipeline::updateImageGroup(std::string name, ImageGroup imageGroup) { if (imageItem.name == element.name && imageItem.image) image = std::dynamic_pointer_cast(imageItem.image); - // Update descriptor + // Update descriptor: if no image, use a default pink image if (image != nullptr) imageGroupInfo.descriptorSet->update(binding++, image); else { - // Bind pink - std::shared_ptr image = std::dynamic_pointer_cast(gfx::Manager::getInstance().getImages().at("textures/pink.png")); - imageGroupInfo.descriptorSet->update(binding++, image); + // Set default pink image to this binding + std::shared_ptr defaultImage; + StringId defaultImageSid{}; + if (element.type == BufferLayout::Element::Type::SAMPLER_2D) + defaultImageSid = "atta::gfx::pink"; + else if (element.type == BufferLayout::Element::Type::SAMPLER_CUBE) + defaultImageSid = "atta::gfx::pinkCubemap"; + else { + LOG_ERROR("gfx::vk::Pipeline", "Could not set default image for [w]$0[], unknown image type", element.name); + continue; + } + defaultImage = std::dynamic_pointer_cast(gfx::Manager::getInstance().getImage(defaultImageSid)); + imageGroupInfo.descriptorSet->update(binding++, defaultImage); } } diff --git a/src/atta/graphics/apis/vulkan/pipeline.h b/src/atta/graphics/apis/vulkan/pipeline.h index b6c71993..a04b04ca 100644 --- a/src/atta/graphics/apis/vulkan/pipeline.h +++ b/src/atta/graphics/apis/vulkan/pipeline.h @@ -27,9 +27,6 @@ class Pipeline final : public gfx::Pipeline { void resize(uint32_t width, uint32_t height) override; void renderMesh(StringId meshSid, size_t numVertices = 0) override; - void renderQuad() override; - void renderQuad3() override; - void renderCube() override; void* getImGuiTexture() const override; diff --git a/src/atta/graphics/apis/vulkan/renderPass.cpp b/src/atta/graphics/apis/vulkan/renderPass.cpp index 8e11fee7..99b0ad38 100644 --- a/src/atta/graphics/apis/vulkan/renderPass.cpp +++ b/src/atta/graphics/apis/vulkan/renderPass.cpp @@ -23,14 +23,14 @@ RenderPass::RenderPass(const graphics::RenderPass::CreateInfo& info) : graphics: colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; - colorAttachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; // VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + colorAttachment.finalLayout = _framebuffer->isSwapchain() ? VK_IMAGE_LAYOUT_PRESENT_SRC_KHR : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; attachments.push_back(colorAttachment); } else if (Image::isDepthFormat(format) || Image::isStencilFormat(format)) { VkAttachmentDescription depthAttachment{}; depthAttachment.format = vk::Image::convertFormat(format); depthAttachment.samples = VK_SAMPLE_COUNT_1_BIT; depthAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; - depthAttachment.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + depthAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; depthAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; depthAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; depthAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; @@ -131,7 +131,9 @@ void RenderPass::begin(VkCommandBuffer commandBuffer) { } clearValues.push_back(clearColor); } else if (Image::isDepthFormat(format)) { - VkClearValue clearDepth = {1.0f, 0}; + VkClearValue clearDepth = {}; + clearDepth.depthStencil.depth = 1.0f; + clearDepth.depthStencil.stencil = 0; clearValues.push_back(clearDepth); } } diff --git a/src/atta/graphics/apis/vulkan/shader.cpp b/src/atta/graphics/apis/vulkan/shader.cpp index 32de5f58..c724cfeb 100644 --- a/src/atta/graphics/apis/vulkan/shader.cpp +++ b/src/atta/graphics/apis/vulkan/shader.cpp @@ -14,8 +14,11 @@ Shader::Shader(const fs::path& file) : gfx::Shader(file), _device(common::getDev shaderCode.apiCode = generateApiCode(type, shaderCode.iCode); compile(); - // Create uniform buffer - _uniformBuffer = std::make_shared(_perFrameLayout.getStride()); + // Create uniform buffer (only if shader has perFrame non-image uniforms) + if (_perFrameLayout.getStride() > 0) + _uniformBuffer = std::make_shared(_perFrameLayout.getStride()); + else + _uniformBuffer = nullptr; // Create push constant if (_perDrawLayout.getStride() > 0) @@ -109,6 +112,7 @@ std::string Shader::generateApiCode(ShaderType type, std::string iCode) { // GLSL version apiCode += "#version 450\n"; + apiCode += "#define ATTA_VULKAN 1\n"; // Move custom type definitions to the beginning std::regex structRegex(R"(struct\s+\w+\s*\{([^\}]*)\};)"); @@ -278,7 +282,10 @@ void Shader::unbind() { _uniformImages.clear(); } -void Shader::pushUniformBuffer() { _uniformBuffer->writeInstance(_uniformBufferData); } +void Shader::pushUniformBuffer() { + if (_uniformBuffer) + _uniformBuffer->writeInstance(_uniformBufferData); +} void Shader::pushConstants(VkCommandBuffer commandBuffer, std::shared_ptr pipelineLayout) { if (!_pushConstantData.empty()) diff --git a/src/atta/graphics/apis/vulkan/vulkanAPI.cpp b/src/atta/graphics/apis/vulkan/vulkanAPI.cpp index 758947a2..fcc54cae 100644 --- a/src/atta/graphics/apis/vulkan/vulkanAPI.cpp +++ b/src/atta/graphics/apis/vulkan/vulkanAPI.cpp @@ -57,6 +57,7 @@ void VulkanAPI::startUp() { gfx::Framebuffer::CreateInfo framebufferInfo{}; framebufferInfo.width = _swapChain->getImages()[0]->getWidth(); framebufferInfo.height = _swapChain->getImages()[0]->getHeight(); + framebufferInfo.isSwapchain = true; size_t i = 0; for (std::shared_ptr image : _swapChain->getImages()) { gfx::Framebuffer::Attachment attachment{}; diff --git a/src/atta/graphics/compute/brdfLut.cpp b/src/atta/graphics/compute/brdfLut.cpp new file mode 100644 index 00000000..cf845f80 --- /dev/null +++ b/src/atta/graphics/compute/brdfLut.cpp @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT +// SPDX-FileCopyrightText: 2020-2026 Breno Cunha Queiroz +#include +#include + +namespace atta::graphics { + +BrdfLut::BrdfLut() : _width(512), _height(512) {} + +std::shared_ptr BrdfLut::generate() { + LOG_VERBOSE("gfx::BrdfLut", "Generating BRDF LUT"); + + // Create output image (RG16F: R=scale, G=bias) + gfx::Image::CreateInfo imageInfo{}; + imageInfo.format = gfx::Image::Format::RG16F; + imageInfo.width = _width; + imageInfo.height = _height; + imageInfo.isCubemap = false; + imageInfo.debugName = "BRDF LUT"; + std::shared_ptr lutImage = graphics::create(imageInfo); + + // Create Render Queue + _renderQueue = graphics::create(); + + // Create Framebuffer + Framebuffer::Attachment attachment{}; + attachment.image = lutImage; + Framebuffer::CreateInfo framebufferInfo{}; + framebufferInfo.attachments.push_back(attachment); + framebufferInfo.width = _width; + framebufferInfo.height = _height; + framebufferInfo.clearColor = {0, 0, 0, 0}; + framebufferInfo.debugName = StringId("BrdfLut Framebuffer"); + std::shared_ptr framebuffer = graphics::create(framebufferInfo); + + // Create Render Pass + RenderPass::CreateInfo renderPassInfo{}; + renderPassInfo.framebuffer = framebuffer; + renderPassInfo.debugName = StringId("BrdfLut Render Pass"); + _renderPass = graphics::create(renderPassInfo); + + // Create Pipeline + Pipeline::CreateInfo pipelineInfo{}; + pipelineInfo.shader = graphics::create("shaders/compute/brdf.asl"); + pipelineInfo.renderPass = _renderPass; + _pipeline = graphics::create(pipelineInfo); + + // Render a full-screen quad — UV (0,0)-(1,1) maps to (NdotV, roughness) + _renderQueue->begin(); + { + _renderPass->begin(_renderQueue); + { + _pipeline->begin(); + { + _pipeline->renderQuad(); + } + _pipeline->end(); + } + _renderPass->end(); + } + _renderQueue->end(); + + lutImage->prepareForSampling(); + + return lutImage; +} + +} // namespace atta::graphics diff --git a/src/atta/graphics/compute/brdfLut.h b/src/atta/graphics/compute/brdfLut.h new file mode 100644 index 00000000..0cb985e2 --- /dev/null +++ b/src/atta/graphics/compute/brdfLut.h @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +// SPDX-FileCopyrightText: 2020-2026 Breno Cunha Queiroz +#pragma once + +#include +#include +#include + +namespace atta::graphics { + +class BrdfLut { + public: + BrdfLut(); + + /** + * @brief Generate the BRDF lookup table + * + * Pre-integrates the Cook-Torrance BRDF for all (NdotV, roughness) pairs. + * The result is stored in a 2D texture: R = scale (A), G = bias (B). + * Used in the split-sum specular IBL approximation. + * + * @return The BRDF LUT image, already transitioned to shader-read layout + */ + std::shared_ptr generate(); + + private: + uint32_t _width; + uint32_t _height; + + std::shared_ptr _renderQueue; + std::shared_ptr _renderPass; + std::shared_ptr _pipeline; +}; + +} // namespace atta::graphics diff --git a/src/atta/graphics/compute/equiToCubemap.cpp b/src/atta/graphics/compute/equiToCubemap.cpp new file mode 100644 index 00000000..bc81ad9c --- /dev/null +++ b/src/atta/graphics/compute/equiToCubemap.cpp @@ -0,0 +1,103 @@ +//-------------------------------------------------- +// Atta Graphics Module +// equiToCubemap.cpp +// Date: 2025-04-06 +// By Breno Cunha Queiroz +//-------------------------------------------------- +#include +#include +#include + +namespace atta::graphics { + +EquiToCubemap::EquiToCubemap() : _width(512), _height(512) {} + +std::shared_ptr EquiToCubemap::createCubemap(StringId imageSid) { + LOG_VERBOSE("gfx::EquiToCubemap", "Create cubemap from image [w]$0[]", imageSid); + + // Get equirectangular image + std::shared_ptr image = gfx::Manager::getInstance().getImage(imageSid); + if (image == nullptr) { + LOG_ERROR("gfx::EquiToCubemap", "Could not create cubemap, image [w]$0[] does not exist", imageSid); + return nullptr; + } + + // Create cubemap image + gfx::Image::CreateInfo imageInfo{}; + imageInfo.format = image->getFormat(); + imageInfo.width = _width; + imageInfo.height = _height; + imageInfo.isCubemap = true; + imageInfo.debugName = imageSid.getString() + " Cubemap"; + std::shared_ptr cubemapImage = graphics::create(imageInfo); + + // Create Render Queue + _renderQueue = graphics::create(); + + // Create Framebuffer + Framebuffer::Attachment attachment{}; + attachment.image = cubemapImage; + Framebuffer::CreateInfo framebufferInfo{}; + framebufferInfo.attachments.push_back(attachment); + framebufferInfo.width = _width; + framebufferInfo.height = _height; + framebufferInfo.clearColor = {255, 0, 255, 255}; + framebufferInfo.debugName = StringId("EquiToCubemap Framebuffer"); + std::shared_ptr framebuffer = graphics::create(framebufferInfo); + + // Create Render Pass + RenderPass::CreateInfo renderPassInfo{}; + renderPassInfo.framebuffer = framebuffer; + renderPassInfo.debugName = StringId("EquiToCubemap Render Pass"); + _renderPass = graphics::create(renderPassInfo); + + //---------- Graphics pipeline ----------// + // Create Pipeline + Pipeline::CreateInfo pipelineInfo{}; + pipelineInfo.shader = graphics::create("shaders/compute/equiToCubemap.asl"); + pipelineInfo.renderPass = _renderPass; + renderPassInfo.debugName = StringId("EquiToCubemap Pipeline"); + _pipeline = graphics::create(pipelineInfo); + + // Create Image Group + _pipeline->createImageGroup(Pipeline::ImageGroupType::PER_FRAME, "equiImg"); + _pipeline->updateImageGroup("equiImg", {{"uEquirectangularMap", image}}); + + // Define projection and view matrices. + mat4 proj = perspective(M_PI / 2.0f, 1.0f, 0.1f, 10.0f); + std::array views = { + lookAt(vec3(0.0f), vec3(1.0f, 0.0f, 0.0f), vec3(0.0f, -1.0f, 0.0f)), // +X face + lookAt(vec3(0.0f), vec3(-1.0f, 0.0f, 0.0f), vec3(0.0f, -1.0f, 0.0f)), // -X face + lookAt(vec3(0.0f), vec3(0.0f, 1.0f, 0.0f), vec3(0.0f, 0.0f, 1.0f)), // +Y face + lookAt(vec3(0.0f), vec3(0.0f, -1.0f, 0.0f), vec3(0.0f, 0.0f, -1.0f)), // -Y face + lookAt(vec3(0.0f), vec3(0.0f, 0.0f, 1.0f), vec3(0.0f, -1.0f, 0.0f)), // +Z face + lookAt(vec3(0.0f), vec3(0.0f, 0.0f, -1.0f), vec3(0.0f, -1.0f, 0.0f)), // -Z face + }; + + // Render + for (size_t i = 0; i < 6; i++) { + framebuffer->setLayer(i); + _renderQueue->begin(); + { + _renderPass->begin(_renderQueue); + { + _pipeline->begin(); + { + _pipeline->setMat4("uProjection", proj); + _pipeline->setMat4("uView", views[i]); + _pipeline->setImageGroup("equiImg"); + _pipeline->renderCube(); + } + _pipeline->end(); + } + _renderPass->end(); + } + _renderQueue->end(); + } + + cubemapImage->prepareForSampling(); + + return cubemapImage; +} + +} // namespace atta::graphics diff --git a/src/atta/graphics/compute/equiToCubemap.h b/src/atta/graphics/compute/equiToCubemap.h new file mode 100644 index 00000000..6ccf2839 --- /dev/null +++ b/src/atta/graphics/compute/equiToCubemap.h @@ -0,0 +1,39 @@ +//-------------------------------------------------- +// Atta Graphics Module +// equiToCubemap.h +// Date: 2025-04-06 +// By Breno Cunha Queiroz +//-------------------------------------------------- +#ifndef ATTA_GRAPHICS_COMPUTE_EQUI_TO_CUBEMAP_H +#define ATTA_GRAPHICS_COMPUTE_EQUI_TO_CUBEMAP_H + +#include +#include +#include + +namespace atta::graphics { + +class EquiToCubemap { + public: + EquiToCubemap(); + + /** + * @brief Create a cubemap image from an equirectangular image + * + * @param textureSid The name gfx::Image to be created from + */ + std::shared_ptr createCubemap(StringId imageSid); + + private: + // Cubemap face width/height + uint32_t _width; + uint32_t _height; + + std::shared_ptr _renderQueue; + std::shared_ptr _renderPass; + std::shared_ptr _pipeline; +}; + +} // namespace atta::graphics + +#endif // ATTA_GRAPHICS_COMPUTE_EQUI_TO_CUBEMAP_H diff --git a/src/atta/graphics/compute/irradiance.cpp b/src/atta/graphics/compute/irradiance.cpp new file mode 100644 index 00000000..f3fc5d7e --- /dev/null +++ b/src/atta/graphics/compute/irradiance.cpp @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: MIT +// SPDX-FileCopyrightText: 2020-2026 Breno Cunha Queiroz +#include +#include + +namespace atta::graphics { + +Irradiance::Irradiance() : _width(128), _height(128) {} + +std::shared_ptr Irradiance::createIrradianceCubemap(StringId environmentCubemapSid) { + LOG_VERBOSE("gfx::Irradiance", "Create irradiance cubemap from [w]$0[]", environmentCubemapSid); + + std::shared_ptr environmentCubemap = gfx::Manager::getInstance().getImage(environmentCubemapSid); + if (environmentCubemap == nullptr) { + LOG_ERROR("gfx::Irradiance", "Could not create irradiance cubemap, image [w]$0[] does not exist", environmentCubemapSid); + return nullptr; + } + + // Create irradiance cubemap image + gfx::Image::CreateInfo imageInfo{}; + imageInfo.format = environmentCubemap->getFormat(); + imageInfo.width = _width; + imageInfo.height = _height; + imageInfo.isCubemap = true; + imageInfo.debugName = environmentCubemapSid.getString() + " Irradiance"; + std::shared_ptr irradianceImage = graphics::create(imageInfo); + + // Create Render Queue + _renderQueue = graphics::create(); + + // Create Framebuffer + Framebuffer::Attachment attachment{}; + attachment.image = irradianceImage; + Framebuffer::CreateInfo framebufferInfo{}; + framebufferInfo.attachments.push_back(attachment); + framebufferInfo.width = _width; + framebufferInfo.height = _height; + framebufferInfo.clearColor = {255, 0, 255, 255}; + framebufferInfo.debugName = StringId("Irradiance Framebuffer"); + std::shared_ptr framebuffer = graphics::create(framebufferInfo); + + // Create Render Pass + RenderPass::CreateInfo renderPassInfo{}; + renderPassInfo.framebuffer = framebuffer; + renderPassInfo.debugName = StringId("Irradiance Render Pass"); + _renderPass = graphics::create(renderPassInfo); + + // Create Pipeline + Pipeline::CreateInfo pipelineInfo{}; + pipelineInfo.shader = graphics::create("shaders/compute/irradiance.asl"); + pipelineInfo.renderPass = _renderPass; + _pipeline = graphics::create(pipelineInfo); + + // Bind environment cubemap + _pipeline->createImageGroup(Pipeline::ImageGroupType::PER_FRAME, "envImg"); + _pipeline->updateImageGroup("envImg", {{"uEnvironmentMap", environmentCubemap}}); + + // Same face orientations as equiToCubemap (learnopengl convention, no Y-flip) + mat4 proj = perspective(M_PI / 2.0f, 1.0f, 0.1f, 10.0f); + std::array views = { + lookAt(vec3(0.0f), vec3(1.0f, 0.0f, 0.0f), vec3(0.0f, -1.0f, 0.0f)), // +X face + lookAt(vec3(0.0f), vec3(-1.0f, 0.0f, 0.0f), vec3(0.0f, -1.0f, 0.0f)), // -X face + lookAt(vec3(0.0f), vec3(0.0f, 1.0f, 0.0f), vec3(0.0f, 0.0f, 1.0f)), // +Y face + lookAt(vec3(0.0f), vec3(0.0f, -1.0f, 0.0f), vec3(0.0f, 0.0f, -1.0f)), // -Y face + lookAt(vec3(0.0f), vec3(0.0f, 0.0f, 1.0f), vec3(0.0f, -1.0f, 0.0f)), // +Z face + lookAt(vec3(0.0f), vec3(0.0f, 0.0f, -1.0f), vec3(0.0f, -1.0f, 0.0f)), // -Z face + }; + + for (size_t i = 0; i < 6; i++) { + framebuffer->setLayer(i); + _renderQueue->begin(); + { + _renderPass->begin(_renderQueue); + { + _pipeline->begin(); + { + _pipeline->setMat4("uProjection", proj); + _pipeline->setMat4("uView", views[i]); + _pipeline->setImageGroup("envImg"); + _pipeline->renderCube(); + } + _pipeline->end(); + } + _renderPass->end(); + } + _renderQueue->end(); + } + + irradianceImage->prepareForSampling(); + + return irradianceImage; +} + +} // namespace atta::graphics diff --git a/src/atta/graphics/compute/irradiance.h b/src/atta/graphics/compute/irradiance.h new file mode 100644 index 00000000..80f99e32 --- /dev/null +++ b/src/atta/graphics/compute/irradiance.h @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +// SPDX-FileCopyrightText: 2020-2026 Breno Cunha Queiroz +#pragma once + +#include +#include +#include + +namespace atta::graphics { + +class Irradiance { + public: + Irradiance(); + + /** + * @brief Create an irradiance cubemap from an environment cubemap + * + * The resulting cubemap encodes the pre-integrated diffuse irradiance for every + * possible surface normal direction, enabling real-time IBL diffuse lighting. + * + * @param environmentCubemapSid Name of the environment cubemap gfx::Image to convolve + * @return The irradiance cubemap image, already transitioned to shader-read layout + */ + std::shared_ptr createIrradianceCubemap(StringId environmentCubemapSid); + + private: + uint32_t _width; + uint32_t _height; + + std::shared_ptr _renderQueue; + std::shared_ptr _renderPass; + std::shared_ptr _pipeline; +}; + +} // namespace atta::graphics diff --git a/src/atta/graphics/compute/prefilter.cpp b/src/atta/graphics/compute/prefilter.cpp new file mode 100644 index 00000000..105232ff --- /dev/null +++ b/src/atta/graphics/compute/prefilter.cpp @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: MIT +// SPDX-FileCopyrightText: 2020-2026 Breno Cunha Queiroz +#include +#include + +namespace atta::graphics { + +Prefilter::Prefilter() {} + +std::shared_ptr Prefilter::createPrefilterCubemap(StringId environmentCubemapSid) { + LOG_VERBOSE("gfx::Prefilter", "Create prefilter cubemap from [w]$0[]", environmentCubemapSid); + + std::shared_ptr environmentCubemap = gfx::Manager::getInstance().getImage(environmentCubemapSid); + if (environmentCubemap == nullptr) { + LOG_ERROR("gfx::Prefilter", "Could not create prefilter cubemap, image [w]$0[] does not exist", environmentCubemapSid); + return nullptr; + } + + // Create prefilter cubemap image with mip levels + gfx::Image::CreateInfo imageInfo{}; + imageInfo.format = environmentCubemap->getFormat(); + imageInfo.width = _baseSize; + imageInfo.height = _baseSize; + imageInfo.mipLevels = _numMips; + imageInfo.isCubemap = true; + imageInfo.debugName = environmentCubemapSid.getString() + " Prefilter"; + std::shared_ptr prefilterImage = graphics::create(imageInfo); + + // Create render queue (reused for all passes) + auto renderQueue = graphics::create(); + + // Same face orientations as equiToCubemap/irradiance (learnopengl convention, no Y-flip) + mat4 proj = perspective(M_PI / 2.0f, 1.0f, 0.1f, 10.0f); + std::array views = { + lookAt(vec3(0.0f), vec3(1.0f, 0.0f, 0.0f), vec3(0.0f, -1.0f, 0.0f)), // +X face + lookAt(vec3(0.0f), vec3(-1.0f, 0.0f, 0.0f), vec3(0.0f, -1.0f, 0.0f)), // -X face + lookAt(vec3(0.0f), vec3(0.0f, 1.0f, 0.0f), vec3(0.0f, 0.0f, 1.0f)), // +Y face + lookAt(vec3(0.0f), vec3(0.0f, -1.0f, 0.0f), vec3(0.0f, 0.0f, -1.0f)), // -Y face + lookAt(vec3(0.0f), vec3(0.0f, 0.0f, 1.0f), vec3(0.0f, -1.0f, 0.0f)), // +Z face + lookAt(vec3(0.0f), vec3(0.0f, 0.0f, -1.0f), vec3(0.0f, -1.0f, 0.0f)), // -Z face + }; + + // One framebuffer + renderpass + pipeline per mip level (different dimensions) + for (uint32_t mip = 0; mip < _numMips; mip++) { + uint32_t mipSize = std::max(1u, _baseSize >> mip); + float roughness = static_cast(mip) / static_cast(_numMips - 1); + + // Create framebuffer at this mip's resolution + Framebuffer::Attachment attachment{}; + attachment.image = prefilterImage; + Framebuffer::CreateInfo framebufferInfo{}; + framebufferInfo.attachments.push_back(attachment); + framebufferInfo.width = mipSize; + framebufferInfo.height = mipSize; + framebufferInfo.clearColor = {0, 0, 0, 0}; + framebufferInfo.debugName = StringId("Prefilter Framebuffer mip" + std::to_string(mip)); + auto framebuffer = graphics::create(framebufferInfo); + + // Create render pass + RenderPass::CreateInfo renderPassInfo{}; + renderPassInfo.framebuffer = framebuffer; + renderPassInfo.debugName = StringId("Prefilter Render Pass mip" + std::to_string(mip)); + auto renderPass = graphics::create(renderPassInfo); + + // Create pipeline (reuses same shader, different renderpass/framebuffer per mip) + Pipeline::CreateInfo pipelineInfo{}; + pipelineInfo.shader = graphics::create("shaders/compute/prefilter.asl"); + pipelineInfo.renderPass = renderPass; + auto pipeline = graphics::create(pipelineInfo); + + // Bind environment cubemap + pipeline->createImageGroup(Pipeline::ImageGroupType::PER_FRAME, "envImg"); + pipeline->updateImageGroup("envImg", {{"uEnvironmentMap", environmentCubemap}}); + + // Render 6 faces at this mip level + for (uint32_t face = 0; face < 6; face++) { + framebuffer->setLayerAndMip(face, mip); + renderQueue->begin(); + { + renderPass->begin(renderQueue); + { + pipeline->begin(); + { + pipeline->setMat4("uProjection", proj); + pipeline->setMat4("uView", views[face]); + pipeline->setFloat("uRoughness", roughness); + pipeline->setImageGroup("envImg"); + pipeline->renderCube(); + } + pipeline->end(); + } + renderPass->end(); + } + renderQueue->end(); + } + } + + prefilterImage->prepareForSampling(); + + return prefilterImage; +} + +} // namespace atta::graphics diff --git a/src/atta/graphics/compute/prefilter.h b/src/atta/graphics/compute/prefilter.h new file mode 100644 index 00000000..97fc956d --- /dev/null +++ b/src/atta/graphics/compute/prefilter.h @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +// SPDX-FileCopyrightText: 2020-2026 Breno Cunha Queiroz +#pragma once + +#include +#include +#include + +namespace atta::graphics { + +class Prefilter { + public: + Prefilter(); + + /** + * @brief Create a prefiltered specular cubemap from an environment cubemap + * + * Generates a cubemap with multiple mip levels, each representing the environment + * convolved with a GGX lobe of increasing roughness (mip 0 = mirror-like, mip 4 = fully rough). + * Used for the specular IBL split-sum approximation. + * + * @param environmentCubemapSid Name of the environment cubemap gfx::Image to convolve + * @return The prefiltered cubemap image with mip levels, already transitioned to shader-read layout + */ + std::shared_ptr createPrefilterCubemap(StringId environmentCubemapSid); + + private: + static constexpr uint32_t _baseSize = 128; + static constexpr uint32_t _numMips = 5; +}; + +} // namespace atta::graphics diff --git a/src/atta/graphics/framebuffer.cpp b/src/atta/graphics/framebuffer.cpp index 2683cd01..8705f386 100644 --- a/src/atta/graphics/framebuffer.cpp +++ b/src/atta/graphics/framebuffer.cpp @@ -5,7 +5,8 @@ namespace atta::graphics { Framebuffer::Framebuffer(const CreateInfo& info) - : _width(info.width), _height(info.height), _clearColor(info.clearColor), _attachments(info.attachments), _debugName(info.debugName) { + : _width(info.width), _height(info.height), _clearColor(info.clearColor), _isSwapchain(info.isSwapchain), _attachments(info.attachments), + _debugName(info.debugName) { DASSERT(_attachments.size() > 0, "It is not possible to create framebuffer with no attachments"); // Check consistency and populate color and depth attachments diff --git a/src/atta/graphics/framebuffer.h b/src/atta/graphics/framebuffer.h index 489b1653..cee7be77 100644 --- a/src/atta/graphics/framebuffer.h +++ b/src/atta/graphics/framebuffer.h @@ -30,6 +30,7 @@ class Framebuffer { uint32_t width; ///< Framebuffer width uint32_t height; ///< Framebuffer height vec4 clearColor = {0.0f, 0.0f, 0.0f, 0.0f}; ///< Color to be used when cleaning the color attachments + bool isSwapchain = false; ///< Whether this framebuffer wraps a swapchain image std::vector attachments = {}; @@ -44,12 +45,18 @@ class Framebuffer { virtual void resize(uint32_t width, uint32_t height, bool forceRecreate = false) = 0; + // When the attachment is a cubemap image, you can use this method set the layer to be used + virtual void setLayer(uint32_t layer) = 0; + // When the attachment is a cubemap image with multiple mip levels, you can use this method to set the layer and mip level + virtual void setLayerAndMip(uint32_t layer, uint32_t mipLevel) = 0; + std::vector> getImages() const; std::shared_ptr getImage(uint32_t attachment = 0); uint32_t getWidth() const { return _width; } uint32_t getHeight() const { return _height; } vec4 getClearColor() const { return _clearColor; } StringId getDebugName() const { return _debugName; } + bool isSwapchain() const { return _isSwapchain; } bool hasColorAttachment() const; bool hasDepthAttachment() const; @@ -62,6 +69,7 @@ class Framebuffer { uint32_t _width; uint32_t _height; vec4 _clearColor; + bool _isSwapchain; std::vector _attachments; int _colorAttachmentIndex; diff --git a/src/atta/graphics/image.h b/src/atta/graphics/image.h index d29798ab..0b8016d6 100644 --- a/src/atta/graphics/image.h +++ b/src/atta/graphics/image.h @@ -51,6 +51,7 @@ class Image { virtual void write(uint8_t* data) = 0; virtual std::vector read(vec2i offset = {0, 0}, vec2i size = {0, 0}) = 0; virtual void resize(uint32_t width, uint32_t height, bool forceRecreate = false) = 0; + virtual void prepareForSampling() = 0; /* Get format * diff --git a/src/atta/graphics/manager.cpp b/src/atta/graphics/manager.cpp index 31feb3b7..959d92e8 100644 --- a/src/atta/graphics/manager.cpp +++ b/src/atta/graphics/manager.cpp @@ -70,6 +70,15 @@ void Manager::startUpImpl() { _graphicsAPI = std::static_pointer_cast(std::make_shared(_window)); _graphicsAPI->startUp(); + //----- Create default images/meshes -----// + // Create images (e.g. white, black, pink) + createDefaultImages(); + // Create meshes (e.g. quad, quad3, cube) + createDefaultMeshes(); + + //----- Create shaders -----// + _equiToCubemap = std::make_unique(); + //----- Resource sync -----// event::subscribe(BIND_EVENT_FUNC(Manager::onMeshLoadEvent)); event::subscribe(BIND_EVENT_FUNC(Manager::onMeshUpdateEvent)); @@ -88,6 +97,7 @@ void Manager::shutDownImpl() { _graphicsAPI->waitDevice(); + _equiToCubemap.reset(); _meshes.clear(); _images.clear(); _graphicsAPI->shutDown(); @@ -175,17 +185,116 @@ gfx::Image::Format Manager::convertFormat(res::Image::Format format) const { return gfx::Image::Format::NONE; } -void Manager::syncResources() { - _meshes.clear(); - _images.clear(); - // Initialize meshes already loaded - for (auto meshSid : resource::getResources()) - createMesh(meshSid); - // Initialize textures already loaded - for (auto imgSid : resource::getResources()) - createImage(imgSid); +void Manager::createDefaultImages() { + //----- Create default 2D images -----// + // White + { + static uint8_t white[] = {255, 255, 255, 255}; + Image::CreateInfo info{}; + info.format = Image::Format::RGBA; + info.width = 1; + info.height = 1; + info.data = white; + info.debugName = "White"; + _images["atta::gfx::white"] = create(info); + } + // Black + { + static uint8_t black[] = {0, 0, 0, 255}; + Image::CreateInfo info{}; + info.format = Image::Format::RGBA; + info.width = 1; + info.height = 1; + info.data = black; + info.debugName = "Black"; + _images["atta::gfx::black"] = create(info); + } + // Pink + { + static uint8_t pink[] = {255, 0, 255, 255}; + Image::CreateInfo info{}; + info.format = Image::Format::RGBA; + info.width = 1; + info.height = 1; + info.data = pink; + info.debugName = "Pink"; + _images["atta::gfx::pink"] = create(info); + } - //----- Create basic meshes -----// + //----- Create default cubemap images -----// + // WhiteCubemap: each face has a slightly different white tone + { + static uint8_t whiteCubemap[] = {// +X face: pure white + 255, 255, 255, 255, + // -X face: slightly off white + 240, 240, 240, 255, + // +Y face: warm white + 255, 250, 240, 255, + // -Y face: cool white + 230, 240, 255, 255, + // +Z face: bright white + 255, 245, 245, 255, + // -Z face: neutral white + 245, 245, 245, 255}; + Image::CreateInfo info{}; + info.format = Image::Format::RGBA; + info.width = 1; // Each face is 1x1 pixel + info.height = 1; + info.data = whiteCubemap; + info.debugName = "WhiteCubemap"; + info.isCubemap = true; + _images["atta::gfx::whiteCubemap"] = create(info); + } + // BlackCubemap: each face uses a different dark shade + { + static uint8_t blackCubemap[] = {// +X face: absolute black + 0, 0, 0, 255, + // -X face: very dark gray + 10, 10, 10, 255, + // +Y face: dark gray + 20, 20, 20, 255, + // -Y face: slightly lighter dark gray + 30, 30, 30, 255, + // +Z face: medium-dark gray + 40, 40, 40, 255, + // -Z face: light dark gray + 50, 50, 50, 255}; + Image::CreateInfo info{}; + info.format = Image::Format::RGBA; + info.width = 1; + info.height = 1; + info.data = blackCubemap; + info.debugName = "BlackCubemap"; + info.isCubemap = true; + _images["atta::gfx::blackCubemap"] = create(info); + } + // PinkCubemap: each face with a unique pink tone + { + static uint8_t pinkCubemap[] = {// +X face: vivid pink + 255, 0, 255, 255, + // -X face: deep magenta + 200, 0, 200, 255, + // +Y face: soft pink + 255, 150, 255, 255, + // -Y face: muted pink + 180, 0, 180, 255, + // +Z face: hot pink + 255, 0, 230, 255, + // -Z face: pastel pink + 240, 0, 240, 255}; + Image::CreateInfo info{}; + info.format = Image::Format::RGBA; + info.width = 1; + info.height = 1; + info.data = pinkCubemap; + info.debugName = "PinkCubemap"; + info.isCubemap = true; + _images["atta::gfx::pinkCubemap"] = create(info); + } +} + +void Manager::createDefaultMeshes() { + //----- Create default meshes -----// // Quad { static float vertices[] = { @@ -308,6 +417,15 @@ void Manager::syncResources() { } } +void Manager::syncResources() { + // Initialize meshes already loaded + for (auto meshSid : resource::getResources()) + createMesh(meshSid); + // Initialize textures already loaded + for (auto imgSid : resource::getResources()) + createImage(imgSid); +} + void Manager::onMeshLoadEvent(event::Event& event) { event::MeshLoad& e = reinterpret_cast(event); createMesh(e.sid); @@ -415,9 +533,25 @@ void Manager::createImage(StringId sid) { info.data = image->getData(); info.format = convertFormat(image->getFormat()); info.debugName = sid; - _images[sid] = create(info); + if (sid.getString().find(".hdr") == std::string::npos) { + // If it is not HDR image, just create the 2D image + _images[sid] = create(info); + } else { + // If it is HDR image, create both 2D image and cubemap image + StringId sid2D = sid.getString() + "::2D"; + _images[sid2D] = create(info); + _images[sid] = _equiToCubemap->createCubemap(sid2D); + } } +std::shared_ptr Manager::getMesh(StringId sid) const { + auto it = _meshes.find(sid); + return it != _meshes.end() ? it->second : nullptr; +} +std::shared_ptr Manager::getImage(StringId sid) const { + auto it = _images.find(sid); + return it != _images.end() ? it->second : nullptr; +} const std::unordered_map>& Manager::getMeshes() const { return _meshes; } const std::unordered_map>& Manager::getImages() const { return _images; } diff --git a/src/atta/graphics/manager.h b/src/atta/graphics/manager.h index 23d1cb25..e5ce667c 100644 --- a/src/atta/graphics/manager.h +++ b/src/atta/graphics/manager.h @@ -3,6 +3,7 @@ #pragma once #include +#include #include #include #include @@ -42,6 +43,8 @@ class Manager final { friend std::shared_ptr getWindow(); friend void* getImGuiImage(StringId sid); + std::shared_ptr getMesh(StringId sid) const; + std::shared_ptr getImage(StringId sid) const; const std::unordered_map>& getMeshes() const; const std::unordered_map>& getImages() const; @@ -67,6 +70,8 @@ class Manager final { gfx::Image::Format convertFormat(res::Image::Format format) const; // Handle resources + void createDefaultImages(); + void createDefaultMeshes(); void syncResources(); void onMeshLoadEvent(event::Event& event); void onMeshUpdateEvent(event::Event& event); @@ -83,6 +88,9 @@ class Manager final { GraphicsAPI::Type _desiredGraphicsAPI; std::shared_ptr _graphicsAPI; + // Compute shaders + std::unique_ptr _equiToCubemap; ///< Used to convert equirectangular res::Image to cubemap gfx::Image + // Resource binding std::unordered_map> _meshes; std::unordered_map> _images; diff --git a/src/atta/graphics/pipeline.cpp b/src/atta/graphics/pipeline.cpp index fdab8dbf..a1dfb6a4 100644 --- a/src/atta/graphics/pipeline.cpp +++ b/src/atta/graphics/pipeline.cpp @@ -13,7 +13,7 @@ namespace atta::graphics { Pipeline::Pipeline(const CreateInfo& info) : _shader(info.shader), _renderPass(info.renderPass), _primitive(info.primitive), _backfaceCulling(info.backfaceCulling), - _wireframe(info.wireframe), _lineWidth(info.lineWidth), _debugName(info.debugName) { + _wireframe(info.wireframe), _lineWidth(info.lineWidth), _enableDepthTest(info.enableDepthTest), _debugName(info.debugName) { //---------- Track material update ----------// event::subscribe(BIND_EVENT_FUNC(Pipeline::onMaterialCreate)); event::subscribe(BIND_EVENT_FUNC(Pipeline::onMaterialDestroy)); @@ -41,10 +41,9 @@ void Pipeline::setMat4(const char* name, mat4 m) { _shader->setMat4(name, m); } void Pipeline::setImageGroup(std::string name) { setImageGroup(name.c_str()); } void Pipeline::setImageGroup(StringId name) { setImageGroup(name.getString().c_str()); } -void Pipeline::renderMesh(StringId meshSid, size_t numVertices) { LOG_WARN("Pipeline", "[w]renderMesh[] was not implemented yet"); } -void Pipeline::renderQuad() { LOG_WARN("Pipeline", "[w]renderQuad[] was not implemented yet"); } -void Pipeline::renderQuad3() { LOG_WARN("Pipeline", "[w]renderQuad3[] was not implemented yet"); } -void Pipeline::renderCube() { LOG_WARN("Pipeline", "[w]renderCube[] was not implemented yet"); } +void Pipeline::renderQuad() { renderMesh("atta::gfx::quad"); } +void Pipeline::renderQuad3() { renderMesh("atta::gfx::quad3"); } +void Pipeline::renderCube() { renderMesh("atta::gfx::cube"); } std::shared_ptr Pipeline::getShader() const { return _shader; }; std::shared_ptr Pipeline::getRenderPass() const { return _renderPass; }; diff --git a/src/atta/graphics/pipeline.h b/src/atta/graphics/pipeline.h index 707853d8..c008265d 100644 --- a/src/atta/graphics/pipeline.h +++ b/src/atta/graphics/pipeline.h @@ -22,6 +22,7 @@ class Pipeline { bool backfaceCulling = true; bool wireframe = false; bool lineWidth = 1.0f; + bool enableDepthTest = true; // Can be set to false to disable depth testing for this pipeline StringId debugName = StringId("Unnamed Pipeline"); }; @@ -71,7 +72,7 @@ class Pipeline { * @note If numVertices is set to zero, all vertices are rendered * @warning numVertices should only be used if there is no index buffer */ - virtual void renderMesh(StringId meshSid, size_t numVertices = 0); + virtual void renderMesh(StringId meshSid, size_t numVertices = 0) = 0; virtual void renderQuad(); virtual void renderQuad3(); virtual void renderCube(); @@ -89,6 +90,7 @@ class Pipeline { const bool _backfaceCulling; const bool _wireframe; const bool _lineWidth; + const bool _enableDepthTest; const StringId _debugName; diff --git a/src/atta/graphics/renderers/common/drawerPipeline.cpp b/src/atta/graphics/renderers/common/drawerPipeline.cpp index cf30a76c..5ef094ba 100644 --- a/src/atta/graphics/renderers/common/drawerPipeline.cpp +++ b/src/atta/graphics/renderers/common/drawerPipeline.cpp @@ -10,7 +10,7 @@ DrawerPipeline::DrawerPipeline(std::shared_ptr renderPass) { //---------- Create line pipeline ----------// { Pipeline::CreateInfo pipelineInfo{}; - pipelineInfo.shader = graphics::create("shaders/drawer/line.asl"); + pipelineInfo.shader = graphics::create("shaders/common/drawerLine.asl"); pipelineInfo.renderPass = renderPass; pipelineInfo.primitive = Pipeline::Primitive::LINE; pipelineInfo.debugName = StringId("Drawer Line Pipeline"); @@ -20,7 +20,7 @@ DrawerPipeline::DrawerPipeline(std::shared_ptr renderPass) { //---------- Create point pipeline ----------// { Pipeline::CreateInfo pipelineInfo{}; - pipelineInfo.shader = graphics::create("shaders/drawer/point.asl"); + pipelineInfo.shader = graphics::create("shaders/common/drawerPoint.asl"); pipelineInfo.renderPass = renderPass; pipelineInfo.primitive = Pipeline::Primitive::POINT; pipelineInfo.debugName = StringId("Drawer Point Pipeline"); diff --git a/src/atta/graphics/renderers/common/gridPipeline.cpp b/src/atta/graphics/renderers/common/gridPipeline.cpp index 184d2c88..7a8f858d 100644 --- a/src/atta/graphics/renderers/common/gridPipeline.cpp +++ b/src/atta/graphics/renderers/common/gridPipeline.cpp @@ -10,7 +10,7 @@ size_t GridPipeline::_gridId = 0; GridPipeline::GridPipeline(std::shared_ptr renderPass) : _numLines(0) { Pipeline::CreateInfo pipelineInfo{}; - pipelineInfo.shader = gfx::create("shaders/grid/grid.asl"); + pipelineInfo.shader = gfx::create("shaders/common/grid.asl"); pipelineInfo.renderPass = renderPass; pipelineInfo.primitive = Pipeline::Primitive::LINE; pipelineInfo.debugName = StringId("Grid Pipeline"); diff --git a/src/atta/graphics/renderers/common/skyboxPipeline.cpp b/src/atta/graphics/renderers/common/skyboxPipeline.cpp new file mode 100644 index 00000000..46dbca2b --- /dev/null +++ b/src/atta/graphics/renderers/common/skyboxPipeline.cpp @@ -0,0 +1,50 @@ +//-------------------------------------------------- +// Atta Graphics Module +// skyboxPipeline.cpp +// Date: 2025-04-06 +// By Breno Cunha Queiroz +//-------------------------------------------------- +#include +#include + +namespace atta::graphics { + +SkyboxPipeline::SkyboxPipeline(std::shared_ptr renderPass) { + Pipeline::CreateInfo pipelineInfo{}; + pipelineInfo.shader = gfx::create("shaders/common/skybox.asl"); + pipelineInfo.renderPass = renderPass; + pipelineInfo.enableDepthTest = false; // Disable depth testing for the skybox + pipelineInfo.debugName = StringId("Skybox Pipeline"); + _pipeline = gfx::create(pipelineInfo); + + // Create image group + _pipeline->createImageGroup(Pipeline::ImageGroupType::PER_FRAME, "envImg"); +} + +void SkyboxPipeline::render(std::shared_ptr camera, StringId envImg) { + _pipeline->updateImageGroup("envImg", {{"uEnvironmentMap", envImg}}); + + _pipeline->begin(); + { + _pipeline->setImageGroup("envImg"); + _pipeline->setMat4("uProjection", camera->getProj()); + _pipeline->setMat4("uView", camera->getView()); + _pipeline->renderCube(); + } + _pipeline->end(); +} + +void SkyboxPipeline::render(std::shared_ptr camera, std::shared_ptr envImg) { + _pipeline->updateImageGroup("envImg", {{"uEnvironmentMap", envImg}}); + + _pipeline->begin(); + { + _pipeline->setImageGroup("envImg"); + _pipeline->setMat4("uProjection", camera->getProj()); + _pipeline->setMat4("uView", camera->getView()); + _pipeline->renderCube(); + } + _pipeline->end(); +} + +} // namespace atta::graphics diff --git a/src/atta/graphics/renderers/common/skyboxPipeline.h b/src/atta/graphics/renderers/common/skyboxPipeline.h new file mode 100644 index 00000000..28ce7e94 --- /dev/null +++ b/src/atta/graphics/renderers/common/skyboxPipeline.h @@ -0,0 +1,28 @@ +//-------------------------------------------------- +// Atta Graphics Module +// skyboxPipeline.h +// Date: 2025-04-06 +// By Breno Cunha Queiroz +//-------------------------------------------------- +#ifndef ATTA_GRAPHICS_RENDERERS_COMMON_SKYBOX_PIPELINE_H +#define ATTA_GRAPHICS_RENDERERS_COMMON_SKYBOX_PIPELINE_H +#include +#include + +namespace atta::graphics { + +class SkyboxPipeline final { + public: + SkyboxPipeline(std::shared_ptr renderPass); + + /// Render skybox + void render(std::shared_ptr camera, StringId envImg); + void render(std::shared_ptr camera, std::shared_ptr envImg); + + private: + std::shared_ptr _pipeline; +}; + +} // namespace atta::graphics + +#endif // ATTA_GRAPHICS_RENDERERS_COMMON_SKYBOX_PIPELINE_H diff --git a/src/atta/graphics/renderers/pbrRenderer.cpp b/src/atta/graphics/renderers/pbrRenderer.cpp index 996819ca..16d528e4 100644 --- a/src/atta/graphics/renderers/pbrRenderer.cpp +++ b/src/atta/graphics/renderers/pbrRenderer.cpp @@ -20,7 +20,11 @@ namespace atta::graphics { -PbrRenderer::PbrRenderer() : Renderer("PbrRenderer"), _firstRender(true), _wasResized(false), _lastEnvironmentMap(StringId("Not defined")) { +PbrRenderer::PbrRenderer() : Renderer("PbrRenderer"), _firstRender(true), _wasResized(false), _lastEnvironmentImg(StringId()) { + _irradiance = std::make_unique(); + _prefilter = std::make_unique(); + _brdfLut = std::make_unique(); + // Render Queue _renderQueue = graphics::create(); @@ -48,84 +52,44 @@ PbrRenderer::PbrRenderer() : Renderer("PbrRenderer"), _firstRender(true), _wasRe geometryPipelineInfo.renderPass = _geometryRenderPass; geometryPipelineInfo.debugName = StringId("PbrRenderer Pipeline"); _geometryPipeline = graphics::create(geometryPipelineInfo); + _geometryPipeline->createImageGroup(Pipeline::ImageGroupType::PER_FRAME, "iblImg"); } //---------- Common pipelines ----------// //_selectedPipeline = std::make_unique(_geometryRenderPass); + _skyboxPipeline = std::make_unique(_geometryRenderPass); _gridPipeline = std::make_unique(_geometryRenderPass); _drawerPipeline = std::make_unique(_geometryRenderPass); - ////---------- Create background shader ----------// - //{ _backgroundShader = graphics::create("shaders/pbrRenderer/background.asl"); } - - ////---------- Directional shadow mapping ----------// - //{ - // // Framebuffer - // Image::CreateInfo imageInfo; - // imageInfo.format = Image::Format::DEPTH32F; - // imageInfo.samplerWrap = Image::Wrap::BORDER; - // imageInfo.borderColor = vec4(1.0f); - // imageInfo.width = 1024; - // imageInfo.height = 1024; - // imageInfo.debugName = StringId("PbrRenderer::dirShadowMap::image"); - // std::shared_ptr depthImage = graphics::create(imageInfo); - - // Framebuffer::CreateInfo framebufferInfo{}; - // framebufferInfo.attachments.push_back({Image::Format::NONE, depthImage}); - // framebufferInfo.width = 1024; - // framebufferInfo.height = 1024; - // framebufferInfo.debugName = StringId("PbrRenderer::shadowMap::framebuffer"); - // std::shared_ptr framebuffer = graphics::create(framebufferInfo); - - // // Shader - // std::shared_ptr shader = graphics::create("shaders/pbrRenderer/shadow.asl"); - - // // Render Pass - // RenderPass::CreateInfo renderPassInfo{}; - // renderPassInfo.framebuffer = framebuffer; - // renderPassInfo.debugName = StringId("PbrRenderer::shadowMap::renderPass"); - // std::shared_ptr renderPass = graphics::create(renderPassInfo); - - // // Pipeline - // Pipeline::CreateInfo pipelineInfo{}; - // pipelineInfo.shader = shader; - // pipelineInfo.renderPass = renderPass; - // _shadowMapPipeline = graphics::create(pipelineInfo); - //} - - ////---------- Omnidirectional shadow mapping ----------// - //{ - // // Framebuffer - // Image::CreateInfo imageInfo; - // imageInfo.format = Image::Format::DEPTH32F; - // imageInfo.width = 1024; - // imageInfo.height = 1024; - // imageInfo.isCubemap = true; - // imageInfo.debugName = StringId("PbrRenderer::omniShadowMap::image"); - // _omnidirectionalShadowMap = graphics::create(imageInfo); - - // Framebuffer::CreateInfo framebufferInfo{}; - // framebufferInfo.attachments.push_back({Image::Format::NONE, _omnidirectionalShadowMap}); - // framebufferInfo.width = 1024; - // framebufferInfo.height = 1024; - // framebufferInfo.debugName = StringId("PbrRenderer::omniShadowMap::framebuffer"); - // std::shared_ptr framebuffer = graphics::create(framebufferInfo); - - // // Shader - // std::shared_ptr shader = graphics::create("shaders/pbrRenderer/omniShadow.asl"); - - // // Render Pass - // RenderPass::CreateInfo renderPassInfo{}; - // renderPassInfo.framebuffer = framebuffer; - // renderPassInfo.debugName = StringId("PbrRenderer::omniShadowMap::renderPass"); - // std::shared_ptr renderPass = graphics::create(renderPassInfo); - - // // Pipeline - // Pipeline::CreateInfo pipelineInfo{}; - // pipelineInfo.shader = shader; - // pipelineInfo.renderPass = renderPass; - // _omniShadowMapPipeline = graphics::create(pipelineInfo); - //} + //---------- Directional shadow mapping ----------// + { + Image::CreateInfo imageInfo{}; + imageInfo.format = Image::Format::DEPTH32F; + imageInfo.samplerWrap = Image::Wrap::CLAMP; + imageInfo.width = 1024; + imageInfo.height = 1024; + imageInfo.debugName = StringId("PbrRenderer::dirShadowMap"); + _directionalShadowMap = graphics::create(imageInfo); + + Framebuffer::Attachment attachment{}; + attachment.image = _directionalShadowMap; + Framebuffer::CreateInfo framebufferInfo{}; + framebufferInfo.attachments.push_back(attachment); + framebufferInfo.width = 1024; + framebufferInfo.height = 1024; + framebufferInfo.debugName = StringId("PbrRenderer::shadowMap::framebuffer"); + _shadowMapFramebuffer = graphics::create(framebufferInfo); + + RenderPass::CreateInfo renderPassInfo{}; + renderPassInfo.framebuffer = _shadowMapFramebuffer; + renderPassInfo.debugName = StringId("PbrRenderer::shadowMap::renderPass"); + _shadowMapRenderPass = graphics::create(renderPassInfo); + + Pipeline::CreateInfo pipelineInfo{}; + pipelineInfo.shader = graphics::create("shaders/pbrRenderer/shadow.asl"); + pipelineInfo.renderPass = _shadowMapRenderPass; + _shadowMapPipeline = graphics::create(pipelineInfo); + } } PbrRenderer::~PbrRenderer() {} @@ -135,30 +99,33 @@ void PbrRenderer::render(std::shared_ptr camera) { _geometryPipeline->resize(_width, _height); _wasResized = false; } - // if (_firstRender) - // brdfLUT(); - - // Check current envinroment map - // std::vector entities = component::getNoPrototypeView(); - // StringId currEnvironmentMap = StringId("textures/white.png"); // Default environment map is white texture - // for (auto entity : entities) { - // component::EnvironmentLight* el = component::getComponent(entity); + if (_firstRender) { + _brdfLutImage = _brdfLut->generate(); + updateIblImageGroup(); + } - // if (el) { - // currEnvironmentMap = el->sid; - // break; - // } - //} + // Check current environment map + std::vector entities = component::getNoPrototypeView(); + StringId currEnvironmentImg{}; + for (auto entity : entities) { + component::EnvironmentLight* el = component::getComponent(entity); + if (el) { + currEnvironmentImg = el->sid; + break; + } + } // Recreate environment map if it is different from last one - // if (currEnvironmentMap != _lastEnvironmentMap) { - // _lastEnvironmentMap = currEnvironmentMap; - // graphics::getGraphicsAPI()->generateCubemap(_lastEnvironmentMap); - // irradianceCubemap(); - // prefilterCubemap(); - //} - - // shadowPass(); + if (currEnvironmentImg != _lastEnvironmentImg) { + _lastEnvironmentImg = currEnvironmentImg; + if (_lastEnvironmentImg != StringId()) { + _irradianceImage = _irradiance->createIrradianceCubemap(_lastEnvironmentImg); + _prefilterImage = _prefilter->createPrefilterCubemap(_lastEnvironmentImg); + } + updateIblImageGroup(); + } + + shadowPass(); geometryPass(camera); // if (_renderSelected) @@ -178,107 +145,61 @@ void PbrRenderer::resize(uint32_t width, uint32_t height) { } void PbrRenderer::shadowPass() { - // std::vector entities = component::getNoPrototypeView(); - - ////----- Directional shadow mapping -----// - // component::EntityId directionalLightEntity = -1; - // for (auto entity : entities) { - // component::DirectionalLight* dl = component::getComponent(entity); - // component::Transform* t = component::getComponent(entity); - // if (dl && t) { - // directionalLightEntity = entity; - // break; - // } - // } - - // if (directionalLightEntity != -1) { - // _shadowMapPipeline->begin(); - // { - // std::shared_ptr shader = _shadowMapPipeline->getShader(); - // shader->bind(); - - // // Create light matrix - // component::Transform* t = component::getComponent(directionalLightEntity); - - // float height = 10.0f; - // float ratio = 1.0f; - // float far = 25.0f; - // mat4 proj = orthographic(height, ratio, far); - // mat4 view; - // view.setPosOri(vec3(), t->orientation); - // _directionalLightMatrix = proj * view; - // shader->setMat4("lightSpaceMatrix", transpose(_directionalLightMatrix)); - - // // Fill shadow map rendering the scene - // for (auto entity : entities) { - // component::Mesh* mesh = component::getComponent(entity); - // component::Transform* transform = component::getComponent(entity); - - // if (mesh && transform) { - // shader->setMat4("model", transpose(transform->getWorldTransformMatrix(entity))); - // graphics::getGraphicsAPI()->renderMesh(mesh->sid); - // } - // } - // } - // _shadowMapPipeline->end(); - //} - - ////----- Omnidirectional shadow mapping -----// - // component::EntityId pointLightEntity = -1; - // for (auto entity : entities) { - // component::PointLight* pl = component::getComponent(entity); - // component::Transform* t = component::getComponent(entity); - // if (pl && t) { - // pointLightEntity = entity; - // break; - // } - // } - - // if (pointLightEntity != -1) { - // _omniShadowMapPipeline->begin(); - // { - // std::shared_ptr shader = _omniShadowMapPipeline->getShader(); - // shader->bind(); - - // // Create light matrix - // component::Transform* t = component::getComponent(pointLightEntity); - - // // TODO world position - // float fov = radians(90.0f); - // float ratio = 1.0f; - // float near = 0.01f; - // float far = 25.0f; - // mat4 proj = perspective(fov, ratio, near, far); - // std::vector shadowMatrices = {proj * lookAt(t->position, t->position + vec3(1.0f, 0.0f, 0.0f), vec3(0.0f, -1.0f, 0.0f)), - // proj * lookAt(t->position, t->position + vec3(-1.0f, 0.0f, 0.0f), vec3(0.0f, -1.0f, 0.0f)), - // proj * lookAt(t->position, t->position + vec3(0.0f, 1.0f, 0.0f), vec3(0.0f, 0.0f, 1.0f)), - // proj * lookAt(t->position, t->position + vec3(0.0f, -1.0f, 0.0f), vec3(0.0f, 0.0f, -1.0f)), - // proj * lookAt(t->position, t->position + vec3(0.0f, 0.0f, 1.0f), vec3(0.0f, -1.0f, 0.0f)), - // proj * lookAt(t->position, t->position + vec3(0.0f, 0.0f, -1.0f), vec3(0.0f, -1.0f, 0.0f))}; - - // shader->setMat4("shadowMatrices[0]", transpose(shadowMatrices[0])); - // shader->setMat4("shadowMatrices[1]", transpose(shadowMatrices[1])); - // shader->setMat4("shadowMatrices[2]", transpose(shadowMatrices[2])); - // shader->setMat4("shadowMatrices[3]", transpose(shadowMatrices[3])); - // shader->setMat4("shadowMatrices[4]", transpose(shadowMatrices[4])); - // shader->setMat4("shadowMatrices[5]", transpose(shadowMatrices[5])); - // shader->setMat4("model", transpose(t->getWorldTransformMatrix(pointLightEntity))); - // shader->setVec3("lightPos", t->position); - // shader->setFloat("far_plane", far); - - // // Fill shadow map rendering the scene - // for (auto entity : entities) { - // component::Mesh* mesh = component::getComponent(entity); - // component::Transform* transform = component::getComponent(entity); - - // if (mesh && transform) { - // shader->setMat4("model", transpose(transform->getWorldTransformMatrix(entity))); - // graphics::getGraphicsAPI()->renderMesh(mesh->sid); - // } - // } - // } - // _omniShadowMapPipeline->end(); - //} + std::vector entities = component::getNoPrototypeView(); + + //----- Directional shadow mapping -----// + component::EntityId directionalLightEntity = -1; + for (auto entity : entities) { + component::DirectionalLight* dl = component::getComponent(entity); + component::Transform* t = component::getComponent(entity); + if (dl && t) { + directionalLightEntity = entity; + break; + } + } + + // Always run the shadow pass to keep the depth image in a valid layout for sampling. + // When a directional light exists, render scene geometry into the shadow map. + _renderQueue->begin(); + { + _shadowMapRenderPass->begin(_renderQueue); + { + if (directionalLightEntity != -1) { + component::Transform* t = component::getComponent(directionalLightEntity); + + // Compute light direction (forward is -Z in atta's Z-up coordinate system) + mat4 rotation; + rotation.setPosOri({0, 0, 0}, t->orientation); + vec3 lightDir = normalize(mat3(rotation) * vec3(0.0f, 0.0f, -1.0f)); + + // Position the light camera behind the scene along the incoming light direction. + // Use Z as up; if lightDir is nearly parallel to Z, fall back to Y. + vec3 lightPos = -lightDir * 20.0f; + vec3 up = (std::abs(lightDir.z) < 0.99f) ? vec3(0.0f, 0.0f, 1.0f) : vec3(0.0f, 1.0f, 0.0f); + mat4 lightView = lookAt(lightPos, vec3(0.0f, 0.0f, 0.0f), up); + mat4 lightProj = orthographic(20.0f, 1.0f, 40.0f); + _directionalLightMatrix = lightProj * lightView; + + _shadowMapPipeline->begin(); + { + _shadowMapPipeline->setMat4("lightSpaceMatrix", _directionalLightMatrix); + + for (auto entity : entities) { + component::Mesh* mesh = component::getComponent(entity); + component::Transform* transform = component::getComponent(entity); + if (mesh && transform) { + _shadowMapPipeline->setMat4("model", transform->getWorldTransformMatrix(entity)); + _shadowMapPipeline->renderMesh(mesh->sid); + } + } + } + _shadowMapPipeline->end(); + } + } + _shadowMapRenderPass->end(); + } + _renderQueue->end(); + _directionalShadowMap->prepareForSampling(); } void PbrRenderer::geometryPass(std::shared_ptr camera) { @@ -308,6 +229,11 @@ void PbrRenderer::geometryPass(std::shared_ptr camera) { { _geometryRenderPass->begin(_renderQueue); { + // Render skybox if there is an environment light + if (_lastEnvironmentImg != StringId()) + _skyboxPipeline->render(camera, _lastEnvironmentImg); + + // Render meshes _geometryPipeline->begin(); { //---------- PBR shader ----------// @@ -315,21 +241,10 @@ void PbrRenderer::geometryPass(std::shared_ptr camera) { _geometryPipeline->setMat4("view", camera->getView()); _geometryPipeline->setVec3("camPos", camera->getPosition()); _geometryPipeline->setMat4("directionalLightMatrix", _directionalLightMatrix); - // _geometryPipeline->setImage("directionalShadowMap", _shadowMapPipeline->getRenderPass()->getFramebuffer()->getImage()); - // _geometryPipeline->setCubemap("omniShadowMap", _omnidirectionalShadowMap); - _geometryPipeline->setFloat("omniFarPlane", 25.0f); - - // Always set environment textures (if there is no environment map, use white texture) - // _geometryPipeline->setCubemap("irradianceMap", "PbrRenderer::irradiance"); - // _geometryPipeline->setCubemap("prefilterMap", "PbrRenderer::prefilter"); - //_geometryPipeline->setImage("brdfLUT", "PbrRenderer::brdfLUT"); - - // if (_lastEnvironmentMap != "Not defined"_sid) - // _geometryPipeline->setInt("numEnvironmentLights", 1); - // else { - // LOG_WARN("graphics::PbrRenderer", "Number of environment light should always be 1 (white texture if not defined)"); - // _geometryPipeline->setInt("numEnvironmentLights", 0); - // } + + // Bind IBL textures + _geometryPipeline->setImageGroup("iblImg"); + _geometryPipeline->setInt("numEnvironmentLights", _irradianceImage ? 1 : 0); _geometryPipeline->setMat3("environmentLightOri", mat3(1.0f)); //----- Lighting -----// @@ -413,205 +328,19 @@ void PbrRenderer::geometryPass(std::shared_ptr camera) { _geometryRenderPass->end(); } _renderQueue->end(); - //_geometryPipeline->begin(); - //{ - // //---------- PBR shader ----------// - // std::shared_ptr shader = _geometryPipeline->getShader(); - // shader->bind(); - - // shader->setMat4("projection", transpose(camera->getProj())); - // shader->setMat4("view", transpose(camera->getView())); - // shader->setVec3("camPos", camera->getPosition()); - // shader->setMat4("directionalLightMatrix", transpose(_directionalLightMatrix)); - // shader->setImage("directionalShadowMap", _shadowMapPipeline->getRenderPass()->getFramebuffer()->getImage()); - // shader->setCubemap("omniShadowMap", _omnidirectionalShadowMap); - // shader->setFloat("omniFarPlane", 25.0f); - - // // Always set environment textures (if there is no environment map, use white texture) - // shader->setCubemap("irradianceMap", "PbrRenderer::irradiance"); - // shader->setCubemap("prefilterMap", "PbrRenderer::prefilter"); - // shader->setImage("brdfLUT", "PbrRenderer::brdfLUT"); - - // if (_lastEnvironmentMap != "Not defined"_sid) - // shader->setInt("numEnvironmentLights", 1); - // else { - // LOG_WARN("graphics::PbrRenderer", "Number of environment light should always be 1 (white texture if not defined)"); - // shader->setInt("numEnvironmentLights", 0); - // } - // shader->setMat3("environmentLightOri", mat3(1.0f)); - - // //----- Lighting -----// - // _environmentMapOri = mat3(1.0f); - // int numPointLights = 0; - // int numDirectionalLights = 0; - // for (auto entity : entities) { - // component::Transform* transform = component::getComponent(entity); - // component::PointLight* pl = component::getComponent(entity); - // component::DirectionalLight* dl = component::getComponent(entity); - // component::EnvironmentLight* el = component::getComponent(entity); - - // if (transform && (pl || dl || el)) { - // if (pl && numPointLights < 10) { - // vec3 position = transform->getWorldTransformMatrix(entity).getPosition(); - // int i = numPointLights++; - // shader->setVec3(("pointLights[" + std::to_string(i) + "].position").c_str(), position); - // shader->setVec3(("pointLights[" + std::to_string(i) + "].intensity").c_str(), pl->intensity); - // } - // if (dl) { - // vec3 base = {0.0f, 0.0f, -1.0f}; - // mat4 rotation; - // rotation.setPosOri({0, 0, 0}, transform->orientation); - // base = transpose(mat3(rotation)) * base; - // shader->setVec3("directionalLight.direction", base); - // shader->setVec3("directionalLight.intensity", dl->intensity); - // numDirectionalLights++; - // } - // if (el) { - // mat4 ori; - // ori.setPosOri(vec3(0.0f), transform->orientation); - // _environmentMapOri = mat3(ori); - // shader->setMat3("environmentLightOri", transpose(_environmentMapOri)); - // } - // if (numPointLights++ == 10) - // LOG_WARN("graphics::PbrRenderer", "Maximum number of point lights reached, 10 lights"); - // } - // } - // shader->setInt("numPointLights", numPointLights); - // shader->setInt("numDirectionalLights", numDirectionalLights ? 1 : 0); - - // //----- Entities -----// - // for (auto entity : entities) { - // component::Mesh* mesh = component::getComponent(entity); - // component::Transform* transform = component::getComponent(entity); - // component::Material* compMat = component::getComponent(entity); - // resource::Material* material = compMat ? compMat->getResource() : nullptr; - - // if (mesh && transform) { - // mat4 model = transpose(transform->getWorldTransformMatrix(entity)); - // mat4 invModel = inverse(model); - // shader->setMat4("model", model); - // shader->setMat4("invModel", invModel); - - // if (material) { - // if (material->colorIsImage()) { - // shader->setImage("albedoTexture", material->colorImage); - // shader->setVec3("material.albedo", {-1, -1, -1}); - // } else - // shader->setVec3("material.albedo", material->color); - - // if (material->metallicIsImage()) { - // shader->setImage("metallicTexture", material->metallicImage); - // shader->setFloat("material.metallic", -1); - // } else - // shader->setFloat("material.metallic", material->metallic); - - // if (material->roughnessIsImage()) { - // shader->setImage("roughnessTexture", material->roughnessImage); - // shader->setFloat("material.roughness", -1); - // } else - // shader->setFloat("material.roughness", material->roughness); - - // if (material->aoIsImage()) { - // shader->setImage("aoTexture", material->aoImage); - // shader->setFloat("material.ao", -1); - // } else - // shader->setFloat("material.ao", material->ao); - - // if (material->hasNormalImage()) { - // shader->setImage("normalTexture", material->normalImage); - // shader->setInt("material.hasNormalTexture", 1); - // } else - // shader->setInt("material.hasNormalTexture", 0); - // } else { - // resource::Material::CreateInfo defaultMaterial{}; - // shader->setVec3("material.albedo", defaultMaterial.color); - // shader->setFloat("material.metallic", defaultMaterial.metallic); - // shader->setFloat("material.roughness", defaultMaterial.roughness); - // shader->setFloat("material.ao", defaultMaterial.ao); - // shader->setInt("material.hasNormalTexture", 0); - // } - - // graphics::getGraphicsAPI()->renderMesh(mesh->sid); - // } - // } - - // //---------- Background shader ----------// - // if (_lastEnvironmentMap != "Not defined"_sid) { - // _backgroundShader->bind(); - // _backgroundShader->setMat4("projection", transpose(camera->getProj())); - // _backgroundShader->setMat4("view", transpose(camera->getView())); - // _backgroundShader->setCubemap("environmentMap", _lastEnvironmentMap); - // //_backgroundShader->setCubemap("environmentMap", _omnidirectionalShadowMap); - // //_backgroundShader->setCubemap("environmentMap", "PbrRenderer::irradiance"); - // _backgroundShader->setMat3("environmentMapOri", _environmentMapOri); - - // graphics::getGraphicsAPI()->renderCube(); - // } - //} - //_geometryPipeline->end(); -} - -void PbrRenderer::irradianceCubemap() { - //// Create shader - // std::shared_ptr shader = graphics::create("shaders/compute/irradiance.asl"); - - //// Generate irradiance cubemap - // GraphicsAPI::GenerateProcessedCubemapInfo info; - // info.cubemapSid = StringId("PbrRenderer::irradiance"); - // info.shader = shader; - // info.width = 128; - // info.height = 128; - // info.numMipLevels = 1; - // info.func = [&](std::shared_ptr shader, mat4 proj, mat4 view, int face, int mipLevel) { - // if (mipLevel == 0 && face == 0) { - // // TODO shader->setCubemap("environmentMap", _lastEnvironmentMap); - // shader->setMat4("projection", transpose(proj)); - // } - // shader->setMat4("view", transpose(view)); - // }; - // graphics::getGraphicsAPI()->generateProcessedCubemap(info); -} - -void PbrRenderer::prefilterCubemap() { - //// Create shader - // std::shared_ptr shader = graphics::create("shaders/compute/asl.vert"); - - //// Generate prefilter cubemap - // GraphicsAPI::GenerateProcessedCubemapInfo info; - // info.cubemapSid = StringId("PbrRenderer::prefilter"); - // info.shader = shader; - // info.width = 512; - // info.height = 512; - // info.numMipLevels = 5; - // info.func = [&](std::shared_ptr shader, mat4 proj, mat4 view, int face, int mipLevel) { - // if (mipLevel == 0 && face == 0) { - // // TODO shader->setCubemap("environmentMap", _lastEnvironmentMap); - // shader->setMat4("projection", transpose(proj)); - // } - - // shader->setMat4("view", transpose(view)); - - // float roughness = (float)mipLevel / (float)(info.numMipLevels - 1); - // shader->setFloat("roughness", roughness); - //}; - // graphics::getGraphicsAPI()->generateProcessedCubemap(info); } -void PbrRenderer::brdfLUT() { - // std::shared_ptr shader = graphics::create("shaders/compute/brdf.asl"); - - //// Generate brdf LUT - // GraphicsAPI::GenerateProcessedTextureInfo info; - // info.textureSid = StringId("PbrRenderer::brdfLUT"); - // info.shader = shader; - // info.imageInfo.format = Image::Format::RG16F; - // info.imageInfo.samplerWrap = Image::Wrap::REPEAT; - // info.imageInfo.width = 512; - // info.imageInfo.height = 512; - // info.imageInfo.mipLevels = 1; - // info.imageInfo.debugName = StringId("PbrRenderer brdfLUT image"); - - // graphics::getGraphicsAPI()->generateProcessedTexture(info); +void PbrRenderer::updateIblImageGroup() { + Pipeline::ImageGroup iblImages; + if (_irradianceImage) + iblImages.emplace_back("irradianceMap", _irradianceImage); + if (_prefilterImage) + iblImages.emplace_back("prefilterMap", _prefilterImage); + if (_brdfLutImage) + iblImages.emplace_back("brdfLUT", _brdfLutImage); + if (_directionalShadowMap) + iblImages.emplace_back("directionalShadowMap", _directionalShadowMap); + _geometryPipeline->updateImageGroup("iblImg", iblImages); } } // namespace atta::graphics diff --git a/src/atta/graphics/renderers/pbrRenderer.h b/src/atta/graphics/renderers/pbrRenderer.h index c3b1f1bf..144fee06 100644 --- a/src/atta/graphics/renderers/pbrRenderer.h +++ b/src/atta/graphics/renderers/pbrRenderer.h @@ -2,11 +2,15 @@ // SPDX-FileCopyrightText: 2020-2026 Breno Cunha Queiroz #pragma once +#include +#include +#include #include #include #include #include #include +#include #include namespace atta::graphics { @@ -26,13 +30,13 @@ class PbrRenderer final : public Renderer { void shadowPass(); void geometryPass(std::shared_ptr camera); - void irradianceCubemap(); - void prefilterCubemap(); - void brdfLUT(); + void updateIblImageGroup(); std::shared_ptr _renderQueue; std::shared_ptr _geometryRenderPass; std::shared_ptr _geometryPipeline; + + std::unique_ptr _skyboxPipeline; std::unique_ptr _drawerPipeline; std::unique_ptr _gridPipeline; std::unique_ptr _selectedPipeline; @@ -42,14 +46,23 @@ class PbrRenderer final : public Renderer { bool _wasResized; //----- Lighting -----// - // Directional light - mat3 _directionalLightMatrix; // Environment light - StringId _lastEnvironmentMap; + StringId _lastEnvironmentImg; mat3 _environmentMapOri; + // Irradiance image + std::unique_ptr _irradiance; + std::shared_ptr _irradianceImage; + std::unique_ptr _prefilter; + std::shared_ptr _prefilterImage; + std::unique_ptr _brdfLut; + std::shared_ptr _brdfLutImage; // Shadow mapping std::shared_ptr _shadowMapPipeline; + std::shared_ptr _shadowMapRenderPass; + std::shared_ptr _shadowMapFramebuffer; + std::shared_ptr _directionalShadowMap; + mat4 _directionalLightMatrix; std::shared_ptr _omniShadowMapPipeline; std::shared_ptr _omnidirectionalShadowMap; }; diff --git a/src/atta/graphics/renderers/phongRenderer.cpp b/src/atta/graphics/renderers/phongRenderer.cpp index ada7f8eb..b4d9a8ca 100644 --- a/src/atta/graphics/renderers/phongRenderer.cpp +++ b/src/atta/graphics/renderers/phongRenderer.cpp @@ -110,6 +110,8 @@ void PhongRenderer::render(std::shared_ptr camera) { int i = numPointLights++; _geometryPipeline->setVec3(("uPointLights[" + std::to_string(i) + "].position").c_str(), position); _geometryPipeline->setVec3(("uPointLights[" + std::to_string(i) + "].intensity").c_str(), pl->intensity); + } else if (pl) { + LOG_WARN("gfx::PhongRenderer", "Maximum number of point lights reached, 10 lights"); } if (dl) { hasDirectionalLight = true; @@ -118,8 +120,6 @@ void PhongRenderer::render(std::shared_ptr camera) { _geometryPipeline->setVec3("uDirectionalLight.direction", direction); _geometryPipeline->setVec3("uDirectionalLight.intensity", dl->intensity); } - if (numPointLights++ == 10) - LOG_WARN("gfx::PhongRenderer", "Maximum number of point lights reached, 10 lights"); } } _geometryPipeline->setInt("uNumPointLights", numPointLights); diff --git a/src/atta/graphics/shader.cpp b/src/atta/graphics/shader.cpp index a932eb06..cdc411cd 100644 --- a/src/atta/graphics/shader.cpp +++ b/src/atta/graphics/shader.cpp @@ -175,7 +175,7 @@ std::string Shader::generateICode(ShaderType type, std::string aslCode) { for (size_t i = 0; i < _vertexLayout.getElements().size(); i++) { BufferLayout::Element element = _vertexLayout.getElements()[i]; std::string typeStr = BufferLayout::Element::typeToString(element.type); - input += "in " + typeStr + " " + element.name + ";\n"; + input += "layout(location = " + std::to_string(i) + ") in " + typeStr + " " + element.name + ";\n"; params += element.name; if (i != _vertexLayout.getElements().size() - 1) params += ", "; diff --git a/src/atta/ui/manager.cpp b/src/atta/ui/manager.cpp index 3ac84b01..46144e7f 100644 --- a/src/atta/ui/manager.cpp +++ b/src/atta/ui/manager.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: 2020-2026 Breno Cunha Queiroz #include +#include #include #include #include @@ -114,6 +115,9 @@ bool renderComboImage(std::string attribute, StringId& image) { if (ImGui::BeginCombo(("##ComboImage" + imguiId).c_str(), selectedName.c_str())) { std::vector rImages = resource::getResources(); for (StringId rImage : rImages) { + // Ignore .hdr images (they are created as cubemaps instead of 2D images) + if (rImage.getString().find(".hdr") != std::string::npos) + continue; std::string imageStr = rImage.getString(); if (imageStr == "") imageStr = "##"; @@ -320,6 +324,31 @@ void Manager::registerCustomComponentUIs() { m->setAoImage(aoImage); }); + //---------- Environment Light ----------// + ui::registerComponentUI([](cmp::Entity entity, cmp::Component* comp) { + cmp::EnvironmentLight* envLight = static_cast(comp); + + std::string selectedImage = envLight->sid.getString(); + + if (ImGui::BeginCombo("Image##EnvComboImage", selectedImage.c_str())) { + std::vector rImages = resource::getResources(); + for (StringId rImage : rImages) { + std::string imageStr = rImage.getString(); + bool isHdrImage = imageStr.find(".hdr") != std::string::npos; + if (!isHdrImage) + continue; + const bool selected = (rImage == selectedImage); + if (ImGui::Selectable(imageStr.c_str(), selected)) { + envLight->sid = rImage; + selectedImage = rImage.getString(); + } + if (selected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + }); + //---------- Polygon Collider 2D ----------// ui::registerComponentUI([](cmp::Entity entity, cmp::Component* comp) { // Render offset diff --git a/src/extern/solveResources.cmake b/src/extern/solveResources.cmake index c6ebbbf2..1cdc9ef1 100644 --- a/src/extern/solveResources.cmake +++ b/src/extern/solveResources.cmake @@ -1,8 +1,27 @@ FetchContent_Declare(atta_resources - URL "https://atta-resources.s3.amazonaws.com/v0.3.7.zip" - SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/resources + URL "https://atta-resources.s3.amazonaws.com/v0.3.8.zip" ) atta_log(Info Extern "Downloading resources...") FetchContent_Populate(atta_resources) atta_log(Success Extern "Resources downloaded") + +# Copy to resources directory +if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/resources/fonts") + file(COPY "${atta_resources_SOURCE_DIR}/fonts" DESTINATION "${CMAKE_CURRENT_SOURCE_DIR}/resources") +endif() + +if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/resources/icons") + file(COPY "${atta_resources_SOURCE_DIR}/icons" DESTINATION "${CMAKE_CURRENT_SOURCE_DIR}/resources") +endif() + +if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/resources/meshes") + file(COPY "${atta_resources_SOURCE_DIR}/meshes" DESTINATION "${CMAKE_CURRENT_SOURCE_DIR}/resources") +endif() + +if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/resources/textures") + file(COPY "${atta_resources_SOURCE_DIR}/textures" DESTINATION "${CMAKE_CURRENT_SOURCE_DIR}/resources") +endif() + +# Clean up the temporary directory +file(REMOVE_RECURSE "${atta_resources_SOURCE_DIR}")