diff --git a/Classes/MeshBatch.cs b/Classes/MeshBatch.cs index 137f68e..17b6d24 100644 --- a/Classes/MeshBatch.cs +++ b/Classes/MeshBatch.cs @@ -342,7 +342,7 @@ private void BindMesh(ModelEntity modelEntity, Matrix4? matrix = null, TextureBi } mesh.SetData(numElements, positionList, normalList, colorList, uvList, tiledAreaList); } - if (textureBinder != null && modelEntity.Texture != null && modelEntity.RenderFlags.HasFlag(RenderFlags.Textured)) + if (textureBinder != null && modelEntity.Texture != null && modelEntity.IsTextured) { mesh.Texture = textureBinder.GetTexture((int)modelEntity.TexturePage); } diff --git a/Classes/ModelEntity.cs b/Classes/ModelEntity.cs index 9ebc99c..6986da4 100644 --- a/Classes/ModelEntity.cs +++ b/Classes/ModelEntity.cs @@ -76,6 +76,9 @@ public bool HasTiled } } + [Browsable(false)] + public bool IsTextured => RenderFlags.HasFlag(RenderFlags.Textured); + //[ReadOnly(true)] //public uint PrimitiveIndex { get; set; } diff --git a/Classes/ObjExporter.cs b/Classes/ObjExporter.cs index e7188bf..2fbec1e 100644 --- a/Classes/ObjExporter.cs +++ b/Classes/ObjExporter.cs @@ -68,7 +68,7 @@ public void Export(RootEntity[] entities, string selectedPath, bool experimental private void WriteModel(ModelEntity model) { - if (model.Texture != null && model.RenderFlags.HasFlag(RenderFlags.Textured)) + if (model.Texture != null && model.IsTextured) { if (_mtlExporter.AddMaterial((int) model.TexturePage)) { diff --git a/Classes/PlyExporter.cs b/Classes/PlyExporter.cs index 95a8a5e..ad5f468 100644 --- a/Classes/PlyExporter.cs +++ b/Classes/PlyExporter.cs @@ -28,7 +28,7 @@ public void Export(RootEntity[] entities, string selectedPath) { var model = (ModelEntity)entityBase; faceCount += model.Triangles.Count(); - if (model.RenderFlags.HasFlag(RenderFlags.Textured)) + if (model.IsTextured) { var texturePage = model.TexturePage; if (!materialsDic.ContainsKey((int)texturePage)) diff --git a/Classes/RenderInfo.cs b/Classes/RenderInfo.cs index 5a111e0..9165db1 100644 --- a/Classes/RenderInfo.cs +++ b/Classes/RenderInfo.cs @@ -16,17 +16,17 @@ public enum RenderFlags Subdivision = (1 << 6), AutomaticDivision = (1 << 7), - // Bits 30 and 31 are reserved for MixtureRate. + // Bits 29-31 are reserved for MixtureRate. } // Blending when RenderFlags.SemiTransparent is set. public enum MixtureRate { - None, - Back50_Poly50, // 50% back + 50% poly - Back100_Poly100, // 100% back + 100% poly - Back100_PolyM100, // 100% back - 100% poly - Back100_Poly25, // 100% back + 25% poly + None = 0, + Back50_Poly50 = 1, // 50% back + 50% poly + Back100_Poly100 = 2, // 100% back + 100% poly + Back100_PolyM100 = 3, // 100% back - 100% poly + Back100_Poly25 = 4, // 100% back + 25% poly } // A named Tuple for render information used to separate models/meshes. @@ -47,7 +47,7 @@ private ulong RawValue { return (((ulong)TexturePage << 0) | ((ulong)RenderFlags << 32) | - ((ulong)MixtureRate << 62)); + ((ulong)MixtureRate << 61)); } } diff --git a/Classes/TiledUV.cs b/Classes/TiledUV.cs index 906f1cf..15a631d 100644 --- a/Classes/TiledUV.cs +++ b/Classes/TiledUV.cs @@ -5,23 +5,37 @@ namespace PSXPrev.Classes public class TiledUV { public Vector2[] BaseUv { get; set; } - public Vector2 Offset { get; set; } // Position added to BaseUv after wrapping. - public Vector2 Size { get; set; } // Denominator of modulus with BaseUv for wrapping. + public float X { get; set; } // Position added to BaseUv after wrapping. + public float Y { get; set; } + public float Width { get; set; } // Denominator of modulus with BaseUv for wrapping. + public float Height { get; set; } - public Vector4 Area => new Vector4(Offset.X, Offset.Y, Size.X, Size.Y); + public Vector2 Offset => new Vector2(X, Y); + public Vector2 Size => new Vector2(Width, Height); + public Vector4 Area => new Vector4(X, Y, Width, Height); public TiledUV(Vector2[] baseUv, float x, float y, float width, float height) { BaseUv = baseUv; - Offset = new Vector2(x, y); - Size = new Vector2(width, height); + X = x; + Y = y; + Width = width; + Height = height; + } + + public TiledUV(Vector2[] baseUv, Vector2 offset, Vector2 size) + : this(baseUv, offset.X, offset.Y, size.X, size.Y) + { + } + + public TiledUV(Vector2[] baseUv, Vector4 area) + : this(baseUv, area.X, area.Y, area.Z, area.W) + { } public TiledUV(TiledUV fromTiledUv) + : this(fromTiledUv.BaseUv, fromTiledUv.X, fromTiledUv.Y, fromTiledUv.Width, fromTiledUv.Height) { - BaseUv = fromTiledUv.BaseUv; - Offset = fromTiledUv.Offset; - Size = fromTiledUv.Size; } // This function isn't used, since the tiled (display) UV can be calculated with integer math. @@ -30,12 +44,12 @@ public Vector2[] ConvertBaseUv() var uv = new Vector2[BaseUv.Length]; for (var i = 0; i < uv.Length; i++) { - uv[i] = Convert(BaseUv[i], Offset.X, Offset.Y, Size.X, Size.Y); + uv[i] = Convert(BaseUv[i], X, Y, Width, Height); } return uv; } - public Vector2 Convert(Vector2 uv) => Convert(uv, Offset.X, Offset.Y, Size.X, Size.Y); + public Vector2 Convert(Vector2 uv) => Convert(uv, X, Y, Width, Height); public static Vector2 Convert(Vector2 uv, float x, float y, float width, float height) diff --git a/Classes/VRAMPages.cs b/Classes/VRAMPages.cs index d8b95ff..4d7c227 100644 --- a/Classes/VRAMPages.cs +++ b/Classes/VRAMPages.cs @@ -13,26 +13,28 @@ public class VRAMPages : IReadOnlyList, IDisposable private readonly Scene _scene; - private readonly Texture[] _vramPage; - private readonly bool[] _modifiedPage; + private readonly Texture[] _vramPages; + private readonly bool[] _modifiedPages; // Pages that that require a scene update. + private readonly bool[] _usedPages; // Pages that have textures drawn to them (not reset unless cleared). public System.Drawing.Color BackgroundColor { get; set; } = System.Drawing.Color.White; public VRAMPages(Scene scene) { _scene = scene; - _vramPage = new Texture[PageCount]; - _modifiedPage = new bool[PageCount]; + _vramPages = new Texture[PageCount]; + _modifiedPages = new bool[PageCount]; + _usedPages = new bool[PageCount]; } - public Texture this[uint index] => _vramPage[index]; - public Texture this[int index] => _vramPage[index]; + public Texture this[uint index] => _vramPages[index]; + public Texture this[int index] => _vramPages[index]; public int Count => PageCount; public IEnumerator GetEnumerator() { - return ((IReadOnlyList)_vramPage).GetEnumerator(); + return ((IReadOnlyList)_vramPages).GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); @@ -41,8 +43,10 @@ public void Dispose() { for (var i = 0; i < PageCount; i++) { - _vramPage[i]?.Dispose(); - //_vramPage[i] = null; + _vramPages[i]?.Dispose(); + //_vramPages[i] = null; + _modifiedPages[i] = false; + _usedPages[i] = false; } } @@ -50,25 +54,41 @@ public void Setup(bool suppressUpdate = false) { for (var i = 0; i < PageCount; i++) { - if (_vramPage[i] == null) + if (_vramPages[i] == null) { // X coordinates [0,256) store texture data. // X coordinates [256,512) store semi-transparency information for textures. - _vramPage[i] = new Texture(PageSize * 2, PageSize, 0, 0, 32, i, true); // Is VRAM page + _vramPages[i] = new Texture(PageSize * 2, PageSize, 0, 0, 32, i, true); // Is VRAM page ClearPage(i, suppressUpdate); } } } - + + // Gets if a page has had at least one texture drawn to it. + public bool IsPageUsed(uint index) => IsPageUsed((int)index); + + public bool IsPageUsed(int index) + { + return _usedPages[index]; + } + + // Returns true if the index is a valid VRAM texture page number. + public bool ContainsPage(uint index) => ContainsPage((int)index); + + public bool ContainsPage(int index) + { + return index >= 0 && index < PageCount; + } + // Update page textures in the scene. public void UpdatePage(uint index, bool force = false) => UpdatePage((int)index, force); public void UpdatePage(int index, bool force = false) { - if (force || _modifiedPage[index]) + if (force || _modifiedPages[index]) { - _scene.UpdateTexture(_vramPage[index].Bitmap, index); - _modifiedPage[index] = false; + _scene.UpdateTexture(_vramPages[index].Bitmap, index); + _modifiedPages[index] = false; } } @@ -85,7 +105,7 @@ public void UpdateAllPages() public void ClearPage(int index, bool suppressUpdate = false) { - using (var graphics = Graphics.FromImage(_vramPage[index].Bitmap)) + using (var graphics = Graphics.FromImage(_vramPages[index].Bitmap)) { graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.None; @@ -99,9 +119,10 @@ public void ClearPage(int index, bool suppressUpdate = false) } } + _usedPages[index] = false; if (suppressUpdate) { - _modifiedPage[index] = true; + _modifiedPages[index] = true; } else { @@ -127,7 +148,7 @@ public void DrawTexture(Texture texture, bool suppressUpdate = false) var textureHeight = texture.Height; var textureBitmap = texture.Bitmap; var textureSemiTransparentMap = texture.SemiTransparentMap; - using (var graphics = Graphics.FromImage(_vramPage[index].Bitmap)) + using (var graphics = Graphics.FromImage(_vramPages[index].Bitmap)) { // Use SourceCopy to overwrite image alpha with alpha stored in textures. graphics.CompositingMode = System.Drawing.Drawing2D.CompositingMode.SourceCopy; @@ -154,9 +175,10 @@ public void DrawTexture(Texture texture, bool suppressUpdate = false) graphics.ResetClip(); } + _usedPages[index] = true; if (suppressUpdate) { - _modifiedPage[index] = true; + _modifiedPages[index] = true; } else { diff --git a/Forms/PreviewForm.Designer.cs b/Forms/PreviewForm.Designer.cs index f122239..1a1dbe6 100644 --- a/Forms/PreviewForm.Designer.cs +++ b/Forms/PreviewForm.Designer.cs @@ -531,7 +531,10 @@ private void InitializeComponent() this.vramComboBox.Size = new System.Drawing.Size(172, 21); this.vramComboBox.TabIndex = 10; this.vramComboBox.Text = "Select"; - this.vramComboBox.SelectedIndexChanged += new System.EventHandler(this.comboBox1_SelectedIndexChanged); + this.vramComboBox.SelectedIndexChanged += new System.EventHandler(this.vramComboBox_SelectedIndexChanged); + this.vramComboBox.TextChanged += new System.EventHandler(this.vramComboBox_TextChanged); + this.vramComboBox.KeyDown += new System.Windows.Forms.KeyEventHandler(this.vramComboBox_KeyDown); + this.vramComboBox.Leave += new System.EventHandler(this.vramComboBox_Leave); // // vramPagePictureBox // diff --git a/Forms/PreviewForm.cs b/Forms/PreviewForm.cs index d6b9ea1..cb40c57 100644 --- a/Forms/PreviewForm.cs +++ b/Forms/PreviewForm.cs @@ -20,6 +20,8 @@ public partial class PreviewForm : Form private static readonly Pen Black3Px = new Pen(Color.Black, 3f); private static readonly Pen White1Px = new Pen(Color.White, 1f); + private static readonly Pen Cyan1Px = new Pen(Color.Cyan, 1f); + private readonly List _animations; private readonly Action _refreshAction; private readonly List _rootEntities; @@ -47,6 +49,7 @@ public partial class PreviewForm : Form private EntitySelectionSource _selectionSource; private bool _showUv = true; private readonly VRAMPages _vram; + private int _vramSelectedPage = -1; // Used because combo box SelectedIndex can be -1 while typing. private Bitmap _maskColorBitmap; private Bitmap _ambientColorBitmap; private Bitmap _backgroundColorBitmap; @@ -102,7 +105,7 @@ private void EntityAdded(RootEntity entity) { var model = (ModelEntity)entityBase; model.TexturePage = VRAMPages.ClampTexturePage(model.TexturePage); - if (model.RenderFlags.HasFlag(RenderFlags.Textured)) + if (model.IsTextured) { model.Texture = _vram[model.TexturePage]; } @@ -223,6 +226,7 @@ public void DrawAllTexturesToVRAM() _vram.DrawTexture(texture, true); // Suppress updates to scene until all textures are drawn. } _vram.UpdateAllPages(); + UpdateVRAMComboBoxPageItems(); } } @@ -820,17 +824,77 @@ private void drawToVRAMButton_Click(object sender, EventArgs e) _vram.DrawTexture(GetSelectedTexture(index), true); // Suppress updates to scene until all textures are drawn. } _vram.UpdateAllPages(); + UpdateVRAMComboBoxPageItems(); + } + + private void UpdateVRAMComboBoxPageItems() + { + // Mark page numbers that have had textures drawn to them. + // note: This assumes the combo box items are strings, which they currently are. + for (var i = 0; i < _vram.Count; i++) + { + var used = _vram.IsPageUsed(i); + vramComboBox.Items[i] = $"{i}" + (used ? " (drawn to)" : string.Empty); + } + } + + private void UpdateVRAMComboBoxTypedPage(bool changeSelection) + { + // Allow typing into the combo box to change the page. + var text = vramComboBox.Text; + if (vramComboBox.SelectedIndex <= -1 && !string.IsNullOrWhiteSpace(text)) + { + // Stop parsing after the end of digits. This is so we can start typing based off + // the real combo box text (which may have non-numeric text after the number). + for (var i = 0; i < text.Length; i++) + { + if (!char.IsDigit(text[i])) + { + text = text.Substring(0, i); + break; + } + } + if (int.TryParse(text, out var index) && _vram.ContainsPage(index)) + { + _vramSelectedPage = index; + vramPagePictureBox.Image = _vram[index].Bitmap; + if (changeSelection) + { + vramComboBox.SelectedIndex = index; + } + } + } } - private void comboBox1_SelectedIndexChanged(object sender, EventArgs e) + private void vramComboBox_SelectedIndexChanged(object sender, EventArgs e) { var index = vramComboBox.SelectedIndex; if (index > -1) { + _vramSelectedPage = index; vramPagePictureBox.Image = _vram[index].Bitmap; } } + private void vramComboBox_TextChanged(object sender, EventArgs e) + { + UpdateVRAMComboBoxTypedPage(false); // Don't update combo box selected index while typing. + } + + private void vramComboBox_Leave(object sender, EventArgs e) + { + UpdateVRAMComboBoxTypedPage(true); // Update combo box selected index when focus is lost. + } + + private void vramComboBox_KeyDown(object sender, KeyEventArgs e) + { + if (e.KeyCode == Keys.Enter) + { + UpdateVRAMComboBoxTypedPage(true); // Update combo box selected index when enter is pressed. + e.Handled = true; + } + } + private void modelPropertyGrid_PropertyValueChanged(object s, PropertyValueChangedEventArgs e) { var selectedNode = entitiesTreeView.SelectedNode; @@ -841,7 +905,7 @@ private void modelPropertyGrid_PropertyValueChanged(object s, PropertyValueChang if (_selectedModelEntity != null) { _selectedModelEntity.TexturePage = VRAMPages.ClampTexturePage(_selectedModelEntity.TexturePage); - if (_selectedModelEntity.RenderFlags.HasFlag(RenderFlags.Textured)) + if (_selectedModelEntity.IsTextured) { _selectedModelEntity.Texture = _vram[_selectedModelEntity.TexturePage]; } @@ -887,7 +951,7 @@ private void texturePropertyGrid_PropertyValueChanged(object s, PropertyValueCha private void btnClearPage_Click(object sender, EventArgs e) { - var index = vramComboBox.SelectedIndex; + var index = _vramSelectedPage; if (index <= -1) { MessageBox.Show("Select a page first"); @@ -901,6 +965,7 @@ private void ClearPage(int index) { _vram.ClearPage(index); vramPagePictureBox.Image = _vram[index].Bitmap; + UpdateVRAMComboBoxPageItems(); } private void cmsModelExport_ItemClicked(object sender, ToolStripItemClickedEventArgs e) @@ -988,28 +1053,54 @@ private void texturesListView_SelectedIndexChanged(object sender, EventArgs e) texturePropertyGrid.SelectedObject = texture; } + private void DrawUVLines(Graphics graphics, Pen pen, Vector2[] uvs) + { + for (var i = 0; i < uvs.Length; i++) + { + var i2 = (i + 1) % uvs.Length; + graphics.DrawLine(pen, uvs[i].X * 255f, uvs[i].Y * 255f, uvs[i2].X * 255f, uvs[i2].Y * 255f); + } + } + + private void DrawTiledUVRectangle(Graphics graphics, Pen pen, TiledUV tiledUv) + { + graphics.DrawRectangle(pen, tiledUv.X * 255f, tiledUv.Y * 255f, tiledUv.Width * 255f, tiledUv.Height * 255f); + } + private void DrawUV(EntityBase entity, Graphics graphics) { if (entity == null) { return; } - if (entity is ModelEntity modelEntity) // && modelEntity.HasUvs) + // Don't draw UVs for this model unless it uses the same texture page that we're on. + if (entity is ModelEntity model && model.IsTextured && model.TexturePage == _vramSelectedPage) { - foreach (var triangle in modelEntity.Triangles) + // Draw all black outlines before inner fill lines, so that black outline edges don't overlap fill lines. + foreach (var triangle in model.Triangles) { - graphics.DrawLine(Black3Px, triangle.Uv[0].X * 255f, triangle.Uv[0].Y * 255f, triangle.Uv[1].X * 255f, triangle.Uv[1].Y * 255f); - graphics.DrawLine(Black3Px, triangle.Uv[1].X * 255f, triangle.Uv[1].Y * 255f, triangle.Uv[2].X * 255f, triangle.Uv[2].Y * 255f); - graphics.DrawLine(Black3Px, triangle.Uv[2].X * 255f, triangle.Uv[2].Y * 255f, triangle.Uv[0].X * 255f, triangle.Uv[0].Y * 255f); + if (triangle.IsTiled) + { + // Triangle.Uv is useless when tiled, so draw the TiledUv area instead. + DrawTiledUVRectangle(graphics, Black3Px, triangle.TiledUv); + } + else + { + DrawUVLines(graphics, Black3Px, triangle.Uv); + } } - } - if (entity is ModelEntity modelEntity2) //&& modelEntity2.HasUvs) - { - foreach (var triangle in modelEntity2.Triangles) + + foreach (var triangle in model.Triangles) { - graphics.DrawLine(White1Px, triangle.Uv[0].X * 255f, triangle.Uv[0].Y * 255f, triangle.Uv[1].X * 255f, triangle.Uv[1].Y * 255f); - graphics.DrawLine(White1Px, triangle.Uv[1].X * 255f, triangle.Uv[1].Y * 255f, triangle.Uv[2].X * 255f, triangle.Uv[2].Y * 255f); - graphics.DrawLine(White1Px, triangle.Uv[2].X * 255f, triangle.Uv[2].Y * 255f, triangle.Uv[0].X * 255f, triangle.Uv[0].Y * 255f); + if (triangle.IsTiled) + { + // Different color for tiled area. + DrawTiledUVRectangle(graphics, Cyan1Px, triangle.TiledUv); + } + else + { + DrawUVLines(graphics, White1Px, triangle.Uv); + } } } if (entity.ChildEntities == null) @@ -1040,6 +1131,7 @@ private void wireframeToolStripMenuItem_CheckedChanged(object sender, EventArgs private void clearAllPagesToolStripMenuItem_Click(object sender, EventArgs e) { _vram.ClearAllPages(); + UpdateVRAMComboBoxPageItems(); MessageBox.Show("Pages cleared"); } @@ -1117,12 +1209,17 @@ private void menusTabControl_SelectedIndexChanged(object sender, EventArgs e) switch (menusTabControl.SelectedTab.TabIndex) { - case 0: + case 0: // Models animationsTreeView.SelectedNode = null; _openTkControl.Parent = modelsSplitContainer.Panel2; _openTkControl.Show(); break; - case 3: + case 1: // Textures + break; + case 2: // VRAM + UpdateVRAMComboBoxPageItems(); + break; + case 3: // Animations _inAnimationTab = true; _openTkControl.Parent = animationsSplitContainer.Panel2; _openTkControl.Show(); @@ -1231,7 +1328,7 @@ private void vramPagePictureBox_Paint(object sender, PaintEventArgs e) DrawUV(checkedEntity, e.Graphics); } } - DrawUV(_selectedRootEntity, e.Graphics); + DrawUV((EntityBase)_selectedRootEntity ?? _selectedModelEntity, e.Graphics); } private void entitiesTreeView_AfterCheck(object sender, TreeViewEventArgs e)