Working normal maps!

This commit is contained in:
sergeypdev 2024-02-19 08:05:00 +04:00
parent 6e75910bef
commit 8f40efc6a2
6 changed files with 192 additions and 55 deletions

View File

@ -22,21 +22,21 @@ layout(std140, binding = 1) uniform Lights {
// Uniforms // Uniforms
layout(location = 1) uniform mat4 model; layout(location = 1) uniform mat4 model;
layout(location = 2) uniform vec3 color; 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 // Input, output blocks
VERTEX_EXPORT VertexData { VERTEX_EXPORT VertexData {
vec3 position; vec3 position;
vec3 normal;
vec2 uv; vec2 uv;
vec3 tangent; mat3 TBN;
} VertexOut; } VertexOut;
#if VERTEX_SHADER #if VERTEX_SHADER
layout(location = 0) in vec3 aPos; 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 = 2) in vec2 aUV;
layout(location = 3) in vec3 aTangent; layout(location = 3) in vec3 aTangent;
@ -44,9 +44,12 @@ void main() {
gl_Position = projection * view * model * vec4(aPos.xyz, 1.0); gl_Position = projection * view * model * vec4(aPos.xyz, 1.0);
vec4 posWorld = model * vec4(aPos, 1.0); vec4 posWorld = model * vec4(aPos, 1.0);
VertexOut.position = posWorld.xyz / posWorld.w; VertexOut.position = posWorld.xyz / posWorld.w;
VertexOut.normal = aNorm;
VertexOut.uv = aUV; 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 #endif // VERTEX_SHADER
@ -55,7 +58,14 @@ void main() {
out vec4 FragColor; out vec4 FragColor;
void main() { 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); vec3 finalColor = vec3(0);
for (int i = 0; i < lights_count; i++) { for (int i = 0; i < lights_count; i++) {
@ -70,9 +80,9 @@ void main() {
// TODO: cutoff // TODO: cutoff
att = max(att, 0); 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); FragColor = vec4(finalColor, 1.0f);

BIN
assets/tile.norm.jpg (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -375,10 +375,16 @@ fn loadTextureErr(self: *AssetManager, id: AssetId) !*const LoadedTexture {
} }
errdefer gl.deleteTextures(1, &name); 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( gl.textureStorage2D(
name, name,
@intCast(texture.mipLevels()), @intCast(texture.mipLevels()),
gl.COMPRESSED_RGBA_BPTC_UNORM, gl_format,
@intCast(texture.header.width), @intCast(texture.header.width),
@intCast(texture.header.height), @intCast(texture.header.height),
); );
@ -393,7 +399,7 @@ fn loadTextureErr(self: *AssetManager, id: AssetId) !*const LoadedTexture {
0, 0,
@intCast(desc.width), @intCast(desc.width),
@intCast(desc.height), @intCast(desc.height),
gl.COMPRESSED_RGBA_BPTC_UNORM, gl_format,
@intCast(texture.data[mip_level].len), @intCast(texture.data[mip_level].len),
@ptrCast(texture.data[mip_level].ptr), @ptrCast(texture.data[mip_level].ptr),
); );

View File

@ -178,12 +178,16 @@ pub fn getPointLights(self: *Render) *PointLightArray {
} }
pub fn draw(self: *Render, cmd: DrawCommand) void { pub fn draw(self: *Render, cmd: DrawCommand) void {
gl.uniformMatrix4fv(1, 1, gl.FALSE, @ptrCast(&cmd.transform.data)); gl.uniformMatrix4fv(Uniform.ModelMatrix.value(), 1, gl.FALSE, @ptrCast(&cmd.transform.data));
gl.uniform3fv(2, 1, @ptrCast(&cmd.material.albedo.data)); gl.uniform3fv(Uniform.Color.value(), 1, @ptrCast(&cmd.material.albedo.data));
gl.GL_ARB_bindless_texture.uniformHandleui64ARB( gl.GL_ARB_bindless_texture.uniformHandleui64ARB(
3, Uniform.AlbedoMap.value(),
self.assetman.resolveTexture(cmd.material.albedo_map).handle, 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); const mesh = self.assetman.resolveMesh(cmd.mesh);
mesh.positions.bind(Render.Attrib.Position.value()); 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 // TODO: support ortho
pub const Camera = struct { pub const Camera = struct {
fovy: f32 = 60, fovy: f32 = 60,
@ -288,4 +303,5 @@ pub const PointLightArray = extern struct {
pub const Material = struct { pub const Material = struct {
albedo: Vec3 = Vec3.one(), albedo: Vec3 = Vec3.one(),
albedo_map: AssetManager.Handle.Texture = .{}, albedo_map: AssetManager.Handle.Texture = .{},
normal_map: AssetManager.Handle.Texture = .{},
}; };

View File

@ -162,8 +162,13 @@ export fn game_init(global_allocator: *std.mem.Allocator) void {
// Plane // Plane
_ = globals.g_mem.world.addEntity(.{ _ = globals.g_mem.world.addEntity(.{
.flags = .{ .mesh = true }, .flags = .{ .mesh = true },
.transform = .{ .scale = Vec3.one().scale(100) }, .transform = .{ .scale = Vec3.one().scale(1) },
.mesh = .{ .handle = a.Meshes.plane }, .mesh = .{
.handle = a.Meshes.plane,
.material = .{
.normal_map = a.Textures.@"tile.norm",
},
},
}); });
// 10 bunnies // 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) }, .transform = .{ .pos = Vec3.new(@as(f32, @floatFromInt(i)) * 0.3, 0, 0) },
.flags = .{ .mesh = true }, .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",
},
},
}); });
} }
} }

View File

@ -209,22 +209,29 @@ const MipLevel = struct {
fn processTexture(allocator: std.mem.Allocator, input: [*:0]const u8, output: []const u8, hdr: bool) !void { fn processTexture(allocator: std.mem.Allocator, input: [*:0]const u8, output: []const u8, hdr: bool) !void {
_ = hdr; // autofix _ = 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 width_int: c_int = undefined;
var height_int: c_int = undefined; var height_int: c_int = undefined;
var comps: c_int = undefined; var comps: c_int = undefined;
c.stbi_set_flip_vertically_on_load(1); c.stbi_set_flip_vertically_on_load(1);
const FORCED_COMPONENTS = 4; // force rgb const rgba_data_c = c.stbi_load(input, &width_int, &height_int, &comps, 4);
const data_c = c.stbi_load(input, &width_int, &height_int, &comps, FORCED_COMPONENTS); if (rgba_data_c == null) {
if (data_c == null) {
return error.ImageLoadError; 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 width: usize = @intCast(width_int);
const height: usize = @intCast(height_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 // TODO: support textures not divisible by 4
if (width % 4 != 0 or height % 4 != 0) { 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; return error.ImageSizeShouldBeDivisibleBy4;
} }
var settings: c.bc7_enc_settings = undefined; if (comps == 4) {
if (comps == 3) {
c.GetProfile_ultrafast(&settings);
} else if (comps == 4) {
premultiplyAlpha(data); 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, u32,
@intFromFloat(@log2(@as(f32, @floatFromInt(@max(width, height))))), @intFromFloat(@log2(@as(f32, @floatFromInt(@max(width, height))))),
); );
@ -270,7 +269,7 @@ fn processTexture(allocator: std.mem.Allocator, input: [*:0]const u8, output: []
MipLevel{ MipLevel{
.width = mip_width, .width = mip_width,
.height = mip_height, .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; 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| { for (0..actual_mip_count) |mip_level| {
const mip_data = &mip_pyramid.items[mip_level]; const mip_data = &mip_pyramid.items[mip_level];
if (mip_level > 0) { 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; mip_data.out_data = try compressBlocksAlloc(allocator, mip_data.data, data_channels, format, @intCast(comps), mip_data.width, mip_data.height);
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;
} }
const out_data = try allocator.alloc([]const u8, actual_mip_count); 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{ const texture = formats.Texture{
.header = .{ .header = .{
.format = .bc7, .format = format,
.width = @intCast(width), .width = @intCast(width),
.height = @intCast(height), .height = @intCast(height),
.mip_count = @intCast(actual_mip_count), .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(); 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 gamma = 2.2;
const srgb_to_linear: [256]u8 = blk: { const srgb_to_linear: [256]u8 = blk: {
@setEvalBranchQuota(10000); @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))); 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 srcStride = src.width * 4;
const dstStride = dst.width * 4; const dstStride = dst.width * 4;
for (0..dst.height) |y| { for (0..dst.height) |y| {
@ -379,17 +450,37 @@ fn downsampleImage2X(src: *const MipLevel, dst: *const MipLevel) void {
const srcX = x0 + x1; const srcX = x0 + x1;
const srcY = y0 + y1; const srcY = y0 + y1;
result += loadColorVec(src.data[srcY * srcStride + srcX * 4 ..]); result += loadColorVec4(src.data[srcY * srcStride + srcX * 4 ..]);
} }
} }
result /= @splat(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); @setRuntimeSafety(false);
std.debug.assert(pixel.len >= 4); 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)); } / @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); @setRuntimeSafety(false);
std.debug.assert(pixel.len >= 4); std.debug.assert(pixel.len >= 4);