diff --git a/src/skate3_native_scene.cpp b/src/skate3_native_scene.cpp index 5fb5d81..f5dcb76 100644 --- a/src/skate3_native_scene.cpp +++ b/src/skate3_native_scene.cpp @@ -812,6 +812,9 @@ bool BuildItemFromMesh(uint8_t* base, uint32_t mesh, DrawItem& item) { item.decal = false; item.decal_tileable = false; item.transparent = false; + item.water = false; + item.water_normal = 0; + item.water_env = 0; item.unlit = false; item.tint[0] = item.tint[1] = item.tint[2] = item.tint[3] = 0.0f; const uint32_t material = REX_LOAD_U32(mesh + kMeshMaterial); @@ -837,6 +840,13 @@ bool BuildItemFromMesh(uint8_t* base, uint32_t mesh, DrawItem& item) { slot = &item.macro_tex; } else if (std::memcmp(text, "decal", 6) == 0) { slot = &item.decal_art; + } else if (std::memcmp(text, "normal", 7) == 0) { + // Exact match only ("normal2" is the second flowing-water tap). + // Consumed by the water branch; other families ignore it. + slot = &item.water_normal; + } else if (std::memcmp(text, "environment", 12) == 0) { + // Environment CUBE map: the water reflection term. + slot = &item.water_env; } else if (std::memcmp(text, "macroOverlayUVScale", 20) == 0 || std::memcmp(text, "macroOverlayOpacity", 20) == 0) { // Shader-constant channel: the float lives in the first guid word. @@ -874,6 +884,9 @@ bool BuildItemFromMesh(uint8_t* base, uint32_t mesh, DrawItem& item) { // sheets, glass, fences); see DrawItem::transparent. item.transparent = std::memcmp(mat_name, "environment.transparent", 23) == 0; + // water.*: canal/ocean surfaces, transparent sub-pass with the + // dedicated water shading branch (see DrawItem::water). + item.water = std::memcmp(mat_name, "water.", 6) == 0; } continue; } @@ -2632,6 +2645,10 @@ struct RendererState { uint32_t srv_next = 0; std::unordered_map textures; GuestTexture white; + // Water environment CUBE maps (t6): separate cache; same guest object + // addresses decode differently (6 faces, TextureCube SRV). + std::unordered_map cube_textures; + GuestTexture white_cube; // Bone palette ring: persistent-mapped upload buffer, one region per // in-flight frame. static constexpr uint32_t kBoneRegionSize = 1u << 20; @@ -2731,6 +2748,7 @@ Texture2D lightmap : register(t1); Texture2D macro : register(t3); Texture2D decal_art : register(t4); Texture2D shadow_atlas : register(t5); +TextureCube env_cube : register(t6); // Raw bone palette: 3 float4 rows per bone, column-vector affine [R | t], // applied with explicit dots (StructuredBuffer default packing is // column-major and would silently transpose the matrices). @@ -2806,7 +2824,7 @@ float4 ps_main(VSOut i) : SV_Target { // linear (squared) color space, so the gamma-space equivalent is // sqrt(m); a direct multiply doubles the darkening (harsh black // patchwork vs the emulated subtle weathering). - if (overlay.z > 0.0) { + if (overlay.z > 0.0 && misc.x < 1.5) { // water reuses overlay.z (ripple map flag) float4 m = macro.Sample(smp, i.uv * overlay.x); albedo.rgb *= lerp(float3(1.0, 1.0, 1.0), sqrt(m.rgb), overlay.y * m.a); } @@ -2875,6 +2893,68 @@ float4 ps_main(VSOut i) : SV_Target { if (mat_tint.w > 0.0 && misc.x == 0.0) { lit *= mat_tint.rgb; } + if (misc.x > 1.5) { + // water.* (flowingwater.fx approximation): the real shader is + // near-black diffuse + dual time-scrolled ripple-normal taps + an + // environment-cube reflection + sun specular. We have no cube map + // bound, so the reflection term is the frame fog color (the best + // single approximation of the surroundings' haze tone) scaled by a + // fresnel curve; the ripple normal perturbs both the fresnel and a sun + // sparkle along the captured CSM light axis. The lightmap (x2) keeps + // the baked bridge/wall shading on the surface. Calibrated against the + // canal capture (emulated mid-canal mean ~(24,28,32)/255). + float t = overlay.x; + float2 rip; + if (overlay.z > 0.0) { + // Dual scrolled taps of the material's ripple normal map (macro slot). + float2 wuv = i.uv * 6.0; + float3 n1 = macro.Sample(smp, wuv + t * float2(0.11, 0.06)).rgb; + float3 n2 = macro.Sample(smp, wuv * 1.71 - t * float2(0.07, 0.13)).rgb; + rip = (n1.xy + n2.xy) - 1.0; + } else { + // Normal map unresolved: procedural ripples from world position. + // Wavelengths ~0.4-1m (emulated ripples are decimeter-scale); low + // frequencies formed meter-wide chevron interference bands that read + // as giant arrows on the surface. + float3 wp = i.rpos + cam_pos.xyz; + rip = 0.35 * float2(sin(wp.x * 9.7 + wp.z * 6.1 + t * 2.3) + + 0.6 * sin(wp.x * 17.3 - wp.z * 11.9 + t * 3.4), + cos(wp.x * 7.1 - wp.z * 13.7 + t * 2.7) + + 0.6 * cos(wp.x * 12.9 + wp.z * 18.3 + t * 3.1)); + } + float3 wn = normalize(float3(rip.x * 0.4, 2.0, rip.y * 0.4)); + float3 vd = -normalize(i.rpos); + float fres = pow(1.0 - saturate(dot(vd, wn)), 3.0); + // The flowing-water lightmap unwrap decodes unreliably (bands across + // atlas gutters), so the water term deliberately ignores it: near-black + // body + ripple-perturbed cube reflection + sun sparkle. + // Deep body: the water "diffuse" is a faint STRIPE MASK (max 24/255, + // WaterFallFoamAlpha, a lookup for the real shader, not a color). The + // game consumes it in linear space where 0.09^2 vanishes; squaring here + // likewise kills the visible blue/black banding. + float3 col = albedo.rgb * albedo.rgb * 0.6; + // Reflection tint: the environment cube when resolved (t6); otherwise a + // haze derived from the frame fog color, lifted toward neutral so dark + // dusk fog doesn't collapse the water to black (fit: emulated canal + // mean ~(24,28,32)/255 with fog color (0.02,0.07,0.13)). + float3 renv = overlay.y > 0.0 + ? env_cube.Sample(smp, reflect(-vd, wn)).rgb + : mat_tint.rgb * 0.5 + 0.06; + col += renv * (0.55 + 0.45 * fres); + if (sh_misc.y > 0.0) { + float3 h = normalize(vd + normalize(-sh_z.xyz)); + col += pow(saturate(dot(wn, h)), 90.0) * 0.35; // sun sparkle + } + float fade = saturate(length(i.rpos) * misc.y + misc.z); + if (misc.w != 1.0) { + fade = pow(max(fade, 1e-4), misc.w); + } + col = sqrt(max(col * col * saturate(1.0 + fade * mat_tint.w) + + fade * mat_tint.rgb, 0.0)); + // Opaque: the game's murk hides the canal bed entirely (and our bed + // shading is untrustworthy under water: striped lightmap unwraps). + return float4(col, 1.0); + } if (misc.x > 0.0) { // transparentenvironment.fx (Skate 2 source; disassembled Skate 3 PS // matches): outcolor.rgb = diffTerm * diffuse.rgb * diffuse.a; the rgb @@ -3985,21 +4065,180 @@ bool EnsureGuestTexture(const NativeGuestOutputRenderContext& context, uint8_t* return EnsureGuestTextureFromWords(context, base, words, out); } +// Environment CUBE map for the water reflection term (t6). Mip 0 only, six +// faces untiled independently (Xenos cubes are 2D-tiled per face slice). +bool EnsureGuestCubeTexture(const NativeGuestOutputRenderContext& context, uint8_t* base, + uint32_t tex_ptr, GuestTexture& out) { + uint32_t words[6] = {}; + { + uint32_t raw[6]; + if (!GuestTryCopy(raw, base + tex_ptr + 7 * 4, sizeof(raw))) { + return false; + } + for (uint32_t i = 0; i < 6; ++i) { + words[i] = SwapU32(raw[i]); + } + } + std::memcpy(out.fetch_words, words, sizeof(words)); + xenos::xe_gpu_texture_fetch_t fetch = {}; + fetch.dword_0 = words[0]; + fetch.dword_1 = words[1]; + fetch.dword_2 = words[2]; + fetch.dword_3 = words[3]; + fetch.dword_4 = words[4]; + fetch.dword_5 = words[5]; + if (fetch.type != xenos::FetchConstantType::kTexture || fetch.base_address == 0) { + return false; + } + rex::graphics::TextureInfo info; + if (!rex::graphics::TextureInfo::Prepare(fetch, &info)) { + return false; + } + if (info.dimension != xenos::DataDimension::kCube || info.memory.base_address == 0 || + info.width >= 1024 || info.height >= 1024) { + return false; + } + HostTextureFormat host; + if (!GetHostTextureFormat(info.format, host)) { + return false; + } + const rex::graphics::FormatInfo* format_info = info.format_info(); + const uint32_t bytes_per_block = format_info->bytes_per_block(); + if (bytes_per_block == 0 || (bytes_per_block & (bytes_per_block - 1)) != 0) { + return false; + } + const uint32_t bytes_per_block_log2 = uint32_t(std::countr_zero(bytes_per_block)); + const uint32_t width = info.width + 1u; + const uint32_t height = info.height + 1u; + const uint32_t block_w = format_info->block_width; + const uint32_t block_h = format_info->block_height; + const uint32_t host_width = ((width + block_w - 1) / block_w) * block_w; + const uint32_t host_height = ((height + block_h - 1) / block_h) * block_h; + const uint32_t cols = (width + block_w - 1) / block_w; + const uint32_t rows = (height + block_h - 1) / block_h; + const uint32_t pitch_blocks = info.extent.block_pitch_h; + const uint32_t slice_blocks = info.extent.block_pitch_h * info.extent.block_pitch_v; + const uint32_t slice_bytes = slice_blocks * bytes_per_block; + const uint32_t total = slice_bytes * 6; + if (total == 0 || total > 16u * 1024u * 1024u) { + return false; + } + static thread_local std::vector cube_scratch; + cube_scratch.resize(total); + if (!GuestTryCopy(cube_scratch.data(), base + (0xA0000000u | info.memory.base_address), + total)) { + return false; + } + + const uint32_t row_bytes = cols * bytes_per_block; + const uint32_t pitch = (row_bytes + (D3D12_TEXTURE_DATA_PITCH_ALIGNMENT - 1u)) & + ~(D3D12_TEXTURE_DATA_PITCH_ALIGNMENT - 1u); + const uint32_t face_upload = ((pitch * rows + (D3D12_TEXTURE_DATA_PLACEMENT_ALIGNMENT - 1u)) & + ~(D3D12_TEXTURE_DATA_PLACEMENT_ALIGNMENT - 1u)); + ID3D12Device* device = context.d3d12.device; + D3D12_HEAP_PROPERTIES heap{}; + heap.Type = D3D12_HEAP_TYPE_DEFAULT; + D3D12_RESOURCE_DESC desc{}; + desc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; + desc.Width = host_width; + desc.Height = host_height; + desc.DepthOrArraySize = 6; + desc.MipLevels = 1; + desc.Format = host.resource_format; + desc.SampleDesc.Count = 1; + if (FAILED(device->CreateCommittedResource(&heap, D3D12_HEAP_FLAG_NONE, &desc, + D3D12_RESOURCE_STATE_COPY_DEST, nullptr, + IID_PPV_ARGS(&out.texture)))) { + out.texture = nullptr; + return false; + } + out.upload = CreateUploadBuffer(device, face_upload * 6); + if (!out.upload) { + out.texture->Release(); + out.texture = nullptr; + return false; + } + uint8_t* mapping = nullptr; + out.upload->Map(0, nullptr, reinterpret_cast(&mapping)); + for (uint32_t face = 0; face < 6; ++face) { + const uint8_t* guest = cube_scratch.data() + size_t(face) * slice_bytes; + for (uint32_t by = 0; by < rows; ++by) { + uint8_t* out_row = mapping + size_t(face) * face_upload + size_t(by) * pitch; + for (uint32_t bx = 0; bx < cols; ++bx) { + uint32_t source_offset; + if (info.is_tiled) { + source_offset = uint32_t(rex::graphics::texture_util::GetTiledOffset2D( + int32_t(bx), int32_t(by), pitch_blocks, bytes_per_block_log2)); + } else { + source_offset = (by * pitch_blocks + bx) * bytes_per_block; + } + if (source_offset + bytes_per_block > slice_bytes) { + std::memset(out_row + size_t(bx) * bytes_per_block, 0, bytes_per_block); + continue; + } + std::memcpy(out_row + size_t(bx) * bytes_per_block, guest + source_offset, + bytes_per_block); + } + SwapGuestEndian(out_row, row_bytes, info.endianness); + } + } + out.upload->Unmap(0, nullptr); + + auto& list = context.d3d12.command_processor->GetDeferredCommandList(); + for (uint32_t face = 0; face < 6; ++face) { + D3D12_TEXTURE_COPY_LOCATION dst{}; + dst.pResource = out.texture; + dst.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; + dst.SubresourceIndex = face; + D3D12_TEXTURE_COPY_LOCATION src{}; + src.pResource = out.upload; + src.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT; + src.PlacedFootprint.Offset = size_t(face) * face_upload; + src.PlacedFootprint.Footprint.Format = host.resource_format; + src.PlacedFootprint.Footprint.Width = host_width; + src.PlacedFootprint.Footprint.Height = host_height; + src.PlacedFootprint.Footprint.Depth = 1; + src.PlacedFootprint.Footprint.RowPitch = pitch; + list.D3DCopyTextureRegion(&dst, 0, 0, 0, &src, nullptr); + } + context.d3d12.push_transition_barrier(context.d3d12.command_processor_user_data, + out.texture, D3D12_RESOURCE_STATE_COPY_DEST, + D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); + if (g_r.srv_next >= 8192) { + return false; + } + out.srv_slot = g_r.srv_next++; + D3D12_SHADER_RESOURCE_VIEW_DESC srv{}; + srv.Format = host.srv_format; + srv.ViewDimension = D3D12_SRV_DIMENSION_TEXTURECUBE; + srv.Shader4ComponentMapping = ComposeSrvSwizzle(fetch.swizzle, host.host_swizzle); + srv.TextureCube.MipLevels = 1; + D3D12_CPU_DESCRIPTOR_HANDLE slot = g_r.srv_heap->GetCPUDescriptorHandleForHeapStart(); + slot.ptr += size_t(out.srv_slot) * g_r.srv_size; + device->CreateShaderResourceView(out.texture, &srv, slot); + out.payload_addr = 0xA0000000u | info.memory.base_address; + out.payload_size = total; + out.payload_fp = SamplePayloadFingerprint(base, out.payload_addr, out.payload_size); + out.recheck_frame = 0; + out.valid = true; + return true; +} + bool EnsurePipeline(const NativeGuestOutputRenderContext& context) { if (g_r.failed) return false; ID3D12Device* device = context.d3d12.device; g_r.device = device; if (!g_r.root_signature) { - D3D12_ROOT_PARAMETER params[8] = {}; + D3D12_ROOT_PARAMETER params[9] = {}; params[0].ParameterType = D3D12_ROOT_PARAMETER_TYPE_32BIT_CONSTANTS; params[0].Constants.ShaderRegister = 0; - // NOTE the 64-DWORD root-signature budget: 52 constants + 5 descriptor - // tables (1 each) + 1 root SRV (2) + 1 root CBV (2) = 61. Going past 64 + // NOTE the 64-DWORD root-signature budget: 52 constants + 6 descriptor + // tables (1 each) + 1 root SRV (2) + 1 root CBV (2) = 62. Going past 64 // makes CreateRootSignature fail (renderer falls back to emulated). params[0].Constants.Num32BitValues = 52; params[0].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL; - D3D12_DESCRIPTOR_RANGE srv_range[5] = {}; + D3D12_DESCRIPTOR_RANGE srv_range[6] = {}; srv_range[0].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV; srv_range[0].NumDescriptors = 1; srv_range[0].BaseShaderRegister = 0; @@ -4011,6 +4250,8 @@ bool EnsurePipeline(const NativeGuestOutputRenderContext& context) { srv_range[3].BaseShaderRegister = 4; // environment.decal art (t4) srv_range[4] = srv_range[0]; srv_range[4].BaseShaderRegister = 5; // shadow atlas (t5) + srv_range[5] = srv_range[0]; + srv_range[5].BaseShaderRegister = 6; // water environment cube (t6) params[1].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE; params[1].DescriptorTable.NumDescriptorRanges = 1; params[1].DescriptorTable.pDescriptorRanges = &srv_range[0]; @@ -4031,6 +4272,9 @@ bool EnsurePipeline(const NativeGuestOutputRenderContext& context) { params[6].ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL; params[7] = params[1]; params[7].DescriptorTable.pDescriptorRanges = &srv_range[4]; + // Water environment cube (t6): the flowingwater/ocean reflection term. + params[8] = params[1]; + params[8].DescriptorTable.pDescriptorRanges = &srv_range[5]; D3D12_STATIC_SAMPLER_DESC samplers[2] = {}; samplers[0].Filter = D3D12_FILTER_ANISOTROPIC; samplers[0].MaxAnisotropy = 8; @@ -4053,7 +4297,7 @@ bool EnsurePipeline(const NativeGuestOutputRenderContext& context) { samplers[1].AddressW = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; samplers[1].ShaderRegister = 1; D3D12_ROOT_SIGNATURE_DESC desc{}; - desc.NumParameters = 8; + desc.NumParameters = 9; desc.pParameters = params; desc.NumStaticSamplers = 2; desc.pStaticSamplers = samplers; @@ -4598,6 +4842,68 @@ bool EnsurePipeline(const NativeGuestOutputRenderContext& context) { device->CreateShaderResourceView(g_r.white.texture, &srv, slot); g_r.white.valid = true; } + if (!g_r.white_cube.valid) { + // 1x1x6 mid-gray fallback cube for the water reflection slot (t6): a + // TextureCube SRV must always be bound where the shader declares one. + D3D12_HEAP_PROPERTIES heap_props{}; + heap_props.Type = D3D12_HEAP_TYPE_DEFAULT; + D3D12_RESOURCE_DESC desc{}; + desc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; + desc.Width = 1; + desc.Height = 1; + desc.DepthOrArraySize = 6; + desc.MipLevels = 1; + desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + desc.SampleDesc.Count = 1; + if (FAILED(device->CreateCommittedResource(&heap_props, D3D12_HEAP_FLAG_NONE, &desc, + D3D12_RESOURCE_STATE_COPY_DEST, nullptr, + IID_PPV_ARGS(&g_r.white_cube.texture)))) { + g_r.failed = true; + return false; + } + g_r.white_cube.upload = CreateUploadBuffer(device, 512 * 6); + if (!g_r.white_cube.upload) { + g_r.failed = true; + return false; + } + uint8_t* mapping = nullptr; + g_r.white_cube.upload->Map(0, nullptr, reinterpret_cast(&mapping)); + for (uint32_t f = 0; f < 6; ++f) { + std::memset(mapping + f * 512, 0x80, 4); + } + g_r.white_cube.upload->Unmap(0, nullptr); + auto& list = context.d3d12.command_processor->GetDeferredCommandList(); + for (uint32_t f = 0; f < 6; ++f) { + D3D12_TEXTURE_COPY_LOCATION dst{}; + dst.pResource = g_r.white_cube.texture; + dst.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; + dst.SubresourceIndex = f; + D3D12_TEXTURE_COPY_LOCATION src{}; + src.pResource = g_r.white_cube.upload; + src.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT; + src.PlacedFootprint.Offset = f * 512; + src.PlacedFootprint.Footprint.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + src.PlacedFootprint.Footprint.Width = 1; + src.PlacedFootprint.Footprint.Height = 1; + src.PlacedFootprint.Footprint.Depth = 1; + src.PlacedFootprint.Footprint.RowPitch = 256; + list.D3DCopyTextureRegion(&dst, 0, 0, 0, &src, nullptr); + } + context.d3d12.push_transition_barrier(context.d3d12.command_processor_user_data, + g_r.white_cube.texture, + D3D12_RESOURCE_STATE_COPY_DEST, + D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); + g_r.white_cube.srv_slot = g_r.srv_next++; + D3D12_SHADER_RESOURCE_VIEW_DESC srv{}; + srv.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + srv.ViewDimension = D3D12_SRV_DIMENSION_TEXTURECUBE; + srv.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; + srv.TextureCube.MipLevels = 1; + D3D12_CPU_DESCRIPTOR_HANDLE slot = g_r.srv_heap->GetCPUDescriptorHandleForHeapStart(); + slot.ptr += size_t(g_r.white_cube.srv_slot) * g_r.srv_size; + device->CreateShaderResourceView(g_r.white_cube.texture, &srv, slot); + g_r.white_cube.valid = true; + } const uint32_t width = context.guest_output_width; const uint32_t height = context.guest_output_height; @@ -5054,6 +5360,17 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat // they ARE the wall/ground sections they cover, with the art composited // in-shader (alpha-blending them as separate overlay geometry punched // holes in the world). + // Ripple scroll clock for the water branch (seconds; wraps every hour to + // keep float precision on the scrolled UVs). + static const std::chrono::steady_clock::time_point water_t0 = + std::chrono::steady_clock::now(); + const float water_time = float( + std::chrono::duration(std::chrono::steady_clock::now() - water_t0).count() - + std::floor( + std::chrono::duration(std::chrono::steady_clock::now() - water_t0).count() / + 3600.0) * + 3600.0); + const auto draw_item = [&](const DrawItem& item) { auto it = g_r.meshes.find(item.mesh); if (it != g_r.meshes.end() && it->second.fingerprint != item.fingerprint && @@ -5089,7 +5406,7 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat // front/back copies z-fight into lightmap flicker at range (banners/ // flags). Opaque depth pass only; a mirrored instance (negative world // determinant) would flip winding, so those stay uncull(ed). - if (use_depth && !item.transparent && g_r.pso_cullback != nullptr) { + if (use_depth && !item.transparent && !item.water && g_r.pso_cullback != nullptr) { const float* w = item.world; const float det3 = w[0] * (w[5] * w[10] - w[6] * w[9]) - w[1] * (w[4] * w[10] - w[6] * w[8]) + @@ -5272,6 +5589,33 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat item.macro_tex != 0 && REXCVAR_GET(skate3_native_render_scene_macro) ? resolve_texture(item.macro_tex) : &g_r.white; + if (item.water && item.water_normal != 0) { + // Water rides its ripple normal map in the macro slot (water never + // carries a macro overlay; overlay.z stays 0 below so the macro + // composite path never runs). + macro_tex = resolve_texture(item.water_normal); + } + // Water environment cube (t6, root param 8): decoded once per guest + // object into the cube cache; the gray fallback cube otherwise. + const GuestTexture* cube_tex = &g_r.white_cube; + if (item.water && item.water_env != 0) { + auto cit = g_r.cube_textures.find(item.water_env); + if (cit == g_r.cube_textures.end()) { + GuestTexture c{}; + if (EnsureGuestCubeTexture(context, base, item.water_env, c)) { + cit = g_r.cube_textures.emplace(item.water_env, c).first; + } else { + if (c.upload) c.upload->Release(); + if (c.texture) c.texture->Release(); + c = GuestTexture{}; + c.valid = false; + cit = g_r.cube_textures.emplace(item.water_env, c).first; // negative-cache + } + } + if (cit->second.valid) { + cube_tex = &cit->second; + } + } const GuestTexture* decal_tex = item.decal && item.decal_art != 0 && REXCVAR_GET(skate3_native_render_scene_decals) ? resolve_texture(item.decal_art) @@ -5284,11 +5628,22 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat // decal (art wraps; clamping a many-period uv range stretched the // border texels into the giant cliff-face streaks). constants[47] = is_decal ? (item.decal_tileable ? 2.0f : 1.0f) : 0.0f; - // misc.x: alpha-blended sub-pass item (transparentenvironment shading); - // fog rides in misc.yzw (ramp) and the mat_tint row (color), unused by - // transparent items otherwise (root-signature DWORD budget). - constants[48] = item.transparent ? 1.0f : 0.0f; - if (item.transparent) { + if (item.water) { + // overlay.x = ripple scroll time, overlay.y = real environment cube + // bound at t6, overlay.z = ripple normal map resolved (in the macro + // slot; the shader synthesizes procedural ripples otherwise). + // overlay.w stays 0: no macro composite, no decal. + constants[44] = water_time; + constants[45] = cube_tex != &g_r.white_cube ? 1.0f : 0.0f; + constants[46] = macro_tex != &g_r.white ? 1.0f : 0.0f; + constants[47] = 0.0f; + } + // misc.x: alpha-blended sub-pass item (1 = transparentenvironment + // shading, 2 = water branch); fog rides in misc.yzw (ramp) and the + // mat_tint row (color), unused by these items otherwise + // (root-signature DWORD budget). + constants[48] = item.water ? 2.0f : (item.transparent ? 1.0f : 0.0f); + if (item.transparent || item.water) { constants[49] = scene.fog_ramp[0]; constants[50] = scene.fog_ramp[1]; constants[51] = scene.fog_ramp[2]; @@ -5315,6 +5670,10 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat decal_gpu.ptr += size_t(decal_tex->srv_slot) * g_r.srv_size; context.d3d12.set_graphics_root_descriptor_table( context.d3d12.command_processor_user_data, 5, decal_gpu); + D3D12_GPU_DESCRIPTOR_HANDLE cube_gpu = heap_start; + cube_gpu.ptr += size_t(cube_tex->srv_slot) * g_r.srv_size; + context.d3d12.set_graphics_root_descriptor_table( + context.d3d12.command_processor_user_data, 8, cube_gpu); list.D3DIASetVertexBuffers(0, 1, &buffers.vb_view); list.D3DIASetIndexBuffer(&buffers.ib_view); for (const DrawEntry& draw : item.draws) { @@ -5344,7 +5703,7 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat if (debug_mode == 3 && index >= 20) { break; } - if (item.transparent && debug_mode == 0) { + if ((item.transparent || item.water) && debug_mode == 0) { if (REXCVAR_GET(skate3_native_render_scene_transparents)) { transparent_items.push_back(&item); } diff --git a/src/skate3_native_scene.h b/src/skate3_native_scene.h index 5f6f2ac..478052a 100644 --- a/src/skate3_native_scene.h +++ b/src/skate3_native_scene.h @@ -75,6 +75,14 @@ struct DrawItem { // pass's alpha-test turned the soft mist gradients into solid hard-edged // white cloud blobs. bool transparent; + // AttribulatorMaterialName starts "water.": canal/ocean surfaces + // (flowingwater.fx family). Drawn in the transparent sub-pass with a + // dedicated shading branch (ripple normal taps + fresnel reflection); + // `water_normal` is the material's `normal` channel texture, bound in the + // macro slot (water never carries a macro overlay). + bool water; + uint32_t water_normal; + uint32_t water_env; // `environment` channel cube map (reflection term) // character.cloth_ropa (Ropa cloth-simulated garments, e.g. player tees): // the VS variant branches on a flag row kept in front of the bone palette // - sim active means the dynamic VB already holds deformed root-local