Skip to content

Commit

Permalink
Merge pull request #350 from RagnarokResearchLab/131-lightmap-texture…
Browse files Browse the repository at this point in the history
…-binding

Add support for lightmap textures to the WebGPU renderer
  • Loading branch information
rdw-software authored Jan 30, 2024
2 parents fa91a61 + 8a31d45 commit a079412
Show file tree
Hide file tree
Showing 12 changed files with 199 additions and 28 deletions.
41 changes: 41 additions & 0 deletions Core/FileFormats/RagnarokGND.lua
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ function RagnarokGND:DecodeTexturePaths()

local groundMeshSection = Mesh("GroundMeshSection" .. textureIndex)
groundMeshSection.material = GroundMeshMaterial("GroundMeshSection" .. textureIndex .. "Material")
-- Should preallocate based on observed sizes here? (same as for the other buffers)
groundMeshSection.lightmapTextureCoords = {}
table_insert(self.groundMeshSections, groundMeshSection)
end
end
Expand Down Expand Up @@ -451,6 +453,16 @@ function RagnarokGND:GenerateSurfaceGeometry(surfaceConstructionInfo)
table_insert(mesh.diffuseTextureCoords, surface.uvs.top_right_u)
table_insert(mesh.diffuseTextureCoords, surface.uvs.top_right_v)

local lightmapTextureCoords = self:ComputeLightmapTextureCoords(surface.lightmap_slice_id)
table_insert(mesh.lightmapTextureCoords, lightmapTextureCoords.bottomLeftU)
table_insert(mesh.lightmapTextureCoords, lightmapTextureCoords.bottomLeftV)
table_insert(mesh.lightmapTextureCoords, lightmapTextureCoords.bottomRightU)
table_insert(mesh.lightmapTextureCoords, lightmapTextureCoords.bottomRightV)
table_insert(mesh.lightmapTextureCoords, lightmapTextureCoords.topLeftU)
table_insert(mesh.lightmapTextureCoords, lightmapTextureCoords.topLeftV)
table_insert(mesh.lightmapTextureCoords, lightmapTextureCoords.topRightU)
table_insert(mesh.lightmapTextureCoords, lightmapTextureCoords.topRightV)

if surfaceConstructionInfo.facing == RagnarokGND.SURFACE_DIRECTION_UP then
local flatFaceNormalLeft = self:ComputeFlatFaceNormalLeft(gridU, gridV)
local flatFaceNormalRight = self:ComputeFlatFaceNormalRight(gridU, gridV)
Expand Down Expand Up @@ -770,6 +782,35 @@ function RagnarokGND:ComputeFlatFaceNormalRight(gridU, gridV)
return rightFaceNormal
end

function RagnarokGND:GenerateLightmapTextureImage()
-- Should replace with a power-of-two texture containing the actual lightmap slices
local textureFilePath = path.join("Core", "NativeClient", "Assets", "DebugTexture256.png")
local pngFileContents = C_FileSystem.ReadFile(textureFilePath)

local rgbaImageBytes, width, height = C_ImageProcessing.DecodeFileContents(pngFileContents)
local placeholderLightmapTexture = {
rgbaImageBytes = rgbaImageBytes,
width = width,
height = height,
}

return placeholderLightmapTexture
end

function RagnarokGND:ComputeLightmapTextureCoords(lightmapSliceID)
-- Should replace this with the actual lightmap coordinates (to be computed)
return {
bottomLeftU = 0,
bottomLeftV = 0,
bottomRightU = 0,
bottomRightV = 0,
topLeftU = 0,
topLeftV = 0,
topRightU = 0,
topRightV = 0,
}
end

ffi.cdef(RagnarokGND.cdefs)

return RagnarokGND
4 changes: 4 additions & 0 deletions Core/FileFormats/RagnarokMap.lua
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ function RagnarokMap:LoadTerrainGeometry(mapID)
local gnd = self.gnd

local groundMeshSections = gnd:GenerateGroundMeshSections()
local sharedLightmapTextureImage = gnd:GenerateLightmapTextureImage()
for sectionID, groundMeshSection in pairs(groundMeshSections) do
local texturePath = "texture/" .. gnd.diffuseTexturePaths[sectionID]
local normalizedTextureImagePath = RagnarokGRF:DecodeFileName(texturePath)
Expand All @@ -106,6 +107,9 @@ function RagnarokMap:LoadTerrainGeometry(mapID)
width = width,
height = height,
}

-- Should only upload once and bind the same texture?
groundMeshSection.lightmapTextureImage = sharedLightmapTextureImage
end

return groundMeshSections
Expand Down
75 changes: 63 additions & 12 deletions Core/NativeClient/Renderer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,11 @@ local Renderer = {
INVALID_INDEX_BUFFER = "Cannot upload geometry with invalid index buffer",
INVALID_COLOR_BUFFER = "Cannot upload geometry with invalid color buffer",
INVALID_UV_BUFFER = "Cannot upload geometry with invalid diffuse texture coordinates buffer",
INVALID_LIGHTMAP_UV_BUFFER = "Cannot upload geometry with invalid lightmap texture coordinates buffer",
INVALID_NORMAL_BUFFER = "Cannot upload geometry with invalid normal buffer",
INCOMPLETE_COLOR_BUFFER = "Cannot upload geometry with missing or incomplete vertex colors",
INCOMPLETE_UV_BUFFER = "Cannot upload geometry with missing or incomplete diffuse texture coordinates ",
INCOMPLETE_LIGHTMAP_UV_BUFFER = "Cannot upload geometry with missing or incomplete lightmap texture coordinates ",
INVALID_MATERIAL = "Invalid material assigned to mesh",
INCOMPLETE_NORMAL_BUFFER = "Cannot upload geometry with missing or incomplete surface normals ",
},
Expand Down Expand Up @@ -427,6 +429,13 @@ function Renderer:DrawMesh(renderPass, mesh)
self.dummyTextureMaterial:UpdateMaterialPropertiesUniform() -- Wasteful, should only do it once?
end

if rawget(mesh.material, "lightmapTexture") then
-- This binding slot is usually reserved for instance transforms, but they aren't needed for the terrain
RenderPassEncoder:SetBindGroup(renderPass, 2, mesh.material.lightmapTextureBindGroup, 0, nil)
local lightmapTexCoordsBufferSize = #mesh.lightmapTextureCoords * ffi.sizeof("float") or GPU.MAX_VERTEX_COUNT
RenderPassEncoder:SetVertexBuffer(renderPass, 4, mesh.lightmapTexCoordsBuffer, 0, lightmapTexCoordsBufferSize)
end

local instanceCount = 1
local firstVertexIndex = 0
local firstInstanceIndex = 0
Expand Down Expand Up @@ -561,6 +570,19 @@ function Renderer:UploadMeshGeometry(mesh)
printf("Uploading geometry: %d surface normals (%s)", normalCount, filesize(normalBufferSize))
local surfaceNormalsBuffer = Buffer:CreateVertexBuffer(self.wgpuDevice, mesh.surfaceNormals)

if rawget(mesh, "lightmapTextureCoords") then
local lightmapTextureCoordsCount = #mesh.lightmapTextureCoords / 2
local lightmapTextureCoordsBufferSize = #mesh.lightmapTextureCoords * ffi.sizeof("float")
printf(
"Uploading geometry: %d lightmap texture coordinates (%s)",
lightmapTextureCoordsCount,
filesize(lightmapTextureCoordsBufferSize)
)
local lightmapTextureCoordinatesBuffer = Buffer:CreateVertexBuffer(self.wgpuDevice, mesh.lightmapTextureCoords)

mesh.lightmapTexCoordsBuffer = lightmapTextureCoordinatesBuffer
end

mesh.vertexBuffer = vertexBuffer
mesh.colorBuffer = vertexColorsBuffer
mesh.indexBuffer = triangleIndicesBuffer
Expand Down Expand Up @@ -605,17 +627,26 @@ function Renderer:ValidateGeometry(mesh)
error(self.errorStrings.INCOMPLETE_NORMAL_BUFFER, 0)
end

if not mesh.diffuseTextureCoords then
return
end
if mesh.diffuseTextureCoords then
local diffuseTextureCoordsCount = #mesh.diffuseTextureCoords / 2
if (diffuseTextureCoordsCount * 2) % 2 ~= 0 then
error(self.errorStrings.INVALID_UV_BUFFER, 0)
end

local diffuseTextureCoordsCount = #mesh.diffuseTextureCoords / 2
if (diffuseTextureCoordsCount * 2) % 2 ~= 0 then
error(self.errorStrings.INVALID_UV_BUFFER, 0)
if vertexCount ~= diffuseTextureCoordsCount then
error(self.errorStrings.INCOMPLETE_UV_BUFFER, 0)
end
end

if vertexCount ~= diffuseTextureCoordsCount then
error(self.errorStrings.INCOMPLETE_UV_BUFFER, 0)
if rawget(mesh, "lightmapTextureCoords") then
local lightmapTextureCoordsCount = #mesh.lightmapTextureCoords / 2
if (lightmapTextureCoordsCount * 2) % 2 ~= 0 then
error(self.errorStrings.INVALID_LIGHTMAP_UV_BUFFER, 0)
end

if vertexCount ~= lightmapTextureCoordsCount then
error(self.errorStrings.INCOMPLETE_LIGHTMAP_UV_BUFFER, 0)
end
end
end

Expand All @@ -625,6 +656,7 @@ function Renderer:DestroyMeshGeometry(mesh)
Buffer:Destroy(rawget(mesh, "colorBuffer"))
Buffer:Destroy(rawget(mesh, "indexBuffer"))
Buffer:Destroy(rawget(mesh, "diffuseTexCoordsBuffer"))
Buffer:Destroy(rawget(mesh, "lightmapTexCoordsBuffer"))
Buffer:Destroy(rawget(mesh, "surfaceNormalsBuffer"))

self.meshes = {}
Expand Down Expand Up @@ -837,6 +869,7 @@ function Renderer:LoadSceneObjects(scene)
textureImages = { textureImage }
end

-- Process diffuse texture(s)
local diffuseTextures = {}
for textureIndex = 1, #textureImages, 1 do
local image = textureImages[textureIndex]
Expand All @@ -851,6 +884,15 @@ function Renderer:LoadSceneObjects(scene)
elseif #diffuseTextures > 1 then -- Water material (using texture array)
mesh.material:AssignDiffuseTextureArray(diffuseTextures)
end

-- Process lightmap texture
local image = rawget(mesh, "lightmapTextureImage")
if image then
self:DebugDumpTextures(mesh, format("lightmap-texture-%s-in.png", scene.mapID))
local wgpuTextureHandle = self:CreateTextureFromImage(image.rgbaImageBytes, image.width, image.height)
self:DebugDumpTextures(mesh, format("lightmap-texture-%s-out.png", scene.mapID))
mesh.material:AssignLightmapTexture(wgpuTextureHandle)
end
end

if scene.ambientLight then
Expand Down Expand Up @@ -883,17 +925,26 @@ function Renderer:DebugDumpTextures(mesh, fileName)
end

local diffuseTextureImage = mesh.diffuseTextureImage
if not diffuseTextureImage then
return
end

C_FileSystem.MakeDirectoryTree("Exports")
local pngBytes = C_ImageProcessing.EncodePNG(
diffuseTextureImage.rgbaImageBytes,
diffuseTextureImage.width,
diffuseTextureImage.height
)
C_FileSystem.WriteFile(path.join("Exports", fileName), pngBytes)

local lightmapTextureImage = rawget(mesh, "lightmapTextureImage")
if not lightmapTextureImage then
return
end

C_FileSystem.MakeDirectoryTree("Exports")
pngBytes = C_ImageProcessing.EncodePNG(
lightmapTextureImage.rgbaImageBytes,
lightmapTextureImage.width,
lightmapTextureImage.height
)
C_FileSystem.WriteFile(path.join("Exports", fileName), pngBytes)
end

function Renderer:ResetScene()
Expand Down
6 changes: 3 additions & 3 deletions Core/NativeClient/WebGPU/GPU.lua
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,9 @@ function GPU:RequestLogicalDevice(adapter, options)
maxTextureDimension2D = Texture.MAX_TEXTURE_DIMENSION,
maxTextureDimension3D = 0,
maxTextureArrayLayers = 1, -- For the depth/stencil texture
maxVertexAttributes = 4, -- Vertex positions, vertex colors, diffuse texture UVs, normals
maxVertexBuffers = 4, -- Vertex positions, vertex colors, diffuse texture UVs, normals
maxInterStageShaderComponents = 8, -- #(vec3f color, vec2f diffuseTextureCoords, float alpha), normal(vec3f)
maxVertexAttributes = 5, -- Vertex positions, vertex colors, diffuse UVs, normals, lightmap UVs
maxVertexBuffers = 5, -- Vertex positions, vertex colors, diffuse UVs, normals, lightmap UVs
maxInterStageShaderComponents = 10, -- #(vec3f color, vec2f diffuseTextureCoords, float alpha), normal(vec3f), lightmapUV(vec2f)
maxBufferSize = GPU.MAX_BUFFER_SIZE, -- DEFAULT
maxVertexBufferArrayStride = 20, -- #(Rml::Vertex)
maxBindGroups = 3, -- Camera, material, transforms
Expand Down
6 changes: 6 additions & 0 deletions Core/NativeClient/WebGPU/Materials/GroundMeshMaterial.lua
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ function GroundMeshMaterial:Construct(...)
return self.super.Construct(self, ...)
end

function GroundMeshMaterial:AssignLightmapTexture(texture)
-- It's probably safe to use the diffuse bind group layout here, at least for now?
self.lightmapTextureBindGroup = self:CreateMaterialPropertiesBindGroup(texture)
self.lightmapTexture = texture
end

class("GroundMeshMaterial", GroundMeshMaterial)
extend(GroundMeshMaterial, InvisibleBaseMaterial)

Expand Down
4 changes: 4 additions & 0 deletions Core/NativeClient/WebGPU/Materials/InvisibleBaseMaterial.lua
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ function InvisibleBaseMaterial:AssignDiffuseTexture(texture, wgpuTexture)
self.diffuseTexture = texture
end

function InvisibleBaseMaterial:AssignLightmapTexture(texture, wgpuTexture)
error("Lightmap textures aren't yet supported by this material", 0)
end

function InvisibleBaseMaterial:UpdateMaterialPropertiesUniform()
-- Should only send if the data has actually changed? (optimize later)
self.materialPropertiesUniform.data.diffuseRed = self.diffuseColor.red
Expand Down
2 changes: 1 addition & 1 deletion Core/NativeClient/WebGPU/Mesh.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ local UnlitMeshMaterial = require("Core.NativeClient.WebGPU.Materials.UnlitMeshM
local uuid = require("uuid")

local Mesh = {
NUM_BUFFERS_PER_MESH = 5, -- Positions, indices, colors, diffuse UVs, normals
MAX_BUFFER_COUNT_PER_MESH = 6, -- Positions, indices, colors, diffuse UVs, normals, lightmap UVs
}

function Mesh:Construct(name)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,23 @@ function BasicTriangleDrawingPipeline:CreateVertexBufferLayout()
stepMode = ffi.C.WGPUVertexStepMode_Vertex,
})

return new("WGPUVertexBufferLayout[?]", 4, {
local lightmapTexCoordsLayout = new("WGPUVertexBufferLayout", {
attributeCount = 1, -- UV
attributes = new("WGPUVertexAttribute", {
shaderLocation = 4, -- Pass as 5th argument
format = ffi.C.WGPUVertexFormat_Float32x2, -- Vector2D (float) = UV coords
offset = 0,
}),
arrayStride = 2 * sizeof("float"), -- sizeof(Vector2D) = uv
stepMode = ffi.C.WGPUVertexStepMode_Vertex,
})

return new("WGPUVertexBufferLayout[?]", 5, {
vertexPositionsLayout,
vertexColorsLayout,
diffuseTexCoordsLayout,
surfaceNormalsLayout,
lightmapTexCoordsLayout,
})
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ function GroundMeshDrawingPipeline:Construct(wgpuDeviceHandle, textureFormatID)
local sharedShaderModule = self:CreateShaderModule(wgpuDeviceHandle)
local renderPipelineDescriptor = new("WGPURenderPipelineDescriptor", {
vertex = {
bufferCount = 4, -- Vertex positions, colors, diffuse UVs, normals
bufferCount = 5, -- Vertex positions, colors, diffuse UVs, normals, lightmap UVs
module = sharedShaderModule,
entryPoint = "vs_main",
constantCount = 0,
Expand Down Expand Up @@ -82,10 +82,11 @@ function GroundMeshDrawingPipeline:Construct(wgpuDeviceHandle, textureFormatID)

-- Configure resource layout for the vertex shader
local pipelineLayoutDescriptor = new("WGPUPipelineLayoutDescriptor", {
bindGroupLayoutCount = 2,
bindGroupLayouts = new("WGPUBindGroupLayout[?]", 2, {
bindGroupLayoutCount = 3,
bindGroupLayouts = new("WGPUBindGroupLayout[?]", 3, {
UniformBuffer.cameraBindGroupLayout,
UniformBuffer.materialBindGroupLayout,
UniformBuffer.materialBindGroupLayout, -- Re-used for lightmaps
}),
})

Expand Down
16 changes: 12 additions & 4 deletions Core/NativeClient/WebGPU/Shaders/TerrainGeometryShader.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ struct VertexInput {
@location(1) color: vec3f,
@location(2) diffuseTextureCoords: vec2f,
@location(3) surfaceNormal: vec3f,
@location(4) lightmapTextureCoords: vec2f,
};

struct VertexOutput {
@builtin(position) position: vec4f,
@location(0) color: vec3f,
@location(1) diffuseTextureCoords: vec2f,
@location(2) surfaceNormal: vec3f,
@location(4) lightmapTextureCoords: vec2f,
};

// CameraBindGroup: Updated once per frame
Expand Down Expand Up @@ -40,8 +42,9 @@ struct PerMaterialData {
@group(1) @binding(2)
var<uniform> uMaterialInstanceData: PerMaterialData;

// InstanceBindGroup: Updated once per mesh instance
// NYI (only for RML UI widgets)
// InstanceBindGroup: Re-used to store the lightmap texture for this material (hacky; I know...)
@group(2) @binding(0) var lightmapTexture: texture_2d<f32>;
@group(2) @binding(1) var lightmapTextureSampler: sampler;

const MATH_PI = 3.14159266;
const DEBUG_ALPHA_OFFSET = 0.0; // Set to non-zero value (e.g., 0.2) to make transparent background pixels visible
Expand Down Expand Up @@ -115,6 +118,7 @@ fn vs_main(in: VertexInput) -> VertexOutput {
out.color = in.color;
out.surfaceNormal = in.surfaceNormal;
out.diffuseTextureCoords = in.diffuseTextureCoords;
out.lightmapTextureCoords = in.lightmapTextureCoords;
return out;
}

Expand All @@ -124,7 +128,11 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4f {
let diffuseTextureColor = textureSample(diffuseTexture, diffuseTextureSampler, textureCoords);
let normal = normalize(in.surfaceNormal);
let sunlightColor = uPerSceneData.directionalLightColor.rgb;
let ambientColor = uPerSceneData.ambientLight.rgb;
let ambientColor = uPerSceneData.ambientLight.rgb;

let lightmapTexCoords = in.lightmapTextureCoords;
var lightmapTextureColor = textureSample(lightmapTexture, lightmapTextureSampler, lightmapTexCoords);
lightmapTextureColor = vec4f(0.05, 0.05, 0.05, 0.05); // Remove once the real lightmaps are sampled here

// Simulated fixed-function pipeline (DirectX7/9) - no specular highlights needed AFAICT?
let sunlightRayOrigin = -normalize(uPerSceneData.directionalLightDirection.xyz);
Expand All @@ -134,7 +142,7 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4f {

// Screen blending increases the vibrancy of colors (see https://en.wikipedia.org/wiki/Blend_modes#Screen)
let contrastCorrectionColor = clampToUnitRange(ambientColor + sunlightColor - (sunlightColor * ambientColor));
let fragmentColor = in.color * contrastCorrectionColor * combinedLightContribution * diffuseTextureColor.rgb ;
let fragmentColor = in.color * contrastCorrectionColor * combinedLightContribution * diffuseTextureColor.rgb + lightmapTextureColor.rgb;

// Gamma-correction:
// WebGPU assumes that the colors output by the fragment shader are given in linear space
Expand Down
7 changes: 7 additions & 0 deletions Tests/FileFormats/RagnarokGND.spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -566,11 +566,18 @@ describe("RagnarokGND", function()
triangleConnections = {},
diffuseTextureCoords = {},
surfaceNormals = {},
lightmapTextureCoords = {},
},
}
local sections = gnd:GenerateGroundMeshSections()
assertEquals(#sections, 1) -- Index starts at zero
assertEquals(table.count(sections), 1)

-- Remove this once the actual lightmap UVs are being computed
for index, section in ipairs(sections) do
section.lightmapTextureCoords = nil -- No need to snapshot placeholder UVs
end

local json = require("json")
local jsonDump = json.prettier(sections)
-- Leaving this here because the snapshot likely needs to be recreated once lightmaps/normals are needed
Expand Down
Loading

0 comments on commit a079412

Please sign in to comment.