diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index fc41d3580..6119484ca 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -107,3 +107,4 @@ if(SUPPORT_GLSL_PARSER AND SUPPORT_WGSL_PARSER) add_cpp_example(glsl_to_wgsl) endif() add_cpp_example(textures_storage) +add_example(textures_cubemap) diff --git a/examples/textures_cubemap.c b/examples/textures_cubemap.c new file mode 100644 index 000000000..82625f633 --- /dev/null +++ b/examples/textures_cubemap.c @@ -0,0 +1,126 @@ +#include +#include +#include + +const char cubemapVS[] = "#version 450\n" + "layout(location = 0) in vec3 in_position;\n" + "layout(location = 1) in vec2 in_uv;\n" + "layout(location = 2) in vec3 in_normal;\n" + "layout(location = 3) in vec4 in_color;\n" + "\n" + "layout(location = 0) out vec3 localPos;\n" + "\n" + "layout(binding = 0) uniform Perspective_View {\n" + " mat4 pvmatrix;\n" + "};\n" + "\n" + "void main() {\n" + " gl_Position = pvmatrix * vec4(in_position, 1.0);\n" + " localPos = in_position;\n" + "}\n"; + +const char cubemapFS[] = "#version 450\n" + "layout(location = 0) in vec3 localPos;\n" + "\n" + "layout(location = 0) out vec4 outColor;\n" + "\n" + "layout(binding = 1) uniform textureCube cubeTexture;\n" + "layout(binding = 2) uniform sampler cubeSampler;\n" + "\n" + "void main() {\n" + " outColor = texture(samplerCube(cubeTexture, cubeSampler), normalize(localPos));\n" + "}\n"; + +Camera3D cam; +Mesh cube; +Shader pl; +TextureCubemap cubemap; +DescribedSampler smp; +float angle; + +static Image GenCubemapStrip(int faceSize){ + int w = faceSize * 6; + int h = faceSize; + Color* pixels = (Color*)RL_CALLOC((size_t)w * h, sizeof(Color)); + + Color faceColors[6] = { + {255, 80, 80, 255}, + { 80, 255, 80, 255}, + { 80, 80, 255, 255}, + {255, 255, 80, 255}, + {255, 80, 255, 255}, + { 80, 255, 255, 255}, + }; + + for(int face = 0; face < 6; face++){ + for(int y = 0; y < faceSize; y++){ + for(int x = 0; x < faceSize; x++){ + int px = face * faceSize + x; + int border = (x == 0 || x == faceSize - 1 || y == 0 || y == faceSize - 1); + if(border){ + pixels[y * w + px] = (Color){40, 40, 40, 255}; + } else { + pixels[y * w + px] = faceColors[face]; + } + } + } + } + + return (Image){ + .data = pixels, + .width = w, + .height = h, + .mipmaps = 1, + .format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8, + .rowStrideInBytes = (size_t)w * 4, + }; +} + +void setup(){ + cam = CLITERAL(Camera3D){ + .position = CLITERAL(Vector3){0, 0, 5}, + .target = CLITERAL(Vector3){0, 0, 0}, + .up = CLITERAL(Vector3){0, 1, 0}, + .fovy = 60.0f, + }; + + cube = GenMeshCube(2.f, 2.f, 2.f); + pl = LoadShaderFromMemory(cubemapVS, cubemapFS); + PrepareShader(pl, cube.vao); + + Image strip = GenCubemapStrip(64); + cubemap = LoadTextureCubemap(strip, CUBEMAP_LAYOUT_LINE_HORIZONTAL); + UnloadImage(strip); + + smp = LoadSampler(TEXTURE_WRAP_CLAMP, TEXTURE_FILTER_BILINEAR); + SetShaderTexture(pl, GetUniformLocation(pl, "cubeTexture"), cubemap); + SetShaderSampler(pl, GetUniformLocation(pl, "cubeSampler"), smp); + + angle = 0.0f; +} + +void render(){ + angle += GetFrameTime(); + cam.position = (Vector3){sinf(angle) * 5.f, 2.0f, cosf(angle) * 5.f}; + + BeginDrawing(); + ClearBackground(CLITERAL(Color){30, 30, 30, 255}); + BeginShaderMode(pl); + BeginMode3D(cam); + BindVertexArray(cube.vao); + DrawArraysIndexed(RL_TRIANGLES, *cube.ibo, 36); + EndMode3D(); + EndShaderMode(); + DrawFPS(0, 0); + EndDrawing(); +} + +int main(void){ + InitProgram((ProgramInfo){ + .windowTitle = "Cubemap Texture", + .windowWidth = 800, + .windowHeight = 600, + .setupFunction = setup, + .renderFunction = render, + }); +} diff --git a/include/raygpu.h b/include/raygpu.h index 140036df9..ed812226c 100644 --- a/include/raygpu.h +++ b/include/raygpu.h @@ -327,6 +327,7 @@ typedef struct Texture2D{ }Texture2D; typedef Texture2D Texture; +typedef Texture TextureCubemap; typedef struct Texture3D{ WGPUTexture id; @@ -451,6 +452,7 @@ typedef enum uniform_type { texture3d, storage_texture3d, storage_texture2d_array, + texture_cube, acceleration_structure, combined_image_sampler, uniform_type_enumcount, @@ -1104,6 +1106,14 @@ typedef enum { #define MATERIAL_MAP_DIFFUSE MATERIAL_MAP_ALBEDO #define MATERIAL_MAP_SPECULAR MATERIAL_MAP_METALNESS +typedef enum { + CUBEMAP_LAYOUT_AUTO_DETECT = 0, + CUBEMAP_LAYOUT_LINE_VERTICAL, + CUBEMAP_LAYOUT_LINE_HORIZONTAL, + CUBEMAP_LAYOUT_CROSS_THREE_BY_FOUR, + CUBEMAP_LAYOUT_CROSS_FOUR_BY_THREE, +} CubemapLayout; + typedef enum { FONT_DEFAULT = 0, // Default font generation, anti-aliased FONT_BITMAP, // Bitmap font generation, no anti-aliasing @@ -1767,6 +1777,7 @@ RGAPI void UnloadRenderTexture(RenderTexture tex); RGAPI size_t GetPixelSizeInBytes(PixelFormat format); RGAPI Texture LoadBlankTexture(uint32_t width, uint32_t height); RGAPI Texture LoadTexture(const char* filename); +RGAPI TextureCubemap LoadTextureCubemap(Image image, int layout); RGAPI Texture LoadDepthTexture(uint32_t width, uint32_t height); RGAPI Texture LoadTextureEx(uint32_t width, uint32_t height, PixelFormat format, bool to_be_used_as_rendertarget); RGAPI Texture LoadTexturePro(uint32_t width, uint32_t height, PixelFormat format, RGTextureUsage usage, uint32_t sampleCount, uint32_t mipmaps); diff --git a/src/backend_wgpu.c b/src/backend_wgpu.c index fa9ca1c6f..8c48b02e8 100644 --- a/src/backend_wgpu.c +++ b/src/backend_wgpu.c @@ -460,6 +460,11 @@ WGPUBindGroupLayoutEntry toWGPUBindGroupLayoutEntry(const ResourceTypeDescriptor ret.texture.sampleType = toTextureSampleType(rtd->fstype); ret.texture.viewDimension = WGPUTextureViewDimension_3D; break; + case texture_cube: + ret.visibility = shaderStage; + ret.texture.sampleType = toTextureSampleType(rtd->fstype); + ret.texture.viewDimension = WGPUTextureViewDimension_Cube; + break; case storage_texture2d: ret.storageTexture.access = toStorageTextureAccess(rtd->access); ret.visibility = shaderStage; @@ -1758,6 +1763,174 @@ Texture LoadTextureFromImage(Image img) { TRACELOG(LOG_INFO, "Successfully loaded %u x %u texture from image", (unsigned)img.width, (unsigned)img.height); return ret; } + +TextureCubemap LoadTextureCubemap(Image image, int layout){ + if(image.data == NULL){ + TRACELOG(LOG_WARNING, "LoadTextureCubemap: image data is NULL"); + return (TextureCubemap){0}; + } + + // Auto-detect layout from aspect ratio + if(layout == CUBEMAP_LAYOUT_AUTO_DETECT){ + if(image.width > image.height){ + if((image.width / 6) == image.height){ + layout = CUBEMAP_LAYOUT_LINE_HORIZONTAL; + } else if((image.width / 4) == (image.height / 3)){ + layout = CUBEMAP_LAYOUT_CROSS_FOUR_BY_THREE; + } else { + layout = CUBEMAP_LAYOUT_LINE_HORIZONTAL; + } + } else { + if((image.height / 6) == image.width){ + layout = CUBEMAP_LAYOUT_LINE_VERTICAL; + } else if((image.width / 3) == (image.height / 4)){ + layout = CUBEMAP_LAYOUT_CROSS_THREE_BY_FOUR; + } else { + layout = CUBEMAP_LAYOUT_LINE_VERTICAL; + } + } + } + + uint32_t faceSize = 0; + + // Face positions: [col, row] for each of the 6 faces (+X, -X, +Y, -Y, +Z, -Z) + int faceX[6] = {0}; + int faceY[6] = {0}; + + switch(layout){ + case CUBEMAP_LAYOUT_LINE_VERTICAL: { + faceSize = image.width; + for(int i = 0; i < 6; i++){ + faceX[i] = 0; + faceY[i] = (int)faceSize * i; + } + } break; + case CUBEMAP_LAYOUT_LINE_HORIZONTAL: { + faceSize = image.height; + for(int i = 0; i < 6; i++){ + faceX[i] = (int)faceSize * i; + faceY[i] = 0; + } + } break; + case CUBEMAP_LAYOUT_CROSS_THREE_BY_FOUR: { + // 3 cols x 4 rows: + // [+Y] (row 0, col 1) + // [-X] [+Z] [+X] (row 1, cols 0,1,2) + // [-Y] (row 2, col 1) + // [-Z] (row 3, col 1) + faceSize = image.width / 3; + faceX[0] = (int)faceSize * 2; faceY[0] = (int)faceSize * 1; // +X + faceX[1] = (int)faceSize * 0; faceY[1] = (int)faceSize * 1; // -X + faceX[2] = (int)faceSize * 1; faceY[2] = (int)faceSize * 0; // +Y + faceX[3] = (int)faceSize * 1; faceY[3] = (int)faceSize * 2; // -Y + faceX[4] = (int)faceSize * 1; faceY[4] = (int)faceSize * 1; // +Z + faceX[5] = (int)faceSize * 1; faceY[5] = (int)faceSize * 3; // -Z + } break; + case CUBEMAP_LAYOUT_CROSS_FOUR_BY_THREE: { + // 4 cols x 3 rows: + // [+Y] (row 0, col 1) + // [-X] [+Z] [+X] [-Z] (row 1, cols 0,1,2,3) + // [-Y] (row 2, col 1) + faceSize = image.width / 4; + faceX[0] = (int)faceSize * 2; faceY[0] = (int)faceSize * 1; // +X + faceX[1] = (int)faceSize * 0; faceY[1] = (int)faceSize * 1; // -X + faceX[2] = (int)faceSize * 1; faceY[2] = (int)faceSize * 0; // +Y + faceX[3] = (int)faceSize * 1; faceY[3] = (int)faceSize * 2; // -Y + faceX[4] = (int)faceSize * 1; faceY[4] = (int)faceSize * 1; // +Z + faceX[5] = (int)faceSize * 3; faceY[5] = (int)faceSize * 1; // -Z + } break; + default: { + TRACELOG(LOG_WARNING, "LoadTextureCubemap: unknown layout %d", layout); + return (TextureCubemap){0}; + } + } + + if(faceSize == 0){ + TRACELOG(LOG_WARNING, "LoadTextureCubemap: face size is zero"); + return (TextureCubemap){0}; + } + + // Ensure image is RGBA8 for upload + Image imgCopy = ImageFromImage(image, CLITERAL(Rectangle){0, 0, (float)image.width, (float)image.height}); + ImageFormat(&imgCopy, PIXELFORMAT_UNCOMPRESSED_R8G8B8A8); + + WGPUTextureFormat wgpuFormat = WGPUTextureFormat_RGBA8Unorm; + + WGPUTextureDescriptor tDesc = { + .usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_CopyDst | WGPUTextureUsage_CopySrc, + .dimension = WGPUTextureDimension_2D, + .size = {faceSize, faceSize, 6}, + .format = wgpuFormat, + .mipLevelCount = 1, + .sampleCount = 1, + .viewFormatCount = 1, + .viewFormats = &wgpuFormat, + }; + + WGPUTexture gpuTex = wgpuDeviceCreateTexture(GetDevice(), &tDesc); + if(gpuTex == NULL){ + TRACELOG(LOG_WARNING, "LoadTextureCubemap: wgpuDeviceCreateTexture failed"); + UnloadImage(imgCopy); + return (TextureCubemap){0}; + } + + TextureCubemap ret = { + .id = gpuTex, + .width = faceSize, + .height = faceSize, + .format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8, + .sampleCount = 1, + .mipmaps = 1, + }; + + uint32_t bytesPerPixel = 4; + uint64_t faceBytes = (uint64_t)faceSize * faceSize * bytesPerPixel; + uint8_t* allFaces = (uint8_t*)RL_CALLOC(faceBytes * 6, 1); + + for(int face = 0; face < 6; face++){ + uint8_t* faceDst = allFaces + face * faceBytes; + for(uint32_t y = 0; y < faceSize; y++){ + uint8_t* src = (uint8_t*)imgCopy.data + ((faceY[face] + y) * imgCopy.rowStrideInBytes) + (faceX[face] * bytesPerPixel); + uint8_t* dst = faceDst + (y * faceSize * bytesPerPixel); + memcpy(dst, src, faceSize * bytesPerPixel); + } + } + + const WGPUTexelCopyTextureInfo destination = { + .texture = (WGPUTexture)ret.id, + .mipLevel = 0, + .origin = {0, 0, 0}, + .aspect = WGPUTextureAspect_All, + }; + + const WGPUTexelCopyBufferLayout source = { + .offset = 0, + .bytesPerRow = faceSize * bytesPerPixel, + .rowsPerImage = faceSize, + }; + + const WGPUExtent3D writeSize = {faceSize, faceSize, 6}; + wgpuQueueWriteTexture(GetQueue(), &destination, allFaces, faceBytes * 6, &source, &writeSize); + + RL_FREE(allFaces); + UnloadImage(imgCopy); + + const WGPUTextureViewDescriptor vDesc = { + .format = wgpuFormat, + .dimension = WGPUTextureViewDimension_Cube, + .baseMipLevel = 0, + .mipLevelCount = 1, + .baseArrayLayer = 0, + .arrayLayerCount = 6, + .aspect = WGPUTextureAspect_All, + }; + + ret.view = wgpuTextureCreateView((WGPUTexture)ret.id, &vDesc); + + TRACELOG(LOG_INFO, "Successfully loaded %u x %u cubemap texture", faceSize, faceSize); + return ret; +} + void ResizeSurface(FullSurface *fsurface, int newWidth, int newHeight) { fsurface->renderTarget.colorMultisample.width = newWidth; fsurface->renderTarget.colorMultisample.height = newHeight; @@ -2762,6 +2935,8 @@ static inline uniform_type uniform_type_from_binding(const SpvReflectDescriptorB switch (b->image.dim) { case SpvDim3D: return texture3d; + case SpvDimCube: + return texture_cube; default: return b->image.arrayed ? texture2d_array : texture2d; } @@ -2769,6 +2944,8 @@ static inline uniform_type uniform_type_from_binding(const SpvReflectDescriptorB switch (b->image.dim) { case SpvDim3D: return texture3d; + case SpvDimCube: + return texture_cube; default: return b->image.arrayed ? texture2d_array : texture2d; } diff --git a/src/shader_parse.cpp b/src/shader_parse.cpp index f8ca02fe9..19de600e1 100644 --- a/src/shader_parse.cpp +++ b/src/shader_parse.cpp @@ -279,6 +279,18 @@ StringToUniformMap* getBindingsWGSL_Tint(ShaderSources sources) { out.emplace(name, desc); continue; } + if (tname.rfind("texture_cube", 0) == 0) { + desc.type = texture_cube; + + if (auto* tid = id->As()) { + if (tid->arguments.Length() >= 1) { + if (auto* a0 = tid->arguments[0]->As()) + desc.fstype = ti_parse_format(a0->identifier->symbol.Name()); + } + } + out.emplace(name, desc); + continue; + } if (tname.rfind("texture_2d", 0) == 0 || tname.rfind("texture_3d", 0) == 0) { bool is2d = tname.rfind("texture_2d", 0) == 0; bool is3d = tname.rfind("texture_3d", 0) == 0; diff --git a/src/wgsl_parse_lite.c b/src/wgsl_parse_lite.c index e2505273b..bb7e630fd 100644 --- a/src/wgsl_parse_lite.c +++ b/src/wgsl_parse_lite.c @@ -106,6 +106,7 @@ static inline access_type sw_parse_access_token(const char* tkn) { typedef struct SW_ParsedTextureMeta { bool is_storage; + bool is_cube; bool is_array; bool is_3d; format_or_sample_type fmt_or_sample; @@ -118,6 +119,7 @@ static inline SW_ParsedTextureMeta sw_parse_texture_typenode(const WgslAstNode* const char* name = T->type_node.name ? T->type_node.name : ""; m.is_storage = sw_starts_with(name, "texture_storage_"); + m.is_cube = sw_starts_with(name, "texture_cube"); m.is_array = sw_starts_with(name, "texture_2d_array") || sw_starts_with(name, "texture_storage_2d_array"); m.is_3d = sw_starts_with(name, "texture_3d") || sw_starts_with(name, "texture_storage_3d"); @@ -248,7 +250,8 @@ StringToUniformMap* getBindingsWGSL_Simple(ShaderSources sources) { desc.fstype = meta.fmt_or_sample; desc.access = meta.access; } else { - if (meta.is_array) desc.type = texture2d_array; + if (meta.is_cube) desc.type = texture_cube; + else if (meta.is_array) desc.type = texture2d_array; else if (meta.is_3d) desc.type = texture3d; else desc.type = texture2d; desc.fstype = meta.fmt_or_sample; // sample type