mirror of
https://github.com/jessicanataliagta/PSPRecomp
synced 2026-09-26 08:41:08 -04:00
feat(vcs): add configurable CloudWorks volumetric clouds
This commit is contained in:
@@ -222,6 +222,8 @@ endif()
|
||||
add_custom_command(TARGET VCSNative POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${VCS_PROFILE_DIR}/config/VCSNative.ini" "$<TARGET_FILE_DIR:VCSNative>/VCSNative.ini"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${VCS_PROFILE_DIR}/config/ProperShaders.ini" "$<TARGET_FILE_DIR:VCSNative>/ProperShaders.ini"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${VCS_PROFILE_DIR}/data/VCSProject2DFX_Lights.bin"
|
||||
"$<TARGET_FILE_DIR:VCSNative>/VCSProject2DFX_Lights.bin"
|
||||
@@ -230,7 +232,10 @@ add_custom_command(TARGET VCSNative POST_BUILD
|
||||
"$<TARGET_FILE_DIR:VCSNative>/PROJECT2DFX_ATTRIBUTION.md"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${VCS_PROFILE_DIR}/third_party/project2dfx/LICENSE.txt"
|
||||
"$<TARGET_FILE_DIR:VCSNative>/PROJECT2DFX_LICENSE.txt")
|
||||
"$<TARGET_FILE_DIR:VCSNative>/PROJECT2DFX_LICENSE.txt"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${VCS_PROFILE_DIR}/third_party/cloudworks/ATTRIBUTION.md"
|
||||
"$<TARGET_FILE_DIR:VCSNative>/CLOUDWORKS_ATTRIBUTION.md")
|
||||
|
||||
# Profile-specific generator. The root psp_recomp target stays game-neutral.
|
||||
add_executable(vcs_recomp tools/vcs_codegen_main.cpp)
|
||||
|
||||
@@ -6,5 +6,7 @@ The VCS profile includes components that are distributed under licenses separate
|
||||
- SMAA shader resources: see `third_party/smaa/LICENSE.txt`.
|
||||
- Project2DFX-derived VCS LOD-light data/behavior reference by ThirteenAG: MIT. See `third_party/project2dfx/LICENSE.txt` and `third_party/project2dfx/ATTRIBUTION.md`.
|
||||
- HDR shader material under `shaders/hdr`: see `shaders/hdr/LICENSE_SIMULATEHDR.txt`.
|
||||
- CloudWorks Alpha 4.0 volumetric-cloud density/noise model by Brian Tu (RTU):
|
||||
CC BY-NC-SA 3.0. See `third_party/cloudworks/ATTRIBUTION.md`.
|
||||
|
||||
Commercial GTA assets and executables are not part of the repository.
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
; Standalone CloudWorks sky for VCSNative. This does not use the game's
|
||||
; timecycle or weather. Set Enabled=false to restore the unmodified game sky.
|
||||
[VolumetricClouds]
|
||||
Enabled=true
|
||||
MarchSteps=24
|
||||
Coverage=0.72
|
||||
Opacity=0.84
|
||||
; Keep the density field completely static while validating camera anchoring.
|
||||
; A small positive value can be restored after the camera test is confirmed.
|
||||
Speed=0.0
|
||||
@@ -0,0 +1,167 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <limits>
|
||||
|
||||
namespace vcs {
|
||||
|
||||
// GE affine matrices use the same layout as ge_renderer::transform_4x3:
|
||||
//
|
||||
// view = A * world + t
|
||||
// A = [m0 m3 m6; m1 m4 m7; m2 m5 m8], t = [m9 m10 m11].
|
||||
//
|
||||
// view_to_world is A^-1 in row-major order. The three named axes are its
|
||||
// columns, so A^-1 * view_direction is exactly
|
||||
// right*x + up*y + forward*z. They are intentionally not normalized: retaining
|
||||
// scale/shear makes the result a true inverse for every nonsingular GE view.
|
||||
struct GeCloudCameraFrame {
|
||||
std::array<float, 9> view_to_world{};
|
||||
std::array<float, 3> position{};
|
||||
std::array<float, 3> right{};
|
||||
std::array<float, 3> up{};
|
||||
std::array<float, 3> forward{};
|
||||
};
|
||||
|
||||
namespace ge_cloud_camera_detail {
|
||||
|
||||
constexpr double absolute(double value) noexcept {
|
||||
return value < 0.0 ? -value : value;
|
||||
}
|
||||
|
||||
constexpr bool finite(double value) noexcept {
|
||||
return value == value &&
|
||||
value <= std::numeric_limits<double>::max() &&
|
||||
value >= -std::numeric_limits<double>::max();
|
||||
}
|
||||
|
||||
constexpr bool finite_float_result(double value) noexcept {
|
||||
return finite(value) &&
|
||||
absolute(value) <= static_cast<double>(std::numeric_limits<float>::max());
|
||||
}
|
||||
|
||||
} // namespace ge_cloud_camera_detail
|
||||
|
||||
// Computes a full inverse of the GE view's 3x3 linear part. Returns false for
|
||||
// non-finite or (numerically) singular input and leaves `out` unchanged.
|
||||
[[nodiscard]] inline constexpr bool ge_cloud_invert_view_linear(
|
||||
const std::array<float, 12> &view,
|
||||
std::array<float, 9> &out) noexcept {
|
||||
using ge_cloud_camera_detail::absolute;
|
||||
using ge_cloud_camera_detail::finite;
|
||||
using ge_cloud_camera_detail::finite_float_result;
|
||||
|
||||
for (float value : view) {
|
||||
if (!finite(static_cast<double>(value))) return false;
|
||||
}
|
||||
|
||||
const double a = view[0], b = view[3], c = view[6];
|
||||
const double d = view[1], e = view[4], f = view[7];
|
||||
const double g = view[2], h = view[5], i = view[8];
|
||||
|
||||
double scale = 0.0;
|
||||
for (std::size_t index = 0; index < 9; ++index) {
|
||||
const double value = absolute(static_cast<double>(view[index]));
|
||||
if (value > scale) scale = value;
|
||||
}
|
||||
if (scale == 0.0) return false;
|
||||
|
||||
const double determinant =
|
||||
a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g);
|
||||
const double singular_threshold =
|
||||
8.0 * static_cast<double>(std::numeric_limits<float>::epsilon()) *
|
||||
scale * scale * scale;
|
||||
if (!finite(determinant) || absolute(determinant) <= singular_threshold)
|
||||
return false;
|
||||
|
||||
const double inverse_determinant = 1.0 / determinant;
|
||||
const std::array<double, 9> inverse{
|
||||
(e * i - f * h) * inverse_determinant,
|
||||
(c * h - b * i) * inverse_determinant,
|
||||
(b * f - c * e) * inverse_determinant,
|
||||
(f * g - d * i) * inverse_determinant,
|
||||
(a * i - c * g) * inverse_determinant,
|
||||
(c * d - a * f) * inverse_determinant,
|
||||
(d * h - e * g) * inverse_determinant,
|
||||
(b * g - a * h) * inverse_determinant,
|
||||
(a * e - b * d) * inverse_determinant,
|
||||
};
|
||||
|
||||
std::array<float, 9> result{};
|
||||
for (std::size_t index = 0; index < inverse.size(); ++index) {
|
||||
if (!finite_float_result(inverse[index])) return false;
|
||||
result[index] = static_cast<float>(inverse[index]);
|
||||
}
|
||||
out = result;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Builds the world-space camera origin and the exact view-to-world basis from
|
||||
// a GE world-to-view matrix. Returns false and leaves `out` unchanged if the
|
||||
// matrix cannot be inverted or any derived value is non-finite.
|
||||
[[nodiscard]] inline constexpr bool ge_cloud_camera_frame_from_view(
|
||||
const std::array<float, 12> &view,
|
||||
GeCloudCameraFrame &out) noexcept {
|
||||
std::array<float, 9> inverse{};
|
||||
if (!ge_cloud_invert_view_linear(view, inverse)) return false;
|
||||
|
||||
const double tx = view[9], ty = view[10], tz = view[11];
|
||||
const std::array<double, 3> position{
|
||||
-(static_cast<double>(inverse[0]) * tx +
|
||||
static_cast<double>(inverse[1]) * ty +
|
||||
static_cast<double>(inverse[2]) * tz),
|
||||
-(static_cast<double>(inverse[3]) * tx +
|
||||
static_cast<double>(inverse[4]) * ty +
|
||||
static_cast<double>(inverse[5]) * tz),
|
||||
-(static_cast<double>(inverse[6]) * tx +
|
||||
static_cast<double>(inverse[7]) * ty +
|
||||
static_cast<double>(inverse[8]) * tz),
|
||||
};
|
||||
for (double value : position) {
|
||||
if (!ge_cloud_camera_detail::finite_float_result(value)) return false;
|
||||
}
|
||||
|
||||
GeCloudCameraFrame result{};
|
||||
result.view_to_world = inverse;
|
||||
result.position = {static_cast<float>(position[0]),
|
||||
static_cast<float>(position[1]),
|
||||
static_cast<float>(position[2])};
|
||||
result.right = {inverse[0], inverse[3], inverse[6]};
|
||||
result.up = {inverse[1], inverse[4], inverse[7]};
|
||||
result.forward = {inverse[2], inverse[5], inverse[8]};
|
||||
out = result;
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace ge_cloud_camera_detail {
|
||||
|
||||
// Compile-time contract check using an exact 90-degree rotation and
|
||||
// translation. This also catches accidental row/column transposition.
|
||||
static_assert([] {
|
||||
constexpr std::array<float, 12> view{
|
||||
0.0f, 1.0f, 0.0f,
|
||||
-1.0f, 0.0f, 0.0f,
|
||||
0.0f, 0.0f, 1.0f,
|
||||
3.0f, -2.0f, -4.0f,
|
||||
};
|
||||
GeCloudCameraFrame frame{};
|
||||
return ge_cloud_camera_frame_from_view(view, frame) &&
|
||||
frame.position == std::array<float, 3>{2.0f, 3.0f, 4.0f} &&
|
||||
frame.right == std::array<float, 3>{0.0f, -1.0f, 0.0f} &&
|
||||
frame.up == std::array<float, 3>{1.0f, 0.0f, 0.0f} &&
|
||||
frame.forward == std::array<float, 3>{0.0f, 0.0f, 1.0f};
|
||||
}());
|
||||
|
||||
static_assert([] {
|
||||
constexpr std::array<float, 12> singular{
|
||||
1.0f, 0.0f, 0.0f,
|
||||
1.0f, 0.0f, 0.0f,
|
||||
0.0f, 0.0f, 1.0f,
|
||||
0.0f, 0.0f, 0.0f,
|
||||
};
|
||||
GeCloudCameraFrame frame{};
|
||||
return !ge_cloud_camera_frame_from_view(singular, frame);
|
||||
}());
|
||||
|
||||
} // namespace ge_cloud_camera_detail
|
||||
} // namespace vcs
|
||||
@@ -0,0 +1,324 @@
|
||||
#pragma once
|
||||
|
||||
namespace vcs {
|
||||
|
||||
// Standalone, single-present-pass adaptation of the low CloudWorks Alpha 4.0
|
||||
// cloud profile by Brian Tu (RTU). CloudWorks is licensed CC BY-NC-SA 3.0.
|
||||
//
|
||||
// Root-constant contract (b1, 21 DWORDs total):
|
||||
// 0..3 unused draw-state padding
|
||||
// 4 unused draw-state padding
|
||||
// 5..7 CloudRayRight.xyz (NDC-x world-ray coefficient)
|
||||
// 8 CloudTime (seconds * configured speed)
|
||||
// 9..11 CloudRayUp.xyz (NDC-y world-ray coefficient)
|
||||
// 12 CloudCoverage [0, 1]
|
||||
// 13..15 CloudRayForward.xyz (world ray at NDC 0,0)
|
||||
// 16 CloudOpacity [0, 1]
|
||||
// 17..19 CloudCameraPosition.xyz (world space, Z up)
|
||||
// 20 CloudSettings: bits 0..7 = march steps [4,64], bit 8 = enabled
|
||||
//
|
||||
// CloudRayRight/Up are deliberately not normalized camera axes. The host must
|
||||
// bake inverse projection into them. CloudRayForward must come directly from
|
||||
// unprojection (never cross(right, up)); this keeps the noise field fixed in
|
||||
// world space even for a reflected/scaled GE view basis.
|
||||
inline constexpr char kCloudWorksPresentShaderHlsl[] = R"CLOUD_HLSL(
|
||||
Texture2D<float4> SourceTexture : register(t0);
|
||||
SamplerState SourceSampler : register(s0);
|
||||
|
||||
cbuffer CloudPresentState : register(b1) {
|
||||
uint4 UnusedDrawState0;
|
||||
uint UnusedDrawState1;
|
||||
float3 CloudRayRight;
|
||||
float CloudTime;
|
||||
float3 CloudRayUp;
|
||||
float CloudCoverage;
|
||||
float3 CloudRayForward;
|
||||
float CloudOpacity;
|
||||
float3 CloudCameraPosition;
|
||||
uint CloudSettings;
|
||||
};
|
||||
|
||||
struct PresentVertexOutput {
|
||||
float4 position : SV_POSITION;
|
||||
float2 uv : TEXCOORD0;
|
||||
};
|
||||
|
||||
PresentVertexOutput PresentVS(uint id : SV_VertexID) {
|
||||
PresentVertexOutput output;
|
||||
if (id == 0u) {
|
||||
output.position = float4(-1.0, -1.0, 0.0, 1.0);
|
||||
output.uv = float2(0.0, 1.0);
|
||||
} else if (id == 1u) {
|
||||
output.position = float4(-1.0, 3.0, 0.0, 1.0);
|
||||
output.uv = float2(0.0, -1.0);
|
||||
} else {
|
||||
output.position = float4(3.0, -1.0, 0.0, 1.0);
|
||||
output.uv = float2(2.0, 1.0);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
// CloudWorks' original scalar sine hash and linearly interpolated value noise.
|
||||
float CwHash(float value) {
|
||||
return frac(sin(value / 1873.1873) * 1618.03398875);
|
||||
}
|
||||
|
||||
float CwNoise2(float3 p) {
|
||||
float3 cell = floor(p);
|
||||
float3 f = frac(p);
|
||||
float n = 1153.0 * cell.x + 2381.0 * cell.y + p.z;
|
||||
float right = n + 1153.0;
|
||||
float down = n + 2381.0;
|
||||
float opposite = right + 2381.0;
|
||||
return lerp(lerp(CwHash(n), CwHash(right), f.x),
|
||||
lerp(CwHash(down), CwHash(opposite), f.x), f.y);
|
||||
}
|
||||
|
||||
float CwNoise3(float3 p) {
|
||||
float3 cell = floor(p);
|
||||
float3 f = frac(p);
|
||||
float n = 1153.0 * cell.x + 2381.0 * cell.y + cell.z;
|
||||
float right = n + 1153.0;
|
||||
float down = n + 2381.0;
|
||||
float opposite = right + 2381.0;
|
||||
float a = lerp(CwHash(n), CwHash(n + 1.0), f.z);
|
||||
float b = lerp(CwHash(right), CwHash(right + 1.0), f.z);
|
||||
float c = lerp(CwHash(down), CwHash(down + 1.0), f.z);
|
||||
float d = lerp(CwHash(opposite), CwHash(opposite + 1.0), f.z);
|
||||
return lerp(lerp(a, b, f.x), lerp(c, d, f.x), f.y);
|
||||
}
|
||||
|
||||
float CwSmooth(float edge0, float edge1, float value) {
|
||||
float x = saturate((value - edge0) / (edge1 - edge0));
|
||||
return x * x * (3.0 - 2.0 * x);
|
||||
}
|
||||
|
||||
// x = shape factor, y = high density threshold, z = low threshold.
|
||||
float3 CwCloudShape(float height, float coverage) {
|
||||
const float bottom = 300.0;
|
||||
const float middle = 450.0;
|
||||
const float top = 700.0;
|
||||
float body = CwSmooth(0.0, middle, height) *
|
||||
(1.0 - CwSmooth(middle, top, height));
|
||||
// The handheld sky is a very small target after upscale. Use the same
|
||||
// CloudWorks profile but broaden its occupied threshold range so the deck
|
||||
// reads as a cloud mass rather than a few isolated wisps.
|
||||
float bottomRange = 0.24 + coverage * 0.52;
|
||||
float soft = (height - top) / (middle - top) *
|
||||
(bottomRange - 0.1) + 0.1;
|
||||
float total = rcp(0.92 + coverage * 0.48);
|
||||
return float3(body, total + soft, total - soft);
|
||||
}
|
||||
|
||||
float3 CwOffsetA(float flow) { return float3(-1.8, 1.0, 0.0) * flow; }
|
||||
float3 CwOffsetB(float flow) { return float3(-2.0, -0.2, 0.0) * flow; }
|
||||
float3 CwOffsetC(float flow) { return float3(-3.0, 0.0, -0.5) * flow; }
|
||||
float3 CwOffsetD(float flow) { return float3(-3.5, 0.0, 0.1) * flow; }
|
||||
|
||||
float CwChunk(float3 worldPosition, float shapeFactor, float flow) {
|
||||
float3 p = worldPosition;
|
||||
p += float3(-0.5, 0.0, 0.0) * p.z;
|
||||
float largeNoise = CwNoise3((p + CwOffsetA(flow)) * 0.0008);
|
||||
float smallNoise = CwNoise3((p + CwOffsetB(flow)) * 0.005);
|
||||
return largeNoise * (smallNoise * 0.5 + 0.3) * shapeFactor;
|
||||
}
|
||||
|
||||
float3 CwDistortion(float lump) {
|
||||
return float3(cos(lump * 1.6) * 60.0, 0.0, -lump * 8.0);
|
||||
}
|
||||
|
||||
float CwDetail(float lump, float3 worldPosition, float flow) {
|
||||
float3 distortion = CwDistortion(lump);
|
||||
float detail = 0.3 * CwNoise3(
|
||||
(worldPosition + CwOffsetC(flow) + distortion) * 0.02);
|
||||
distortion.z -= detail * 16.0;
|
||||
float3 detailPosition = worldPosition + CwOffsetD(flow);
|
||||
detail += 0.2 * CwNoise3((detailPosition + distortion / 3.0) * 0.04);
|
||||
detail += detail * 0.6 *
|
||||
CwNoise3((detailPosition + distortion * 8.0) * 0.1);
|
||||
return detail;
|
||||
}
|
||||
|
||||
// x = raw density field, y = extinction density, z = low threshold.
|
||||
float3 CwDensity(float3 worldPosition, float coverage, float flow) {
|
||||
float3 shape = CwCloudShape(worldPosition.z, coverage);
|
||||
float lump = CwChunk(worldPosition, shape.x, flow);
|
||||
float field = lump * (1.0 + CwDetail(lump, worldPosition, flow));
|
||||
float solidness = lerp(0.0, 7.0 * coverage,
|
||||
saturate((worldPosition.z - 300.0) / 400.0));
|
||||
float density = saturate((field - shape.z) /
|
||||
max(shape.y - shape.z, 1.0e-4)) * solidness;
|
||||
return float3(field, density * 1.45, shape.z);
|
||||
}
|
||||
|
||||
// One inexpensive approximation of CloudWorks' eight-sample sun shadow march.
|
||||
// It retains the original Chunk + DetailA profile used by ShadowMarching.
|
||||
float CwShadowDensity(float3 worldPosition, float coverage, float flow) {
|
||||
float3 shape = CwCloudShape(worldPosition.z, coverage);
|
||||
float lump = CwChunk(worldPosition, shape.x, flow);
|
||||
float3 distortion = CwDistortion(lump);
|
||||
float detail = 0.3 * CwNoise3(
|
||||
(worldPosition + CwOffsetC(flow) + distortion) * 0.02) * 1.75;
|
||||
float field = lump * (1.0 + detail);
|
||||
float solidness = lerp(0.0, 5.0 * coverage,
|
||||
saturate((worldPosition.z - 300.0) / 400.0));
|
||||
return saturate((field - (shape.z - 0.1)) /
|
||||
max(shape.y - (shape.z - 0.1), 1.0e-4)) * solidness;
|
||||
}
|
||||
|
||||
float CwSkyMask(float2 uv, float3 source, float rayHeight) {
|
||||
uint width = 1u;
|
||||
uint height = 1u;
|
||||
SourceTexture.GetDimensions(width, height);
|
||||
float2 pixel = rcp(float2(max(width, 1u), max(height, 1u)));
|
||||
float3 left = SourceTexture.SampleLevel(SourceSampler,
|
||||
saturate(uv - float2(pixel.x, 0.0)), 0.0).rgb;
|
||||
float3 right = SourceTexture.SampleLevel(SourceSampler,
|
||||
saturate(uv + float2(pixel.x, 0.0)), 0.0).rgb;
|
||||
float3 above = SourceTexture.SampleLevel(SourceSampler,
|
||||
saturate(uv - float2(0.0, pixel.y)), 0.0).rgb;
|
||||
float3 below = SourceTexture.SampleLevel(SourceSampler,
|
||||
saturate(uv + float2(0.0, pixel.y)), 0.0).rgb;
|
||||
float localEdge = max(max(length(source - left), length(source - right)),
|
||||
max(length(source - above), length(source - below)));
|
||||
float flatSky = 1.0 - CwSmooth(0.025, 0.12, localEdge);
|
||||
float blueOverRed = CwSmooth(0.025, 0.16, source.b - source.r);
|
||||
float blueOverGreen = CwSmooth(-0.04, 0.10, source.b - source.g);
|
||||
float luminance = dot(source, float3(0.2126, 0.7152, 0.0722));
|
||||
float visibleSky = blueOverRed * blueOverGreen *
|
||||
CwSmooth(0.12, 0.34, luminance);
|
||||
float aboveHorizon = CwSmooth(0.035, 0.13, rayHeight);
|
||||
return saturate(aboveHorizon * visibleSky * lerp(0.55, 1.0, flatSky));
|
||||
}
|
||||
|
||||
// Returns premultiplied cloud radiance in rgb and remaining transmittance in a.
|
||||
float4 CwMarchLowLayer(float3 rayOrigin, float3 rayDirection,
|
||||
float coverage, uint marchSteps, float flow) {
|
||||
float4 result = float4(0.0, 0.0, 0.0, 1.0);
|
||||
const float cloudBottom = 300.0;
|
||||
const float cloudTop = 700.0;
|
||||
const float cloudFadeDistance = 6000.0;
|
||||
float validDirection = rayDirection.z > 1.0e-4 ? 1.0 : 0.0;
|
||||
float safeRayHeight = max(rayDirection.z, 1.0e-4);
|
||||
float slabBegin = (cloudBottom - rayOrigin.z) / safeRayHeight;
|
||||
float slabEnd = (cloudTop - rayOrigin.z) / safeRayHeight;
|
||||
float rayBegin = max(min(slabBegin, slabEnd), 0.0);
|
||||
float rayEnd = min(max(slabBegin, slabEnd), cloudFadeDistance);
|
||||
rayEnd = validDirection > 0.5 ? rayEnd : rayBegin;
|
||||
|
||||
// World-anchored jitter breaks coherent contours without producing a
|
||||
// screen-space stipple pattern that turns with the camera.
|
||||
float3 entryPosition = rayOrigin + rayDirection * rayBegin;
|
||||
float nominalStep = (rayEnd - rayBegin) / max(float(marchSteps), 1.0);
|
||||
float jitter = CwNoise3(entryPosition * float3(0.031, 0.031, 0.013) + 19.19);
|
||||
float distanceAlongRay = rayBegin + jitter * min(nominalStep, 40.0);
|
||||
|
||||
const float3 sunDirection = normalize(float3(0.38, -0.28, 0.88));
|
||||
const float3 baseColor = float3(0.27, 0.32, 0.40);
|
||||
const float3 sunColor = float3(1.02, 1.00, 0.93);
|
||||
float3 radiance = 0.0;
|
||||
float transmittance = 1.0;
|
||||
float previousField = 0.0;
|
||||
float previousDensity = 0.0;
|
||||
|
||||
[loop]
|
||||
for (uint stepIndex = 0u; stepIndex < 64u; ++stepIndex) {
|
||||
if (stepIndex >= marchSteps || distanceAlongRay >= rayEnd ||
|
||||
transmittance <= 0.02) break;
|
||||
|
||||
float3 worldPosition = rayOrigin + rayDirection * distanceAlongRay;
|
||||
float3 densitySample = CwDensity(worldPosition, coverage, flow);
|
||||
|
||||
// CloudWorks' dynamic empty-space skipping: dense regions approach a
|
||||
// five-unit step; empty regions approach 80 units, expanding with
|
||||
// distance. Unlike the old uniform slab division, the sampled Z
|
||||
// planes therefore cannot form screen-aligned slices.
|
||||
float occupancy = saturate((2.0 * densitySample.x - previousField) /
|
||||
max(densitySample.z * 0.85, 1.0e-4));
|
||||
float stepLength = lerp(80.0, 5.0, occupancy);
|
||||
stepLength *= lerp(1.0, 8.0,
|
||||
saturate(distanceAlongRay / cloudFadeDistance));
|
||||
stepLength += CwNoise2(worldPosition + float3(0.0, 0.0, flow)) * 5.0;
|
||||
stepLength = min(stepLength, 160.0);
|
||||
stepLength = min(stepLength, rayEnd - distanceAlongRay);
|
||||
|
||||
if (densitySample.y > 1.0e-4 && stepLength > 0.0) {
|
||||
// Trapezoidal Beer-Lambert integration makes opacity independent
|
||||
// of the number of steps and avoids the saturated per-slice alpha
|
||||
// produced by density * uniformStepLength.
|
||||
float meanDensity = 0.5 * (previousDensity + densitySample.y);
|
||||
float opticalDepth = meanDensity * min(stepLength, 80.0) * 0.018;
|
||||
float segmentAlpha = 1.0 - exp(-opticalDepth);
|
||||
|
||||
float shadowDensity = CwShadowDensity(
|
||||
worldPosition + sunDirection * 60.0, coverage, flow);
|
||||
float sunVisibility = exp(-shadowDensity * 1.35);
|
||||
float forwardScatter = pow(saturate(dot(rayDirection, sunDirection)), 24.0);
|
||||
float lighting = saturate(0.28 + sunVisibility * 0.72);
|
||||
float3 cloudColor = lerp(baseColor, sunColor, lighting);
|
||||
cloudColor += sunColor * forwardScatter * 0.10;
|
||||
|
||||
radiance += transmittance * segmentAlpha * cloudColor;
|
||||
transmittance *= 1.0 - segmentAlpha;
|
||||
}
|
||||
|
||||
previousField = densitySample.x;
|
||||
previousDensity = densitySample.y;
|
||||
distanceAlongRay += max(stepLength, 1.0);
|
||||
}
|
||||
|
||||
result = float4(radiance, saturate(transmittance));
|
||||
return result;
|
||||
}
|
||||
|
||||
float4 PresentPS(PresentVertexOutput input) : SV_TARGET {
|
||||
float4 source = SourceTexture.SampleLevel(SourceSampler, input.uv, 0.0);
|
||||
const uint enabledBit = 0x100u;
|
||||
if ((CloudSettings & enabledBit) == 0u) return source;
|
||||
|
||||
uint marchSteps = min(64u, max(4u, CloudSettings & 0xFFu));
|
||||
float2 ndc = float2(input.uv.x * 2.0 - 1.0,
|
||||
1.0 - input.uv.y * 2.0);
|
||||
float3 rayDirection = normalize(CloudRayForward +
|
||||
CloudRayRight * ndc.x +
|
||||
CloudRayUp * ndc.y);
|
||||
float skyMask = CwSkyMask(input.uv, source.rgb, rayDirection.z);
|
||||
if (skyMask <= 1.0e-3) return source;
|
||||
|
||||
// Preserve the existing INI Speed semantics while giving the low profile
|
||||
// offsets a useful world-space velocity.
|
||||
float flow = CloudTime * 25.0;
|
||||
float4 clouds = CwMarchLowLayer(CloudCameraPosition, rayDirection,
|
||||
saturate(CloudCoverage), marchSteps, flow);
|
||||
float3 cloudComposite = clouds.rgb + source.rgb * clouds.a;
|
||||
source.rgb = lerp(source.rgb, cloudComposite,
|
||||
skyMask * saturate(CloudOpacity));
|
||||
return source;
|
||||
}
|
||||
|
||||
// World-target path. This is deliberately separate from PresentPS: its pixels
|
||||
// are in the same coordinate system as the selected GE camera, and the D3D12
|
||||
// pipeline depth-tests against the world's untouched clear depth. The result is
|
||||
// blended behind geometry before VCS scales/composites that target to display.
|
||||
float4 CloudTargetPS(PresentVertexOutput input) : SV_TARGET {
|
||||
const uint enabledBit = 0x100u;
|
||||
if ((CloudSettings & enabledBit) == 0u) discard;
|
||||
uint marchSteps = min(64u, max(4u, CloudSettings & 0xFFu));
|
||||
float2 ndc = float2(input.uv.x * 2.0 - 1.0,
|
||||
1.0 - input.uv.y * 2.0);
|
||||
float3 rayDirection = normalize(CloudRayForward +
|
||||
CloudRayRight * ndc.x +
|
||||
CloudRayUp * ndc.y);
|
||||
if (rayDirection.z <= 1.0e-4) discard;
|
||||
float flow = CloudTime * 25.0;
|
||||
float4 clouds = CwMarchLowLayer(CloudCameraPosition, rayDirection,
|
||||
saturate(CloudCoverage), marchSteps, flow);
|
||||
float alpha = saturate((1.0 - clouds.a) * CloudOpacity);
|
||||
if (alpha <= 1.0e-4) discard;
|
||||
// CwMarchLowLayer returns premultiplied radiance, so the matching PSO uses
|
||||
// ONE / INV_SRC_ALPHA blending.
|
||||
return float4(clouds.rgb * CloudOpacity, alpha);
|
||||
}
|
||||
)CLOUD_HLSL";
|
||||
|
||||
} // namespace vcs
|
||||
@@ -413,6 +413,15 @@ void shutdown_ge_gpu_backend() noexcept;
|
||||
[[nodiscard]] bool ge_gpu_backend_graphics_ready() noexcept;
|
||||
void ge_gpu_backend_record_draw(const GeGpuDrawDescriptor &draw) noexcept;
|
||||
|
||||
// Captures the dominant projected world camera for optional native post effects.
|
||||
// The matrices are observed with the draw, before the GE state advances.
|
||||
void ge_gpu_backend_observe_camera(const std::array<float, 12> &view,
|
||||
const std::array<float, 16> &projection,
|
||||
const std::array<float, 6> &viewport,
|
||||
const std::array<float, 3> &camera_position,
|
||||
const GeGpuDrawDescriptor &draw,
|
||||
std::uint32_t vertex_weight) noexcept;
|
||||
|
||||
// Stages already-decoded vertices into a persistently mapped Vulkan buffer.
|
||||
// This does not replace the software rasterizer yet; it proves that real VCS
|
||||
// geometry can cross the host->Vulkan boundary with no per-draw allocation.
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
#include "ge_gpu_backend.hpp"
|
||||
#include "ge_cloud_camera_math.hpp"
|
||||
#include "ge_cloudworks_present_shader.hpp"
|
||||
#include "vcs_config.hpp"
|
||||
#include "vcs_runtime_log.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -117,6 +120,27 @@ struct Dx12PixelConstants {
|
||||
};
|
||||
static_assert(sizeof(Dx12PixelConstants) == 5u * sizeof(std::uint32_t));
|
||||
|
||||
struct CloudCameraCandidate {
|
||||
std::array<float, 12> view{};
|
||||
std::array<float, 16> projection{};
|
||||
// scale X/Y, center X/Y and offset X/Y from the GE viewport. Keeping this
|
||||
// with the exact draw camera lets the present shader reconstruct the same
|
||||
// rays that the native geometry path rasterized.
|
||||
std::array<float, 6> viewport{};
|
||||
std::array<float, 3> camera_position{};
|
||||
std::uint32_t target{};
|
||||
std::uint64_t weight{};
|
||||
std::uint64_t occluding_weight{};
|
||||
};
|
||||
|
||||
struct CloudPresentConstants {
|
||||
std::array<float, 4> ray_right_time{};
|
||||
std::array<float, 4> ray_up_coverage{};
|
||||
std::array<float, 4> ray_forward_opacity{};
|
||||
std::array<float, 4> camera_settings{};
|
||||
};
|
||||
static_assert(sizeof(CloudPresentConstants) == 16u * sizeof(std::uint32_t));
|
||||
|
||||
struct Dx12FrameResources {
|
||||
ComPtr<ID3D12CommandAllocator> allocator;
|
||||
ComPtr<ID3D12Resource> upload_buffer;
|
||||
@@ -188,6 +212,7 @@ struct Dx12GeState {
|
||||
std::vector<std::byte> packed_0115_vertices;
|
||||
std::vector<std::uint32_t> indices;
|
||||
std::vector<Dx12Batch> batches;
|
||||
std::vector<CloudCameraCandidate> cloud_cameras;
|
||||
std::vector<std::byte> frame_rgba;
|
||||
std::vector<std::byte> last_texture_rgba;
|
||||
|
||||
@@ -249,8 +274,10 @@ struct Dx12GeState {
|
||||
std::uint32_t swap_width{};
|
||||
std::uint32_t swap_height{};
|
||||
ComPtr<ID3D12PipelineState> present_pipeline;
|
||||
ComPtr<ID3D12PipelineState> cloud_target_pipeline;
|
||||
ComPtr<ID3DBlob> present_vertex_shader;
|
||||
ComPtr<ID3DBlob> present_pixel_shader;
|
||||
ComPtr<ID3DBlob> cloud_target_pixel_shader;
|
||||
bool direct_present_ok{};
|
||||
std::uint32_t presented_framebuffer{};
|
||||
std::uint32_t missed_display_intervals{};
|
||||
@@ -813,6 +840,16 @@ cbuffer DrawPixelState : register(b1) {
|
||||
uint TextureEnvPacked;
|
||||
uint FogControlPacked;
|
||||
uint FramebufferFormat;
|
||||
float3 CloudRight;
|
||||
float CloudInvProjectionX;
|
||||
float3 CloudUp;
|
||||
float CloudInvProjectionY;
|
||||
float3 CloudCameraPosition;
|
||||
float CloudTime;
|
||||
float CloudCoverage;
|
||||
float CloudOpacity;
|
||||
float CloudMarchSteps;
|
||||
float CloudEnabled;
|
||||
};
|
||||
struct VSIn {
|
||||
float4 position : POSITION;
|
||||
@@ -1022,19 +1059,7 @@ float4 PSMain(VSOut input) : SV_TARGET {
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *present = R"HLSL(
|
||||
Texture2D<float4> SourceTexture : register(t0);
|
||||
SamplerState SourceSampler : register(s0);
|
||||
struct VSOut { float4 position : SV_POSITION; float2 uv : TEXCOORD0; };
|
||||
VSOut PresentVS(uint id : SV_VertexID) {
|
||||
VSOut o;
|
||||
if (id == 0u) { o.position=float4(-1.0,-1.0,0.0,1.0); o.uv=float2(0.0,1.0); }
|
||||
else if (id == 1u) { o.position=float4(-1.0,3.0,0.0,1.0); o.uv=float2(0.0,-1.0); }
|
||||
else { o.position=float4(3.0,-1.0,0.0,1.0); o.uv=float2(2.0,1.0); }
|
||||
return o;
|
||||
}
|
||||
float4 PresentPS(VSOut input) : SV_TARGET { return SourceTexture.Sample(SourceSampler, input.uv); }
|
||||
)HLSL";
|
||||
const char *present = kCloudWorksPresentShaderHlsl;
|
||||
errors.Reset();
|
||||
hr = D3DCompile(present, std::strlen(present), "VCSNativeDX12GEPresent", nullptr, nullptr,
|
||||
"PresentVS", "vs_5_1", flags, 0u, &s.present_vertex_shader, &errors);
|
||||
@@ -1051,6 +1076,16 @@ float4 PresentPS(VSOut input) : SV_TARGET { return SourceTexture.Sample(SourceSa
|
||||
: hr_text(hr, "D3DCompile(DX12 GE Present PS)");
|
||||
return false;
|
||||
}
|
||||
errors.Reset();
|
||||
hr = D3DCompile(present, std::strlen(present), "VCSNativeDX12GECloudTarget",
|
||||
nullptr, nullptr, "CloudTargetPS", "ps_5_1", flags, 0u,
|
||||
&s.cloud_target_pixel_shader, &errors);
|
||||
if (FAILED(hr)) {
|
||||
error = errors ? std::string(static_cast<const char *>(errors->GetBufferPointer()),
|
||||
errors->GetBufferSize())
|
||||
: hr_text(hr, "D3DCompile(DX12 GE cloud target PS)");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1087,7 +1122,10 @@ bool create_root_signature(Dx12GeState &s, std::string &error) noexcept {
|
||||
parameters[3].ParameterType = D3D12_ROOT_PARAMETER_TYPE_32BIT_CONSTANTS;
|
||||
parameters[3].Constants.ShaderRegister = 1u;
|
||||
parameters[3].Constants.RegisterSpace = 0u;
|
||||
parameters[3].Constants.Num32BitValues = 5u;
|
||||
// 5 draw-state DWORDs plus 16 cloud-present DWORDs. Together with the
|
||||
// 40-DWORD vertex transform and two descriptor tables this uses 63 of the
|
||||
// D3D12 root signature's 64 DWORD budget.
|
||||
parameters[3].Constants.Num32BitValues = 21u;
|
||||
parameters[3].ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL;
|
||||
D3D12_ROOT_SIGNATURE_DESC desc{};
|
||||
desc.NumParameters = static_cast<UINT>(parameters.size());
|
||||
@@ -1664,6 +1702,254 @@ std::uint32_t present_sampler(Dx12GeState &s) noexcept {
|
||||
return ensure_sampler(s, draw);
|
||||
}
|
||||
|
||||
bool invert_cloud_matrix(const std::array<float, 16> &matrix,
|
||||
std::array<double, 16> &inverse) noexcept {
|
||||
// Gauss-Jordan in double precision. GE projection matrices are small, but
|
||||
// the far plane can still make a float-only inverse needlessly fragile.
|
||||
double rows[4][8]{};
|
||||
double scale = 0.0;
|
||||
for (std::size_t row = 0u; row < 4u; ++row) {
|
||||
for (std::size_t column = 0u; column < 4u; ++column) {
|
||||
const double value = matrix[column * 4u + row];
|
||||
if (!std::isfinite(value)) return false;
|
||||
rows[row][column] = value;
|
||||
scale = std::max(scale, std::abs(value));
|
||||
}
|
||||
rows[row][4u + row] = 1.0;
|
||||
}
|
||||
if (!(scale > 0.0)) return false;
|
||||
const double epsilon = scale * 1.0e-12;
|
||||
for (std::size_t column = 0u; column < 4u; ++column) {
|
||||
std::size_t pivot = column;
|
||||
for (std::size_t row = column + 1u; row < 4u; ++row) {
|
||||
if (std::abs(rows[row][column]) > std::abs(rows[pivot][column]))
|
||||
pivot = row;
|
||||
}
|
||||
if (std::abs(rows[pivot][column]) <= epsilon) return false;
|
||||
if (pivot != column) {
|
||||
for (std::size_t entry = 0u; entry < 8u; ++entry)
|
||||
std::swap(rows[pivot][entry], rows[column][entry]);
|
||||
}
|
||||
const double divisor = rows[column][column];
|
||||
for (double &entry : rows[column]) entry /= divisor;
|
||||
for (std::size_t row = 0u; row < 4u; ++row) {
|
||||
if (row == column) continue;
|
||||
const double factor = rows[row][column];
|
||||
for (std::size_t entry = 0u; entry < 8u; ++entry)
|
||||
rows[row][entry] -= factor * rows[column][entry];
|
||||
}
|
||||
}
|
||||
std::array<double, 16> result{};
|
||||
for (std::size_t row = 0u; row < 4u; ++row) {
|
||||
for (std::size_t column = 0u; column < 4u; ++column) {
|
||||
const double value = rows[row][4u + column];
|
||||
if (!std::isfinite(value)) return false;
|
||||
result[column * 4u + row] = value;
|
||||
}
|
||||
}
|
||||
inverse = result;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool create_cloud_target_pipeline(Dx12GeState &s, std::string &error) noexcept {
|
||||
D3D12_GRAPHICS_PIPELINE_STATE_DESC pso{};
|
||||
pso.pRootSignature = s.root_signature.Get();
|
||||
pso.VS = {s.present_vertex_shader->GetBufferPointer(),
|
||||
s.present_vertex_shader->GetBufferSize()};
|
||||
pso.PS = {s.cloud_target_pixel_shader->GetBufferPointer(),
|
||||
s.cloud_target_pixel_shader->GetBufferSize()};
|
||||
pso.SampleMask = UINT_MAX;
|
||||
pso.RasterizerState.FillMode = D3D12_FILL_MODE_SOLID;
|
||||
pso.RasterizerState.CullMode = D3D12_CULL_MODE_NONE;
|
||||
pso.RasterizerState.DepthClipEnable = TRUE;
|
||||
auto &blend = pso.BlendState.RenderTarget[0];
|
||||
blend.BlendEnable = TRUE;
|
||||
blend.SrcBlend = D3D12_BLEND_ONE;
|
||||
blend.DestBlend = D3D12_BLEND_INV_SRC_ALPHA;
|
||||
blend.BlendOp = D3D12_BLEND_OP_ADD;
|
||||
blend.SrcBlendAlpha = D3D12_BLEND_ONE;
|
||||
blend.DestBlendAlpha = D3D12_BLEND_INV_SRC_ALPHA;
|
||||
blend.BlendOpAlpha = D3D12_BLEND_OP_ADD;
|
||||
blend.RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL;
|
||||
pso.DepthStencilState.DepthEnable = TRUE;
|
||||
pso.DepthStencilState.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ZERO;
|
||||
// World targets are cleared to reverse-depth zero. Equality therefore
|
||||
// restricts the fullscreen pass to pixels untouched by world geometry.
|
||||
pso.DepthStencilState.DepthFunc = D3D12_COMPARISON_FUNC_EQUAL;
|
||||
pso.DepthStencilState.StencilEnable = FALSE;
|
||||
pso.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
|
||||
pso.NumRenderTargets = 1u;
|
||||
pso.RTVFormats[0] = kColorFormat;
|
||||
pso.DSVFormat = s.depth_format;
|
||||
pso.SampleDesc.Count = s.sample_count;
|
||||
pso.SampleDesc.Quality = s.sample_quality;
|
||||
const HRESULT hr = s.device->CreateGraphicsPipelineState(
|
||||
&pso, IID_PPV_ARGS(&s.cloud_target_pipeline));
|
||||
if (FAILED(hr)) {
|
||||
error = hr_text(hr, "CreateGraphicsPipelineState(DX12 GE cloud target)");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const CloudCameraCandidate *select_cloud_camera(const Dx12GeState &s) noexcept {
|
||||
if (s.display_framebuffer == 0u) return nullptr;
|
||||
std::vector<std::uint32_t> ancestors;
|
||||
ancestors.reserve(std::min<std::size_t>(s.frame_targets.size() + 1u,
|
||||
kFramebufferTargetCapacity));
|
||||
ancestors.push_back(s.display_framebuffer & 0x001FFFF0u);
|
||||
const auto contains = [&](std::uint32_t address) {
|
||||
address &= 0x001FFFF0u;
|
||||
return std::find(ancestors.begin(), ancestors.end(), address) != ancestors.end();
|
||||
};
|
||||
bool changed = true;
|
||||
while (changed && ancestors.size() < kFramebufferTargetCapacity) {
|
||||
changed = false;
|
||||
for (const Dx12Batch &batch : s.batches) {
|
||||
if (!batch.framebuffer_feedback) continue;
|
||||
const std::uint32_t source = batch.feedback_address & 0x001FFFF0u;
|
||||
const std::uint32_t destination = batch.draw.framebuffer_address & 0x001FFFF0u;
|
||||
if (source == destination || !contains(destination) || contains(source)) continue;
|
||||
ancestors.push_back(source);
|
||||
changed = true;
|
||||
if (ancestors.size() == kFramebufferTargetCapacity) break;
|
||||
}
|
||||
}
|
||||
|
||||
const CloudCameraCandidate *best = nullptr;
|
||||
for (const CloudCameraCandidate &candidate : s.cloud_cameras) {
|
||||
if (!contains(candidate.target) || candidate.occluding_weight == 0u) continue;
|
||||
// Match the camera selector already proven by Project2DFX: the camera
|
||||
// with the most genuinely occluding geometry wins, then total weight.
|
||||
// Prioritising depth writes selected auxiliary/reflection passes in VCS.
|
||||
if (best == nullptr ||
|
||||
candidate.occluding_weight > best->occluding_weight ||
|
||||
(candidate.occluding_weight == best->occluding_weight &&
|
||||
candidate.weight > best->weight)) {
|
||||
best = &candidate;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
CloudPresentConstants cloud_present_constants(const Dx12GeState &s) noexcept {
|
||||
CloudPresentConstants out{};
|
||||
const auto &config = vcs_configuration().volumetric_clouds;
|
||||
if (!config.enabled || s.cloud_cameras.empty()) return out;
|
||||
const CloudCameraCandidate *camera = select_cloud_camera(s);
|
||||
if (camera == nullptr) return out;
|
||||
|
||||
GeCloudCameraFrame frame{};
|
||||
if (!ge_cloud_camera_frame_from_view(camera->view, frame)) return out;
|
||||
const Dx12FramebufferTarget *target = find_framebuffer_target(s, camera->target);
|
||||
const float logical_width = static_cast<float>(std::max<std::uint32_t>(
|
||||
1u, target != nullptr ? target->logical_width : kReferenceWidth));
|
||||
const float logical_height = static_cast<float>(std::max<std::uint32_t>(
|
||||
1u, target != nullptr ? target->logical_height : kReferenceHeight));
|
||||
const float x_a = camera->viewport[0] * (2.0f / logical_width);
|
||||
const float y_a = camera->viewport[1] * (2.0f / logical_height);
|
||||
const float x_b = (camera->viewport[2] - camera->viewport[4]) *
|
||||
(2.0f / logical_width) - 1.0f;
|
||||
const float y_b = (camera->viewport[3] - camera->viewport[5]) *
|
||||
(2.0f / logical_height) - 1.0f;
|
||||
if (!std::isfinite(x_a) || !std::isfinite(y_a) ||
|
||||
std::abs(x_a) < 1.0e-6f || std::abs(y_a) < 1.0e-6f)
|
||||
return out;
|
||||
|
||||
// Fold the GE viewport into projection exactly as make_transform_constants
|
||||
// does for native geometry. This eliminates the camera-relative drift that
|
||||
// came from treating raw P00/P11 as if every pass occupied the full target.
|
||||
std::array<float, 16> effective_projection{};
|
||||
for (std::size_t column = 0u; column < 4u; ++column) {
|
||||
const std::size_t base = column * 4u;
|
||||
effective_projection[base + 0u] =
|
||||
x_a * camera->projection[base + 0u] + x_b * camera->projection[base + 3u];
|
||||
effective_projection[base + 1u] =
|
||||
-y_a * camera->projection[base + 1u] - y_b * camera->projection[base + 3u];
|
||||
effective_projection[base + 2u] = camera->projection[base + 2u];
|
||||
effective_projection[base + 3u] = camera->projection[base + 3u];
|
||||
}
|
||||
std::array<double, 16> inverse_projection{};
|
||||
if (!invert_cloud_matrix(effective_projection, inverse_projection)) return out;
|
||||
|
||||
const auto view_direction = [&](double ndc_x, double ndc_y,
|
||||
std::array<double, 3> &direction) {
|
||||
constexpr double clip_z = 0.5;
|
||||
const std::array<double, 4> clip{ndc_x, ndc_y, clip_z, 1.0};
|
||||
std::array<double, 4> point{};
|
||||
for (std::size_t row = 0u; row < 4u; ++row) {
|
||||
for (std::size_t column = 0u; column < 4u; ++column)
|
||||
point[row] += inverse_projection[column * 4u + row] * clip[column];
|
||||
}
|
||||
if (!std::isfinite(point[3]) || std::abs(point[3]) < 1.0e-12) return false;
|
||||
for (std::size_t axis = 0u; axis < 3u; ++axis) {
|
||||
direction[axis] = point[axis] / point[3];
|
||||
if (!std::isfinite(direction[axis])) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
std::array<double, 3> center_view{}, right_view{}, up_view{};
|
||||
if (!view_direction(0.0, 0.0, center_view) ||
|
||||
!view_direction(1.0, 0.0, right_view) ||
|
||||
!view_direction(0.0, 1.0, up_view)) return out;
|
||||
const auto to_world = [&](const std::array<double, 3> &value) {
|
||||
return std::array<double, 3>{
|
||||
frame.view_to_world[0] * value[0] + frame.view_to_world[1] * value[1] +
|
||||
frame.view_to_world[2] * value[2],
|
||||
frame.view_to_world[3] * value[0] + frame.view_to_world[4] * value[1] +
|
||||
frame.view_to_world[5] * value[2],
|
||||
frame.view_to_world[6] * value[0] + frame.view_to_world[7] * value[1] +
|
||||
frame.view_to_world[8] * value[2]};
|
||||
};
|
||||
const std::array<double, 3> center_world = to_world(center_view);
|
||||
const std::array<double, 3> right_world = to_world(right_view);
|
||||
const std::array<double, 3> up_world = to_world(up_view);
|
||||
for (std::size_t axis = 0u; axis < 3u; ++axis) {
|
||||
const double ray_right = right_world[axis] - center_world[axis];
|
||||
const double ray_up = up_world[axis] - center_world[axis];
|
||||
if (!std::isfinite(ray_right) || !std::isfinite(ray_up) ||
|
||||
!std::isfinite(center_world[axis])) return {};
|
||||
out.ray_right_time[axis] = static_cast<float>(ray_right);
|
||||
out.ray_up_coverage[axis] = static_cast<float>(ray_up);
|
||||
out.ray_forward_opacity[axis] = static_cast<float>(center_world[axis]);
|
||||
out.camera_settings[axis] = camera->camera_position[axis];
|
||||
}
|
||||
out.ray_right_time[3] =
|
||||
static_cast<float>(s.frame_epoch) * (1.0f / 60.0f) * config.speed;
|
||||
out.ray_up_coverage[3] = config.coverage;
|
||||
out.ray_forward_opacity[3] = config.opacity;
|
||||
const std::uint32_t settings =
|
||||
std::clamp<std::uint32_t>(config.march_steps, 4u, 64u) | 0x100u;
|
||||
out.camera_settings[3] = std::bit_cast<float>(settings);
|
||||
return out;
|
||||
}
|
||||
|
||||
void record_clouds_into_world_target(Dx12GeState &s, Dx12FramebufferTarget &target,
|
||||
const CloudPresentConstants &clouds) noexcept {
|
||||
prepare_target_for_render(s, target);
|
||||
const D3D12_CPU_DESCRIPTOR_HANDLE rtv = rtv_cpu(s, target.rtv_index);
|
||||
const D3D12_CPU_DESCRIPTOR_HANDLE dsv = dsv_cpu(s, target.dsv_index);
|
||||
s.list->OMSetRenderTargets(1u, &rtv, FALSE, &dsv);
|
||||
D3D12_VIEWPORT viewport{0.0f, 0.0f, static_cast<float>(s.target_width),
|
||||
static_cast<float>(s.target_height), 0.0f, 1.0f};
|
||||
D3D12_RECT scissor{0, 0, static_cast<LONG>(s.target_width),
|
||||
static_cast<LONG>(s.target_height)};
|
||||
s.list->RSSetViewports(1u, &viewport);
|
||||
s.list->RSSetScissorRects(1u, &scissor);
|
||||
s.list->SetPipelineState(s.cloud_target_pipeline.Get());
|
||||
s.list->SetGraphicsRootSignature(s.root_signature.Get());
|
||||
ID3D12DescriptorHeap *heaps[]{s.srv_heap.Get(), s.sampler_heap.Get()};
|
||||
s.list->SetDescriptorHeaps(2u, heaps);
|
||||
s.list->SetGraphicsRootDescriptorTable(0u, srv_gpu(s, 0u));
|
||||
s.list->SetGraphicsRootDescriptorTable(1u, sampler_gpu(s, 0u));
|
||||
std::array<std::uint32_t, 21> constants{};
|
||||
std::memcpy(constants.data() + 5u, &clouds, sizeof(clouds));
|
||||
s.list->SetGraphicsRoot32BitConstants(
|
||||
3u, static_cast<UINT>(constants.size()), constants.data(), 0u);
|
||||
s.list->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
|
||||
s.list->DrawInstanced(3u, 1u, 0u, 0u);
|
||||
}
|
||||
|
||||
bool record_direct_present(Dx12GeState &s, Dx12FramebufferTarget &source,
|
||||
std::string &error) noexcept {
|
||||
if (!ensure_swapchain(s, error)) return false;
|
||||
@@ -1692,6 +1978,14 @@ bool record_direct_present(Dx12GeState &s, Dx12FramebufferTarget &source,
|
||||
s.list->SetDescriptorHeaps(2u, heaps);
|
||||
s.list->SetGraphicsRootDescriptorTable(0u, srv_gpu(s, source.srv_index));
|
||||
s.list->SetGraphicsRootDescriptorTable(1u, sampler_gpu(s, present_sampler(s)));
|
||||
// Clouds are rendered into the selected 3D world target before VCS samples
|
||||
// it for composition. Applying them here would mix world-camera rays with
|
||||
// final-display pixels and make the layer follow the screen.
|
||||
const CloudPresentConstants clouds{};
|
||||
std::array<std::uint32_t, 21> present_constants{};
|
||||
std::memcpy(present_constants.data() + 5u, &clouds, sizeof(clouds));
|
||||
s.list->SetGraphicsRoot32BitConstants(
|
||||
3u, static_cast<UINT>(present_constants.size()), present_constants.data(), 0u);
|
||||
s.list->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
|
||||
s.list->DrawInstanced(3u, 1u, 0u, 0u);
|
||||
transition(s.list.Get(), backbuffer, D3D12_RESOURCE_STATE_RENDER_TARGET,
|
||||
@@ -2082,6 +2376,7 @@ void clear_accumulation(Dx12GeState &s) noexcept {
|
||||
s.packed_0115_vertices.clear();
|
||||
s.indices.clear();
|
||||
s.batches.clear();
|
||||
s.cloud_cameras.clear();
|
||||
}
|
||||
|
||||
bool create_backend(Dx12GeState &s, std::string &error) noexcept {
|
||||
@@ -2168,6 +2463,7 @@ bool create_backend(Dx12GeState &s, std::string &error) noexcept {
|
||||
if (!create_root_signature(s, error)) return false;
|
||||
if (!create_targets(s, error)) return false;
|
||||
if (!create_present_pipeline(s, error)) return false;
|
||||
if (!create_cloud_target_pipeline(s, error)) return false;
|
||||
s.vertices.reserve(262144u);
|
||||
s.packed_0115_vertices.reserve(2621440u);
|
||||
s.indices.reserve(524288u);
|
||||
@@ -2208,6 +2504,8 @@ void destroy_backend(Dx12GeState &s) noexcept {
|
||||
s.swapchain.Reset();
|
||||
s.swap_rtv_heap.Reset();
|
||||
s.present_pipeline.Reset();
|
||||
s.cloud_target_pipeline.Reset();
|
||||
s.cloud_target_pixel_shader.Reset();
|
||||
s.present_pixel_shader.Reset();
|
||||
s.present_vertex_shader.Reset();
|
||||
s.pipelines.clear();
|
||||
@@ -2417,6 +2715,43 @@ void ge_gpu_backend_record_draw(const GeGpuDrawDescriptor &draw) noexcept {
|
||||
}
|
||||
}
|
||||
|
||||
void ge_gpu_backend_observe_camera(const std::array<float, 12> &view,
|
||||
const std::array<float, 16> &projection,
|
||||
const std::array<float, 6> &viewport,
|
||||
const std::array<float, 3> &camera_position,
|
||||
const GeGpuDrawDescriptor &draw,
|
||||
std::uint32_t vertex_weight) noexcept {
|
||||
Dx12GeState &s = state();
|
||||
if (!s.enabled || !vcs_configuration().volumetric_clouds.enabled ||
|
||||
vertex_weight == 0u || !draw.depth_test_enabled) return;
|
||||
if (!std::all_of(view.begin(), view.end(), [](float value) { return std::isfinite(value); }) ||
|
||||
!std::all_of(projection.begin(), projection.end(), [](float value) { return std::isfinite(value); }) ||
|
||||
!std::all_of(viewport.begin(), viewport.end(), [](float value) { return std::isfinite(value); }) ||
|
||||
!std::all_of(camera_position.begin(), camera_position.end(),
|
||||
[](float value) { return std::isfinite(value); }))
|
||||
return;
|
||||
const std::uint32_t target = draw.framebuffer_address & 0x001FFFF0u;
|
||||
const auto found = std::find_if(
|
||||
s.cloud_cameras.begin(), s.cloud_cameras.end(),
|
||||
[&](const CloudCameraCandidate &candidate) {
|
||||
return candidate.target == target && candidate.view == view &&
|
||||
candidate.projection == projection && candidate.viewport == viewport;
|
||||
});
|
||||
if (found != s.cloud_cameras.end()) {
|
||||
found->weight += vertex_weight;
|
||||
found->camera_position = camera_position;
|
||||
if ((draw.depth_function & 7u) >= 2u) found->occluding_weight += vertex_weight;
|
||||
return;
|
||||
}
|
||||
// Normal gameplay has only a handful of camera variants per frame. A hard
|
||||
// cap prevents malformed guest state from growing this host-only observer.
|
||||
if (s.cloud_cameras.size() >= 16u) return;
|
||||
const std::uint64_t occluding_weight = (draw.depth_function & 7u) >= 2u
|
||||
? vertex_weight : 0u;
|
||||
s.cloud_cameras.push_back({view, projection, viewport, camera_position,
|
||||
target, vertex_weight, occluding_weight});
|
||||
}
|
||||
|
||||
bool ge_gpu_backend_stage_vertices(const GeGpuDrawDescriptor &, std::span<const GeGpuVertex> vertices) noexcept {
|
||||
Dx12GeState &s = state();
|
||||
if (!s.enabled) return false;
|
||||
@@ -2976,10 +3311,42 @@ bool ge_gpu_backend_finish_color_frame(std::uint64_t vblank) noexcept {
|
||||
bool active_vertex_layout_valid = false;
|
||||
D3D12_PRIMITIVE_TOPOLOGY active_topology = D3D_PRIMITIVE_TOPOLOGY_UNDEFINED;
|
||||
bool touched_display = false;
|
||||
const CloudCameraCandidate *cloud_camera = select_cloud_camera(s);
|
||||
const CloudPresentConstants clouds = cloud_present_constants(s);
|
||||
const std::uint32_t cloud_target_address = cloud_camera != nullptr
|
||||
? cloud_camera->target : 0u;
|
||||
bool clouds_injected = false;
|
||||
|
||||
constexpr float black[4]{0.0f, 0.0f, 0.0f, 1.0f};
|
||||
for (const Dx12Batch &batch : s.batches) {
|
||||
const std::uint32_t address = batch.draw.framebuffer_address & 0x001FFFF0u;
|
||||
// Insert immediately before the first non-self pass samples the chosen
|
||||
// world target. At this point its geometry/depth are complete, while
|
||||
// the later VCS composition has not consumed its color yet.
|
||||
if (!clouds_injected && cloud_camera != nullptr &&
|
||||
batch.framebuffer_feedback &&
|
||||
(batch.feedback_address & 0x001FFFF0u) == cloud_target_address &&
|
||||
address != cloud_target_address) {
|
||||
if (Dx12FramebufferTarget *cloud_target =
|
||||
find_framebuffer_target(s, cloud_target_address);
|
||||
cloud_target != nullptr && cloud_target->color && cloud_target->depth) {
|
||||
if (current_target != nullptr && current_target != cloud_target)
|
||||
resolve_target_for_sampling(s, *current_target, false);
|
||||
record_clouds_into_world_target(s, *cloud_target, clouds);
|
||||
current_target = cloud_target;
|
||||
current_address = cloud_target_address;
|
||||
clouds_injected = true;
|
||||
active_pipeline = nullptr;
|
||||
active_pipeline_key = std::numeric_limits<std::uint64_t>::max();
|
||||
bound_srv = bound_sampler = std::numeric_limits<std::uint32_t>::max();
|
||||
active_transform_valid = false;
|
||||
active_pixel_valid = false;
|
||||
active_scissor_valid = false;
|
||||
active_blend_fix = std::numeric_limits<std::uint32_t>::max();
|
||||
active_vertex_layout_valid = false;
|
||||
active_topology = D3D_PRIMITIVE_TOPOLOGY_UNDEFINED;
|
||||
}
|
||||
}
|
||||
Dx12FramebufferTarget *target = address == current_address
|
||||
? current_target : find_framebuffer_target(s, address);
|
||||
if (target == nullptr || !target->color || !target->depth) continue;
|
||||
@@ -3381,6 +3748,12 @@ bool ge_gpu_backend_active() noexcept { return false; }
|
||||
bool ge_gpu_backend_transfer_ready() noexcept { return false; }
|
||||
bool ge_gpu_backend_graphics_ready() noexcept { return false; }
|
||||
void ge_gpu_backend_record_draw(const GeGpuDrawDescriptor &) noexcept {}
|
||||
void ge_gpu_backend_observe_camera(const std::array<float, 12> &,
|
||||
const std::array<float, 16> &,
|
||||
const std::array<float, 6> &,
|
||||
const std::array<float, 3> &,
|
||||
const GeGpuDrawDescriptor &,
|
||||
std::uint32_t) noexcept {}
|
||||
bool ge_gpu_backend_stage_vertices(const GeGpuDrawDescriptor &, std::span<const GeGpuVertex>) noexcept { return false; }
|
||||
bool ge_gpu_backend_texture_needed(const GeGpuDrawDescriptor &) noexcept { return false; }
|
||||
void ge_gpu_backend_prepare_texture_keys(GeGpuDrawDescriptor &) noexcept {}
|
||||
|
||||
@@ -4188,6 +4188,31 @@ bool render_ge_primitive(psprecomp::GuestMemory &memory,
|
||||
gpu_draw.texture_content_signature = any_signature ? signature : 0u;
|
||||
}
|
||||
ge_gpu_backend_record_draw(gpu_draw);
|
||||
if (!gpu_draw.through && !gpu_draw.clear_mode &&
|
||||
primitive >= 3u && primitive <= 5u) {
|
||||
const std::array<float, 6> cloud_viewport{
|
||||
decode_float24(data24(commands[0x42u])),
|
||||
decode_float24(data24(commands[0x43u])),
|
||||
decode_float24(data24(commands[0x45u])),
|
||||
decode_float24(data24(commands[0x46u])),
|
||||
static_cast<float>(data24(commands[0x4Cu]) & 0xFFFFu) / 16.0f,
|
||||
static_cast<float>(data24(commands[0x4Du]) & 0xFFFFu) / 16.0f};
|
||||
// VCS' authoritative camera origin. The affine GE view used by
|
||||
// individual passes is not guaranteed to encode this position as
|
||||
// a rigid inverse (reflections and camera-relative passes do not),
|
||||
// which made a world-space cloud slab orbit while only turning.
|
||||
constexpr std::uint32_t kVcsCameraPosition = 0x08BC87E0u;
|
||||
std::array<float, 3> cloud_camera_position{};
|
||||
if (memory.contains(kVcsCameraPosition, 12u)) {
|
||||
cloud_camera_position = {
|
||||
std::bit_cast<float>(memory.load32(kVcsCameraPosition + 0u)),
|
||||
std::bit_cast<float>(memory.load32(kVcsCameraPosition + 4u)),
|
||||
std::bit_cast<float>(memory.load32(kVcsCameraPosition + 8u))};
|
||||
}
|
||||
ge_gpu_backend_observe_camera(transform.view, transform.projection,
|
||||
cloud_viewport, cloud_camera_position,
|
||||
gpu_draw, count);
|
||||
}
|
||||
if (!gpu_draw.clear_mode && primitive >= 3u && primitive <= 6u)
|
||||
fps_overlay_observe_draw(gpu_draw, count);
|
||||
if (!gpu_draw.through && !gpu_draw.clear_mode &&
|
||||
|
||||
@@ -106,12 +106,70 @@ bool parse_u64(std::string value, std::uint64_t minimum, std::uint64_t maximum,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parse_float(std::string value, float minimum, float maximum, float &out) {
|
||||
value = trim_copy(std::move(value));
|
||||
if (value.empty()) return false;
|
||||
errno = 0;
|
||||
char *end = nullptr;
|
||||
const float parsed = std::strtof(value.c_str(), &end);
|
||||
if (errno == ERANGE || end == value.c_str() || *end != '\0' ||
|
||||
!std::isfinite(parsed) || parsed < minimum || parsed > maximum) {
|
||||
return false;
|
||||
}
|
||||
out = parsed;
|
||||
return true;
|
||||
}
|
||||
|
||||
void warning(VcsConfiguration &config, std::size_t line, const std::string &message) {
|
||||
std::ostringstream stream;
|
||||
stream << "line " << line << ": " << message;
|
||||
config.warnings.push_back(stream.str());
|
||||
}
|
||||
|
||||
void load_proper_shaders_configuration(VcsConfiguration &config,
|
||||
const std::filesystem::path &path) {
|
||||
std::ifstream input(path);
|
||||
if (!input) return;
|
||||
std::string section;
|
||||
std::string raw_line;
|
||||
std::size_t line_number = 0u;
|
||||
while (std::getline(input, raw_line)) {
|
||||
++line_number;
|
||||
std::string line = trim_copy(raw_line);
|
||||
if (line.empty() || line[0] == ';' || line[0] == '#') continue;
|
||||
if (line.front() == '[' && line.back() == ']') {
|
||||
section = lowercase_copy(trim_copy(line.substr(1u, line.size() - 2u)));
|
||||
continue;
|
||||
}
|
||||
if (section != "volumetricclouds" && section != "volumetric clouds") continue;
|
||||
const std::size_t separator = line.find('=');
|
||||
if (separator == std::string::npos) {
|
||||
warning(config, line_number, "ProperShaders.ini: expected key=value");
|
||||
continue;
|
||||
}
|
||||
const std::string key = lowercase_copy(trim_copy(line.substr(0u, separator)));
|
||||
const std::string value = strip_inline_comment(line.substr(separator + 1u));
|
||||
auto bad_float = [&](const char *name) {
|
||||
warning(config, line_number, std::string("ProperShaders.ini: invalid ") + name);
|
||||
};
|
||||
if (key == "enabled") {
|
||||
if (!parse_bool(value, config.volumetric_clouds.enabled))
|
||||
warning(config, line_number, "ProperShaders.ini: Enabled expects true/false");
|
||||
} else if (key == "marchsteps") {
|
||||
if (!parse_u32(value, 4u, 64u, config.volumetric_clouds.march_steps))
|
||||
warning(config, line_number, "ProperShaders.ini: MarchSteps must be between 4 and 64");
|
||||
} else if (key == "coverage") {
|
||||
if (!parse_float(value, 0.0f, 1.0f, config.volumetric_clouds.coverage)) bad_float("Coverage");
|
||||
} else if (key == "opacity") {
|
||||
if (!parse_float(value, 0.0f, 1.0f, config.volumetric_clouds.opacity)) bad_float("Opacity");
|
||||
} else if (key == "speed") {
|
||||
if (!parse_float(value, 0.0f, 1.0f, config.volumetric_clouds.speed)) bad_float("Speed");
|
||||
} else {
|
||||
warning(config, line_number, "ProperShaders.ini: unknown [VolumetricClouds] key '" + key + "'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void apply_display_key(VcsConfiguration &config, const std::string &key,
|
||||
const std::string &value, std::size_t line) {
|
||||
if (key == "enabled") {
|
||||
@@ -635,6 +693,7 @@ void initialize_vcs_configuration(const std::filesystem::path &executable_direct
|
||||
}
|
||||
|
||||
VcsConfiguration loaded = load_vcs_configuration(path);
|
||||
load_proper_shaders_configuration(loaded, executable_directory / "ProperShaders.ini");
|
||||
loaded.initialized = true;
|
||||
loaded.executable_directory = executable_directory;
|
||||
{
|
||||
|
||||
@@ -173,6 +173,17 @@ struct WidescreenConfiguration {
|
||||
std::uint32_t aspect_y{0u};
|
||||
};
|
||||
|
||||
// Standalone ProperShaders.ini feature. These values deliberately do not read
|
||||
// the guest timecycle/weather: the first port is a fixed, independently
|
||||
// configurable CloudWorks sky layer.
|
||||
struct VolumetricCloudsConfiguration {
|
||||
bool enabled{false};
|
||||
std::uint32_t march_steps{20u};
|
||||
float coverage{0.52f};
|
||||
float opacity{0.78f};
|
||||
float speed{0.018f};
|
||||
};
|
||||
|
||||
// The aspect the game itself builds its projection with. VCS loads the
|
||||
// constant 0x3FE38E39 -- exactly 16/9 -- and everything is relative to it.
|
||||
inline constexpr float kGameNativeAspectRatio = 16.0f / 9.0f;
|
||||
@@ -211,6 +222,7 @@ struct VcsConfiguration {
|
||||
TimingConfiguration timing{};
|
||||
DiagnosticsConfiguration diagnostics{};
|
||||
WidescreenConfiguration widescreen{};
|
||||
VolumetricCloudsConfiguration volumetric_clouds{};
|
||||
std::filesystem::path source_path{};
|
||||
// Where the executable lives. Saves go beside it rather than into the game
|
||||
// data, so a player who points the runtime at a read-only or shared copy of
|
||||
|
||||
@@ -153,6 +153,23 @@ int main() {
|
||||
|
||||
const vcs::VcsConfiguration missing =
|
||||
vcs::load_vcs_configuration(root / "missing.ini");
|
||||
{
|
||||
std::ofstream proper(root / "ProperShaders.ini", std::ios::trunc);
|
||||
proper << "[VolumetricClouds]\n"
|
||||
<< "Enabled=true\n"
|
||||
<< "MarchSteps=28\n"
|
||||
<< "Coverage=0.61\n"
|
||||
<< "Opacity=0.72\n"
|
||||
<< "Speed=0.03\n";
|
||||
}
|
||||
vcs::initialize_vcs_configuration(root);
|
||||
const auto &clouds = vcs::vcs_configuration().volumetric_clouds;
|
||||
require(clouds.enabled, "ProperShaders.ini VolumetricClouds.Enabled was not parsed");
|
||||
require(clouds.march_steps == 28u, "ProperShaders.ini MarchSteps was not parsed");
|
||||
require(std::abs(clouds.coverage - 0.61f) < 0.0001f &&
|
||||
std::abs(clouds.opacity - 0.72f) < 0.0001f &&
|
||||
std::abs(clouds.speed - 0.03f) < 0.0001f,
|
||||
"ProperShaders.ini cloud parameters were not parsed");
|
||||
// Widescreen: explicit ratio, "auto", and off. The correction must be
|
||||
// exactly neutral when disabled -- PSP parity stays the baseline.
|
||||
require(config.widescreen.enabled, "Widescreen.Enabled was not parsed");
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# CloudWorks attribution
|
||||
|
||||
The volumetric-cloud density and noise model in the VCS DirectX 12 presenter is
|
||||
adapted from **CloudWorks Alpha 4.0** by Brian Tu (RTU), dated 2021-07-25.
|
||||
|
||||
Original project: <https://github.com/keroroxzz>
|
||||
|
||||
The original shader identifies its license as Creative Commons
|
||||
Attribution-NonCommercial-ShareAlike 3.0 Unported (CC BY-NC-SA 3.0):
|
||||
<https://creativecommons.org/licenses/by-nc-sa/3.0/>
|
||||
|
||||
This adaptation replaces RenderHook/timecycle inputs with fixed parameters from
|
||||
`ProperShaders.ini`, uses the PSP GE camera captured by VCSNative, and composites
|
||||
the result in the native DirectX 12 presentation pass.
|
||||
Reference in New Issue
Block a user