diff --git a/features/Skin/Shaders/Features/Skin.ini b/features/Skin/Shaders/Features/Skin.ini new file mode 100644 index 0000000000..19f01444dc --- /dev/null +++ b/features/Skin/Shaders/Features/Skin.ini @@ -0,0 +1,2 @@ +[Info] +Version = 1-0-0 \ No newline at end of file diff --git a/features/Skin/Shaders/Skin/Skin.hlsli b/features/Skin/Shaders/Skin/Skin.hlsli new file mode 100644 index 0000000000..312973e8ad --- /dev/null +++ b/features/Skin/Shaders/Skin/Skin.hlsli @@ -0,0 +1,300 @@ +#ifndef __SKIN_HLSLI__ +#define __SKIN_HLSLI__ + +#include "Common/BRDF.hlsli" +#include "Common/Color.hlsli" +#include "Common/LightingCommon.hlsli" +#include "Common/Math.hlsli" +#include "Common/Shading.hlsli" +#include "Common/SharedData.hlsli" + +namespace Skin +{ + float CalculateCurvature(float3 N) + { + const float3 dNdx = ddx(N); + const float3 dNdy = ddy(N); + return length(float2(dot(dNdx, dNdx), dot(dNdy, dNdy))); + } + +#if defined(PSHADER) + cbuffer SkinPerGeometry : register(b7) + { + float4 skinPerGeometry; + }; +#endif +#if defined(SKIN) + Texture2D TexSkinDetailNormal : register(t72); + + // [Jorge Jimenez, Diego Gutierrez 2015, "Separable Subsurface Scattering"] + // https://www.iryoku.com/separable-sss/ + float3 SSSSTransmittance(float translucency, float sssWidth, float3 worldNormal, float3 light, float d) + { + /** + * Calculate the scale of the effect. + */ + float scale = 8.25 * (1.0 - translucency) / sssWidth; + + /** + * First we shrink the position inwards the surface to avoid artifacts: + * (Note that this can be done once for all the lights) + */ + // float4 shrinkedPos = float4(worldPosition - 0.005 * worldNormal, 1.0); + + /** + * Now we calculate the thickness from the light point of view: + */ + // float4 shadowPosition = mul(shrinkedPos, lightViewProjection); + // float d1 = SSSSSampleShadowmap(shadowPosition.xy / shadowPosition.w).r; // 'd1' has a range of 0..1 + // float d2 = shadowPosition.z; // 'd2' has a range of 0..'lightFarPlane' + // d1 *= lightFarPlane; // So we scale 'd1' accordingly: + // float d = scale * abs(d1 - d2); + d = scale * abs(d); // Use the passed 'd' value instead of calculating it here. + + /** + * Armed with the thickness, we can now calculate the color by means of the + * precalculated transmittance profile. + * (It can be precomputed into a texture, for maximum performance): + */ + float dd = -d * d; + float3 profile = float3(0.233, 0.455, 0.649) * exp(dd / 0.0064) + + float3(0.1, 0.336, 0.344) * exp(dd / 0.0484) + + float3(0.118, 0.198, 0.0) * exp(dd / 0.187) + + float3(0.113, 0.007, 0.007) * exp(dd / 0.567) + + float3(0.358, 0.004, 0.0) * exp(dd / 1.99) + + float3(0.078, 0.0, 0.0) * exp(dd / 7.41); + + /** + * Using the profile, we finally approximate the transmitted lighting from + * the back of the object: + */ + return profile * saturate(0.3 + dot(light, -worldNormal)); + } + + float3 DualSpecularGGX(float AverageRoughness, float Lobe0Roughness, float Lobe1Roughness, float LobeMix, float3 SpecularColor, float NdotL, float NdotV, float NdotH, float VdotH, out float3 F) + { + float D = lerp(BRDF::D_GGX(Lobe0Roughness, NdotH), BRDF::D_GGX(Lobe1Roughness, NdotH), LobeMix); + float G = BRDF::Vis_SmithJointApprox(AverageRoughness, NdotV, NdotL); + F = BRDF::F_Schlick(SpecularColor, VdotH); + + return D * G * F; + } + + // a contact shadow approximation, totally not physically correct; a riff on "Chan 2018, "Material Advances in Call of Duty: WWII" and "The Technical Art of Uncharted 4" http://advances.realtimerendering.com/other/2016/naughty_dog/NaughtyDog_TechArt_Final.pdf (microshadowing)" + float ApproximateDirectOcculusion(float aoVisibility, float NdotL) + { + float aperture = rsqrt(1.0000001 - aoVisibility); + NdotL += 0.1; // when using bent normals, avoids overshadowing - bent normals are just approximation anyhow + return saturate(NdotL * aperture); + } + + void SkinDirectLightInput( + out DirectLightingOutput lightingOutput, + DirectContext context, + MaterialProperties material) + { + lightingOutput = (DirectLightingOutput)0; + context.lightColor *= Color::PBRLightingCompensation * context.detailedShadow; + + const float3 N = context.worldNormal; + const float3 V = context.viewDir; + const float3 L = context.lightDir; + const float3 H = context.halfVector; + + const float oNdotL = dot(N, L); + float NdotL = clamp(oNdotL, 1e-5, 1.0); + float NdotV = saturate(abs(dot(N, V)) + 1e-5); + float NdotH = saturate(dot(N, H)); + float VdotH = saturate(dot(V, H)); + + context.lightColor *= ApproximateDirectOcculusion(material.AO, NdotL); + + float averageRoughness = lerp(material.Roughness, material.RoughnessSecondary, material.SecondarySpecIntensity); + + lightingOutput.diffuse += context.lightColor * NdotL * BRDF::Diffuse_Burley(averageRoughness, NdotV, NdotL, VdotH); + float3 F; + float3 F0 = material.F0 * saturate(1 - material.Curvature); + + lightingOutput.specular += DualSpecularGGX(averageRoughness, material.Roughness, material.RoughnessSecondary, material.SecondarySpecIntensity, F0, NdotL, NdotV, NdotH, VdotH, F) * context.lightColor * NdotL; + + float2 specularBRDF = BRDF::EnvBRDF(averageRoughness, NdotV); + lightingOutput.specular *= 1 + F0 * (1 / (specularBRDF.x + specularBRDF.y) - 1); + lightingOutput.diffuse *= 1 - F; + + if (material.FuzzWeight > 0.0) { + float3 FuzzF0 = material.FuzzColor * saturate(1 - material.Curvature); + float fuzzD = BRDF::D_Charlie(material.FuzzRoughness, NdotH); + float fuzzG = BRDF::Vis_Neubelt(NdotV, NdotL); + float3 fuzzF = BRDF::F_Schlick(FuzzF0, VdotH); + float3 fuzzSpecular = fuzzD * fuzzG * fuzzF * context.lightColor * NdotL; + float2 fuzzSpecularBRDF = BRDF::EnvBRDFApproxLazarov(material.FuzzRoughness, NdotV); + fuzzSpecular *= 1 + material.FuzzColor * (1 / (fuzzSpecularBRDF.x + fuzzSpecularBRDF.y) - 1); + + lightingOutput.specular += fuzzSpecular * material.FuzzWeight; + } + + float3 sssTransmittance = SSSSTransmittance( + SharedData::skinData.sssParams.x, + SharedData::skinData.sssParams.y, + N, + L, + material.Thickness) * + SharedData::skinData.sssParams.w; + lightingOutput.transmission = min(sssTransmittance * context.lightColor * context.softShadow * material.BaseColor, context.lightColor); + } + + void SkinIndirectLobeWeights( + out IndirectLobeWeights lobeWeights, + MaterialProperties material, + IndirectContext context) + { + lobeWeights = (IndirectLobeWeights)0; + + const float3 N = context.worldNormal; + const float3 V = context.viewDir; + const float3 VN = context.vertexNormal; + + float NdotV = saturate(dot(N, V)); + + float averageRoughness = lerp(material.Roughness, material.RoughnessSecondary, material.SecondarySpecIntensity); + + float2 specularBRDF = BRDF::EnvBRDF(averageRoughness, NdotV); + + lobeWeights.specular = material.F0 * specularBRDF.x + specularBRDF.y; + + lobeWeights.diffuse = material.BaseColor * (1.0 - lobeWeights.specular.x - lobeWeights.specular.y); + lobeWeights.specular *= 1 + material.F0 * (1 / (specularBRDF.x + specularBRDF.y) - 1); + + float3 R = reflect(-V, N); + float horizon = min(1.0 + dot(R, VN), 1.0); + horizon *= horizon; + lobeWeights.specular *= horizon; + + float3 diffuseAO = material.AO; + float3 specularAO = SpecularAOLagarde(NdotV, material.AO, averageRoughness); + + diffuseAO = MultiBounceAO(material.BaseColor, diffuseAO.x).y; + specularAO = MultiBounceAO(material.F0, specularAO.x).y; + + lobeWeights.diffuse *= diffuseAO; + lobeWeights.specular *= specularAO; + + lobeWeights.specular *= saturate(1 - material.Curvature); + } + + // https://blog.selfshadow.com/publications/blending-in-detail/ + // geometric normal s, a base normal t and a secondary (or detail) normal u + float3 ReorientNormal(float3 u, float3 t, float3 s) + { + // Build the shortest-arc quaternion + float4 q = float4(cross(s, t), dot(s, t) + 1) / sqrt(2 * (dot(s, t) + 1)); + + // Rotate the normal + return u * (q.w * q.w - dot(q.xyz, q.xyz)) + 2 * q.xyz * dot(q.xyz, u) + 2 * q.w * cross(q.xyz, u); + } + + // for when s = (0,0,1) + float3 ReorientNormal(float3 n1, float3 n2) + { + n1 += float3(0, 0, 1); + n2 *= float3(-1, -1, 1); + + return n1 * dot(n1, n2) / n1.z - n2; + } + + float3x3 ReconstructTBN(float3 worldPos, float3 worldNormal, float2 uv) + { + float3 dFdx = ddx(worldPos); + float3 dFdy = ddy(worldPos); + float2 dUVdx = ddx(uv); + float2 dUVdy = ddy(uv); + float3 tangent = normalize(dFdx * dUVdy.y - dFdy * dUVdx.y); + float3 bitangent = normalize(dFdy * dUVdx.x - dFdx * dUVdy.x); + tangent = normalize(tangent - worldNormal * dot(worldNormal, tangent)); + bitangent = normalize(bitangent - worldNormal * dot(worldNormal, bitangent)); + + return float3x3(tangent, bitangent, normalize(worldNormal)); + } + + float3 CalculateNormalFromHeight(float height, float heightScale, float2 uv) + { + float dHdx = ddx(height); + float dHdy = ddy(height); + float2 dUVdx = ddx(uv); + float2 dUVdy = ddy(uv); + + float det = dUVdx.x * dUVdy.y - dUVdx.y * dUVdy.x; + if (det == 0.0f) { + return float3(0, 0, 1); // Avoid division by zero + } + + float dHdx_Tex = (dHdx * dUVdy.y - dHdy * dUVdx.y) / det; + float dHdy_Tex = (dHdy * dUVdx.x - dHdx * dUVdy.x) / det; + float3 normal = float3(-dHdx_Tex, -dHdy_Tex, 0); + return normal * heightScale + float3(0, 0, 1); + } + + float FBM(float2 uv, float base_scale, int octaves, float lacunarity, float persistence, float z_offset_multiplier) + { + float total = 0.0; + float frequency = base_scale; + float amplitude = 1.0; + float max_amplitude = 0.0; + for (int i = 0; i < octaves; i++) { + total += amplitude * (Random::perlinNoise(float3(uv * frequency, (float)i * z_offset_multiplier)) + 1.0) * 0.5; + + max_amplitude += amplitude; + amplitude *= persistence; + frequency *= lacunarity; + } + if (max_amplitude > 0.0) { + return total / max_amplitude; + } + return 0.0; + } + + float PerlinNoise(float2 uv, float scale, float lacunarity, float persistence, float strength) + { + if (strength <= 0.001f) { + return 0.0f; + } + if (strength >= 0.999f) { + return 1.0f; + } + int octaves = 5; + float z_offset_multiplier = 7.375f; + + float noise_value = FBM(uv, scale, octaves, lacunarity, persistence, z_offset_multiplier); + + float dynamic_threshold = 1.0f - strength; + + float sweat_intensity = saturate((noise_value - dynamic_threshold) / strength); + + sweat_intensity = pow(sweat_intensity, 1.5f); + + if (strength > 0.8f) { + sweat_intensity = sweat_intensity * saturate(0.99f - (strength - 0.8f) * 5.0f) + (strength - 0.8f) * 5.0f; + } + return pow(sweat_intensity, 0.1f); + } +#endif + + float2 GetWetness(float z, float3 modelNormal) + { + if (skinPerGeometry.x == 0.f && skinPerGeometry.y == 0.f) + return 0.f; + + float waterWet = 0.0f; + float waterLevel = skinPerGeometry.z + skinPerGeometry.w; + + waterWet = skinPerGeometry.y * (1 - smoothstep(waterLevel - 2.5f, waterLevel + 2.5f, z)); + + float sweatWet = skinPerGeometry.x; +#if !defined(SKIN) + sweatWet *= 1.0f - saturate(dot(modelNormal, float3(0, 0, 1))); +#endif + return float2(sweatWet, waterWet); + } +} + +#endif // __SKIN_HLSLI__ diff --git a/features/Skin/Shaders/Skin/skin_detail_n.dds b/features/Skin/Shaders/Skin/skin_detail_n.dds new file mode 100644 index 0000000000..3293c36ef3 Binary files /dev/null and b/features/Skin/Shaders/Skin/skin_detail_n.dds differ diff --git a/include/DynamicWetness_PublicAPI.h b/include/DynamicWetness_PublicAPI.h new file mode 100644 index 0000000000..e711db7540 --- /dev/null +++ b/include/DynamicWetness_PublicAPI.h @@ -0,0 +1,350 @@ +// Public, header-only C/C++ interface for DynamicWetness (SWE namespace) +// Drop-in for other SKSE plugins, no import lib required (functions are resolved via GetProcAddress). +// +// Quick start: +// #include "DynamicWetness_PublicAPI.h" +// bool ok = SWE::API::Init(); // resolves exports from DynamicWetness.dll +// if (!ok) return; // SWE not present +// SWE::API::SetExternalWetness(actor, "MyMod:buff", 0.5f, 8.0f); // 8s light wetness on default category (Skin) +// auto env = SWE::API::DecodeEnv(SWE::API::GetEnvMask(actor)); // query environment (water, rain, roof, heat, +// ...) +// +// Notes: +// - All intensities are clamped to [0..1]. +// - Durations use seconds, <= 0 means "indefinite until cleared". +// - Category mask low 4 bits select target materials, high bits are behavior flags. +// - Keys are normalized (trim + lowercase) and identify your external source per actor. +// - Environmental wetness (water/rain) can override external sources internally. +// - Thread-safe internally, but always pass valid Actor* (lifetime: game thread best). + +#pragma once +#include + +#ifdef _WIN32 + #include +#endif + +namespace RE { + class Actor; +} + +#ifndef SWE_DLL_NAME + #define SWE_DLL_NAME "DynamicWetness.dll" +#endif + +namespace SWE { + namespace API { + + // =========================== + // Categories (low 4 bits) + // =========================== + /** + * @brief Material categories targetable by external wetness. + * + * Combine any of the low 4 bits to select affected materials. + * Typical usage: + * unsigned mask = CAT_SKIN_FACE | CAT_HAIR; // skin + hair + */ + static constexpr std::uint32_t CAT_SKIN_FACE = 1u << 0; /// Skin & face materials + static constexpr std::uint32_t CAT_HAIR = 1u << 1; /// Hair + static constexpr std::uint32_t CAT_ARMOR_CLOTH = 1u << 2; /// Armor & clothing + static constexpr std::uint32_t CAT_WEAPON = 1u << 3; /// Weapons + static constexpr std::uint32_t CAT_MASK_4BIT = 0x0Fu; /// Mask of all category bits + + // =========================== + // Behavior flags (high bits) + // =========================== + /** + * @brief Flags that modify how SWE blends your external wetness with its internal system. + * + * These live in the upper bits of the same integer you pass as "catMask". + * You can OR them together with category bits. + */ + static constexpr std::uint32_t FLAG_PASSTHROUGH = + 1u << 16; /// Add AFTER SWE's own mixing/drying (additive post). + static constexpr std::uint32_t FLAG_NO_AUTODRY = 1u << 17; /// Your value won't be reduced by SWE's auto-dry. + static constexpr std::uint32_t FLAG_ZERO_BASE = 1u << 18; /// Base wetness in the marked categories is zeroed. + + /// @brief Handy preset: only Skin, additive post, no auto-dry, zero base contribution. + static constexpr std::uint32_t MASK_SKIN_PASSTHROUGH = + (CAT_SKIN_FACE | FLAG_PASSTHROUGH | FLAG_NO_AUTODRY | FLAG_ZERO_BASE); + + // =========================== + // Environment bit mask + // =========================== + /** + * @brief Bits returned by GetEnvMask() describing the actor's environment this frame. + */ + static constexpr std::uint32_t ENV_WATER = 1u << 0; /// Actor is in water/submerged. + static constexpr std::uint32_t ENV_WET_WEATHER = 1u << 1; /// Precipitation affecting actor (rain/snow). + static constexpr std::uint32_t ENV_NEAR_HEAT = 1u << 2; /// Near a heat source (campfire/forge/etc.). + static constexpr std::uint32_t ENV_UNDER_ROOF = 1u << 3; /// Under roof/cover (heuristic). + static constexpr std::uint32_t ENV_EXTERIOR_OPEN = 1u << 4; /// In exterior and not under cover. + + /** + * @brief Convenience struct for decoded environment state. + */ + struct EnvState { + bool inWater{false}; + bool wetWeather{false}; + bool nearHeat{false}; + bool underRoof{false}; + bool exteriorOpen{false}; + }; + + /** + * @brief Decode ENV_* mask returned by GetEnvMask() into booleans. + * @param m Bitmask from GetEnvMask(actor) + */ + inline EnvState DecodeEnv(std::uint32_t m) { + EnvState e; + e.inWater = (m & ENV_WATER) != 0; + e.wetWeather = (m & ENV_WET_WEATHER) != 0; + e.nearHeat = (m & ENV_NEAR_HEAT) != 0; + e.underRoof = (m & ENV_UNDER_ROOF) != 0; + e.exteriorOpen = (m & ENV_EXTERIOR_OPEN) != 0; + return e; + } + + // =========================== + // C-ABI function signatures + // =========================== + // These match the exported DLL functions exactly. Prefer the safe inline wrappers below. + + using PFN_GetFinalWetness = float(__cdecl*)(RE::Actor*); /// Final mixed wetness [0..1]. + using PFN_GetExternalWetness = float(__cdecl*)(RE::Actor*, const char*); /// Last value set for @key [0..1]. + using PFN_GetBaseWetness = float(__cdecl*)(RE::Actor*); /// Internal/base wetness [0..1]. + using PFN_SetExternalWetness = void(__cdecl*)(RE::Actor*, const char*, float, float); + using PFN_ClearExternalWetness = void(__cdecl*)(RE::Actor*, const char*); + using PFN_SetExternalWetnessMask = void(__cdecl*)(RE::Actor*, const char*, float, float, unsigned int); + using PFN_SetExternalWetnessEx = void(__cdecl*)(RE::Actor*, const char*, float, float, unsigned int, float, + float, float, float, float, float, float); + using PFN_GetActorSubmergeLevel = float(__cdecl*)(RE::Actor*); /// Submerge level [0..1]. + using PFN_IsActorInWater = bool(__cdecl*)(RE::Actor*); + using PFN_IsWetWeatherAround = bool(__cdecl*)(RE::Actor*); + using PFN_IsNearHeatSource = bool(__cdecl*)(RE::Actor*, float); /// radius: Skyrim world units. + using PFN_IsUnderRoof = bool(__cdecl*)(RE::Actor*); + using PFN_IsActorInExteriorWet = bool(__cdecl*)(RE::Actor*); + using PFN_GetEnvMask = unsigned(__cdecl*)(RE::Actor*); + + // Resolved at runtime by Init()/LoadFromModule() + inline PFN_GetFinalWetness pGetFinalWetness = nullptr; + inline PFN_GetExternalWetness pGetExternalWetness = nullptr; + inline PFN_GetBaseWetness pGetBaseWetness = nullptr; + inline PFN_SetExternalWetness pSetExternalWetness = nullptr; + inline PFN_ClearExternalWetness pClearExternalWetness = nullptr; + inline PFN_SetExternalWetnessMask pSetExternalWetnessMask = nullptr; + inline PFN_SetExternalWetnessEx pSetExternalWetnessEx = nullptr; + inline PFN_GetActorSubmergeLevel pGetActorSubmergeLevel = nullptr; + inline PFN_IsActorInWater pIsActorInWater = nullptr; + inline PFN_IsWetWeatherAround pIsWetWeatherAround = nullptr; + inline PFN_IsNearHeatSource pIsNearHeatSource = nullptr; + inline PFN_IsUnderRoof pIsUnderRoof = nullptr; + inline PFN_IsActorInExteriorWet pIsActorInExteriorWet = nullptr; + inline PFN_GetEnvMask pGetEnvMask = nullptr; + + // =========================== + // Loader helpers + // =========================== + /** + * @brief Resolve all SWE_* exports from a given module handle. + * @return true if core functions were found (enough to use the API). + */ + inline bool LoadFromModule(HMODULE h) { +#ifdef _WIN32 + if (!h) return false; + auto gp = [&](const char* n) { return GetProcAddress(h, n); }; + + pGetFinalWetness = (PFN_GetFinalWetness)gp("SWE_GetFinalWetness"); + pGetExternalWetness = (PFN_GetExternalWetness)gp("SWE_GetExternalWetness"); + pGetBaseWetness = (PFN_GetBaseWetness)gp("SWE_GetBaseWetness"); + pSetExternalWetness = (PFN_SetExternalWetness)gp("SWE_SetExternalWetness"); + pClearExternalWetness = (PFN_ClearExternalWetness)gp("SWE_ClearExternalWetness"); + pSetExternalWetnessMask = (PFN_SetExternalWetnessMask)gp("SWE_SetExternalWetnessMask"); + pSetExternalWetnessEx = (PFN_SetExternalWetnessEx)gp("SWE_SetExternalWetnessEx"); + pGetActorSubmergeLevel = (PFN_GetActorSubmergeLevel)gp("SWE_GetActorSubmergeLevel"); + pIsActorInWater = (PFN_IsActorInWater)gp("SWE_IsActorInWater"); + pIsWetWeatherAround = (PFN_IsWetWeatherAround)gp("SWE_IsWetWeatherAround"); + pIsNearHeatSource = (PFN_IsNearHeatSource)gp("SWE_IsNearHeatSource"); + pIsUnderRoof = (PFN_IsUnderRoof)gp("SWE_IsUnderRoof"); + pIsActorInExteriorWet = (PFN_IsActorInExteriorWet)gp("SWE_IsActorInExteriorWet"); + pGetEnvMask = (PFN_GetEnvMask)gp("SWE_GetEnvMask"); + + return pGetFinalWetness && pSetExternalWetness && pSetExternalWetnessMask && pGetEnvMask; +#else + (void)h; + return false; +#endif + } + + /** + * @brief Try to find the module by name (SWE_DLL_NAME), then fallbacks. + */ + inline HMODULE FindModule() { +#ifdef _WIN32 + HMODULE h = GetModuleHandleA(SWE_DLL_NAME); + if (!h) { + // Optional fallback if the DLL is named differently + h = GetModuleHandleA("dynamicwetness.dll"); + } + return h; +#else + return nullptr; +#endif + } + + /** + * @brief One-shot init. Finds the DLL and resolves symbols. + * @param hOverride Pass an explicit module handle if you already have one. + * @return true if initialization succeeded. + */ + inline bool Init(HMODULE hOverride = nullptr) { + HMODULE h = hOverride ? hOverride : FindModule(); + return LoadFromModule(h); + } + + /** + * @brief Check if the core API is available (after Init()). + */ + inline bool IsAvailable() { return pGetFinalWetness != nullptr; } + + // =========================== + // Safe convenience wrappers + // =========================== + + /** + * @brief Final wetness after SWE's internal logic + all external sources. + * @param a Actor pointer + * @return Wetness in [0..1]. Returns 0 if SWE is not available. + */ + inline float GetFinalWetness(RE::Actor* a) { return pGetFinalWetness ? pGetFinalWetness(a) : 0.0f; } + + /** + * @brief Value you last set for @p key on @p a (not the final mixed wetness). + * @param a Actor + * @param key External source identifier (normalized: trimmed + lowercase) + * @return [0..1], 0 if not set or SWE not available. + */ + inline float GetExternalWetness(RE::Actor* a, const char* key) { + return pGetExternalWetness ? pGetExternalWetness(a, key) : 0.0f; + } + + /** + * @brief Internal/base wetness tracked by SWE (before external sources). + * @param a Actor + * @return [0..1], 0 if unavailable. + */ + inline float GetBaseWetness(RE::Actor* a) { return pGetBaseWetness ? pGetBaseWetness(a) : 0.0f; } + + /** + * @brief Set/refresh an external wetness value for @p key on @p a. + * + * If this is the first time @p key is used for @p a and no category was set yet, + * SWE defaults to CAT_SKIN_FACE. Subsequent calls keep the previously configured + * category/flags for this @p key. + * + * @param a Actor + * @param key Your unique source key, e.g. "MyMod:spell". Normalized internally. + * @param v Intensity in [0..1] + * @param durationSec Lifetime in seconds; <= 0 means indefinite (until ClearExternalWetness()). + */ + inline void SetExternalWetness(RE::Actor* a, const char* key, float v, float durationSec) { + if (pSetExternalWetness) pSetExternalWetness(a, key, v, durationSec); + } + + /** + * @brief Remove your external source identified by @p key from @p a. + */ + inline void ClearExternalWetness(RE::Actor* a, const char* key) { + if (pClearExternalWetness) pClearExternalWetness(a, key); + } + + /** + * @brief Set/replace @b category mask and behavior flags for @p key on @p a. + * + * This both sets the value and (re)defines which material categories are affected + * and how SWE blends them (via flags). Use this when you need to change the + * category/flag configuration of an existing key. + * + * @param a Actor + * @param key Your unique source key (normalized internal storage) + * @param v Intensity in [0..1] + * @param durationSec Lifetime; <= 0 = indefinite + * @param catMask Low 4 bits: categories (CAT_*). High bits: flags (FLAG_*). + */ + inline void SetExternalWetnessMask(RE::Actor* a, const char* key, float v, float durationSec, + unsigned catMask) { + if (pSetExternalWetnessMask) pSetExternalWetnessMask(a, key, v, durationSec, catMask); + } + + /** + * @brief Advanced: update shader/material overrides for @p key without altering flags. + * + * Use this to tweak how shiny/specular the result can get per category while keeping + * your previously set flags (e.g., NO_AUTODRY) intact. Parameters use negative + * values to mean "leave unchanged / don't force". + * + * @param a Actor + * @param key Your unique source key (normalized internal storage) + * @param v Intensity in [0..1] + * @param durationSec Lifetime; <= 0 = indefinite + * @param catMask Low 4 bits: categories (CAT_*). (Flags are @b not modified by this call.) + * @param maxGloss [-1 or >=0] Cap for gloss when wet (per-category merge) + * @param maxSpec [-1 or >=0] Cap for specular intensity when wet + * @param minGloss [-1 or >=0] Floor gloss even at low wetness + * @param minSpec [-1 or >=0] Floor specular even at low wetness + * @param glossBoost[-1 or >=0] Additive gloss boost + * @param specBoost [-1 or >=0] Additive specular boost + * @param skinHairMul[-1 or >=0] Extra multiplier applied to skin/hair categories + * + * @note Call SetExternalWetnessMask() first if you need to (re)configure flags. + */ + inline void SetExternalWetnessEx(RE::Actor* a, const char* key, float v, float durationSec, unsigned catMask, + float maxGloss, float maxSpec, float minGloss, float minSpec, float glossBoost, + float specBoost, float skinHairMul) { + if (pSetExternalWetnessEx) + pSetExternalWetnessEx(a, key, v, durationSec, catMask, maxGloss, maxSpec, minGloss, minSpec, glossBoost, + specBoost, skinHairMul); + } + + /** + * @brief Submerge level (0 = dry, 1 = fully submerged). + */ + inline float GetActorSubmergeLevel(RE::Actor* a) { + return pGetActorSubmergeLevel ? pGetActorSubmergeLevel(a) : 0.0f; + } + + /// @brief True if the actor is in water. + inline bool IsActorInWater(RE::Actor* a) { return pIsActorInWater ? pIsActorInWater(a) : false; } + /// @brief True if precipitation affecting the actor is detected (rain/snow). + inline bool IsWetWeatherAround(RE::Actor* a) { return pIsWetWeatherAround ? pIsWetWeatherAround(a) : false; } + /// @brief True if a heat source is found within @p r (Skyrim world units). + inline bool IsNearHeatSource(RE::Actor* a, float r) { + return pIsNearHeatSource ? pIsNearHeatSource(a, r) : false; + } + /// @brief True if the actor is detected to be under a roof/cover. + inline bool IsUnderRoof(RE::Actor* a) { return pIsUnderRoof ? pIsUnderRoof(a) : false; } + /// @brief True if actor is in "exterior wet" area (outside & exposed). + inline bool IsActorInExteriorWet(RE::Actor* a) { + return pIsActorInExteriorWet ? pIsActorInExteriorWet(a) : false; + } + + /** + * @brief Raw environment mask (see ENV_*). Prefer DecodeEnv() for convenience. + */ + inline unsigned GetEnvMask(RE::Actor* a) { return pGetEnvMask ? pGetEnvMask(a) : 0u; } + + /** + * @brief Helper to build a category mask (no flags). + */ + inline unsigned MakeCatMask(bool skin, bool hair, bool armor, bool weapon) { + unsigned m = 0; + if (skin) m |= CAT_SKIN_FACE; + if (hair) m |= CAT_HAIR; + if (armor) m |= CAT_ARMOR_CLOTH; + if (weapon) m |= CAT_WEAPON; + return m; + } + + } +} diff --git a/package/Shaders/Common/LightingCommon.hlsli b/package/Shaders/Common/LightingCommon.hlsli index f229212529..aa50b02c95 100644 --- a/package/Shaders/Common/LightingCommon.hlsli +++ b/package/Shaders/Common/LightingCommon.hlsli @@ -71,6 +71,17 @@ struct MaterialProperties # endif float Roughness; float3 F0; +# if defined(CS_SKIN) && defined(SKIN) + float RoughnessSecondary; + float SecondarySpecIntensity; + float Curvature; + float Thickness; + float3 SubsurfaceColor; + float AO; + float FuzzRoughness; + float3 FuzzColor; + float FuzzWeight; +# endif #else float Roughness; float Metallic; diff --git a/package/Shaders/Common/LightingEval.hlsli b/package/Shaders/Common/LightingEval.hlsli index 2e8699e583..21748a03ec 100644 --- a/package/Shaders/Common/LightingEval.hlsli +++ b/package/Shaders/Common/LightingEval.hlsli @@ -106,6 +106,29 @@ void EvaluateLighting(DirectContext context, MaterialProperties material, float3 Hair::GetHairDirectLight(lightingOutput, context, material, tbnTr, uv); return; } +# endif +# if defined(SKIN) && defined(CS_SKIN) + if (SharedData::skinData.skinParams.w > 0.0f) { + Skin::SkinDirectLightInput(lightingOutput, context, material); + float3 softLightColor = context.lightColor * context.softShadow; + + // SSS fallback for forward skin rendering +# if !defined(DEFERRED) + const float NdotL = dot(context.worldNormal, context.lightDir); +# if defined(SOFT_LIGHTING) + lightingOutput.diffuse += softLightColor * GetSoftLightMultiplier(NdotL) * material.rimSoftLightColor; +# endif + +# if defined(RIM_LIGHTING) + lightingOutput.diffuse += softLightColor * GetRimLightMultiplier(context.lightDir, context.viewDir, context.worldNormal) * material.rimSoftLightColor; +# endif + +# if defined(BACK_LIGHTING) + lightingOutput.diffuse += softLightColor * saturate(-NdotL) * material.backLightColor; +# endif +# endif + return; + } # endif const float NdotL = dot(context.worldNormal, context.lightDir); float3 diffuseLightColor = context.lightColor * context.detailedShadow; @@ -137,6 +160,12 @@ void GetIndirectLobeWeights(out IndirectLobeWeights lobeWeights, IndirectContext Hair::GetHairIndirectLobeWeights(lobeWeights, context, material, uv); return; } +# endif +# if defined(SKIN) && defined(CS_SKIN) + if (SharedData::skinData.skinParams.w > 0.0f) { + Skin::SkinIndirectLobeWeights(lobeWeights, material, context); + return; + } # endif lobeWeights.diffuse = material.BaseColor; # if defined(DYNAMIC_CUBEMAPS) diff --git a/package/Shaders/Common/SharedData.hlsli b/package/Shaders/Common/SharedData.hlsli index 8168507571..05c257b034 100644 --- a/package/Shaders/Common/SharedData.hlsli +++ b/package/Shaders/Common/SharedData.hlsli @@ -275,6 +275,17 @@ namespace SharedData uint3 pad; }; + struct SkinData + { + float4 skinParams; + float4 skinParams2; + float4 skinDetailParams; + float4 sssParams; + float4 fuzzParams; + float4 physicalParams; + float4 wetParams; + }; + cbuffer FeatureData : register(b6) { GrassLightingSettings grassLightingSettings; @@ -294,6 +305,7 @@ namespace SharedData TerrainBlendingSettings terrainBlendingSettings; ExponentialHeightFogSettings exponentialHeightFogSettings; TruePBRSettings truePBRSettings; + SkinData skinData; }; Texture2D DepthTexture : register(t17); diff --git a/package/Shaders/Lighting.hlsl b/package/Shaders/Lighting.hlsl index 60d66b7ecf..3a7455c8dd 100644 --- a/package/Shaders/Lighting.hlsl +++ b/package/Shaders/Lighting.hlsl @@ -552,6 +552,12 @@ Texture2D TexLandLodNoiseSampler : register(t15); Texture2D TexShadowMaskSampler : register(t14); +# if defined(SKIN) && defined(CS_SKIN) +Texture2D TexSkinExtraSampler : register(t71); +Texture2D TexSkinWetnessSampler : register(t74); +Texture2D TexSkinWetnessNormalSampler : register(t75); +# endif + cbuffer PerTechnique : register(b0) { float4 FogColor : packoffset(c0); // Color in xyz, invFrameBufferRange in w @@ -937,6 +943,10 @@ float GetSnowParameterY(float texProjTmp, float alpha) # define ANISOTROPIC_ALPHA # endif +# if defined(CS_SKIN) +# include "Skin/Skin.hlsli" +# endif + # define LinearSampler SampColorSampler # include "Common/ShadowSampling.hlsli" @@ -1329,6 +1339,17 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float4 baseColor = 0; float4 normal = 0; float glossiness = 0; +# if defined(CS_SKIN) + const bool skinEnabled = SharedData::skinData.skinParams.w > 0.0f; +# if defined(SKIN) + float skinRoughness = 0; + float skinSpecular = 0; + float skinFuzzMask = 1; + float skinWetMask = 1; + float skinAO = 1; + bool skinRoughnessSet = false; +# endif +# endif float4 rawRMAOS = 0; @@ -1866,6 +1887,47 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # endif # endif // LOD_BLENDING +# if defined(SKIN) && defined(CS_SKIN) + float4 skinsk = 0; + float4 skinExtra = 0; + float4 skinWetnessSample = 0; + uint2 skinExtraDimensions = uint2(0, 0); + uint2 wetnessDimensions = uint2(0, 0); + bool hasSkinExtra = false; + bool hasSkinWetness = false; + if (skinEnabled) { + skinsk = TexRimSoftLightWorldMapOverlaySampler.Sample(SampRimSoftLightWorldMapOverlaySampler, uv); + TexSkinExtraSampler.GetDimensions(skinExtraDimensions.x, skinExtraDimensions.y); + TexSkinWetnessSampler.GetDimensions(wetnessDimensions.x, wetnessDimensions.y); + hasSkinExtra = skinExtraDimensions.x > 32 && skinExtraDimensions.y > 32; + hasSkinWetness = wetnessDimensions.x > 32 && wetnessDimensions.y > 32; + } + float4 skinWetnessNormal = float4(0.f, 0.f, 0.f, 1.f); + + if (hasSkinExtra && SharedData::skinData.skinParams.x > 0.0f) { + skinExtra = TexSkinExtraSampler.Sample(SampColorSampler, uv); + skinRoughness = skinExtra.x; + skinFuzzMask = skinExtra.y; + skinAO = skinExtra.z; + skinSpecular = skinExtra.w; + skinRoughnessSet = true; + } else { + skinRoughnessSet = false; + } + if (hasSkinWetness && skinEnabled) { + skinWetnessSample = TexSkinWetnessSampler.Sample(SampColorSampler, uv); + if ((skinWetnessSample.y == 0 && skinWetnessSample.z == 0) || (skinWetnessSample.x == skinWetnessSample.y && skinWetnessSample.y == skinWetnessSample.z && skinWetnessSample.w >= 0.99f)) { + skinWetMask = skinWetnessSample.x; + skinWetnessNormal.xyz = Skin::CalculateNormalFromHeight(skinWetMask, SharedData::skinData.wetParams.w * 0.0001, uv) * 0.5 + 0.5; + } else { + skinWetnessNormal.xyz = skinWetnessSample.xyz; + skinWetMask = skinWetnessSample.w; + } + } else { + skinWetMask = 1.0; + } +# endif + float landSnowMask1 = GetLandSnowMaskValue(baseColor.w); # if defined(MODELSPACENORMALS) @@ -1928,6 +1990,12 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) } # endif // FACEGEN +# if defined(SKIN) && defined(CS_SKIN) + if (skinEnabled) { + baseColor.xyz = baseColor.xyz * SharedData::skinData.skinParams2.w; + } +# endif // CS_SKIN + # if defined(HAIR) && defined(CS_HAIR) float3 hairTint = 0; @@ -2023,6 +2091,59 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # endif // SPARKLE # endif // defined (MODELSPACENORMALS) && !defined (SKINNED) +# if defined(SKIN) && defined(CS_SKIN) +# if defined(WETNESS_EFFECTS) + float3 skinWetNormal = worldNormal.xyz; +# if defined(FACEGEN) + float2 wetUV = uv; +# else + float2 wetUV = uv * SharedData::skinData.skinDetailParams.y; +# endif + float2 dynamicWet = Skin::GetWetness(input.WorldPosition.z + FrameBuffer::CameraPosAdjust[eyeIndex].z, worldNormal.xyz); + float skinWetness = Skin::PerlinNoise(wetUV, SharedData::skinData.wetParams.x, SharedData::skinData.wetParams.y, SharedData::skinData.wetParams.z, clamp(dynamicWet.x + dynamicWet.y + SharedData::skinData.skinParams2.y, 0.f, 2.f) * (hasSkinWetness ? 1.0 : 0.5)); + if ((SharedData::skinData.skinDetailParams.w > 0.0f || skinWetness > 0.0f) && skinEnabled) +# else + if (SharedData::skinData.skinDetailParams.w > 0.0f && skinEnabled) +# endif + { +# if defined(FACEGEN) + float2 detailUV = input.TexCoord0.xy * SharedData::skinData.skinDetailParams.x; +# else + float2 detailUV = input.TexCoord0.xy * SharedData::skinData.skinDetailParams.x * SharedData::skinData.skinDetailParams.y; +# endif // FACEGEN +# if defined(MODELSPACENORMALS) + const float3x3 tbnTr = Skin::ReconstructTBN(input.WorldPosition.xyz, worldNormal, screenUV); + const float3x3 tbn = transpose(tbnTr); + const float3 tangentNormal = mul(tbnTr, worldNormal.xyz); +# else + const float3 tangentNormal = normal.xyz; +# endif // MODELSPACENORMALS + float3 detailNormal = float3(Skin::TexSkinDetailNormal.SampleBias(SampNormalSampler, detailUV, SharedData::MipBias - 1.0f).xy, 0.5f); + skinAO *= Skin::TexSkinDetailNormal.Sample(SampNormalSampler, detailUV).w; + detailNormal = (detailNormal * 2.0 - 1.0) * SharedData::skinData.skinDetailParams.z; + float3 combinedTangentNormal = normalize(float3(Skin::ReorientNormal(detailNormal, tangentNormal).xy, tangentNormal.z)); + float3 combinedNormal = normalize(mul(tbn, combinedTangentNormal)); + if (SharedData::skinData.skinDetailParams.w > 0.0f) + worldNormal.xyz = combinedNormal; +# if defined(WETNESS_EFFECTS) + if (skinWetness > 0.0f) { + float3 wetNormal = Skin::CalculateNormalFromHeight(skinWetness, SharedData::skinData.wetParams.w * 0.0005, uv); + if (hasSkinWetness) { + // float3 wetMaskNormal = Skin::CalculateNormalFromHeight(skinWetMask, SharedData::skinData.wetParams.w * 0.00005, uv); + float3 wetMaskNormal = (skinWetnessNormal.xyz * 2.0 - 1.0); + wetNormal = Skin::ReorientNormal(wetMaskNormal, wetNormal); + } + if (SharedData::skinData.skinParams2.y > 1.0f) { + wetNormal = lerp(wetNormal, tangentNormal, saturate(SharedData::skinData.skinParams2.y - 1.0f)); + } + float3 combinedWetNormal = skinWetMask ? wetNormal : combinedTangentNormal; + skinWetNormal = normalize(mul(tbn, combinedWetNormal)); + skinWetNormal = lerp(worldNormal.xyz, skinWetNormal, skinWetness > 0 ? 1 : 0); + } +# endif + } +# endif // CS_SKIN + float2 baseShadowUV = 1.0.xx; float4 shadowColor = 1.0; if ((Permutation::PixelShaderDescriptor & Permutation::LightingFlags::DefShadow) && ((Permutation::PixelShaderDescriptor & Permutation::LightingFlags::ShadowDir) || inWorld) || numShadowLights > 0) { @@ -2267,6 +2388,34 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) # endif # endif // TRUE_PBR +# if defined(SKIN) && defined(CS_SKIN) + const float ExtraRoughness = BRDF::F_Schlick(0.04, saturate(dot(worldNormal.xyz, viewDirection))) * SharedData::skinData.fuzzParams.w; + material.Roughness = SharedData::skinData.skinParams.x; + material.Roughness = saturate(SharedData::skinData.skinParams.x - SharedData::skinData.skinParams.z * material.Glossiness); + material.RoughnessSecondary = SharedData::skinData.skinParams.y; + if (skinRoughnessSet) { + material.Roughness = skinRoughness * SharedData::skinData.physicalParams.x; + material.RoughnessSecondary = skinRoughness * SharedData::skinData.physicalParams.y; + } + material.Roughness = min(1.0, material.Roughness + ExtraRoughness); + material.RoughnessSecondary = min(1.0, material.RoughnessSecondary + ExtraRoughness); + material.SecondarySpecIntensity = SharedData::skinData.skinParams2.x; + material.Thickness = 1 - skinsk.x; + material.SubsurfaceColor = skinsk.xyz; + material.F0 = SharedData::skinData.skinParams2.zzz; + material.AO = skinAO; + material.Curvature = Skin::CalculateCurvature(worldNormal.xyz); + + material.FuzzWeight = SharedData::skinData.fuzzParams.x; + material.FuzzRoughness = SharedData::skinData.fuzzParams.y; + material.FuzzColor = SharedData::skinData.fuzzParams.zzz; + + if (skinRoughnessSet) { + material.F0 = 0.08f * skinSpecular * SharedData::skinData.physicalParams.z; + material.FuzzWeight *= skinFuzzMask; + } +# endif // CS_SKIN + # if defined(CS_HAIR) && defined(HAIR) if (SharedData::hairSpecularSettings.Enabled) { material.Shininess = SharedData::hairSpecularSettings.HairGlossiness; @@ -2426,6 +2575,19 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) rainWetness = SharedData::wetnessEffectsSettings.SkinWetness * SharedData::wetnessEffectsSettings.Wetness; # endif +# if defined(CS_SKIN) && !defined(SKIN) + if (skinEnabled) { + float2 dynamicWetness = Skin::GetWetness(input.WorldPosition.z + FrameBuffer::CameraPosAdjust[eyeIndex].z, worldNormal.xyz); +# if defined(TRUE_PBR) + dynamicWetness.x = lerp(dynamicWetness.x, 0.0f, material.Metallic); +# endif + float dynamicWetnessValue = clamp(dynamicWetness.x + dynamicWetness.y, 0.f, 2.f); +# if defined(HAIR) + dynamicWetnessValue = min(SharedData::skinData.skinParams2.y + dynamicWetnessValue, 2.0f); +# endif + rainWetness += min(dynamicWetnessValue, 1.f); + } +# endif float shoreWetness = shoreFactor * SharedData::wetnessEffectsSettings.MaxShoreWetness; wetness = max(shoreWetness, rainWetness); @@ -2433,7 +2595,7 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float puddleWetness = SharedData::wetnessEffectsSettings.PuddleWetness * minWetnessAngle; float puddle = wetness; -# if !defined(SKINNED) +# if !defined(SKINNED) && !(defined(SKIN) && defined(CS_SKIN)) if (wetness > 0.0 || puddleWetness > 0.0) { float3 puddleCoords = ((input.WorldPosition.xyz + FrameBuffer::CameraPosAdjust[eyeIndex].xyz) * 0.5 + 0.5) * 0.01 / SharedData::wetnessEffectsSettings.PuddleRadius; puddle = Random::perlinNoise(puddleCoords) * 0.5 + 0.5; @@ -2462,6 +2624,13 @@ PS_OUTPUT main(PS_INPUT input, bool frontFace : SV_IsFrontFace) float3 rippleNormal = normalize(lerp(float3(0, 0, 1), raindropInfo.xyz, lerp(flatnessAmount, 1.0, 0.5))); wetnessNormal = WetnessEffects::ReorientNormal(rippleNormal, wetnessNormal); +# if defined(SKIN) && defined(CS_SKIN) + if (skinEnabled && (skinWetness > 0.0f)) { + wetnessNormal = skinWetNormal; + wetnessGlossinessSpecular = saturate(max(wetnessGlossinessSpecular, skinWetness)); + } +# endif + // Minimum roughness prevents an extreme retroreflective peak (NdotH→1) for near-zero // roughness puddles. Real water has ripples and surface tension that keep it from being // optically perfect; the ripple normal map adds micro-variation but GGX still peaks diff --git a/src/Feature.cpp b/src/Feature.cpp index aa0e23992f..b5e7287a79 100644 --- a/src/Feature.cpp +++ b/src/Feature.cpp @@ -22,6 +22,7 @@ #include "Features/ScreenSpaceGI.h" #include "Features/ScreenSpaceShadows.h" #include "Features/ScreenshotFeature.h" +#include "Features/Skin.h" #include "Features/SkySync.h" #include "Features/Skylighting.h" #include "Features/SubsurfaceScattering.h" @@ -245,7 +246,8 @@ const std::vector& Feature::GetFeatureList() &globals::features::linearLighting, &globals::features::unifiedWater, &globals::features::exponentialHeightFog, - &globals::features::hdrDisplay + &globals::features::hdrDisplay, + &globals::features::skin }; if (REL::Module::IsVR()) { diff --git a/src/FeatureBuffer.cpp b/src/FeatureBuffer.cpp index 98f5aa834b..f68f563e57 100644 --- a/src/FeatureBuffer.cpp +++ b/src/FeatureBuffer.cpp @@ -11,6 +11,7 @@ #include "Features/LODBlending.h" #include "Features/LightLimitFix.h" #include "Features/LinearLighting.h" +#include "Features/Skin.h" #include "Features/Skylighting.h" #include "Features/TerrainBlending.h" #include "Features/TerrainShadows.h" @@ -53,5 +54,6 @@ std::pair GetFeatureBufferData(bool a_inWorld) globals::features::linearLighting.GetCommonBufferData(), globals::features::terrainBlending.settings, globals::features::exponentialHeightFog.settings, - globals::features::truePBR.settings); + globals::features::truePBR.settings, + globals::features::skin.GetCommonBufferData()); } \ No newline at end of file diff --git a/src/Features/Skin.cpp b/src/Features/Skin.cpp new file mode 100644 index 0000000000..9bd9bde1a5 --- /dev/null +++ b/src/Features/Skin.cpp @@ -0,0 +1,625 @@ +#include "Skin.h" +#include + +#include "Deferred.h" +#include "Globals.h" +#include "Hooks.h" +#include "Menu.h" +#include "ShaderCache.h" +#include "State.h" + +#include "DynamicWetness_PublicAPI.h" + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( + Skin::Settings, + EnableSkin, + SkinMainRoughness, + SkinSecondRoughness, + SkinSpecularTexMultiplier, + SecondarySpecularStrength, + F0, + BaseColorMultiplier, + PhysicalMainRoughnessMultiplier, + PhysicalSecondRoughnessMultiplier, + PhysicalSpecularStrength, + ExtraEdgeRoughness, + EnableSkinDetail, + SkinDetailStrength, + SkinDetailTiling, + BodyTilingMultiplier, + ExtraSkinWetness, + WetFadeTime, + StartSweat, + FullSweat, + WetParams, + Translucency, + sssWidth, + UseSSS, + FuzzStrength, + FuzzRoughness, + FuzzF0, + UseDynamicWetness); + +void Skin::DrawSettings() +{ + ImGui::Checkbox("Enable Advanced Skin", &settings.EnableSkin); + + ImGui::Text("Advanced Skin Shader using dual specular lobes."); + + ImGui::Spacing(); + ImGui::SliderFloat("Primary Roughness", &settings.SkinMainRoughness, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Controls microscopic roughness of stratum corneum layer"); + } + + ImGui::SliderFloat("Secondary Roughness", &settings.SkinSecondRoughness, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Smoothness of epidermal cell layer reflections"); + ImGui::BulletText("Should be 30-50%% lower than Primary"); + } + + ImGui::SliderFloat("Specular Texture Multiplier", &settings.SkinSpecularTexMultiplier, 0.0f, 10.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Multiplier for specular map"); + ImGui::BulletText("A multiplier for the vanilla specular map, applied to the first layer's roughness"); + } + + ImGui::SliderFloat("Secondary Specular Strength", &settings.SecondarySpecularStrength, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Intensity of secondary specular highlights"); + } + + ImGui::SliderFloat("Fresnel F0", &settings.F0, 0.0f, 0.1f, "%.4f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Fresnel reflectance"); + } + + ImGui::SliderFloat("Base Color Multiplier", &settings.BaseColorMultiplier, 0.0f, 2.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Multiplier for the base color texture"); + } + + ImGui::Spacing(); + ImGui::Text("Options for additional roughness and specular maps."); + + ImGui::SliderFloat("Physical Main Roughness Multiplier", &settings.PhysicalMainRoughnessMultiplier, 0.0f, 2.0f, "%.2f"); + ImGui::SliderFloat("Physical Second Roughness Multiplier", &settings.PhysicalSecondRoughnessMultiplier, 0.0f, 2.0f, "%.2f"); + ImGui::SliderFloat("Physical Specular Multiplier", &settings.PhysicalSpecularStrength, 0.0f, 2.0f, "%.2f"); + + ImGui::Spacing(); + + ImGui::SliderFloat("Extra Edge Roughness", &settings.ExtraEdgeRoughness, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Extra roughness at the edges of the skin, to approximate peach fuzz on the face."); + } + + ImGui::SliderFloat("Fuzz Strength", &settings.FuzzStrength, 0.0f, 2.0f, "%.2f"); + + ImGui::SliderFloat("Fuzz Roughness", &settings.FuzzRoughness, 0.1f, 1.0f, "%.2f"); + + ImGui::SliderFloat("Fuzz F0", &settings.FuzzF0, 0.0f, 0.5f, "%.4f"); + + ImGui::Spacing(); + + ImGui::Checkbox("Enable SSS Transmission", &settings.UseSSS); + + ImGui::SliderFloat("Translucency", &settings.Translucency, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Translucency of the SSS Transmittance effect"); + } + + ImGui::SliderFloat("SSS Width", &settings.sssWidth, 0.0f, 1.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Width of the SSS Transmittance effect"); + } + + ImGui::Spacing(); + + ImGui::SliderFloat("Extra Skin Wetness", &settings.ExtraSkinWetness, 0.0f, 2.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Adds a constant layer of wetness to all skin, making it look slightly damp or sweaty at all times, even when not in water or exerting effort."); + } + + ImGui::SliderFloat("Wetness Fade Out Time", &settings.WetFadeTime, 0.0f, 50.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("How many seconds it takes for skin to fully dry after leaving water. Higher values mean wetness lingers longer."); + } + + if (isDynamicWetnessAvailable) { + ImGui::Text("Dynamic Wetness detected."); + ImGui::Checkbox("Use Dynamic Wetness", &settings.UseDynamicWetness); + } else { + settings.UseDynamicWetness = false; + } + + if (!settings.UseDynamicWetness) { + ImGui::SliderFloat("Stamina Threshold for Sweat", &settings.StartSweat, 0.0f, 1.0f, "%.2f", + ImGuiSliderFlags_AlwaysClamp); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("The character starts sweating when their stamina drops below this percentage. For example, 0.75 means sweat appears below 75%% stamina."); + } + ImGui::SliderFloat("Full Sweat Threshold", &settings.FullSweat, 0.0f, 1.0f, "%.2f", + ImGuiSliderFlags_AlwaysClamp); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("The character reaches maximum sweat when stamina drops below this percentage. For example, 0.15 means full sweat below 15%% stamina."); + } + } + + ImGui::SliderFloat("Wetness Perlin Noise Scale", &settings.WetParams.x, 0.0f, 1024.0f, "%1.f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Controls the size of the wet/dry pattern on skin. Higher values create a finer, more detailed pattern; lower values produce larger, broader wet patches."); + } + ImGui::SliderFloat("Wetness Perlin Noise Lacunarity", &settings.WetParams.y, 0.0f, 2.0f, "%.1f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Controls how much fine detail is added to the wetness pattern. Higher values add more small-scale variation on top of the base pattern."); + } + ImGui::SliderFloat("Wetness Perlin Noise Persistence", &settings.WetParams.z, 0.0f, 20.0f, "%.2f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Controls the overall contrast and roughness of the wetness pattern. Higher values make the pattern more pronounced and varied."); + } + ImGui::SliderFloat("Wetness Normal Scale", &settings.WetParams.w, 0.0f, 20.0f, "%.1f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Controls how bumpy wet skin appears. Higher values create more visible surface ripples and distortion on wet areas."); + } + + ImGui::Spacing(); + + ImGui::Checkbox("Enable Skin Detail", &settings.EnableSkinDetail); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Enable skin detail texture"); + } + + ImGui::SliderFloat("Skin Detail Strength", &settings.SkinDetailStrength, -2.0f, 2.0f); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Strength of skin detail texture"); + } + + ImGui::SliderFloat("Skin Detail Tiling", &settings.SkinDetailTiling, 1.0f, 50.0f, "%1.f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("The more tiling, the more detailed the skin will be"); + } + + ImGui::SliderFloat("Body Tiling Multiplier", &settings.BodyTilingMultiplier, 0.5f, 5.0f, "%.1f"); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Multiply the tiling for the body to match the face"); + } + + if (ImGui::Button("Reload Skin Detail Texture")) { + ReloadSkinDetail(); + } + + BUFFER_VIEWER_NODE(texSkinDetail, 1.0f) +} + +void Skin::LoadSkinDetailTexture() +{ + auto device = globals::d3d::device; + + DirectX::ScratchImage image; + try { + std::filesystem::path path{ "Data\\Shaders\\Skin\\skin_detail_n.dds" }; + DX::ThrowIfFailed(LoadFromDDSFile(path.c_str(), DirectX::DDS_FLAGS_NONE, nullptr, image)); + } catch (const DX::com_exception& e) { + logger::error("{}", e.what()); + return; + } + + ID3D11Resource* pResource = nullptr; + try { + DX::ThrowIfFailed(CreateTexture(device, + image.GetImages(), image.GetImageCount(), + image.GetMetadata(), &pResource)); + } catch (const DX::com_exception& e) { + logger::error("{}", e.what()); + return; + } + + texSkinDetail = eastl::make_unique(reinterpret_cast(pResource)); + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = { + .Format = texSkinDetail->desc.Format, + .ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D, + .Texture2D = { + .MostDetailedMip = 0, + .MipLevels = static_cast(image.GetMetadata().mipLevels) } + }; + texSkinDetail->CreateSRV(srvDesc); +} + +void Skin::SetupResources() +{ + logger::debug("Loading skin detail texture..."); + LoadSkinDetailTexture(); + + PerGeometryCB = eastl::make_unique(ConstantBufferDesc()); + + // Check for Dynamic Wetness availability + isDynamicWetnessAvailable = SWE::API::Init(); +} + +void Skin::ReloadSkinDetail() +{ + logger::debug("Reloading skin detail texture..."); + LoadSkinDetailTexture(); +} + +void Skin::Prepass() +{ + auto context = globals::d3d::context; + + if (texSkinDetail) { + ID3D11ShaderResourceView* srv = texSkinDetail->srv.get(); + context->PSSetShaderResources(72, 1, &srv); + } +} + +struct SKIN_BSLightingShader_SetupMaterial +{ + static void thunk(RE::BSLightingShader* shader, RE::BSLightingShaderMaterialBase const* material) + { + func(shader, material); + + auto& skin = globals::features::skin; + if (skin.loaded) { + skin.BSLightingShader_SetupMaterial(material); + } + } + static inline REL::Relocation func; +}; + +void Skin::PostPostLoad() +{ + logger::info("[Advanced Skin] Hooking BSLightingShader::SetupMaterial"); + stl::write_vfunc<0x4, SKIN_BSLightingShader_SetupMaterial>(RE::VTABLE_BSLightingShader[0]); + Hooks::Install(); +} + +Skin::SkinData Skin::GetCommonBufferData() +{ + SkinData data{}; + data.skinParams = float4(settings.SkinMainRoughness, settings.SkinSecondRoughness, settings.SkinSpecularTexMultiplier, float(settings.EnableSkin)); + data.skinParams2 = float4(settings.SecondarySpecularStrength, settings.ExtraSkinWetness, settings.F0, settings.BaseColorMultiplier); + data.skinDetailParams = float4(settings.SkinDetailTiling, settings.BodyTilingMultiplier, settings.SkinDetailStrength, float(settings.EnableSkinDetail && settings.EnableSkin)); + data.sssParams = float4(settings.Translucency, settings.sssWidth, 0.0f, float(settings.UseSSS)); + data.fuzzParams = float4(settings.FuzzStrength, settings.FuzzRoughness, settings.FuzzF0, settings.ExtraEdgeRoughness); + data.physicalParams = float4(settings.PhysicalMainRoughnessMultiplier, settings.PhysicalSecondRoughnessMultiplier, settings.PhysicalSpecularStrength, 0.0f); + data.wetParams = settings.WetParams; + return data; +} + +void Skin::LoadSettings(json& o_json) +{ + settings = o_json; +} + +void Skin::SaveSettings(json& o_json) +{ + o_json = settings; +} + +void Skin::RestoreDefaultSettings() +{ + settings = {}; +} + +// By PO3 +// https://github.com/powerof3/Splashes-of-Skyrim/blob/master/src/Manager.cpp +float Skin::GetWaterHeight(const RE::TESObjectREFR* a_ref, const RE::NiPoint3& a_pos) +{ + float waterHeight = -RE::NI_INFINITY; + + if (const auto waterManager = RE::TESWaterSystem::GetSingleton()) { + waterHeight = a_ref->GetWaterHeight(); + + if (waterHeight != -RE::NI_INFINITY) { + return waterHeight; + } + + const auto get_nearest_water_object_height = [&]() { + for (const auto& waterObject : waterManager->waterObjects) { + if (waterObject) { + for (const auto& bound : waterObject->multiBounds) { + if (bound) { + if (auto size{ bound->size }; size.z <= 10.0f) { //avoid sloped water + auto center{ bound->center }; + const auto boundMin = center - size; + const auto boundMax = center + size; + if (!(a_pos.x < boundMin.x || a_pos.x > boundMax.x || a_pos.y < boundMin.y || a_pos.y > boundMax.y)) { + return center.z; + } + } + } + } + } + } + + return -RE::NI_INFINITY; + }; + + waterHeight = get_nearest_water_object_height(); + } + + return waterHeight; +} + +float4 Skin::GetWetness(RE::BSGeometry* geometry) +{ + float4 wetness = float4(0.0f, 0.0f, 0.0f, 0.0f); + if (auto userData = geometry->GetUserData()) + if (auto actor = userData->As()) { + const float positionZ = actor->GetPositionZ(); + wetness.z = positionZ; + if (settings.UseDynamicWetness && isDynamicWetnessAvailable) { + float dynamicWetness = SWE::API::GetFinalWetness(actor); + wetness.x = dynamicWetness; + } else { + const float stamina = actor->AsActorValueOwner()->GetActorValue(RE::ActorValue::kStamina); + const float permanentStamina = actor->AsActorValueOwner()->GetPermanentActorValue(RE::ActorValue::kStamina); + const float temporaryStamina = actor->GetActorValueModifier(RE::ACTOR_VALUE_MODIFIER::kTemporary, RE::ActorValue::kStamina); + const float maxStamina = std::max(permanentStamina + temporaryStamina, 1.0f); + const float staminaPercentage = actor->IsDead() ? 1.0f : (stamina / maxStamina); + const float sweatRange = settings.StartSweat - settings.FullSweat; + wetness.x = (std::abs(sweatRange) < 1e-5f) ? 0.0f : + (staminaPercentage >= settings.StartSweat) ? 0.0f : + (staminaPercentage <= settings.FullSweat) ? 1.0f : + (settings.StartSweat - staminaPercentage) / sweatRange; + } + if (actor->IsInWater()) { + wetness.y = 2.0f; + float waterHeight = -RE::NI_INFINITY; + const uint32_t formID = actor->AsReference()->formID; + const uint currentFrame = globals::state->frameCount; + auto cacheIt = waterHeightCache.find(formID); + if (cacheIt != waterHeightCache.end() && cacheIt->second.frameCount == currentFrame) { + waterHeight = cacheIt->second.waterHeight; + } else { + waterHeight = GetWaterHeight(actor->AsReference(), actor->GetPosition()); + waterHeightCache[formID] = { currentFrame, waterHeight }; + } + wetness.w = std::max(0.0f, waterHeight - positionZ); + } else { + wetness.y = 0.0f; + wetness.w = 0.0f; + } + + const uint32_t actorFormID = actor->AsReference()->formID; + + // Prevent unbounded growth: clear stale entries periodically + if (actorWetnessMap.size() > 1024) { + actorWetnessMap.clear(); + } + + auto it = actorWetnessMap.find(actorFormID); + if (it != actorWetnessMap.end()) { + auto& cached = it->second; + + const float fadeTime = std::max(settings.WetFadeTime, 0.001f); + if (cached.x < wetness.x) { + cached.x = wetness.x; + } else if (cached.x > wetness.x) { + cached.x -= *globals::game::deltaTime / fadeTime; + cached.x = std::max(cached.x, 0.0f); + wetness.x = cached.x; + } + + if (cached.y < wetness.y) { + cached.y = wetness.y; + if (cached.w < wetness.w) { + cached.w = wetness.w; + } else { + wetness.w = cached.w; + } + } else if (cached.y > wetness.y) { + cached.y -= *globals::game::deltaTime / fadeTime; + cached.y = std::max(cached.y, 0.0f); + wetness.y = cached.y; + if (wetness.y == 0.0f) { + wetness.w = 0.0f; + cached.w = 0.0f; + } else if (cached.w < wetness.w) { + cached.w = wetness.w; + } else { + wetness.w = cached.w; + } + } else if (cached.w < wetness.w) { + cached.w = wetness.w; + } else { + wetness.w = cached.w; + } + } else { + actorWetnessMap.emplace(actorFormID, wetness); + } + } + return wetness; +} + +struct SkinExtendedRendererState +{ + uint32_t PSResourceModifiedBits = 0; + std::array PSTexture; + + void SetExtraSkinPSTexture(RE::BSGraphics::Texture* newTexture, RE::BSGraphics::Texture* newTexture2) + { + { + PSTexture = { + newTexture ? newTexture->resourceView : nullptr, + newTexture2 ? newTexture2->resourceView : nullptr + }; + PSResourceModifiedBits = 1; + } + } + + SkinExtendedRendererState() + { + PSTexture.fill(nullptr); + } +} skinExtendedRendererState; + +void Skin::SetupExtraTexture(RE::BSLightingShaderMaterialBase const* material, RE::BSTextureSet* inTextureSet, uint32_t i_hashKey) +{ + if (!inTextureSet || material->normalTexture == nullptr) { + logger::error("[Advanced Skin] SetupExtraTexture : Texture set is null for material: {}", i_hashKey); + return; + } + + uint32_t hashKey = 0; + hashKey = material->hashKey; + if (hashKey == 0 || hashKey != i_hashKey) { + logger::error("[Advanced Skin] SetupExtraTexture : Invalid hash key for material: {}", i_hashKey); + return; + } + + const char extraTextureName[] = "_rfaos.dds"; + const char wetnessTextureName[] = "_wet.dds"; + const char* workingNormalPath = nullptr; + const char* workingSpecularPath = nullptr; + auto workingMaterial = static_cast(material); + auto hasSpecular = workingMaterial->specularBackLightingTexture != nullptr; + + auto graphicsState = globals::game::graphicsState; + const auto& stateData = graphicsState->GetRuntimeData(); + + if (hasSpecular) { + if (auto specularPath = inTextureSet->GetTexturePath(RE::BSTextureSet::Texture::kSpecular)) { + workingSpecularPath = specularPath; + } + } + if (auto normalPath = inTextureSet->GetTexturePath(RE::BSTextureSet::Texture::kNormal)) { + workingNormalPath = normalPath; + } else { + logger::error("[Advanced Skin] SetupExtraTexture : No specular or normal texture found in texture set from material: {}", hashKey); + auto& workingExtraPtr = skinExtraTextures.try_emplace(hashKey).first->second; + workingExtraPtr.rfaosTexture = stateData.defaultTextureBlack; + workingExtraPtr.wetnessTexture = stateData.defaultTextureBlack; + workingExtraPtr.extraTexturePath = ""; + workingExtraPtr.wetnessTexturePath = ""; + workingExtraPtr.hasExtraTexture = false; + workingExtraPtr.hasWetnessTexture = false; + return; + } + + const char* foundPath = nullptr; + std::string extraTexturePath = ""; + std::string wetnessTexturePath = ""; + + auto findIgnoreCase = [](std::string_view str, std::string_view pattern) -> size_t { + auto it = std::search(str.begin(), str.end(), pattern.begin(), pattern.end(), + [](char ch1, char ch2) { return std::tolower(ch1) == std::tolower(ch2); }); + return it == str.end() ? std::string_view::npos : std::distance(str.begin(), it); + }; + + auto tryReplaceSuffix = [&](const char* basePath, std::string_view suffix) -> bool { + auto pos = findIgnoreCase(basePath, suffix); + if (pos == std::string_view::npos) + return false; + extraTexturePath = std::string(basePath); + wetnessTexturePath = std::string(basePath); + extraTexturePath.replace(pos, suffix.size(), extraTextureName); + wetnessTexturePath.replace(pos, suffix.size(), wetnessTextureName); + foundPath = basePath; + return true; + }; + + if (hasSpecular && workingSpecularPath) { + tryReplaceSuffix(workingSpecularPath, "_s.dds"); + } + + if (!foundPath && workingNormalPath) { + if (!tryReplaceSuffix(workingNormalPath, "_n.dds")) { + if (!tryReplaceSuffix(workingNormalPath, "_msn.dds")) { + tryReplaceSuffix(workingNormalPath, ".dds"); + } + } + } + + logger::debug("[Advanced Skin] SetupExtraTexture : Extra texture path: {} for {}", extraTexturePath, foundPath ? foundPath : "(none)"); + logger::debug("[Advanced Skin] SetupExtraTexture : Wetness texture path: {} for {}", wetnessTexturePath, foundPath ? foundPath : "(none)"); + + auto& workingExtraPtr = skinExtraTextures.try_emplace(hashKey).first->second; + workingExtraPtr.rfaosTexture = stateData.defaultTextureWhite; + workingExtraPtr.wetnessTexture = stateData.defaultTextureWhite; + workingExtraPtr.extraTexturePath = extraTexturePath; + workingExtraPtr.wetnessTexturePath = wetnessTexturePath; + + inTextureSet->SetTexturePath(RE::BSTextureSet::Texture::kEnvironment, workingExtraPtr.extraTexturePath.c_str()); + inTextureSet->SetTexturePath(RE::BSTextureSet::Texture::kMultilayer, workingExtraPtr.wetnessTexturePath.c_str()); + inTextureSet->SetTexture(RE::BSTextureSet::Texture::kEnvironment, workingExtraPtr.rfaosTexture); + inTextureSet->SetTexture(RE::BSTextureSet::Texture::kMultilayer, workingExtraPtr.wetnessTexture); + + workingExtraPtr.hasExtraTexture = workingExtraPtr.rfaosTexture != nullptr && !workingExtraPtr.extraTexturePath.empty() && workingExtraPtr.rfaosTexture != stateData.defaultTextureBlack; + workingExtraPtr.hasWetnessTexture = workingExtraPtr.wetnessTexture != nullptr && !workingExtraPtr.wetnessTexturePath.empty() && workingExtraPtr.wetnessTexture != stateData.defaultTextureBlack; + + if (workingExtraPtr.hasExtraTexture || workingExtraPtr.hasWetnessTexture) { + logger::debug("[Advanced Skin] SetupExtraTexture : Extra texture set with hash key: {}", hashKey); + } else { + logger::debug("[Advanced Skin] SetupExtraTexture : Failed to set extra texture for material: {}", hashKey); + } +} + +void Skin::BSLightingShader_SetupMaterial(RE::BSLightingShaderMaterialBase const* material) +{ + auto materialFeature = material->GetFeature(); + if (materialFeature != RE::BSShaderMaterial::Feature::kFaceGen && + materialFeature != RE::BSShaderMaterial::Feature::kFaceGenRGBTint) { + return; + } + + auto materialTextureSet = material->textureSet.get(); + + uint32_t hashKey = 0; + hashKey = material->hashKey; + if (hashKey == 0) { + logger::error("[Advanced Skin] BSLightingShader_SetupMaterial : Invalid hash key for material: {}", static_cast(materialFeature)); + return; + } + + if (!skinExtraTextures.contains(hashKey)) { + // logger::debug("[Advanced Skin] BSLightingShader_SetupMaterial : Setting up extra texture for material: {}", static_cast(materialFeature)); + globals::features::skin.SetupExtraTexture(material, materialTextureSet, hashKey); + } + + auto graphicsState = globals::game::graphicsState; + const auto& workingExtraPtr = skinExtraTextures[hashKey]; + + if (workingExtraPtr.hasExtraTexture || workingExtraPtr.hasWetnessTexture) { + skinExtendedRendererState.SetExtraSkinPSTexture(workingExtraPtr.rfaosTexture->rendererTexture, workingExtraPtr.wetnessTexture->rendererTexture); + } else { + skinExtendedRendererState.SetExtraSkinPSTexture(graphicsState->GetRuntimeData().defaultTextureBlack->rendererTexture, graphicsState->GetRuntimeData().defaultTextureBlack->rendererTexture); + } +} + +void Skin::BSLightingShader_SetupGeometry(RE::BSRenderPass* a_pass) +{ + auto context = globals::d3d::context; + + if (settings.EnableSkin) { + auto geometry = a_pass->geometry; + float4 wetness = GetWetness(geometry); + + if (currentWetness != wetness) { + currentWetness = wetness; + PerGeometryData perGeometryData{}; + perGeometryData.skinPerGeometry = wetness; + PerGeometryCB->Update(perGeometryData); + } + + ID3D11Buffer* buffer = { PerGeometryCB->CB() }; + context->PSSetConstantBuffers(7, 1, &buffer); + } +} + +void Skin::SetShaderResources(ID3D11DeviceContext* a_context) +{ + if (skinExtendedRendererState.PSResourceModifiedBits != 0) { + a_context->PSSetShaderResources(71, 1, &skinExtendedRendererState.PSTexture.at(0)); + a_context->PSSetShaderResources(74, 1, &skinExtendedRendererState.PSTexture.at(1)); + } + skinExtendedRendererState.PSResourceModifiedBits = 0; +} + +void Skin::Hooks::BSLightingShader_SetupGeometry::thunk(RE::BSShader* This, RE::BSRenderPass* Pass, uint32_t RenderFlags) +{ + auto& skin = globals::features::skin; + skin.BSLightingShader_SetupGeometry(Pass); + return func(This, Pass, RenderFlags); +} diff --git a/src/Features/Skin.h b/src/Features/Skin.h new file mode 100644 index 0000000000..ce023fa2a2 --- /dev/null +++ b/src/Features/Skin.h @@ -0,0 +1,145 @@ +#pragma once + +struct Skin : Feature +{ + static Skin* GetSingleton() + { + static Skin singleton; + return &singleton; + } + + virtual inline std::string GetName() override { return "Advanced Skin"; } + virtual inline std::string GetShortName() override { return "Skin"; } + virtual inline std::string_view GetShaderDefineName() override { return "CS_SKIN"; } + virtual std::string_view GetCategory() const override { return "Characters"; } + virtual std::pair> GetFeatureSummary() override + { + return { + "Advanced Skin enhances character skin rendering with multiple techniques.", + { "Physically-based dual specular lobes for realistic skin highlights", + "Tiled skin detail textures for enhanced realism", + "Extra textures support for roughness, translucency, and more", + "Reworked wetness system for dynamic skin effects" } + }; + } + virtual inline bool HasShaderDefine(RE::BSShader::Type t) override + { + return t == RE::BSShader::Type::Lighting; + }; + + virtual inline bool SupportsVR() { return true; } + + virtual void RestoreDefaultSettings() override; + virtual void DrawSettings() override; + + virtual void LoadSettings(json& o_json) override; + virtual void SaveSettings(json& o_json) override; + + virtual void Prepass() override; + virtual void PostPostLoad() override; + + virtual void SetupResources() override; + + void ReloadSkinDetail(); + void LoadSkinDetailTexture(); + + struct Settings + { + bool EnableSkin = true; + float SkinMainRoughness = 0.7f; + float SkinSecondRoughness = 0.35f; + float SkinSpecularTexMultiplier = 1.0f; + float SecondarySpecularStrength = 0.15f; + float F0 = 0.0278f; + float BaseColorMultiplier = 1.0f; + float PhysicalMainRoughnessMultiplier = 1.3f; + float PhysicalSecondRoughnessMultiplier = 0.75f; + float PhysicalSpecularStrength = 1.0f; + float ExtraEdgeRoughness = 0.25f; + bool EnableSkinDetail = true; + float SkinDetailStrength = 0.25f; + float SkinDetailTiling = 10.0f; + float BodyTilingMultiplier = 2.0f; + float ExtraSkinWetness = 0.0f; + float WetFadeTime = 10.0f; + float StartSweat = 0.75f; + float FullSweat = 0.15f; + float4 WetParams = { 512.0f, 0.7f, 10.0f, 4.0f }; + float Translucency = 0.1f; + float sssWidth = 0.2f; + bool UseSSS = true; + float FuzzStrength = 1.0f; + float FuzzRoughness = 0.35f; + float FuzzF0 = 0.045f; + bool UseDynamicWetness = false; + } settings; + + struct alignas(16) SkinData + { + float4 skinParams; + float4 skinParams2; + float4 skinDetailParams; + float4 sssParams; + float4 fuzzParams; + float4 physicalParams; + float4 wetParams; + }; + + struct alignas(16) PerGeometryData + { + float4 skinPerGeometry; + }; + + eastl::unique_ptr PerGeometryCB; + float4 currentWetness = { 0.0f, 0.0f, 0.0f, 0.0f }; + float playerStamina = 0.0f; + float playerStaminaMax = 0.0f; + + struct WaterHeightCacheEntry + { + uint frameCount = 0; + float waterHeight = 0.0f; + }; + std::unordered_map waterHeightCache; // keyed by actor formID + + struct ExtraTextures + { + RE::NiSourceTexturePtr rfaosTexture; + RE::NiSourceTexturePtr wetnessTexture; + std::string extraTexturePath; + std::string wetnessTexturePath; + bool hasExtraTexture = false; + bool hasWetnessTexture = false; + }; + + eastl::unique_ptr texSkinDetail = nullptr; + std::unordered_map skinExtraTextures; + std::unordered_map actorWetnessMap; // keyed by actor formID + + SkinData GetCommonBufferData(); + float GetWaterHeight(const RE::TESObjectREFR* a_ref, const RE::NiPoint3& a_pos); + float4 GetWetness(RE::BSGeometry* geometry); + + void SetupExtraTexture(RE::BSLightingShaderMaterialBase const* material, RE::BSTextureSet* inTextureSet, uint32_t i_hashKey); + void BSLightingShader_SetupMaterial(RE::BSLightingShaderMaterialBase const* material); + void BSLightingShader_SetupGeometry(RE::BSRenderPass* a_pass); + void SetShaderResources(ID3D11DeviceContext* a_context); + + struct Hooks + { + struct BSLightingShader_SetupGeometry + { + static void thunk(RE::BSShader* This, RE::BSRenderPass* Pass, uint32_t RenderFlags); + static inline REL::Relocation func; + }; + + static void Install() + { + stl::write_vfunc<0x6, BSLightingShader_SetupGeometry>(RE::VTABLE_BSLightingShader[0]); + logger::info("[Advanced Skin] Installed hooks"); + return; + } + }; + + bool isDynamicWetnessAvailable = false; +}; diff --git a/src/Features/TerrainHelper.cpp b/src/Features/TerrainHelper.cpp index 9ac26eea32..bcd09054e9 100644 --- a/src/Features/TerrainHelper.cpp +++ b/src/Features/TerrainHelper.cpp @@ -121,7 +121,7 @@ struct THExtendedRendererState } } thExtendedRendererState; -void TerrainHelper::SetShaderResouces(ID3D11DeviceContext* a_context) +void TerrainHelper::SetShaderResources(ID3D11DeviceContext* a_context) { uint32_t mask = thExtendedRendererState.PSResourceModifiedBits; diff --git a/src/Features/TerrainHelper.h b/src/Features/TerrainHelper.h index ed20870dd1..c83ee5d79e 100644 --- a/src/Features/TerrainHelper.h +++ b/src/Features/TerrainHelper.h @@ -43,7 +43,7 @@ struct TerrainHelper : Feature virtual bool SupportsVR() override { return true; }; virtual std::string GetFeatureModLink() override { return MakeNexusModURL(MOD_ID); } - void SetShaderResouces(ID3D11DeviceContext* a_context); + void SetShaderResources(ID3D11DeviceContext* a_context); bool TESObjectLAND_SetupMaterial(RE::TESObjectLAND* land); void BSLightingShader_SetupMaterial(RE::BSLightingShaderMaterialBase const* material); }; \ No newline at end of file diff --git a/src/Globals.cpp b/src/Globals.cpp index b388d46f4a..e1b60fc56b 100644 --- a/src/Globals.cpp +++ b/src/Globals.cpp @@ -21,6 +21,7 @@ #include "Features/ScreenSpaceGI.h" #include "Features/ScreenSpaceShadows.h" #include "Features/ScreenshotFeature.h" +#include "Features/Skin.h" #include "Features/SkySync.h" #include "Features/Skylighting.h" #include "Features/SubsurfaceScattering.h" @@ -89,6 +90,7 @@ namespace globals WeatherEditor weatherEditor{}; ExponentialHeightFog exponentialHeightFog{}; TruePBR truePBR{}; + Skin skin{}; namespace llf { diff --git a/src/Globals.h b/src/Globals.h index 342f92ce4f..326b9a34eb 100644 --- a/src/Globals.h +++ b/src/Globals.h @@ -37,6 +37,7 @@ struct WeatherEditor; struct ExponentialHeightFog; struct HDRDisplay; struct ScreenshotFeature; +struct Skin; class State; class Deferred; @@ -97,6 +98,7 @@ namespace globals extern WeatherEditor weatherEditor; extern ExponentialHeightFog exponentialHeightFog; extern TruePBR truePBR; + extern Skin skin; namespace llf { diff --git a/src/Hooks.cpp b/src/Hooks.cpp index a4d6edbb3a..6c9aa5d612 100644 --- a/src/Hooks.cpp +++ b/src/Hooks.cpp @@ -14,6 +14,7 @@ #include "Features/InteriorSun.h" #include "Features/ScreenshotFeature.h" #include "Features/LightLimitFix.h" +#include "Features/Skin.h" #include "Features/Upscaling.h" #include "Features/VR.h" #include "Features/VolumetricLighting.h" diff --git a/src/State.cpp b/src/State.cpp index 058bff6233..253efca58e 100644 --- a/src/State.cpp +++ b/src/State.cpp @@ -10,6 +10,7 @@ #include "Features/HDRDisplay.h" #include "Features/InteriorSun.h" #include "Features/PerformanceOverlay.h" +#include "Features/Skin.h" #include "Features/TerrainBlending.h" #include "Features/TerrainHelper.h" #include "Features/Upscaling.h" @@ -53,6 +54,7 @@ void State::Draw() auto& terrainHelper = globals::features::terrainHelper; auto& cloudShadows = globals::features::cloudShadows; auto& weatherEditor = globals::features::weatherEditor; + auto& skin = globals::features::skin; auto& truePBR = globals::features::truePBR; auto context = globals::d3d::context; auto& volumetricShadows = globals::features::volumetricShadows; @@ -77,13 +79,18 @@ void State::Draw() } if (terrainHelper.loaded) { - ZoneScopedN("TerrainHelper::SetShaderResouces"); - terrainHelper.SetShaderResouces(context); + ZoneScopedN("TerrainHelper::SetShaderResources"); + terrainHelper.SetShaderResources(context); + } + + if (skin.loaded) { + ZoneScopedN("Skin::SetShaderResources"); + skin.SetShaderResources(context); } if (truePBR.loaded) { - ZoneScopedN("TruePBR::SetShaderResouces"); - truePBR.SetShaderResouces(context); + ZoneScopedN("TruePBR::SetShaderResources"); + truePBR.SetShaderResources(context); } if (permutationData != permutationDataPrevious) { diff --git a/src/TruePBR.cpp b/src/TruePBR.cpp index 75be9fe75c..279fc86cd6 100644 --- a/src/TruePBR.cpp +++ b/src/TruePBR.cpp @@ -1529,7 +1529,7 @@ void TruePBR::SetupDefaultPBRLandTextureSet() } } -void TruePBR::SetShaderResouces(ID3D11DeviceContext* a_context) +void TruePBR::SetShaderResources(ID3D11DeviceContext* a_context) { uint32_t mask = extendedRendererState.PSResourceModifiedBits; diff --git a/src/TruePBR.h b/src/TruePBR.h index 3bf5d73e64..5607efbfe6 100644 --- a/src/TruePBR.h +++ b/src/TruePBR.h @@ -58,7 +58,7 @@ struct TruePBR : Feature bool TESObjectLAND_SetupMaterial(RE::TESObjectLAND* land); bool BSLightingShader_SetupMaterial(RE::BSLightingShader* shader, RE::BSLightingShaderMaterialBase const* material); - void SetShaderResouces(ID3D11DeviceContext* a_context); + void SetShaderResources(ID3D11DeviceContext* a_context); virtual void GenerateShaderPermutations(RE::BSShader* shader) override; void SetupGlintsTexture();