From 047fd338bc5366d9409e26f67643ddf6c7926a5a Mon Sep 17 00:00:00 2001 From: ook3d <47336113+ook3D@users.noreply.github.com> Date: Mon, 9 Feb 2026 23:09:49 -0500 Subject: [PATCH] fix lodlight generator --- .../GameFiles/FileTypes/YmapFile.cs | 12 +- .../Panels/GenerateLODLightsPanel.Designer.cs | 72 +- .../Project/Panels/GenerateLODLightsPanel.cs | 762 ++++++++++++++---- 3 files changed, 657 insertions(+), 189 deletions(-) diff --git a/CodeWalker.Core/GameFiles/FileTypes/YmapFile.cs b/CodeWalker.Core/GameFiles/FileTypes/YmapFile.cs index 9dd0f4ddc..1d1276967 100644 --- a/CodeWalker.Core/GameFiles/FileTypes/YmapFile.cs +++ b/CodeWalker.Core/GameFiles/FileTypes/YmapFile.cs @@ -2243,12 +2243,12 @@ public void EnsureLights(DrawableBase db) } var bb = new BoundingBox(abmin, abmax).Transform(Position, Orientation, Scale); var ints = new uint[7]; - ints[0] = (uint)(bb.Minimum.X * 10.0f); - ints[1] = (uint)(bb.Minimum.Y * 10.0f); - ints[2] = (uint)(bb.Minimum.Z * 10.0f); - ints[3] = (uint)(bb.Maximum.X * 10.0f); - ints[4] = (uint)(bb.Maximum.Y * 10.0f); - ints[5] = (uint)(bb.Maximum.Z * 10.0f); + ints[0] = (uint)(int)(bb.Minimum.X * 10.0f); + ints[1] = (uint)(int)(bb.Minimum.Y * 10.0f); + ints[2] = (uint)(int)(bb.Minimum.Z * 10.0f); + ints[3] = (uint)(int)(bb.Maximum.X * 10.0f); + ints[4] = (uint)(int)(bb.Maximum.Y * 10.0f); + ints[5] = (uint)(int)(bb.Maximum.Z * 10.0f); var bones = skel?.BonesMap; var exts = (Archetype.Extensions?.Length ?? 0);// + (Extensions?.Length ?? 0);//seems entity extensions aren't included in this diff --git a/CodeWalker/Project/Panels/GenerateLODLightsPanel.Designer.cs b/CodeWalker/Project/Panels/GenerateLODLightsPanel.Designer.cs index 0b96b569b..991c5f942 100644 --- a/CodeWalker/Project/Panels/GenerateLODLightsPanel.Designer.cs +++ b/CodeWalker/Project/Panels/GenerateLODLightsPanel.Designer.cs @@ -34,59 +34,94 @@ private void InitializeComponent() this.label2 = new System.Windows.Forms.Label(); this.NameTextBox = new System.Windows.Forms.TextBox(); this.label81 = new System.Windows.Forms.Label(); + this.OutputPathTextBox = new System.Windows.Forms.TextBox(); + this.OutputPathLabel = new System.Windows.Forms.Label(); + this.BrowseButton = new System.Windows.Forms.Button(); this.SuspendLayout(); - // + // // StatusLabel - // + // this.StatusLabel.AutoSize = true; - this.StatusLabel.Location = new System.Drawing.Point(73, 218); + this.StatusLabel.Location = new System.Drawing.Point(73, 248); this.StatusLabel.Name = "StatusLabel"; this.StatusLabel.Size = new System.Drawing.Size(95, 13); this.StatusLabel.TabIndex = 56; this.StatusLabel.Text = "Ready to generate"; - // + // // GenerateButton - // - this.GenerateButton.Location = new System.Drawing.Point(76, 167); + // + this.GenerateButton.Location = new System.Drawing.Point(76, 197); this.GenerateButton.Name = "GenerateButton"; this.GenerateButton.Size = new System.Drawing.Size(75, 23); this.GenerateButton.TabIndex = 55; this.GenerateButton.Text = "Generate"; this.GenerateButton.UseVisualStyleBackColor = true; this.GenerateButton.Click += new System.EventHandler(this.GenerateButton_Click); - // + // // label2 - // - this.label2.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + // + this.label2.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); this.label2.Location = new System.Drawing.Point(12, 9); this.label2.Name = "label2"; this.label2.Size = new System.Drawing.Size(459, 108); this.label2.TabIndex = 54; this.label2.Text = resources.GetString("label2.Text"); - // + // // NameTextBox - // + // this.NameTextBox.Location = new System.Drawing.Point(76, 130); this.NameTextBox.Name = "NameTextBox"; this.NameTextBox.Size = new System.Drawing.Size(177, 20); this.NameTextBox.TabIndex = 57; this.NameTextBox.Text = "myproject"; - // + // // label81 - // + // this.label81.AutoSize = true; this.label81.Location = new System.Drawing.Point(11, 133); this.label81.Name = "label81"; this.label81.Size = new System.Drawing.Size(38, 13); this.label81.TabIndex = 58; this.label81.Text = "Name:"; - // + // + // OutputPathLabel + // + this.OutputPathLabel.AutoSize = true; + this.OutputPathLabel.Location = new System.Drawing.Point(11, 163); + this.OutputPathLabel.Name = "OutputPathLabel"; + this.OutputPathLabel.Size = new System.Drawing.Size(58, 13); + this.OutputPathLabel.TabIndex = 59; + this.OutputPathLabel.Text = "Output Dir:"; + // + // OutputPathTextBox + // + this.OutputPathTextBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.OutputPathTextBox.Location = new System.Drawing.Point(76, 160); + this.OutputPathTextBox.Name = "OutputPathTextBox"; + this.OutputPathTextBox.Size = new System.Drawing.Size(354, 20); + this.OutputPathTextBox.TabIndex = 60; + // + // BrowseButton + // + this.BrowseButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.BrowseButton.Location = new System.Drawing.Point(436, 158); + this.BrowseButton.Name = "BrowseButton"; + this.BrowseButton.Size = new System.Drawing.Size(35, 23); + this.BrowseButton.TabIndex = 61; + this.BrowseButton.Text = "..."; + this.BrowseButton.UseVisualStyleBackColor = true; + this.BrowseButton.Click += new System.EventHandler(this.BrowseButton_Click); + // // GenerateLODLightsPanel - // + // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(497, 255); + this.ClientSize = new System.Drawing.Size(497, 285); + this.Controls.Add(this.BrowseButton); + this.Controls.Add(this.OutputPathTextBox); + this.Controls.Add(this.OutputPathLabel); this.Controls.Add(this.NameTextBox); this.Controls.Add(this.label81); this.Controls.Add(this.StatusLabel); @@ -107,5 +142,8 @@ private void InitializeComponent() private System.Windows.Forms.Label label2; private System.Windows.Forms.TextBox NameTextBox; private System.Windows.Forms.Label label81; + private System.Windows.Forms.TextBox OutputPathTextBox; + private System.Windows.Forms.Label OutputPathLabel; + private System.Windows.Forms.Button BrowseButton; } -} \ No newline at end of file +} diff --git a/CodeWalker/Project/Panels/GenerateLODLightsPanel.cs b/CodeWalker/Project/Panels/GenerateLODLightsPanel.cs index 2bc907adb..a92565479 100644 --- a/CodeWalker/Project/Panels/GenerateLODLightsPanel.cs +++ b/CodeWalker/Project/Panels/GenerateLODLightsPanel.cs @@ -1,4 +1,4 @@ -using CodeWalker.GameFiles; +using CodeWalker.GameFiles; using SharpDX; using System; using System.Collections.Generic; @@ -9,6 +9,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using System.IO; using System.Windows.Forms; namespace CodeWalker.Project.Panels @@ -18,6 +19,24 @@ public partial class GenerateLODLightsPanel : ProjectPanel public ProjectForm ProjectForm { get; set; } public ProjectFile CurrentProjectFile { get; set; } + const float MAX_LODLIGHT_INTENSITY = 48.0f; + const float MAX_LODLIGHT_CONE_ANGLE = 180.0f; + const float MAX_LODLIGHT_CAPSULE_EXTENT = 140.0f; + const float MAX_LODLIGHT_CORONA_INTENSITY = 32.0f; + const uint LIGHTFLAG_DONT_USE_IN_CUTSCENE = (1u << 2); + const uint LIGHTFLAG_CORONA_ONLY = (1u << 15); + const uint LIGHTFLAG_FAR_LOD_LIGHT = (1u << 22); + const uint LIGHTFLAG_FORCE_MEDIUM_LOD_LIGHT = (1u << 28); + const uint LIGHTFLAG_CORONA_ONLY_LOD_LIGHT = (1u << 29); + const int LIGHT_CATEGORY_SMALL = 0; + const int LIGHT_CATEGORY_MEDIUM = 1; + const int LIGHT_CATEGORY_LARGE = 2; + + const int MAX_LIGHTS_PER_CELL = 800; + const int MIN_LIGHT_COUNT_TO_CONSOLIDATE = 100; + + static readonly string[] CategoryLabels = { "small", "medium", "large" }; + public GenerateLODLightsPanel(ProjectForm projectForm) { @@ -26,7 +45,6 @@ public GenerateLODLightsPanel(ProjectForm projectForm) if (ProjectForm?.WorldForm == null) { - //could happen in some other startup mode - world form is required for this.. GenerateButton.Enabled = false; UpdateStatus("Unable to generate - World View not available!"); } @@ -36,7 +54,22 @@ public GenerateLODLightsPanel(ProjectForm projectForm) public void SetProject(ProjectFile project) { CurrentProjectFile = project; + } + private void BrowseButton_Click(object sender, EventArgs e) + { + using (var dialog = new FolderBrowserDialog()) + { + dialog.Description = "Select output directory for LOD light ymaps"; + if (!string.IsNullOrEmpty(OutputPathTextBox.Text) && Directory.Exists(OutputPathTextBox.Text)) + { + dialog.SelectedPath = OutputPathTextBox.Text; + } + if (dialog.ShowDialog() == DialogResult.OK) + { + OutputPathTextBox.Text = dialog.SelectedPath; + } + } } private void GenerateComplete() @@ -72,28 +105,62 @@ private void UpdateStatus(string text) catch { } } + private static byte PackU8(float val, float range) + { + return (byte)Math.Max(Math.Min(Math.Round(val * (255.0f / range)), 255), 0); + } + + private static int GetLightCategory(LightAttributes la, float capsuleExtent) + { + uint flags = la.Flags; + + if ((flags & LIGHTFLAG_FAR_LOD_LIGHT) != 0) + { + return LIGHT_CATEGORY_LARGE; + } + + float length = la.Falloff; + if (la.Type == LightType.Capsule) + { + length = 2.0f * la.Falloff + capsuleExtent; + } + + if ((flags & LIGHTFLAG_FORCE_MEDIUM_LOD_LIGHT) != 0 || (length >= 10.0f && la.Intensity >= 1.0f)) + { + return LIGHT_CATEGORY_MEDIUM; + } + + return LIGHT_CATEGORY_SMALL; + } private void GenerateButton_Click(object sender, EventArgs e) { - //var space = ProjectForm?.WorldForm?.Space; - //if (space == null) return; var gameFileCache = ProjectForm?.WorldForm?.GameFileCache; if (gameFileCache == null) return; - var path = ProjectForm.CurrentProjectFile.GetFullFilePath("lodlights") + "\\"; + var outputDir = OutputPathTextBox.Text; + if (string.IsNullOrEmpty(outputDir) || !Directory.Exists(outputDir)) + { + MessageBox.Show("Please select a valid output directory."); + return; + } GenerateButton.Enabled = false; - List projectYmaps = ProjectForm.CurrentProjectFile.YmapFiles; var pname = NameTextBox.Text; Task.Run(async () => { + var lightsSmall = new List(); + var lightsMedium = new List(); + var lightsLarge = new List(); - var lights = new List(); + // Collect all entities and deduplicate archetypes for batch loading + var allEntities = new List<(YmapEntityDef ent, string entName)>(); + var uniqueArchetypes = new HashSet(); foreach (var ymap in projectYmaps) { @@ -101,200 +168,565 @@ private void GenerateButton_Click(object sender, EventArgs e) foreach (var ent in ymap.AllEntities) { if (ent.Archetype == null) continue; + var entName = ent.Archetype.Name?.ToString() ?? ""; + if (entName.Contains("prop_dock_bouy")) continue; + allEntities.Add((ent, entName)); + uniqueArchetypes.Add(ent.Archetype.Hash); + } + } + + // Pre-request all unique drawables so they start loading in parallel + UpdateStatus($"Requesting {uniqueArchetypes.Count} unique drawables..."); + var drawableCache = new Dictionary(); + var pendingArchetypes = new HashSet(); + + foreach (var (ent, _) in allEntities) + { + var hash = ent.Archetype.Hash; + if (drawableCache.ContainsKey(hash) || pendingArchetypes.Contains(hash)) continue; - bool waiting = false; - DrawableBase dwbl = null; + var (dwbl, waiting) = await gameFileCache.TryGetDrawableAsync(ent.Archetype); + if (dwbl != null) + { + drawableCache[hash] = dwbl; + } + else if (waiting) + { + pendingArchetypes.Add(hash); + } + } - (dwbl, waiting) = await gameFileCache.TryGetDrawableAsync(ent.Archetype); - while (waiting) + // Wait for all pending drawables to finish loading (10s timeout per drawable) + if (pendingArchetypes.Count > 0) + { + UpdateStatus($"Waiting for {pendingArchetypes.Count} drawables to load..."); + var archetypeLookup = new Dictionary(); + foreach (var (ent, _) in allEntities) + { + var hash = ent.Archetype.Hash; + if (pendingArchetypes.Contains(hash) && !archetypeLookup.ContainsKey(hash)) { - (dwbl, waiting) = await gameFileCache.TryGetDrawableAsync(ent.Archetype); + archetypeLookup[hash] = ent.Archetype; + } + } + + var pendingTimers = new Dictionary(); + foreach (var hash in pendingArchetypes) + { + var sw = new System.Diagnostics.Stopwatch(); + sw.Start(); + pendingTimers[hash] = sw; + } - UpdateStatus("Waiting for " + ent.Archetype.AssetName + " to load..."); + while (pendingArchetypes.Count > 0) + { + await Task.Delay(3); + var resolved = new List(); + foreach (var hash in pendingArchetypes) + { + var (dwbl, waiting) = await gameFileCache.TryGetDrawableAsync(archetypeLookup[hash]); + if (dwbl != null) + { + drawableCache[hash] = dwbl; + resolved.Add(hash); + } + else if (!waiting) + { + resolved.Add(hash); + } + else if (pendingTimers[hash].Elapsed.TotalMinutes >= 1.0) + { + UpdateStatus($"Timed out loading {hash} after 1 minute, skipping..."); + resolved.Add(hash); + } } - UpdateStatus("Adding lights from " + ent.Archetype.Name + "..."); - if (dwbl != null) + foreach (var hash in resolved) { - var fphys = (dwbl as FragDrawable)?.OwnerFragmentPhys; + pendingArchetypes.Remove(hash); + pendingTimers.Remove(hash); + } + if (pendingArchetypes.Count > 0) + { + UpdateStatus($"Waiting for {pendingArchetypes.Count} drawables to load..."); + } + } + } - ent.EnsureLights(dwbl); - var elights = ent.Lights; - if (elights != null) - { + // Process all entities using cached drawables + UpdateStatus($"Processing {allEntities.Count} entities..."); + foreach (var (ent, entName) in allEntities) + { + if (!drawableCache.TryGetValue(ent.Archetype.Hash, out var dwbl)) continue; - for (int li = 0; li> 10) & 1u) == 1);// (name != null) && (name.Contains("street") || name.Contains("traffic")); - isStreetLight = false; //TODO: fix this! - - - //@Calcium: - //1 = point - //2 = spot - //4 = capsule - uint type = (uint)la.Type; - uint unk = isStreetLight ? 1u : 0;//2 bits - isStreetLight low bit, unk high bit - uint t = la.TimeFlags | (type << 26) | (unk << 24); - - var inner = (byte)Math.Round(la.ConeInnerAngle * 1.4117647f); - var outer = (byte)Math.Round(la.ConeOuterAngle * 1.4117647f); - if (type == 4) - { - outer = (byte)Math.Round(la.Extent.X * 1.82f); - } - - - var light = new Light(); - light.position = new MetaVECTOR3(elight.Position); - light.colour = c; - light.direction = new MetaVECTOR3(elight.Direction); - light.falloff = la.Falloff; - light.falloffExponent = la.FalloffExponent; - light.timeAndStateFlags = t; - light.hash = h; - light.coneInnerAngle = inner; - light.coneOuterAngleOrCapExt = outer; - if (la.CoronaSize != 0) - { - light.coronaIntensity = (byte)(la.CoronaIntensity * 6); - } - light.isStreetLight = isStreetLight; - lights.Add(light); + ent.EnsureLights(dwbl); + var elights = ent.Lights; + if (elights == null) continue; - } + var archBB = new BoundingBox(ent.Archetype.BBMin, ent.Archetype.BBMax).Transform(ent.Position, ent.Orientation, ent.Scale); + var hashInts = new uint[7]; + hashInts[0] = (uint)(int)(archBB.Minimum.X * 10.0f); + hashInts[1] = (uint)(int)(archBB.Minimum.Y * 10.0f); + hashInts[2] = (uint)(int)(archBB.Minimum.Z * 10.0f); + hashInts[3] = (uint)(int)(archBB.Maximum.X * 10.0f); + hashInts[4] = (uint)(int)(archBB.Maximum.Y * 10.0f); + hashInts[5] = (uint)(int)(archBB.Maximum.Z * 10.0f); + int exts = ent.Archetype.Extensions?.Length ?? 0; + + bool isStreetLight = entName.Contains("streetlight") || entName.Contains("street_light") || entName.Contains("nylamp") || entName.Contains("nytraf"); + + for (int li = 0; li < elights.Length; li++) + { + var elight = elights[li]; + var la = elight.Attributes; + + if (la.LightFadeDistance > 0) continue; + + uint type = (uint)la.Type; + float capsuleExtent = la.Extent.X; + + if (type == (uint)LightType.Capsule) + { + float minCapsuleExtent = MAX_LODLIGHT_CAPSULE_EXTENT / 255.0f; + if (capsuleExtent < minCapsuleExtent) + { + type = (uint)LightType.Point; } } + + uint r = la.ColorR; + uint g = la.ColorG; + uint b = la.ColorB; + uint packedIntensity = PackU8(la.Intensity, MAX_LODLIGHT_INTENSITY); + uint colour = (packedIntensity << 24) + (r << 16) + (g << 8) + b; + + byte inner = PackU8(la.ConeInnerAngle, MAX_LODLIGHT_CONE_ANGLE); + byte outer; + if (type == (uint)LightType.Capsule) + { + outer = PackU8(capsuleExtent, MAX_LODLIGHT_CAPSULE_EXTENT); + } + else + { + outer = PackU8(la.ConeOuterAngle, MAX_LODLIGHT_CONE_ANGLE); + } + + byte packedCorona = 0; + if (la.CoronaSize >= 0.05f) + { + packedCorona = PackU8(la.CoronaIntensity, MAX_LODLIGHT_CORONA_INTENSITY); + } + + uint timeAndState = la.TimeFlags & 0x00FFFFFFu; + if (isStreetLight) + { + timeAndState |= (1u << 24); + } + if ((la.Flags & (LIGHTFLAG_CORONA_ONLY | LIGHTFLAG_CORONA_ONLY_LOD_LIGHT)) != 0) + { + timeAndState |= (1u << 25); + } + timeAndState |= (type << 26); + if ((la.Flags & LIGHTFLAG_DONT_USE_IN_CUTSCENE) != 0) + { + timeAndState |= (1u << 31); + } + + var light = new Light(); + light.position = new MetaVECTOR3(elight.Position); + light.colour = colour; + light.direction = new MetaVECTOR3(elight.Direction); + light.falloff = la.Falloff; + light.falloffExponent = la.FalloffExponent; + light.timeAndStateFlags = timeAndState; + hashInts[6] = (uint)(exts + li); + light.hash = YmapEntityDef.ComputeLightHash(hashInts); + light.coneInnerAngle = inner; + light.coneOuterAngleOrCapExt = outer; + light.coronaIntensity = packedCorona; + light.isStreetLight = isStreetLight; + + int category = GetLightCategory(la, capsuleExtent); + switch (category) + { + case LIGHT_CATEGORY_LARGE: + lightsLarge.Add(light); + break; + case LIGHT_CATEGORY_MEDIUM: + lightsMedium.Add(light); + break; + default: + lightsSmall.Add(light); + break; + } } } + int totalLights = lightsSmall.Count + lightsMedium.Count + lightsLarge.Count; - if (lights.Count == 0) + if (totalLights == 0) { MessageBox.Show("No lights found in project!"); + GenerateComplete(); return; } + UpdateStatus($"Collected {totalLights} lights (S:{lightsSmall.Count} M:{lightsMedium.Count} L:{lightsLarge.Count}). Chopping into grid cells..."); + var categoryLights = new[] { lightsSmall, lightsMedium, lightsLarge }; + var allYmaps = new List(); - //final lights should be sorted by isStreetLight (1 first!) and then hash - lights.Sort((a, b) => + for (int cat = 0; cat < 3; cat++) { - if (a.isStreetLight != b.isStreetLight) return b.isStreetLight.CompareTo(a.isStreetLight); - return a.hash.CompareTo(b.hash); - }); + var lights = categoryLights[cat]; + if (lights.Count == 0) continue; + + var cells = ChopLightsIntoGrid(lights); + + UpdateStatus($"Building {cells.Count} ymap pairs for {CategoryLabels[cat]} category..."); + for (int ci = 0; ci < cells.Count; ci++) + { + var cell = cells[ci]; + cell.Lights.Sort((a, b) => + { + if (a.isStreetLight != b.isStreetLight) return b.isStreetLight.CompareTo(a.isStreetLight); + return a.hash.CompareTo(b.hash); + }); - var position = new List(); - var colour = new List(); - var direction = new List(); - var falloff = new List(); - var falloffExponent = new List(); - var timeAndStateFlags = new List(); - var hash = new List(); - var coneInnerAngle = new List(); - var coneOuterAngleOrCapExt = new List(); - var coronaIntensity = new List(); - ushort numStreetLights = 0; - foreach (var light in lights) + var (lodymap, distymap) = BuildYmapPair(cell.Lights, pname, cat, ci); + allYmaps.Add(lodymap); + allYmaps.Add(distymap); + } + } + + UpdateStatus($"Saving {allYmaps.Count} ymaps to {outputDir}..."); + + foreach (var ymap in allYmaps) { - position.Add(light.position); - colour.Add(light.colour); - direction.Add(light.direction); - falloff.Add(light.falloff); - falloffExponent.Add(light.falloffExponent); - timeAndStateFlags.Add(light.timeAndStateFlags); - hash.Add(light.hash); - coneInnerAngle.Add(light.coneInnerAngle); - coneOuterAngleOrCapExt.Add(light.coneOuterAngleOrCapExt); - coronaIntensity.Add(light.coronaIntensity); - if (light.isStreetLight) numStreetLights++; + var data = ymap.Save(); + if (data != null) + { + var filePath = Path.Combine(outputDir, ymap.RpfFileEntry.Name); + File.WriteAllBytes(filePath, data); + } } + int totalYmaps = allYmaps.Count / 2; + UpdateStatus($"Process complete. {totalLights} lights (S:{lightsSmall.Count} M:{lightsMedium.Count} L:{lightsLarge.Count}) in {totalYmaps} ymap pairs saved to {outputDir}"); + GenerateComplete(); + }); + } + + + #region Grid Chopping + + private class GridCell + { + public float StartX; + public float StartY; + public float Width; + public float Height; + public List Lights = new List(); + } + + private static List ChopLightsIntoGrid(List lights) + { + if (lights.Count == 0) return new List(); + + var cell = new GridCell(); + cell.Lights.AddRange(lights); + UpdateCellExtentsFromLights(cell); + + var cells = new List { cell }; + + SubdivideOverpopulatedCells(cells); + ConsolidateSparseCells(cells); + MakeCellsSquareIsh(cells); + + cells.RemoveAll(c => c.Lights.Count == 0); + + return cells; + } + + private static void UpdateCellExtentsFromLights(GridCell cell) + { + if (cell.Lights.Count == 0) return; + float minX = float.MaxValue, minY = float.MaxValue; + float maxX = -float.MaxValue, maxY = -float.MaxValue; + foreach (var light in cell.Lights) + { + float x = light.position.x; + float y = light.position.y; + if (x < minX) minX = x; + if (y < minY) minY = y; + if (x > maxX) maxX = x; + if (y > maxY) maxY = y; + } + cell.StartX = minX; + cell.StartY = minY; + cell.Width = maxX - minX; + cell.Height = maxY - minY; + } + + private static void SubdivideOverpopulatedCells(List cells) + { + // Iterate including newly added cells + for (int i = 0; i < cells.Count; i++) + { + var cell = cells[i]; + if (cell.Lights.Count > MAX_LIGHTS_PER_CELL && cell.Width > 1.0f && cell.Height > 1.0f) + { + var newCells = DivideCellVH(cell); + cells.AddRange(newCells); + } + } + cells.RemoveAll(c => c.Lights.Count == 0); + } + private static List DivideCellVH(GridCell cell) + { + float halfW = cell.Width / 2.0f; + float halfH = cell.Height / 2.0f; + + var tl = new GridCell { StartX = cell.StartX, StartY = cell.StartY, Width = halfW, Height = halfH }; + var tr = new GridCell { StartX = cell.StartX + halfW, StartY = cell.StartY, Width = halfW, Height = halfH }; + var bl = new GridCell { StartX = cell.StartX, StartY = cell.StartY + halfH, Width = halfW, Height = halfH }; + var br = new GridCell { StartX = cell.StartX + halfW, StartY = cell.StartY + halfH, Width = halfW, Height = halfH }; + + var newCells = new List { tl, tr, bl, br }; + MoveLightsToClosestCell(cell, newCells); + foreach (var nc in newCells) UpdateCellExtentsFromLights(nc); + return newCells; + } - UpdateStatus("Creating new ymap files..."); - - var lodymap = new YmapFile(); - var distymap = new YmapFile(); - var ll = new YmapLODLights(); - var dl = new YmapDistantLODLights(); - var cdl = new CDistantLODLight(); - distymap.DistantLODLights = dl; - lodymap.LODLights = ll; - lodymap.Parent = distymap; - cdl.category = 1;//0=small, 1=med, 2=large - cdl.numStreetLights = numStreetLights; - dl.CDistantLODLight = cdl; - dl.positions = position.ToArray(); - dl.colours = colour.ToArray(); - dl.Ymap = distymap; - dl.CalcBB(); - ll.direction = direction.ToArray(); - ll.falloff = falloff.ToArray(); - ll.falloffExponent = falloffExponent.ToArray(); - ll.timeAndStateFlags = timeAndStateFlags.ToArray(); - ll.hash = hash.ToArray(); - ll.coneInnerAngle = coneInnerAngle.ToArray(); - ll.coneOuterAngleOrCapExt = coneOuterAngleOrCapExt.ToArray(); - ll.coronaIntensity = coronaIntensity.ToArray(); - ll.Ymap = lodymap; - ll.BuildLodLights(dl); - ll.CalcBB(); - ll.BuildBVH(); - - lodymap.CalcFlags(); - lodymap.CalcExtents(); - distymap.CalcFlags(); - distymap.CalcExtents(); - - - var lodname = pname + "_lodlights"; - var distname = pname + "_distantlights"; - lodymap.Name = lodname; - lodymap._CMapData.name = JenkHash.GenHash(lodname); - lodymap.RpfFileEntry = new RpfResourceFileEntry(); - lodymap.RpfFileEntry.Name = lodname + ".ymap"; - lodymap.RpfFileEntry.NameLower = lodname + ".ymap"; - distymap.Name = distname; - distymap._CMapData.name = JenkHash.GenHash(distname); - distymap.RpfFileEntry = new RpfResourceFileEntry(); - distymap.RpfFileEntry.Name = distname + ".ymap"; - distymap.RpfFileEntry.NameLower = distname + ".ymap"; - - lodymap._CMapData.parent = distymap._CMapData.name; - lodymap.Loaded = true; - distymap.Loaded = true; - - UpdateStatus("Adding new ymap files to project..."); - - ProjectForm.Invoke((MethodInvoker)delegate + private static List DivideCellVByCount(GridCell cell, int count) + { + float newWidth = cell.Width / count; + var newCells = new List(); + for (int i = 0; i < count; i++) + { + newCells.Add(new GridCell { - ProjectForm.AddYmapToProject(lodymap); - ProjectForm.AddYmapToProject(distymap); + StartX = cell.StartX + newWidth * i, + StartY = cell.StartY, + Width = newWidth, + Height = cell.Height }); + } + MoveLightsToClosestCell(cell, newCells); + foreach (var nc in newCells) UpdateCellExtentsFromLights(nc); + return newCells; + } - var stats = ""; - UpdateStatus("Process complete. " + stats); - GenerateComplete(); + private static List DivideCellHByCount(GridCell cell, int count) + { + float newHeight = cell.Height / count; + var newCells = new List(); + for (int i = 0; i < count; i++) + { + newCells.Add(new GridCell + { + StartX = cell.StartX, + StartY = cell.StartY + newHeight * i, + Width = cell.Width, + Height = newHeight + }); + } + MoveLightsToClosestCell(cell, newCells); + foreach (var nc in newCells) UpdateCellExtentsFromLights(nc); + return newCells; + } - }); + private static void MoveLightsToClosestCell(GridCell source, List targets) + { + foreach (var light in source.Lights) + { + float x = light.position.x; + float y = light.position.y; + float bestDist = float.MaxValue; + GridCell bestCell = targets[0]; + foreach (var target in targets) + { + float dist = DistToRectSq(x, y, target); + if (dist < bestDist) + { + bestDist = dist; + bestCell = target; + } + } + bestCell.Lights.Add(light); + } + source.Lights.Clear(); } + private static float DistToRectSq(float x, float y, GridCell cell) + { + float dx = 0, dy = 0; + float minX = cell.StartX; + float maxX = cell.StartX + cell.Width; + float minY = cell.StartY; + float maxY = cell.StartY + cell.Height; + if (x < minX) dx = x - minX; + else if (x > maxX) dx = x - maxX; + if (y < minY) dy = y - minY; + else if (y > maxY) dy = y - maxY; + return dx * dx + dy * dy; + } + + private static void ConsolidateSparseCells(List cells) + { + int nCells = cells.Count; + for (int i = 0; i < nCells; i++) + { + var cell = cells[i]; + int nLights = cell.Lights.Count; + if (nLights > 0 && nLights < MIN_LIGHT_COUNT_TO_CONSOLIDATE) + { + var otherCells = new List(); + for (int k = 0; k < nCells; k++) + { + if (k != i && cells[k].Lights.Count > MIN_LIGHT_COUNT_TO_CONSOLIDATE) + { + otherCells.Add(cells[k]); + } + } + if (otherCells.Count > 0) + { + foreach (var light in cell.Lights) + { + float x = light.position.x; + float y = light.position.y; + float bestDist = float.MaxValue; + GridCell bestCell = otherCells[0]; + foreach (var other in otherCells) + { + float dist = DistToRectSq(x, y, other); + if (dist < bestDist) + { + bestDist = dist; + bestCell = other; + } + } + bestCell.Lights.Add(light); + } + cell.Lights.Clear(); + foreach (var other in otherCells) UpdateCellExtentsFromLights(other); + } + } + } + cells.RemoveAll(c => c.Lights.Count == 0); + } + + private static void MakeCellsSquareIsh(List cells) + { + int nCells = cells.Count; + for (int i = 0; i < nCells; i++) + { + var cell = cells[i]; + if (cell.Lights.Count == 0 || cell.Height <= 0 || cell.Width <= 0) continue; + + float whRatio = cell.Width / cell.Height; + if (whRatio > 2.0f) + { + var newCells = DivideCellVByCount(cell, (int)whRatio); + cells.AddRange(newCells); + } + else if (whRatio < 0.5f) + { + var newCells = DivideCellHByCount(cell, (int)(1.0f / whRatio)); + cells.AddRange(newCells); + } + } + cells.RemoveAll(c => c.Lights.Count == 0); + } + + #endregion + + + private (YmapFile lodymap, YmapFile distymap) BuildYmapPair(List lights, string pname, int category, int cellIndex) + { + var position = new List(); + var colour = new List(); + var direction = new List(); + var falloff = new List(); + var falloffExponent = new List(); + var timeAndStateFlags = new List(); + var hash = new List(); + var coneInnerAngle = new List(); + var coneOuterAngleOrCapExt = new List(); + var coronaIntensity = new List(); + ushort numStreetLights = 0; + + foreach (var light in lights) + { + position.Add(light.position); + colour.Add(light.colour); + direction.Add(light.direction); + falloff.Add(light.falloff); + falloffExponent.Add(light.falloffExponent); + timeAndStateFlags.Add(light.timeAndStateFlags); + hash.Add(light.hash); + coneInnerAngle.Add(light.coneInnerAngle); + coneOuterAngleOrCapExt.Add(light.coneOuterAngleOrCapExt); + coronaIntensity.Add(light.coronaIntensity); + if (light.isStreetLight) numStreetLights++; + } + + string catLabel = CategoryLabels[category]; + + var lodymap = new YmapFile(); + var distymap = new YmapFile(); + var ll = new YmapLODLights(); + var dl = new YmapDistantLODLights(); + var cdl = new CDistantLODLight(); + distymap.DistantLODLights = dl; + lodymap.LODLights = ll; + lodymap.Parent = distymap; + cdl.category = (ushort)category; + cdl.numStreetLights = numStreetLights; + dl.CDistantLODLight = cdl; + dl.positions = position.ToArray(); + dl.colours = colour.ToArray(); + dl.Ymap = distymap; + dl.CalcBB(); + ll.direction = direction.ToArray(); + ll.falloff = falloff.ToArray(); + ll.falloffExponent = falloffExponent.ToArray(); + ll.timeAndStateFlags = timeAndStateFlags.ToArray(); + ll.hash = hash.ToArray(); + ll.coneInnerAngle = coneInnerAngle.ToArray(); + ll.coneOuterAngleOrCapExt = coneOuterAngleOrCapExt.ToArray(); + ll.coronaIntensity = coronaIntensity.ToArray(); + ll.Ymap = lodymap; + ll.BuildLodLights(dl); + ll.CalcBB(); + ll.BuildBVH(); + + lodymap.CalcFlags(); + lodymap.CalcExtents(); + distymap.CalcFlags(); + distymap.CalcExtents(); + + var lodname = $"{pname}_lodlights_{catLabel}{cellIndex:D3}"; + var distname = $"{pname}_distlodlights_{catLabel}{cellIndex:D3}"; + lodymap.Name = lodname; + lodymap._CMapData.name = JenkHash.GenHash(lodname); + lodymap.RpfFileEntry = new RpfResourceFileEntry(); + lodymap.RpfFileEntry.Name = lodname + ".ymap"; + lodymap.RpfFileEntry.NameLower = lodname + ".ymap"; + distymap.Name = distname; + distymap._CMapData.name = JenkHash.GenHash(distname); + distymap.RpfFileEntry = new RpfResourceFileEntry(); + distymap.RpfFileEntry.Name = distname + ".ymap"; + distymap.RpfFileEntry.NameLower = distname + ".ymap"; + + lodymap._CMapData.parent = distymap._CMapData.name; + lodymap.Loaded = true; + distymap.Loaded = true; + + return (lodymap, distymap); + } public class Light { @@ -308,9 +740,7 @@ public class Light public byte coneInnerAngle { get; set; } public byte coneOuterAngleOrCapExt { get; set; } public byte coronaIntensity { get; set; } - public bool isStreetLight { get; set; } } - } }