Skip to content

Commit

Permalink
Merge pull request #355 from RagnarokResearchLab/132-linear-vertex-fog
Browse files Browse the repository at this point in the history
Implement linear vertex fog for RSW-based scenes
  • Loading branch information
rdw-software authored Jan 31, 2024
2 parents a53889e + e9e8274 commit 4695908
Show file tree
Hide file tree
Showing 11 changed files with 251 additions and 95 deletions.
5 changes: 5 additions & 0 deletions Core/FileFormats/RagnarokMap.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ local RagnarokGND = require("Core.FileFormats.RagnarokGND")
local RagnarokGRF = require("Core.FileFormats.RagnarokGRF")
local RagnarokRSW = require("Core.FileFormats.RagnarokRSW")

local C_Resources = require("Core.NativeClient.C_Resources")

local NormalsVisualization = require("Core.NativeClient.DebugDraw.NormalsVisualization")

local uv = require("uv")
Expand Down Expand Up @@ -64,6 +66,9 @@ function RagnarokMap:Construct(mapID, fileSystem)
}
scene.directionalLight = sun

-- Not really testable with how the resource management works currently; should improve later
scene.fogParameters = C_Resources.PERSISTENT_RESOURCES["data/fogparametertable.txt"][mapID]

printf("[RagnarokMap] Entering world %s (%s)", mapID, scene.displayName)

return scene
Expand Down
32 changes: 32 additions & 0 deletions Core/NativeClient/C_Resources.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
local FogParameters = require("Core.FileFormats.FogParameters")
local RagnarokGRF = require("Core.FileFormats.RagnarokGRF")

local C_Resources = {
GRF_FILE_PATH = "data.grf",
PERSISTENT_RESOURCES = {
["data/sprite/cursors.act"] = false,
["data/sprite/cursors.spr"] = false,
["data/fogparametertable.txt"] = FogParameters,
},
}

local self = C_Resources

function C_Resources.PreloadPersistentResources()
local grf = RagnarokGRF()
grf:Open(self.GRF_FILE_PATH)

printf("Preloading %d persistent resources from %s", table.count(self.PERSISTENT_RESOURCES), self.GRF_FILE_PATH)
for filePath, decoder in pairs(self.PERSISTENT_RESOURCES) do
local fileContents = grf:ExtractFileInMemory(filePath)
self.PERSISTENT_RESOURCES[filePath] = fileContents
if decoder then
printf("Decoding persistent resource: %s", filePath)
self.PERSISTENT_RESOURCES[filePath] = decoder:DecodeFileContents(fileContents)
end
end

self.grf = grf -- No need to close as reopening would be expensive (OS will free the handle)
end

return C_Resources
40 changes: 5 additions & 35 deletions Core/NativeClient/NativeClient.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ local uv = require("uv")

local C_Camera = require("Core.NativeClient.C_Camera")
local C_Cursor = require("Core.NativeClient.C_Cursor")
local C_Resources = require("Core.NativeClient.C_Resources")
local DebugScene = require("Core.NativeClient.DebugDraw.DebugScene")
local Renderer = require("Core.NativeClient.Renderer")
local Vector3D = require("Core.VectorMath.Vector3D")

local RagnarokGRF = require("Core.FileFormats.RagnarokGRF")
local RagnarokMap = require("Core.FileFormats.RagnarokMap")

local PerformanceMetricsOverlay = require("Core.NativeClient.Interface.PerformanceMetricsOverlay")
Expand All @@ -21,20 +21,14 @@ local tonumber = tonumber
local NativeClient = {
mainWindow = nil,
deferredEventQueue = nil,
-- Should probably move this to a dedicated Resources API (later)
GRF_FILE_PATH = "data.grf",
PERSISTENT_RESOURCES = {
["data/sprite/cursors.act"] = false,
["data/sprite/cursors.spr"] = false,
},
FALLBACK_SCENE_ID = "wgpu",
}

function NativeClient:Start(loginSceneID)
self.mainWindow = self:CreateMainWindow()
Renderer:InitializeWithGLFW(self.mainWindow)

self:PreloadPersistentResources()
C_Resources.PreloadPersistentResources()
self:LoadSceneByID(loginSceneID or self.FALLBACK_SCENE_ID)

self:StartRenderLoop()
Expand Down Expand Up @@ -376,36 +370,12 @@ function NativeClient:IsShiftKeyDown()
return (glfw.bindings.glfw_get_key(self.mainWindow, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS)
end

-- Can move to runtime later?
local function table_count(t)
local count = 0

for k, v in pairs(t) do
count = count + 1
end

return count
end

-- Should probably move this to a dedicated Resources API (later)
function NativeClient:PreloadPersistentResources()
local grf = RagnarokGRF()
grf:Open(self.GRF_FILE_PATH)

printf("Preloading %d persistent resources from %s", table_count(self.PERSISTENT_RESOURCES), self.GRF_FILE_PATH)
for filePath, isLoaded in pairs(self.PERSISTENT_RESOURCES) do
self.PERSISTENT_RESOURCES[filePath] = grf:ExtractFileInMemory(filePath)
end

self.grf = grf -- No need to close as reopening would be expensive (OS will free the handle)
end

function NativeClient:LoadSceneByID(globallyUniqueSceneID)
printf("Loading scene %s", globallyUniqueSceneID)
Renderer:ResetScene()

-- This might seem sketchy, but it allows swapping the asset source on the fly (e.g., disk/network/virtual FS)
local grfFileSystem = self.grf:MakeFileSystem(self.GRF_FILE_PATH)
local grfFileSystem = C_Resources.grf:MakeFileSystem(C_Resources.GRF_FILE_PATH)

local map = RagnarokMap(globallyUniqueSceneID, grfFileSystem) or DebugScene(globallyUniqueSceneID)
Renderer:LoadSceneObjects(map)
Expand All @@ -414,7 +384,7 @@ end
function NativeClient:LoadScenesOneByOne(delayInMilliseconds)
delayInMilliseconds = delayInMilliseconds or 1
local mapDB = require("DB.Maps")
local gndFiles = self.grf:FindFilesByType("gnd")
local gndFiles = C_Resources.grf:FindFilesByType("gnd")
local numAvailableGNDs = table.count(gndFiles)

local numMapsLoaded, numMapsSkipped = 0, 0
Expand Down Expand Up @@ -445,7 +415,7 @@ function NativeClient:LoadScenesOneByOne(delayInMilliseconds)
self:LoadSceneByID(mapID)
numMapsLoaded = numMapsLoaded + 1
else
printf("Skipping map %s since it wasn't found in %s", mapID, self.GRF_FILE_PATH)
printf("Skipping map %s since it wasn't found in %s", mapID, C_Resources.GRF_FILE_PATH)
numMapsSkipped = numMapsSkipped + 1
end
end)
Expand Down
20 changes: 20 additions & 0 deletions Core/NativeClient/Renderer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,22 @@ function Renderer:UpdateScenewideUniformBuffer(deltaTime)
perSceneUniformData.directionalLightBlue = self.directionalLight.blue
perSceneUniformData.directionalLightIntensity = self.directionalLight.intensity
assert(self.directionalLight.intensity == 1, "The directional light must always be at full intensity")
perSceneUniformData.cameraWorldPosition.x = cameraWorldPosition.x
perSceneUniformData.cameraWorldPosition.y = cameraWorldPosition.y
perSceneUniformData.cameraWorldPosition.z = cameraWorldPosition.z

if not self.fogParameters then
-- Disabling the effect in a roundabout way to avoid a new uniform just for this
perSceneUniformData.fogNearLimit = 10
perSceneUniformData.fogFarLimit = 1
else
local viewDistance = C_Camera.farPlaneDistanceInWorldUnits - C_Camera.nearPlaneDistanceInWorldUnits
perSceneUniformData.fogNearLimit = self.fogParameters.near * viewDistance
perSceneUniformData.fogFarLimit = self.fogParameters.far * viewDistance
perSceneUniformData.fogColorRed = self.fogParameters.color.red
perSceneUniformData.fogColorGreen = self.fogParameters.color.green
perSceneUniformData.fogColorBlue = self.fogParameters.color.blue
end

Queue:WriteBuffer(
Device:GetQueue(self.wgpuDevice),
Expand Down Expand Up @@ -913,6 +929,8 @@ function Renderer:LoadSceneObjects(scene)
self.directionalLight.intensity = DEFAULT_SUNLIGHT_COLOR.intensity
self.directionalLight.rayDirection = DEFAULT_SUNLIGHT_DIRECTION
end

self.fogParameters = scene.fogParameters
end

function Renderer:DebugDumpTextures(mesh, fileName)
Expand Down Expand Up @@ -966,6 +984,8 @@ function Renderer:ResetScene()
self.directionalLight.rayDirection.x = DEFAULT_SUNLIGHT_DIRECTION.x
self.directionalLight.rayDirection.y = DEFAULT_SUNLIGHT_DIRECTION.y
self.directionalLight.rayDirection.z = DEFAULT_SUNLIGHT_DIRECTION.z

self.fogParameters = nil
end

return Renderer
2 changes: 1 addition & 1 deletion Core/NativeClient/WebGPU/GPU.lua
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ function GPU:RequestLogicalDevice(adapter, options)
maxTextureArrayLayers = 1, -- For the depth/stencil texture
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)
maxInterStageShaderComponents = 11, -- #(vec3f color, vec2f diffuseTextureCoords, float alpha), normal(vec3f), lightmapUV(vec2f), fogFactor:f32
maxBufferSize = GPU.MAX_BUFFER_SIZE, -- DEFAULT
maxVertexBufferArrayStride = 20, -- #(Rml::Vertex)
maxBindGroups = 3, -- Camera, material, transforms
Expand Down
18 changes: 17 additions & 1 deletion Core/NativeClient/WebGPU/Shaders/TerrainGeometryShader.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ struct VertexOutput {
@location(1) diffuseTextureCoords: vec2f,
@location(2) surfaceNormal: vec3f,
@location(4) lightmapTextureCoords: vec2f,
@location(5) fogFactor: f32,
};

// CameraBindGroup: Updated once per frame
Expand All @@ -25,6 +26,9 @@ struct PerSceneData {
unusedPadding: f32,
directionalLightDirection: vec4f,
directionalLightColor: vec4f,
cameraWorldPosition: vec4f,
fogColor: vec4f,
fogLimits: vec4f,
};

@group(0) @binding(0) var<uniform> uPerSceneData: PerSceneData;
Expand Down Expand Up @@ -119,6 +123,15 @@ fn vs_main(in: VertexInput) -> VertexOutput {
out.surfaceNormal = in.surfaceNormal;
out.diffuseTextureCoords = in.diffuseTextureCoords;
out.lightmapTextureCoords = in.lightmapTextureCoords;

let worldPosition = T1 * S * homogeneousPosition;
let distance = length(worldPosition.xyz - uPerSceneData.cameraWorldPosition.xyz);

let fogNearLimit = uPerSceneData.fogLimits.x;
let fogFarLimit = uPerSceneData.fogLimits.y;
let fogFactor = (fogFarLimit - distance) / (fogFarLimit - fogNearLimit);
out.fogFactor = 1.0 - clamp(fogFactor, 0.0, 1.0);

return out;
}

Expand All @@ -143,9 +156,12 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4f {
let contrastCorrectionColor = clampToUnitRange(ambientColor + sunlightColor - (sunlightColor * ambientColor));
let fragmentColor = clampToUnitRange(in.color * contrastCorrectionColor * combinedLightContribution * diffuseTextureColor.rgb + lightmapTextureColor.rgb);

// Should be a no-op if fog is disabled, since the fogFactor would be zero
let foggedColor = mix(fragmentColor.rgb, uPerSceneData.fogColor.rgb, in.fogFactor);

// Gamma-correction:
// WebGPU assumes that the colors output by the fragment shader are given in linear space
// When setting the surface format to BGRA8UnormSrgb it performs a linear to sRGB conversion
let gammaCorrectedColor = pow(fragmentColor.rgb, vec3f(2.2));
let gammaCorrectedColor = pow(foggedColor.rgb, vec3f(2.2));
return vec4f(gammaCorrectedColor, diffuseTextureColor.a + DEBUG_ALPHA_OFFSET);
}
10 changes: 10 additions & 0 deletions Core/NativeClient/WebGPU/UniformBuffer.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
local Device = require("Core.NativeClient.WebGPU.Device")
local GPU = require("Core.NativeClient.WebGPU.GPU")
local _ = require("Core.VectorMath.Vector3D") -- Only needed for the cdefs right now
local _ = require("Core.VectorMath.Matrix4D") -- Only needed for the cdefs right now

local bit = require("bit")
Expand Down Expand Up @@ -34,6 +35,15 @@ local UniformBuffer = {
float directionalLightGreen; // 184
float directionalLightBlue; // 188
float directionalLightIntensity; // 192
Vector3D cameraWorldPosition; // 204
float padding[1]; // 208
float fogColorRed; // 212
float fogColorGreen; // 216
float fogColorBlue; // 220
float padding[1]; // 224
float fogNearLimit; // 228
float fogFarLimit; // 232
float padding[2]; // 240
// Padding needs to be updated whenever the struct changes!
} scenewide_uniform_t;
typedef struct PerMaterialData {
Expand Down
102 changes: 102 additions & 0 deletions Tests/NativeClient/C_Resources.spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
local RagnarokGRF = require("Core.FileFormats.RagnarokGRF")

local C_Resources = require("Core.NativeClient.C_Resources")

describe("C_Resources", function()
describe("PreloadPersistentResources", function()
local DEFAULT_GRF_PATH = C_Resources.GRF_FILE_PATH
local PRELOADED_ASSET_FILES = C_Resources.PERSISTENT_RESOURCES

after(function()
C_Resources.GRF_FILE_PATH = DEFAULT_GRF_PATH
C_Resources.PERSISTENT_RESOURCES = PRELOADED_ASSET_FILES
end)

it("should throw if the configured asset container doesn't exist", function()
local function preloadFromNonExistingGRF()
C_Resources.GRF_FILE_PATH = "invalid.grf"
C_Resources.PreloadPersistentResources()
end
local expectedErrorMessage = "Failed to open archive invalid.grf (No such file exists)"
assertThrows(preloadFromNonExistingGRF, expectedErrorMessage)
end)

it("should throw if the configured asset container isn't a valid GRF archive", function()
local SOME_EXISTING_FILE = path.join("Tests", "Fixtures", "test.rgz")
local function preloadFromInvalidGRF()
C_Resources.GRF_FILE_PATH = SOME_EXISTING_FILE
C_Resources.PreloadPersistentResources()
end
local expectedErrorMessage = format("Failed to open archive %s (Not a .grf file)", SOME_EXISTING_FILE)
assertThrows(preloadFromInvalidGRF, expectedErrorMessage)
end)

it("should load and store all persistent resources from the configured asset container", function()
C_Resources.GRF_FILE_PATH = path.join("Tests", "Fixtures", "test.grf")
C_Resources.PERSISTENT_RESOURCES = {
["hello-grf.txt"] = false,
["subdirectory/hello.txt"] = false,
["uppercase.png"] = false,
["안녕하세요.txt"] = false,
}
C_Resources.PreloadPersistentResources()

local grf = RagnarokGRF()
grf:Open(C_Resources.GRF_FILE_PATH)
local expectedFileContents = {
["hello-grf.txt"] = grf:ExtractFileInMemory("hello-grf.txt"),
["subdirectory/hello.txt"] = grf:ExtractFileInMemory("subdirectory/hello.txt"),
["uppercase.png"] = grf:ExtractFileInMemory("uppercase.png"),
["안녕하세요.txt"] = grf:ExtractFileInMemory("안녕하세요.txt"),
}
grf:Close()

-- Might want to add metadata later, but for now just caching the file contents should suffice
local preloadedAssetFiles = C_Resources.PERSISTENT_RESOURCES
assertEquals(preloadedAssetFiles["hello-grf.txt"], expectedFileContents["hello-grf.txt"])
assertEquals(preloadedAssetFiles["subdirectory/hello.txt"], expectedFileContents["subdirectory/hello.txt"])
assertEquals(preloadedAssetFiles["uppercase.png"], expectedFileContents["uppercase.png"])
assertEquals(preloadedAssetFiles["안녕하세요.txt"], expectedFileContents["안녕하세요.txt"])
end)

it("should decode any persistent resources that have been assigned a decoder", function()
C_Resources.GRF_FILE_PATH = path.join("Tests", "Fixtures", "test.grf")

-- This needs some streamlining once a proper resource management API is implemented
local MakeshiftImageDecoder = {
DecodeFileContents = function(self, fileContents)
local rgbaImageBytes, width, height = C_ImageProcessing.DecodeFileContents(fileContents)
local imageResource = {
width = width,
height = height,
rgbaImageBytes = rgbaImageBytes,
}
return imageResource
end,
}

C_Resources.PERSISTENT_RESOURCES = {
["uppercase.png"] = MakeshiftImageDecoder,
}
C_Resources.PreloadPersistentResources()

local grf = RagnarokGRF()
grf:Open(C_Resources.GRF_FILE_PATH)

local rgbaImageBytes, width, height =
C_ImageProcessing.DecodeFileContents(grf:ExtractFileInMemory("uppercase.png"))
local expectedFileContents = {
["uppercase.png"] = {
rgbaImageBytes = rgbaImageBytes,
width = width,
height = height,
},
}
grf:Close()

-- Might want to add metadata later, but for now just caching the file contents should suffice
local preloadedAssetFiles = C_Resources.PERSISTENT_RESOURCES
assertEquals(preloadedAssetFiles["uppercase.png"], expectedFileContents["uppercase.png"])
end)
end)
end)
Loading

0 comments on commit 4695908

Please sign in to comment.