From 8f40efc6a2efadcc817302c686ff5a714f8d38ed Mon Sep 17 00:00:00 2001 From: sergeypdev Date: Mon, 19 Feb 2024 08:05:00 +0400 Subject: [PATCH] Working normal maps! --- assets/shaders/mesh.glsl | 28 ++++--- assets/tile.norm.jpg | 3 + src/AssetManager.zig | 10 ++- src/Render.zig | 22 +++++- src/game.zig | 17 +++- tools/asset_compiler.zig | 167 ++++++++++++++++++++++++++++++--------- 6 files changed, 192 insertions(+), 55 deletions(-) create mode 100644 assets/tile.norm.jpg diff --git a/assets/shaders/mesh.glsl b/assets/shaders/mesh.glsl index c98317b..dc914f2 100644 --- a/assets/shaders/mesh.glsl +++ b/assets/shaders/mesh.glsl @@ -22,21 +22,21 @@ layout(std140, binding = 1) uniform Lights { // Uniforms layout(location = 1) uniform mat4 model; layout(location = 2) uniform vec3 color; -layout(location = 3, bindless_sampler) uniform sampler2D diffuse; +layout(location = 3, bindless_sampler) uniform sampler2D albedo_map; +layout(location = 4, bindless_sampler) uniform sampler2D normal_map; // Input, output blocks VERTEX_EXPORT VertexData { vec3 position; - vec3 normal; vec2 uv; - vec3 tangent; + mat3 TBN; } VertexOut; #if VERTEX_SHADER layout(location = 0) in vec3 aPos; -layout(location = 1) in vec3 aNorm; +layout(location = 1) in vec3 aNormal; layout(location = 2) in vec2 aUV; layout(location = 3) in vec3 aTangent; @@ -44,9 +44,12 @@ void main() { gl_Position = projection * view * model * vec4(aPos.xyz, 1.0); vec4 posWorld = model * vec4(aPos, 1.0); VertexOut.position = posWorld.xyz / posWorld.w; - VertexOut.normal = aNorm; VertexOut.uv = aUV; - VertexOut.tangent = aTangent; + vec3 aBitangent = cross(aTangent, aNormal); + vec3 T = normalize(vec3(model * vec4(aTangent, 0.0))); + vec3 B = normalize(vec3(model * vec4(aBitangent, 0.0))); + vec3 N = normalize(vec3(model * vec4(aNormal, 0.0))); + VertexOut.TBN = mat3(T, B, N); } #endif // VERTEX_SHADER @@ -55,7 +58,14 @@ void main() { out vec4 FragColor; void main() { - vec3 diffuseColor = textureSize(diffuse, 0) == ivec2(0) ? color : texture(diffuse, VertexOut.uv).rgb; + vec3 albedoColor = textureSize(albedo_map, 0) == ivec2(0) ? color : texture(albedo_map, VertexOut.uv).rgb; + + vec3 N = textureSize(normal_map, 0) == ivec2(0) ? vec3(0.5) : vec3(texture(normal_map, VertexOut.uv).xy, 0); + N = N * 2.0 - 1.0; + N.z = sqrt(clamp(1 - N.x * N.x - N.y * N.y, 0, 1)); + N = normalize(N); + N = normalize(VertexOut.TBN * N); + vec3 finalColor = vec3(0); for (int i = 0; i < lights_count; i++) { @@ -70,9 +80,9 @@ void main() { // TODO: cutoff att = max(att, 0); - float ndotl = max(dot(L, VertexOut.normal), 0); + float ndotl = max(dot(L, N), 0); - finalColor += ndotl * lights[i].color.w * lights[i].color.xyz * att * diffuseColor; + finalColor += ndotl * lights[i].color.w * lights[i].color.xyz * att * albedoColor; } FragColor = vec4(finalColor, 1.0f); diff --git a/assets/tile.norm.jpg b/assets/tile.norm.jpg new file mode 100644 index 0000000..6bcabfa --- /dev/null +++ b/assets/tile.norm.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec14d3b6e3d680f6d75210b297a01858a5f447c84ec11ddbd9f748a517f98dfe +size 131072 diff --git a/src/AssetManager.zig b/src/AssetManager.zig index caf04f8..dd98852 100644 --- a/src/AssetManager.zig +++ b/src/AssetManager.zig @@ -375,10 +375,16 @@ fn loadTextureErr(self: *AssetManager, id: AssetId) !*const LoadedTexture { } errdefer gl.deleteTextures(1, &name); + const gl_format: gl.GLenum = switch (texture.header.format) { + .bc7 => gl.COMPRESSED_RGBA_BPTC_UNORM, + .bc5 => gl.COMPRESSED_RG_RGTC2, + .bc6 => gl.COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT, + }; + gl.textureStorage2D( name, @intCast(texture.mipLevels()), - gl.COMPRESSED_RGBA_BPTC_UNORM, + gl_format, @intCast(texture.header.width), @intCast(texture.header.height), ); @@ -393,7 +399,7 @@ fn loadTextureErr(self: *AssetManager, id: AssetId) !*const LoadedTexture { 0, @intCast(desc.width), @intCast(desc.height), - gl.COMPRESSED_RGBA_BPTC_UNORM, + gl_format, @intCast(texture.data[mip_level].len), @ptrCast(texture.data[mip_level].ptr), ); diff --git a/src/Render.zig b/src/Render.zig index b23db96..db1448c 100644 --- a/src/Render.zig +++ b/src/Render.zig @@ -178,12 +178,16 @@ pub fn getPointLights(self: *Render) *PointLightArray { } pub fn draw(self: *Render, cmd: DrawCommand) void { - gl.uniformMatrix4fv(1, 1, gl.FALSE, @ptrCast(&cmd.transform.data)); - gl.uniform3fv(2, 1, @ptrCast(&cmd.material.albedo.data)); + gl.uniformMatrix4fv(Uniform.ModelMatrix.value(), 1, gl.FALSE, @ptrCast(&cmd.transform.data)); + gl.uniform3fv(Uniform.Color.value(), 1, @ptrCast(&cmd.material.albedo.data)); gl.GL_ARB_bindless_texture.uniformHandleui64ARB( - 3, + Uniform.AlbedoMap.value(), self.assetman.resolveTexture(cmd.material.albedo_map).handle, ); + gl.GL_ARB_bindless_texture.uniformHandleui64ARB( + Uniform.NormalMap.value(), + self.assetman.resolveTexture(cmd.material.normal_map).handle, + ); const mesh = self.assetman.resolveMesh(cmd.mesh); mesh.positions.bind(Render.Attrib.Position.value()); @@ -256,6 +260,17 @@ pub const UBO = enum(gl.GLuint) { } }; +pub const Uniform = enum(gl.GLint) { + ModelMatrix = 1, + Color = 2, + AlbedoMap = 3, + NormalMap = 4, + + pub inline fn value(self: Uniform) gl.GLint { + return @intFromEnum(self); + } +}; + // TODO: support ortho pub const Camera = struct { fovy: f32 = 60, @@ -288,4 +303,5 @@ pub const PointLightArray = extern struct { pub const Material = struct { albedo: Vec3 = Vec3.one(), albedo_map: AssetManager.Handle.Texture = .{}, + normal_map: AssetManager.Handle.Texture = .{}, }; diff --git a/src/game.zig b/src/game.zig index cb8b002..9102103 100644 --- a/src/game.zig +++ b/src/game.zig @@ -162,8 +162,13 @@ export fn game_init(global_allocator: *std.mem.Allocator) void { // Plane _ = globals.g_mem.world.addEntity(.{ .flags = .{ .mesh = true }, - .transform = .{ .scale = Vec3.one().scale(100) }, - .mesh = .{ .handle = a.Meshes.plane }, + .transform = .{ .scale = Vec3.one().scale(1) }, + .mesh = .{ + .handle = a.Meshes.plane, + .material = .{ + .normal_map = a.Textures.@"tile.norm", + }, + }, }); // 10 bunnies @@ -173,7 +178,13 @@ export fn game_init(global_allocator: *std.mem.Allocator) void { .transform = .{ .pos = Vec3.new(@as(f32, @floatFromInt(i)) * 0.3, 0, 0) }, .flags = .{ .mesh = true }, - .mesh = .{ .handle = a.Meshes.bunny, .material = .{ .albedo_map = a.Textures.bunny_tex1 } }, + .mesh = .{ + .handle = a.Meshes.bunny, + .material = .{ + .albedo_map = a.Textures.bunny_tex1, + // .normal_map = a.Textures.@"tile.norm", + }, + }, }); } } diff --git a/tools/asset_compiler.zig b/tools/asset_compiler.zig index fd26a38..ca717eb 100644 --- a/tools/asset_compiler.zig +++ b/tools/asset_compiler.zig @@ -209,22 +209,29 @@ const MipLevel = struct { fn processTexture(allocator: std.mem.Allocator, input: [*:0]const u8, output: []const u8, hdr: bool) !void { _ = hdr; // autofix + + // For tex.norm.png - this will be ".norm" + const sub_ext = std.fs.path.extension(std.fs.path.stem(std.mem.span(input))); + const format = if (std.mem.eql(u8, sub_ext, ".norm")) formats.Texture.Format.bc5 else formats.Texture.Format.bc7; + var width_int: c_int = undefined; var height_int: c_int = undefined; var comps: c_int = undefined; c.stbi_set_flip_vertically_on_load(1); - const FORCED_COMPONENTS = 4; // force rgb - const data_c = c.stbi_load(input, &width_int, &height_int, &comps, FORCED_COMPONENTS); - if (data_c == null) { + const rgba_data_c = c.stbi_load(input, &width_int, &height_int, &comps, 4); + if (rgba_data_c == null) { return error.ImageLoadError; } - defer c.stbi_image_free(data_c); + defer c.stbi_image_free(rgba_data_c); const width: usize = @intCast(width_int); const height: usize = @intCast(height_int); - const data = data_c[0 .. width * height * FORCED_COMPONENTS]; + const rgba_data = rgba_data_c[0 .. width * height * 4]; + + const data_channels: usize = if (format == .bc5) 2 else 4; + const data = if (data_channels < 4) dropChannels(rgba_data, data_channels) else rgba_data; // TODO: support textures not divisible by 4 if (width % 4 != 0 or height % 4 != 0) { @@ -232,19 +239,11 @@ fn processTexture(allocator: std.mem.Allocator, input: [*:0]const u8, output: [] return error.ImageSizeShouldBeDivisibleBy4; } - var settings: c.bc7_enc_settings = undefined; - - if (comps == 3) { - c.GetProfile_ultrafast(&settings); - } else if (comps == 4) { + if (comps == 4) { premultiplyAlpha(data); - c.GetProfile_alpha_ultrafast(&settings); - } else { - std.log.debug("Channel count: {}\n", .{comps}); - return error.UnsupportedChannelCount; } - const mip_levels_to_gen = 1 + @as( + const mip_levels_to_gen = if (data_channels == 2) 1 else 1 + @as( u32, @intFromFloat(@log2(@as(f32, @floatFromInt(@max(width, height))))), ); @@ -270,7 +269,7 @@ fn processTexture(allocator: std.mem.Allocator, input: [*:0]const u8, output: [] MipLevel{ .width = mip_width, .height = mip_height, - .data = try allocator.alloc(u8, mip_width * mip_height * FORCED_COMPONENTS), + .data = try allocator.alloc(u8, mip_width * mip_height * data_channels), }, ); actual_mip_count += 1; @@ -280,24 +279,14 @@ fn processTexture(allocator: std.mem.Allocator, input: [*:0]const u8, output: [] for (0..actual_mip_count) |mip_level| { const mip_data = &mip_pyramid.items[mip_level]; if (mip_level > 0) { - downsampleImage2X(&mip_pyramid.items[mip_level - 1], mip_data); + switch (data_channels) { + 2 => downsampleRGImage2X(&mip_pyramid.items[mip_level - 1], mip_data), + 4 => downsampleRGBAImage2X(&mip_pyramid.items[mip_level - 1], mip_data), + else => unreachable, + } } - const blocks_x: usize = mip_data.width / 4; - const blocks_y: usize = mip_data.height / 4; - - const out_data = try allocator.alloc(u8, blocks_x * blocks_y * 16); - - const rgba_surf = c.rgba_surface{ - .width = @intCast(mip_data.width), - .height = @intCast(mip_data.height), - .stride = @intCast(mip_data.width * FORCED_COMPONENTS), - .ptr = mip_data.data.ptr, - }; - - c.CompressBlocksBC7(&rgba_surf, out_data.ptr, &settings); - - mip_data.out_data = out_data; + mip_data.out_data = try compressBlocksAlloc(allocator, mip_data.data, data_channels, format, @intCast(comps), mip_data.width, mip_data.height); } const out_data = try allocator.alloc([]const u8, actual_mip_count); @@ -307,7 +296,7 @@ fn processTexture(allocator: std.mem.Allocator, input: [*:0]const u8, output: [] const texture = formats.Texture{ .header = .{ - .format = .bc7, + .format = format, .width = @intCast(width), .height = @intCast(height), .mip_count = @intCast(actual_mip_count), @@ -322,6 +311,64 @@ fn processTexture(allocator: std.mem.Allocator, input: [*:0]const u8, output: [] try buf_writer.flush(); } +fn compressBlocksAlloc( + allocator: std.mem.Allocator, + pixels: []u8, + components: usize, // 2 for normal maps, 4 for everything else + format: formats.Texture.Format, + original_components: usize, // how many components in original image. Does not match actual components + width: usize, + height: usize, +) ![]u8 { + std.debug.assert(width % 4 == 0); + std.debug.assert(height % 4 == 0); + + const blocks_x = width / 4; + const blocks_y = height / 4; + + const rgba_surf = c.rgba_surface{ + .width = @intCast(width), + .height = @intCast(height), + .stride = @intCast(width * components), + .ptr = pixels.ptr, + }; + + const output = try allocator.alloc(u8, blocks_x * blocks_y * 16); + switch (format) { + .bc7 => { + var settings: c.bc7_enc_settings = .{}; + if (original_components == 3) { + c.GetProfile_ultrafast(&settings); + } else if (original_components == 4) { + c.GetProfile_alpha_ultrafast(&settings); + } else { + std.log.debug("Channel count: {}\n", .{original_components}); + return error.UnsupportedChannelCount; + } + c.CompressBlocksBC7(&rgba_surf, output.ptr, &settings); + }, + .bc5 => { + std.debug.assert(components == 2); + c.CompressBlocksBC5(&rgba_surf, output.ptr); + }, + .bc6 => { + return error.NotImplementedYet; + }, + } + + return output; +} + +fn dropChannels(rgba_data: []u8, channel_count: usize) []u8 { + for (0..rgba_data.len / 4) |i| { + for (0..channel_count) |j| { + rgba_data[i * 2 + j] = rgba_data[i * 4 + j]; + } + } + + return rgba_data[0 .. (rgba_data.len / 4) * channel_count]; +} + const gamma = 2.2; const srgb_to_linear: [256]u8 = blk: { @setEvalBranchQuota(10000); @@ -365,7 +412,31 @@ inline fn vecPow(x: @Vector(4, f32), y: f32) @Vector(4, f32) { return @exp(@log(x) * @as(@Vector(4, f32), @splat(y))); } -fn downsampleImage2X(src: *const MipLevel, dst: *const MipLevel) void { +fn downsampleRGImage2X(src: *const MipLevel, dst: *const MipLevel) void { + const srcStride = src.width * 2; + const dstStride = dst.width * 2; + for (0..dst.height) |y| { + for (0..dst.width) |x| { + const x0 = x * 2; + const y0 = y * 2; + var result = @Vector(2, f32){ 0, 0 }; + + for (0..2) |y1| { + for (0..2) |x1| { + const srcX = x0 + x1; + const srcY = y0 + y1; + + result += loadColorVec2(src.data[srcY * srcStride + srcX * 2 ..]); + } + } + + result /= @splat(2); + storeColorVec2(dst.data[y * dstStride + x * 2 ..], result); + } + } +} + +fn downsampleRGBAImage2X(src: *const MipLevel, dst: *const MipLevel) void { const srcStride = src.width * 4; const dstStride = dst.width * 4; for (0..dst.height) |y| { @@ -379,17 +450,37 @@ fn downsampleImage2X(src: *const MipLevel, dst: *const MipLevel) void { const srcX = x0 + x1; const srcY = y0 + y1; - result += loadColorVec(src.data[srcY * srcStride + srcX * 4 ..]); + result += loadColorVec4(src.data[srcY * srcStride + srcX * 4 ..]); } } result /= @splat(4); - storeColorVec(dst.data[y * dstStride + x * 4 ..], result); + storeColorVec4(dst.data[y * dstStride + x * 4 ..], result); } } } -inline fn loadColorVec(pixel: []const u8) @Vector(4, f32) { +inline fn loadColorVec2(pixel: []const u8) @Vector(2, f32) { + @setRuntimeSafety(false); + std.debug.assert(pixel.len >= 2); + + return @Vector(2, f32){ + @as(f32, @floatFromInt(pixel[0])), + @as(f32, @floatFromInt(pixel[1])), + } / @as(@Vector(2, f32), @splat(255.0)); +} + +inline fn storeColorVec2(pixel: []u8, vec: @Vector(2, f32)) void { + @setRuntimeSafety(false); + std.debug.assert(pixel.len >= 2); + + const out = vec * @as(@Vector(2, f32), @splat(255.0)); + + pixel[0] = @intFromFloat(out[0]); + pixel[1] = @intFromFloat(out[1]); +} + +inline fn loadColorVec4(pixel: []const u8) @Vector(4, f32) { @setRuntimeSafety(false); std.debug.assert(pixel.len >= 4); @@ -401,7 +492,7 @@ inline fn loadColorVec(pixel: []const u8) @Vector(4, f32) { } / @as(@Vector(4, f32), @splat(255.0)); } -inline fn storeColorVec(pixel: []u8, vec: @Vector(4, f32)) void { +inline fn storeColorVec4(pixel: []u8, vec: @Vector(4, f32)) void { @setRuntimeSafety(false); std.debug.assert(pixel.len >= 4);