From f2180c12a3d1123c4af26e5a1a0eaf9c42a82278 Mon Sep 17 00:00:00 2001 From: ook3d <47336113+ook3D@users.noreply.github.com> Date: Wed, 31 Dec 2025 18:32:06 -0500 Subject: [PATCH] Improve ped scenario node preview --- CodeWalker.Core/World/Scenarios.cs | 285 +++++++++++++++++- .../Project/Panels/EditScenarioNodePanel.cs | 8 +- CodeWalker/Rendering/Renderer.cs | 210 ++++++++++++- CodeWalker/Rendering/ShaderManager.cs | 3 +- CodeWalker/Rendering/Shaders/PathShader.cs | 39 ++- CodeWalker/WorldForm.cs | 8 + 6 files changed, 537 insertions(+), 16 deletions(-) diff --git a/CodeWalker.Core/World/Scenarios.cs b/CodeWalker.Core/World/Scenarios.cs index 367f82b75..95c6e79e8 100644 --- a/CodeWalker.Core/World/Scenarios.cs +++ b/CodeWalker.Core/World/Scenarios.cs @@ -33,7 +33,6 @@ public void Init(GameFileCache gameFileCache, Action updateStatus, Timec ScenarioRegions = new List(); - //rubidium: //the non-replacement [XML] is hash 1074D56E //replacement XML is 203D234 I think and replacement PSO A6F20ADA @@ -47,11 +46,12 @@ public void Init(GameFileCache gameFileCache, Action updateStatus, Timec //int maxcells = 0; var rpfman = gameFileCache.RpfMan; + + // Load base game sp_manifest.ymt string manifestfilename = "update\\update.rpf\\x64\\levels\\gta5\\sp_manifest.ymt"; YmtFile manifestymt = rpfman.GetFile(manifestfilename); if ((manifestymt != null) && (manifestymt.CScenarioPointManifest != null)) { - foreach (var region in manifestymt.CScenarioPointManifest.RegionDefs) { string regionfilename = region.Name.ToString() + ".ymt"; //JenkIndex lookup... ymt should have already loaded path strings into it! maybe change this... @@ -77,8 +77,6 @@ public void Init(GameFileCache gameFileCache, Action updateStatus, Timec { ScenarioRegions.Add(regionymt); - - ////testing stuff... //var gd = regionymt?.CScenarioPointRegion?.Data.AccelGrid.Dimensions ?? new Vector2I(); @@ -96,6 +94,158 @@ public void Init(GameFileCache gameFileCache, Action updateStatus, Timec } + // Scan for additional sp_manifest.meta and sp_manifest.ymt files in DLC packs + if (gameFileCache.EnableDlc && gameFileCache.ActiveMapRpfFiles != null) + { + // Build a list of unique RPF files including parent DLC RPFs + var rpfFilesToScan = new HashSet(); + foreach (var rpf in gameFileCache.ActiveMapRpfFiles.Values) + { + if (rpf != null) + { + rpfFilesToScan.Add(rpf); + // Also add parent RPF files (e.g., dlc.rpf when scanning nested RPFs) + var parent = rpf.Parent; + while (parent != null) + { + rpfFilesToScan.Add(parent); + parent = parent.Parent; + } + } + } + + foreach (var rpf in rpfFilesToScan) + { + if (rpf?.AllEntries == null) continue; + + foreach (var entry in rpf.AllEntries) + { + if (entry is RpfFileEntry fentry) + { + var name = fentry.NameLower; + if (name == "sp_manifest.meta" || name == "sp_manifest.ymt") + { + try + { + CScenarioPointRegionDef[] regionDefs = null; + + // Try loading as PSO/YMT first + YmtFile dlcmanifest = rpfman.GetFile(fentry); + + if ((dlcmanifest != null) && (dlcmanifest.CScenarioPointManifest != null)) + { + regionDefs = dlcmanifest.CScenarioPointManifest.RegionDefs; + } + else if (dlcmanifest != null) + { + // Try loading as XML .meta file + try + { + var xml = rpfman.GetFileXml(fentry.Path); + if (xml?.DocumentElement != null && xml.DocumentElement.Name == "CScenarioPointManifest") + { + var regionDefsNode = xml.DocumentElement.SelectSingleNode("RegionDefs"); + if (regionDefsNode != null) + { + var regionItems = regionDefsNode.SelectNodes("Item"); + if (regionItems != null && regionItems.Count > 0) + { + var regionList = new List(); + foreach (XmlNode regionItem in regionItems) + { + var nameNode = regionItem.SelectSingleNode("Name"); + if (nameNode != null && !string.IsNullOrWhiteSpace(nameNode.InnerText)) + { + var regionDef = new CScenarioPointRegionDef(); + // Add the string to JenkIndex so ToString() can retrieve it + string regionName = nameNode.InnerText; + uint hash = JenkHash.GenHash(regionName.ToLowerInvariant()); + JenkIndex.Ensure(regionName.ToLowerInvariant()); + regionDef.Name = new MetaHash(hash); + regionList.Add(regionDef); + } + } + regionDefs = regionList.ToArray(); + } + } + } + } + catch + { + // Skip invalid XML + } + } + + if (regionDefs != null && regionDefs.Length > 0) + { + foreach (var region in regionDefs) + { + string originalName = region.Name.ToString(); + string regionfilename = originalName + ".ymt"; + + // Extract just the filename from the full path + // Handle various prefixes: "platform:", "dlc_name:/%PLATFORM%/", etc. + int colonIndex = regionfilename.IndexOf(':'); + if (colonIndex >= 0) + { + // Remove the DLC pack prefix + regionfilename = regionfilename.Substring(colonIndex + 1); + } + + // Remove /%PLATFORM%/ or /platform/ path components + regionfilename = regionfilename.Replace("/%PLATFORM%/", "/"); + regionfilename = regionfilename.Replace("/platform/", "/"); + + // Extract the final filename + int lastSlashInFilename = Math.Max(regionfilename.LastIndexOf('/'), regionfilename.LastIndexOf('\\')); + if (lastSlashInFilename >= 0) + { + regionfilename = regionfilename.Substring(lastSlashInFilename + 1); + } + + string regionfilenameLower = regionfilename.ToLowerInvariant(); + + // Search for the region file across ALL RPFs (including parent DLC RPFs) + YmtFile regionymt = null; + bool found = false; + + foreach (var searchRpf in rpfFilesToScan) + { + if (searchRpf?.AllEntries == null) continue; + + foreach (var regionEntry in searchRpf.AllEntries) + { + if (regionEntry is RpfFileEntry regionFentry && regionFentry.NameLower == regionfilenameLower) + { + regionymt = rpfman.GetFile(regionFentry); + if (regionymt != null) + { + var sregion = regionymt.ScenarioRegion; + if (sregion != null) + { + ScenarioRegions.Add(regionymt); + found = true; + regionymt = null; // Mark as handled + break; + } + } + } + } + + if (found) break; // Found it, stop searching + } + } + } + } + catch + { + // Skip invalid manifests + } + } + } + } + } + } Inited = true; } @@ -1649,6 +1799,7 @@ public class ScenarioTypes private Dictionary PedModelSets { get; set; } private Dictionary VehicleModelSets { get; set; } private Dictionary AnimGroups { get; set; } + private Dictionary ClipSets { get; set; } // Maps ClipSet name hash to clipDictionaryName @@ -1662,6 +1813,7 @@ public void Load(GameFileCache gfc) PedModelSets = LoadModelSets(gfc, "common:\\data\\ai\\ambientpedmodelsets.meta"); VehicleModelSets = LoadModelSets(gfc, "common:\\data\\ai\\vehiclemodelsets.meta"); AnimGroups = LoadAnimGroups(gfc, "common:\\data\\ai\\conditionalanims.meta"); + ClipSets = LoadClipSets(gfc, "update:\\x64\\data\\anim\\clip_sets\\clip_sets.ymt"); TypeRefs = new Dictionary(); foreach (var kvp in Types) @@ -1849,6 +2001,67 @@ private Dictionary LoadAnimGroups(GameFileCache gfc return groups; } + private Dictionary LoadClipSets(GameFileCache gfc, string filename) + { + Dictionary clipsets = new Dictionary(); + + try + { + string usestr = filename.Replace("update:", "update\\update.rpf").Replace("common:", "common.rpf"); + + var ymt = gfc.RpfMan.GetFile(usestr); + + if ((ymt != null) && (ymt.Pso != null)) + { + // The PSO file contains a fwClipSetManager structure at the root + // We need to extract the clipSets map which maps ClipSet name -> fwClipSet + // Each fwClipSet contains a clipDictionaryName field which is what we need + + // Export to XML and parse it to extract clipDictionaryName mappings + var xml = PsoXml.GetXml(ymt.Pso); + + if (!string.IsNullOrEmpty(xml)) + { + var doc = new XmlDocument(); + doc.LoadXml(xml); + + // Navigate to clipSets map + var clipSetsNodes = doc.SelectNodes("//clipSets/Item"); + + if (clipSetsNodes != null && clipSetsNodes.Count > 0) + { + foreach (XmlNode itemNode in clipSetsNodes) + { + // The key is an attribute on the Item node + var keyAttr = itemNode.Attributes?["key"]; + var dictNameNode = itemNode.SelectSingleNode("clipDictionaryName"); + + if ((keyAttr != null) && (dictNameNode != null)) + { + var clipSetName = keyAttr.Value; + var clipDictName = dictNameNode.InnerText; + + if (!string.IsNullOrEmpty(clipSetName) && !string.IsNullOrEmpty(clipDictName)) + { + JenkIndex.Ensure(clipSetName); + JenkIndex.Ensure(clipSetName.ToLowerInvariant()); + uint hash = JenkHash.GenHash(clipSetName.ToLowerInvariant()); + clipsets[hash] = clipDictName; + } + } + } + } + } + } + } + catch + { + // Silently fail if we can't load the clip sets + } + + return clipsets; + } + @@ -1940,6 +2153,16 @@ public ConditionalAnimsGroup GetAnimGroup(uint hash) return ag; } } + public string GetClipSet(uint hash) + { + lock (SyncRoot) + { + if (ClipSets == null) return null; + string clipDictName; + ClipSets.TryGetValue(hash, out clipDictName); + return clipDictName; + } + } public ScenarioTypeRef[] GetScenarioTypeRefs() { @@ -2013,6 +2236,8 @@ public ConditionalAnimsGroup[] GetAnimGroups() public bool IsVehicle => IsGroup ? false : Type.IsVehicle; // groups don't support vehicle infos, so always false public string VehicleModelSet => IsGroup ? null : Type.VehicleModelSet; public MetaHash VehicleModelSetHash => IsGroup ? 0 : Type.VehicleModelSetHash; + public string ConditionalAnimsGroupName => IsGroup ? null : Type.ConditionalAnimsGroupName; + public MetaHash ConditionalAnimsGroupHash => IsGroup ? 0 : Type.ConditionalAnimsGroupHash; public bool IsGroup { get; } public ScenarioType Type { get; } @@ -2048,6 +2273,8 @@ public override string ToString() public bool IsVehicle { get; set; } public string VehicleModelSet { get; set; } public MetaHash VehicleModelSetHash { get; set; } + public string ConditionalAnimsGroupName { get; set; } + public MetaHash ConditionalAnimsGroupHash { get; set; } public virtual void Load(XmlNode node) @@ -2066,6 +2293,17 @@ public virtual void Load(XmlNode node) VehicleModelSetHash = JenkHash.GenHash(VehicleModelSet.ToLowerInvariant()); } } + + // Load ConditionalAnimsGroup + var animGroupNode = node.SelectSingleNode("ConditionalAnimsGroup/Name"); + if (animGroupNode != null) + { + ConditionalAnimsGroupName = animGroupNode.InnerText; + if (!string.IsNullOrEmpty(ConditionalAnimsGroupName)) + { + ConditionalAnimsGroupHash = JenkHash.GenHash(ConditionalAnimsGroupName.ToLowerInvariant()); + } + } } public override string ToString() @@ -2075,10 +2313,26 @@ public override string ToString() } [TypeConverter(typeof(ExpandableObjectConverter))] public class ScenarioTypePlayAnims : ScenarioType { + public List BaseAnimClipSets { get; set; } = new List(); public override void Load(XmlNode node) { base.Load(node); + + // Parse BaseAnims clipsets directly from the scenario type + // The structure is: ConditionalAnimsGroup/ConditionalAnims/Item/BaseAnims/Item/ClipSet + var conditionalAnimNodes = node.SelectNodes("ConditionalAnimsGroup/ConditionalAnims/Item/BaseAnims/Item/ClipSet"); + if (conditionalAnimNodes != null && conditionalAnimNodes.Count > 0) + { + foreach (XmlNode clipSetNode in conditionalAnimNodes) + { + var clipSetName = clipSetNode.InnerText; + if (!string.IsNullOrEmpty(clipSetName) && !BaseAnimClipSets.Contains(clipSetName)) + { + BaseAnimClipSets.Add(clipSetName); + } + } + } } } @@ -2191,6 +2445,7 @@ public override string ToString() public string OuterXml { get; set; } public string Name { get; set; } public string NameLower { get; set; } + public List BaseAnimClipSets { get; set; } = new List(); public void Load(XmlNode node) @@ -2198,6 +2453,28 @@ public void Load(XmlNode node) OuterXml = node.OuterXml; Name = Xml.GetChildInnerText(node, "Name"); NameLower = Name.ToLowerInvariant(); + + // Parse ConditionalAnims to extract base animations + var conditionalAnimsNodes = node.SelectNodes("ConditionalAnims/Item"); + if (conditionalAnimsNodes != null) + { + foreach (XmlNode animNode in conditionalAnimsNodes) + { + // Get BaseAnims clipsets + var baseAnimNodes = animNode.SelectNodes("BaseAnims/Item/ClipSet"); + if (baseAnimNodes != null) + { + foreach (XmlNode clipSetNode in baseAnimNodes) + { + var clipSetName = clipSetNode.InnerText; + if (!string.IsNullOrEmpty(clipSetName) && !BaseAnimClipSets.Contains(clipSetName)) + { + BaseAnimClipSets.Add(clipSetName); + } + } + } + } + } } public override string ToString() diff --git a/CodeWalker/Project/Panels/EditScenarioNodePanel.cs b/CodeWalker/Project/Panels/EditScenarioNodePanel.cs index abea33ba6..66ef3043e 100644 --- a/CodeWalker/Project/Panels/EditScenarioNodePanel.cs +++ b/CodeWalker/Project/Panels/EditScenarioNodePanel.cs @@ -61,13 +61,7 @@ public void UpdateScenarioNodeUI() UpdateTabVisibility(); - if (CurrentScenarioNode != null) - { - if (ProjectForm.WorldForm != null) - { - ProjectForm.WorldForm.SelectObject(CurrentScenarioNode); - } - } + //Selection is now managed externally - removed SelectObject call to support multi-selection } private void UpdateTabVisibility() diff --git a/CodeWalker/Rendering/Renderer.cs b/CodeWalker/Rendering/Renderer.cs index f3c510174..74fdd22fe 100644 --- a/CodeWalker/Rendering/Renderer.cs +++ b/CodeWalker/Rendering/Renderer.cs @@ -90,7 +90,9 @@ public class Renderer private Dictionary RequiredParents = new Dictionary(); private List RenderEntities = new List(); - public Dictionary HideEntities = new Dictionary();//dictionary of entities to hide, for cutscenes to use + public Dictionary HideEntities = new Dictionary();//dictionary of entities to hide, for cutscenes to use + + private Dictionary ScenarioPeds = new Dictionary();//cache for scenario ped models public bool ShowScriptedYmaps = true; public List VisibleYmaps = new List(); @@ -3451,6 +3453,212 @@ public void RenderCar(Vector3 pos, Quaternion ori, MetaHash modelHash, MetaHash } } + public void RenderScenarioNode(ScenarioNode node) + { + if (node == null) return; + + var vpoint = node.MyPoint ?? node.ClusterMyPoint; + + // Skip vehicle scenarios - they're rendered differently when selected + if ((vpoint != null) && (vpoint?.Type?.IsVehicle ?? false)) + { + return; + } + + // Render as ped model + var pedhash = (uint)0; + var modelSetHash = vpoint?.ModelSet?.NameHash ?? 0; + + if ((modelSetHash != 0) && (modelSetHash != 493038497)) // "none" + { + // Get ped model from the model set + var stypes = Scenarios.ScenarioTypes; + if (stypes != null) + { + var modelset = stypes.GetPedModelSet(modelSetHash); + if ((modelset != null) && (modelset.Models != null) && (modelset.Models.Length > 0)) + { + pedhash = JenkHash.GenHash(modelset.Models[0].NameLower); + } + } + } + + // Default to mp_m_freemode_01 if no model found + if (pedhash == 0) + { + pedhash = JenkHash.GenHash("mp_m_freemode_01"); + } + + RenderScenarioPed(node.Position, node.Orientation, pedhash, vpoint); + } + + public void RenderScenarioPed(Vector3 pos, Quaternion ori, MetaHash pedHash, MCScenarioPoint point = null) + { + if (pedHash == 0) + { + pedHash = JenkHash.GenHash("mp_m_freemode_01"); + } + + // Get or create cached ped + Ped ped = null; + if (!ScenarioPeds.TryGetValue(pedHash, out ped)) + { + ped = new Ped(); + ped.Init(pedHash, gameFileCache); + + // Load default components for the ped + if (ped.Ymt != null) + { + ped.LoadDefaultComponents(gameFileCache); + } + + ScenarioPeds[pedHash] = ped; + } + + if (ped?.Yft != null) + { + // Load animation based on scenario type + ClipMapEntry animClip = null; + + // Try to get animation from ped's default clip dict first (idle animation) + if (ped.Ycd?.ClipMapEntries != null) + { + var idleHash = JenkHash.GenHash("idle"); + animClip = ped.Ycd.ClipMapEntries.FirstOrDefault(c => + c.Clip != null && c.Hash == idleHash); + + if (animClip == null) + { + animClip = ped.Ycd.ClipMapEntries.FirstOrDefault(c => c.Clip != null); + } + } + + // Try to load scenario-specific animation + string scenarioTypeName = null; + string clipDictName = null; + + if (point?.Type != null) + { + var stypes = Scenarios.ScenarioTypes; + List clipSetNames = null; + + // Get the scenario type name for sitting detection + scenarioTypeName = JenkIndex.TryGetString(point.Type.NameHash); + + // Check if this is a ScenarioTypePlayAnims with direct BaseAnimClipSets + if (!point.Type.IsGroup && point.Type.Type is ScenarioTypePlayAnims playAnimsType && playAnimsType.BaseAnimClipSets != null && playAnimsType.BaseAnimClipSets.Count > 0) + { + clipSetNames = playAnimsType.BaseAnimClipSets; + } + // Otherwise try ConditionalAnimsGroup + else + { + var animGroupHash = point.Type.ConditionalAnimsGroupHash; + if (animGroupHash != 0 && stypes != null) + { + var animGroup = stypes.GetAnimGroup(animGroupHash); + if (animGroup?.BaseAnimClipSets != null && animGroup.BaseAnimClipSets.Count > 0) + { + clipSetNames = animGroup.BaseAnimClipSets; + } + } + } + + if (clipSetNames != null && clipSetNames.Count > 0 && stypes != null) + { + // Use the first base animation clipset + var clipSetName = clipSetNames[0]; + var clipSetHash = JenkHash.GenHash(clipSetName.ToLowerInvariant()); + + // Look up the actual clipDictionaryName from clip_sets.ymt + clipDictName = stypes.GetClipSet(clipSetHash); + + if (!string.IsNullOrEmpty(clipDictName)) + { + var ycdHash = JenkHash.GenHash(clipDictName.ToLowerInvariant()); + var ycd = gameFileCache.GetYcd(ycdHash); + + if ((ycd != null) && (ycd.Loaded) && (ycd.ClipMapEntries != null)) + { + // Try to find a "base" clip or use the first available clip + var baseHash = JenkHash.GenHash("base"); + var scenarioClip = ycd.ClipMapEntries.FirstOrDefault(c => + c.Clip != null && c.Hash == baseHash); + + if (scenarioClip == null) + { + scenarioClip = ycd.ClipMapEntries.FirstOrDefault(c => c.Clip != null); + } + + if (scenarioClip != null) + { + animClip = scenarioClip; + } + } + } + } + } + + // Align ped to ground + float minz = ped.Yft.Fragment?.PhysicsLODGroup?.PhysicsLOD1?.Bound?.BoxMin.Z ?? 0.0f; + pos.Z -= minz; + + // Offset ped up by 1 meter, unless they're sitting or an animal + bool skipOffset = false; + + // Check model set for animal + if (point?.ModelSet != null) + { + var modelSetName = JenkIndex.TryGetString(point.ModelSet.NameHash); + if (!string.IsNullOrEmpty(modelSetName)) + { + var modelSetLower = modelSetName.ToLowerInvariant(); + if (modelSetLower.Contains("animal") || modelSetLower.Contains("bird")) + { + skipOffset = true; + } + } + } + + // Check scenario type name for sitting + if (!skipOffset && !string.IsNullOrEmpty(scenarioTypeName)) + { + var scenarioLower = scenarioTypeName.ToLowerInvariant(); + skipOffset = scenarioLower.Contains("sit") || scenarioLower.Contains("seat"); + } + + // Check clip dictionary name for sitting + if (!skipOffset && !string.IsNullOrEmpty(clipDictName)) + { + var clipDictLower = clipDictName.ToLowerInvariant(); + skipOffset = clipDictLower.Contains("sit") || clipDictLower.Contains("seat"); + } + + // Check animation clip name for sitting + if (!skipOffset && animClip != null) + { + var animName = JenkIndex.TryGetString(animClip.Hash)?.ToLowerInvariant() ?? ""; + skipOffset = animName.Contains("sit") || animName.Contains("seat"); + } + + if (!skipOffset) + { + pos.Z += 1.0f; + } + + ped.Position = pos; + ped.Rotation = ori; + ped.RenderEntity.SetPosition(pos); + ped.RenderEntity.SetOrientation(ori); + + // Update animation clip + ped.AnimClip = animClip; + + // Render the ped with all its components and animation + RenderPed(ped); + } + } + public void RenderVehicle(Vehicle vehicle, ClipMapEntry animClip = null) { diff --git a/CodeWalker/Rendering/ShaderManager.cs b/CodeWalker/Rendering/ShaderManager.cs index 98a1ef7db..18f132b8b 100644 --- a/CodeWalker/Rendering/ShaderManager.cs +++ b/CodeWalker/Rendering/ShaderManager.cs @@ -89,6 +89,7 @@ public class ShaderManager private Camera Camera; public ShaderGlobalLights GlobalLights = new ShaderGlobalLights(); public bool PathsDepthClip = true;//false;// + public Vector3? SelectedScenarioNodePosition = null; // Position of selected scenario node to exclude from cube rendering private GameFileCache GameFileCache; private RenderableCache RenderableCache; @@ -654,7 +655,7 @@ public void RenderQueued(DeviceContext context, Camera camera, Vector4 wind) context.OutputMerger.DepthStencilState = PathsDepthClip ? dsDisableWrite : dsDisableAll;// dsEnabled; // context.Rasterizer.State = rsSolid; - Paths.RenderBatches(context, RenderPathBatches, camera, GlobalLights); + Paths.RenderBatches(context, RenderPathBatches, camera, GlobalLights, SelectedScenarioNodePosition); } diff --git a/CodeWalker/Rendering/Shaders/PathShader.cs b/CodeWalker/Rendering/Shaders/PathShader.cs index 94f1020ab..6418f8aed 100644 --- a/CodeWalker/Rendering/Shaders/PathShader.cs +++ b/CodeWalker/Rendering/Shaders/PathShader.cs @@ -120,7 +120,7 @@ public override void SetGeomVars(DeviceContext context, RenderableGeometry geom) } - public void RenderBatches(DeviceContext context, List batches, Camera camera, ShaderGlobalLights lights) + public void RenderBatches(DeviceContext context, List batches, Camera camera, ShaderGlobalLights lights, Vector3? excludeNodePosition = null) { UseDynamicVerts = false; SetShader(context); @@ -162,10 +162,43 @@ public void RenderBatches(DeviceContext context, List batch foreach (var batch in batches) { if (batch.NodeBuffer == null) continue; + if (batch.Nodes == null) continue; - context.VertexShader.SetShaderResource(0, batch.NodeBuffer.SRV); + int nodeCount = batch.Nodes.Length; - cube.DrawInstanced(context, batch.Nodes.Length); + // If we need to exclude a specific node, filter it out + if (excludeNodePosition.HasValue && nodeCount > 0) + { + var excludePos = excludeNodePosition.Value; + var filteredNodes = new List(); + + foreach (var node in batch.Nodes) + { + // Check if this node matches the position to exclude (with small epsilon for float comparison) + var nodePos = new Vector3(node.X, node.Y, node.Z); + float distSq = (nodePos - excludePos).LengthSquared(); + if (distSq > 0.01f) // If not the selected node, include it + { + filteredNodes.Add(node); + } + } + + // Only render if we have nodes left after filtering + if (filteredNodes.Count > 0) + { + // Create temporary buffer with filtered nodes + var tempBuffer = new GpuSBuffer(context.Device, filteredNodes.ToArray()); + context.VertexShader.SetShaderResource(0, tempBuffer.SRV); + cube.DrawInstanced(context, filteredNodes.Count); + tempBuffer.Dispose(); + } + } + else + { + // No filtering needed, render all nodes + context.VertexShader.SetShaderResource(0, batch.NodeBuffer.SRV); + cube.DrawInstanced(context, nodeCount); + } } UnbindResources(context); diff --git a/CodeWalker/WorldForm.cs b/CodeWalker/WorldForm.cs index 8f641ab5b..35a26567f 100644 --- a/CodeWalker/WorldForm.cs +++ b/CodeWalker/WorldForm.cs @@ -434,6 +434,9 @@ public void RenderScene(DeviceContext context) Renderer.SelectedDrawable = SelectedItem.Drawable; + // Set the selected scenario node position to exclude its cube from rendering + Renderer.shaders.SelectedScenarioNodePosition = SelectedItem.ScenarioNode?.Position; + if (renderworld) { RenderWorld(); @@ -1456,6 +1459,11 @@ private void RenderSelection(ref MapSelection selectionItem) Renderer.RenderCar(sn.Position, sn.Orientation, 0, vhash, true); } + else + { + // Render ped model for non-vehicle scenarios + Renderer.RenderScenarioNode(sn); + } } if (selectionItem.ScenarioEdge != null)