diff --git a/XWOpt/OptFile.cs b/XWOpt/OptFile.cs index 1b37a2e..29bf26a 100644 --- a/XWOpt/OptFile.cs +++ b/XWOpt/OptFile.cs @@ -140,14 +140,10 @@ public IEnumerator GetEnumerator() { foreach (var node in RootNodes) { - yield return node; - - if (node is NodeCollection branch) + // Recursely walk the hierarchy - the first item is always node, then any children + foreach (var nodeItem in node) { - foreach (var subNode in branch) - { - yield return subNode; - } + yield return nodeItem; } } } diff --git a/XWOpt/OptNode/BaseNode.cs b/XWOpt/OptNode/BaseNode.cs index 6fccc3d..5ff23ce 100644 --- a/XWOpt/OptNode/BaseNode.cs +++ b/XWOpt/OptNode/BaseNode.cs @@ -19,23 +19,81 @@ * OR OTHER DEALINGS IN THE SOFTWARE. */ +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using SchmooTech.XWOpt.OptNode.Types; namespace SchmooTech.XWOpt.OptNode { /// /// Common type for all types of nodes in an OPT file. /// - public class BaseNode + public class BaseNode : IEnumerable { /// /// Offset at which this node was read from the file. Use for read debugging. /// - public long OffsetInFile { get; private set; } + public long OffsetInFile { get; } + public string Name { get; } + public NodeType NodeType { get; } - internal BaseNode() { } - internal BaseNode(OptReader opt) + public Collection Children { get; } = new Collection(); + public BaseNode Parent { get; private set; } + internal BaseNode(string name, NodeType nodeType) + { + Name = name; + NodeType = nodeType; + } + + internal BaseNode(OptReader opt, NodeHeader header) { OffsetInFile = opt.BaseStream.Position; + Name = header.Name; + NodeType = header.NodeType; + Parent = header.Parent; + + for (var i = 0; i < header.ChildCount; i++) + { + opt.Seek(header.ChildAddressTable + i * 4); + var childAddress = opt.ReadInt32(); + + if (childAddress != 0) + { + var node = opt.ReadNodeAt(childAddress, this, this); + Children.Add(node); + } + } + } + + public IEnumerator GetEnumerator() + { + yield return this; + + foreach (var node in Children) + { + // Recursive walk down the hierarchy + foreach (var subNode in node) + { + yield return subNode; + } + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public void AddChild(BaseNode node) + { + Children.Add(node); + node.Parent = this; + } + + public bool RemoveChild(BaseNode node) + { + return Children.Remove(node); } } } diff --git a/XWOpt/OptNode/EngineGlow.cs b/XWOpt/OptNode/EngineGlow.cs index a3f73df..e7e4567 100644 --- a/XWOpt/OptNode/EngineGlow.cs +++ b/XWOpt/OptNode/EngineGlow.cs @@ -33,12 +33,10 @@ class EngineGlow : BaseNode public TVector3 Y { get; set; } public TVector3 Z { get; set; } - internal EngineGlow(OptReader reader) : base(reader) + internal EngineGlow(OptReader reader, NodeHeader nodeHeader) : base(reader, nodeHeader) { - reader.ReadUnknownUseValue(0, this); - reader.ReadUnknownUseValue(0, this); - reader.ReadUnknownUseValue(1, this); - reader.FollowPointerToNextByte(this); + reader.Seek(nodeHeader.DataAddress); + reader.ReadUnknownUseValue(0, this); InnerColor = reader.ReadInt32(); diff --git a/XWOpt/OptNode/FaceList.cs b/XWOpt/OptNode/FaceList.cs index 25a1bb2..10d21b0 100644 --- a/XWOpt/OptNode/FaceList.cs +++ b/XWOpt/OptNode/FaceList.cs @@ -32,15 +32,12 @@ public class FaceList : BaseNode private Collection faceNormals; private Collection> basisVectors; - private int edgeCount; - [SuppressMessage("Microsoft.Performance", "CA1814:PreferJaggedArraysOverMultidimensional", MessageId = "Member")] private Collection vertexRef; [SuppressMessage("Microsoft.Performance", "CA1814:PreferJaggedArraysOverMultidimensional", MessageId = "Member")] private Collection edgeRef; [SuppressMessage("Microsoft.Performance", "CA1814:PreferJaggedArraysOverMultidimensional", MessageId = "Member")] private Collection uVRef; - private int count; [SuppressMessage("Microsoft.Performance", "CA1814:PreferJaggedArraysOverMultidimensional", MessageId = "Member")] public Collection VertexRef { get => vertexRef; } @@ -52,25 +49,25 @@ public class FaceList : BaseNode public Collection VertexNormalRef { get => vertexNormalRef; } public Collection FaceNormals { get => faceNormals; } public Collection> BasisVectors { get => basisVectors; } - public int Count { get => count; set => count = value; } - public int EdgeCount { get => edgeCount; set => edgeCount = value; } + public int Count { get; } + public int EdgeCount { get; } + public int FaceListSize { get; } - internal FaceList(OptReader reader) : base(reader) + internal FaceList(OptReader reader, NodeHeader nodeHeader) : base(reader, nodeHeader) { - // unknown zeros - reader.ReadUnknownUseValue(0, this); - reader.ReadUnknownUseValue(0, this); + reader.Seek(nodeHeader.DataAddress); + Count = nodeHeader.DataCount; - count = reader.ReadInt32(); - reader.FollowPointerToNextByte(this); - edgeCount = reader.ReadInt32(); + // read the IFS Size + FaceListSize = reader.ReadInt32(); + // Next up is the DataCount * Faces, size 64 vertexRef = new Collection(); edgeRef = new Collection(); uVRef = new Collection(); vertexNormalRef = new Collection(); - for (int i = 0; i < count; i++) + for (int i = 0; i < nodeHeader.DataCount; i++) { vertexRef.Add(new CoordinateReferenceTuple(reader)); edgeRef.Add(new CoordinateReferenceTuple(reader)); @@ -78,10 +75,12 @@ internal FaceList(OptReader reader) : base(reader) vertexNormalRef.Add(new CoordinateReferenceTuple(reader)); } - faceNormals = reader.ReadVectorCollection(count); + // Next up is the DataCount * Face Normals, size 12 + faceNormals = reader.ReadVectorCollection(nodeHeader.DataCount); + // Next up is the DataCount * Texture Basis Vectors, size 24 basisVectors = new Collection>(); - for (int i = 0; i < count; i++) + for (int i = 0; i < nodeHeader.DataCount; i++) { // Edge case: TIE98 CORVTA.OPT is missing a float at EOF. try diff --git a/XWOpt/OptNode/Hardpoint.cs b/XWOpt/OptNode/Hardpoint.cs index 59af08a..7799118 100644 --- a/XWOpt/OptNode/Hardpoint.cs +++ b/XWOpt/OptNode/Hardpoint.cs @@ -59,23 +59,22 @@ public enum WeaponType public class Hardpoint : BaseNode { - private WeaponType weaponType; - private TVector3 location; + public WeaponType WeaponType { get; } + public TVector3 Location { get; } - public WeaponType WeaponType { get => weaponType; set => weaponType = value; } - public TVector3 Location { get => location; set => location = value; } - - internal Hardpoint(OptReader reader) : base(reader) + internal Hardpoint(OptReader reader, NodeHeader nodeHeader) : base(reader, nodeHeader) { - reader.ReadUnknownUseValue(0, this); - reader.ReadUnknownUseValue(0, this); - reader.ReadUnknownUseValue(1, this); + reader.Seek(nodeHeader.DataAddress); - reader.FollowPointerToNextByte(this); + WeaponType = (WeaponType)reader.ReadUInt32(); - weaponType = (WeaponType)reader.ReadUInt32(); + Location = reader.ReadVector(); + } - location = reader.ReadVector(); + public Hardpoint(string name, WeaponType type, TVector3 position) : base(name, Types.NodeType.Hardpoint) + { + WeaponType = type; + Location = position; } } } diff --git a/XWOpt/OptNode/LodCollection.cs b/XWOpt/OptNode/LodCollection.cs index d2d9e35..e3611d8 100644 --- a/XWOpt/OptNode/LodCollection.cs +++ b/XWOpt/OptNode/LodCollection.cs @@ -25,38 +25,27 @@ namespace SchmooTech.XWOpt.OptNode { - public class LodCollection : NodeCollection + public class LodCollection : BaseNode { /// /// Render cutoff distances associated with each LOD, in same order as Children. (This may be in no logical order.) /// public Collection MaxRenderDistance { get; } = new Collection(); - internal LodCollection(OptReader reader) : base() + internal LodCollection(OptReader reader, NodeHeader nodeHeader) : base(reader, nodeHeader) { - int lodChildCount = reader.ReadInt32(); - int lodChildOffset = reader.ReadInt32(); - int lodThresholdCount = reader.ReadInt32(); - int lodThresholdOffset = reader.ReadInt32(); - // No idea why this would happen, but my understanding of this block is wrong if it does. - if (lodChildCount != lodThresholdCount) + if (nodeHeader.ChildCount != nodeHeader.DataCount) { - reader.logger?.Invoke(String.Format(CultureInfo.CurrentCulture, "Not the same number of LOD meshes ({0}) as LOD offsets ({1}) at {2:X}", lodChildCount, lodThresholdCount, reader.BaseStream.Position)); + reader.logger?.Invoke(String.Format(CultureInfo.CurrentCulture, "Not the same number of LOD meshes ({0}) as LOD offsets ({1}) at {2:X}", nodeHeader.ChildCount, nodeHeader.DataCount, reader.BaseStream.Position)); } - reader.Seek(lodChildOffset); - ReadChildren(reader, lodChildCount, lodChildOffset); - - reader.Seek(lodThresholdOffset); + reader.Seek(nodeHeader.DataAddress); MaxRenderDistance.Clear(); - for (int i = 0; i < lodChildCount; i++) + for (int i = 0; i < nodeHeader.DataCount; i++) { float distance = reader.ReadSingle(); MaxRenderDistance.Add(distance); - // A distance of 0 represents infinate draw distance - // Converting to PositiveInfinity sorts it correctly. - distance = distance == 0 ? float.PositiveInfinity : distance; } } } diff --git a/XWOpt/OptNode/MeshVerticies.cs b/XWOpt/OptNode/MeshVerticies.cs index f8cf851..701655b 100644 --- a/XWOpt/OptNode/MeshVerticies.cs +++ b/XWOpt/OptNode/MeshVerticies.cs @@ -25,19 +25,13 @@ namespace SchmooTech.XWOpt.OptNode { public class MeshVertices : BaseNode { - public Collection Vertices { get; private set; } + public Collection Vertices { get; } - internal MeshVertices(OptReader reader) : base(reader) + internal MeshVertices(OptReader reader, NodeHeader nodeHeader) : base(reader, nodeHeader) { - // unknown zeros - reader.ReadUnknownUseValue(0, this); - reader.ReadUnknownUseValue(0, this); + reader.Seek(nodeHeader.DataAddress); - var count = reader.ReadInt32(); - - reader.FollowPointerToNextByte(this); - - Vertices = reader.ReadVectorCollection(count); + Vertices = reader.ReadVectorCollection(nodeHeader.DataCount); } } } diff --git a/XWOpt/OptNode/NamedGroup.cs b/XWOpt/OptNode/NamedGroup.cs deleted file mode 100644 index a4c501c..0000000 --- a/XWOpt/OptNode/NamedGroup.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace SchmooTech.XWOpt.OptNode -{ - public class NamedNodeCollection : NodeCollection - { - /* - * Node with single child and long name. - * - * Example in SHUTTLE.OPT - * A2A: Pointer to Lambda_Fuse_Roof (start of this block) - * 0 - * 1 - * Jump to A53 (which is right after Lambda_Fuse_Roof null terminator) - * 1 - * FFFC E77F (reverse relative jump) - * "Lambda_Fuse_Roof" - * A53: pointer to A57 - * A57: (beginning of child block which is a standard NodeCollection) - */ - - public string Name { get; set; } - - internal NamedNodeCollection(OptReader reader, int nameOffset) : base() - { - var child_pp = reader.ReadInt32(); - - reader.Seek(nameOffset); - // arbitrary length null-terminated string - Name = reader.ReadString(maxLen: 32); - - ReadChildren(reader, 1, child_pp); - } - } -} diff --git a/XWOpt/OptNode/NodeCollection.cs b/XWOpt/OptNode/NodeCollection.cs deleted file mode 100644 index 86632b0..0000000 --- a/XWOpt/OptNode/NodeCollection.cs +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2017 Jason McNew - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of this - * software and associated documentation files (the "Software"), to deal in the Software - * without restriction, including without limitation the rights to use, copy, modify, - * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be included in all copies - * or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR - * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, - * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE - * OR OTHER DEALINGS IN THE SOFTWARE. - */ - -using System.Collections; -using System.Collections.Generic; -using System.Collections.ObjectModel; - -namespace SchmooTech.XWOpt.OptNode -{ - public class NodeCollection : BaseNode, IEnumerable - - { - Collection children = new Collection(); - - public Collection Children - { - get { return children; } - } - - internal NodeCollection() : base() { } - internal NodeCollection(OptReader reader) : base(reader) - { - ReadChildren(reader); - } - - internal void ReadChildren(OptReader reader) - { - foreach (var child in reader.ReadChildren(this)) - { - Children.Add(child); - } - } - - internal void ReadChildren(OptReader reader, int count, int jumpListOffset) - { - foreach (var child in reader.ReadChildren(count, jumpListOffset, this)) - { - Children.Add(child); - } - } - - public IEnumerator GetEnumerator() - { - foreach (var node in Children) - { - yield return node; - - if(node is NodeCollection branch) { - foreach (var subNode in branch) { - yield return subNode; - } - } - } - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - } -} diff --git a/XWOpt/OptNode/NodeHeader.cs b/XWOpt/OptNode/NodeHeader.cs new file mode 100644 index 0000000..138819b --- /dev/null +++ b/XWOpt/OptNode/NodeHeader.cs @@ -0,0 +1,64 @@ +/* + * Copyright 2017 Jason McNew + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this + * software and associated documentation files (the "Software"), to deal in the Software + * without restriction, including without limitation the rights to use, copy, modify, + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies + * or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR + * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ + +using SchmooTech.XWOpt.OptNode.Types; + +namespace SchmooTech.XWOpt.OptNode +{ + public class NodeHeader + { + public int NameOffset { get; } + public NodeType NodeType { get; } + + public int ChildCount { get; } + public int ChildAddressTable { get; } + + public int DataCount { get; } + public int DataAddress { get; } + + public string Name { get; } + public BaseNode Parent { get; } + + internal NodeHeader(OptReader reader, BaseNode parent) + { + Parent = parent; + + NameOffset = reader.ReadInt32(); + NodeType = (NodeType)reader.ReadInt32(); + + ChildCount = reader.ReadInt32(); + ChildAddressTable = reader.ReadInt32(); + + DataCount = reader.ReadInt32(); + DataAddress = reader.ReadInt32(); + + if (NameOffset != 0) + { + reader.Seek(NameOffset); + Name = reader.ReadString(100); + } + else + { + Name = string.Empty; + } + } + } +} diff --git a/XWOpt/OptNode/PartDescriptor.cs b/XWOpt/OptNode/PartDescriptor.cs index 966f637..2a8be8f 100644 --- a/XWOpt/OptNode/PartDescriptor.cs +++ b/XWOpt/OptNode/PartDescriptor.cs @@ -66,8 +66,8 @@ public class PartDescriptor : BaseNode public PartType PartType { get; set; } /// - /// 0,1,4,5,8,9 = Mesh continues in straight line when destroyed 2,3,6,10 = Mesh breaks off and explodes 7 = destructible parts - /// Looks like bitmask but not sure + /// 0,1,4,5,8,9 = Mesh continues in straight line when destroyed 2,3,6,10 = Mesh breaks off and explodes + /// Bitmask. 2 is Destructable. /// public int ExplosionType { get; set; } @@ -102,13 +102,9 @@ public class PartDescriptor : BaseNode /// public TVector3 TargetPoint { get; set; } - internal PartDescriptor(OptReader reader) : base(reader) + internal PartDescriptor(OptReader reader, NodeHeader nodeHeader) : base(reader, nodeHeader) { - reader.ReadUnknownUseValue(0, this); - reader.ReadUnknownUseValue(0, this); - reader.ReadUnknownUseValue(1, this); - - reader.FollowPointerToNextByte(this); + reader.Seek(nodeHeader.DataAddress); PartType = (PartType)reader.ReadUInt32(); ExplosionType = reader.ReadInt32(); diff --git a/XWOpt/OptNode/RotationInfo.cs b/XWOpt/OptNode/RotationInfo.cs index 7f0fd94..3598f51 100644 --- a/XWOpt/OptNode/RotationInfo.cs +++ b/XWOpt/OptNode/RotationInfo.cs @@ -27,40 +27,38 @@ namespace SchmooTech.XWOpt.OptNode /// public class RotationInfo : BaseNode { - private TVector3 offset; // Seems the same as MeshDescriptor.centerPoint - private TVector3 yawAxis; - private TVector3 rollAxis; - private TVector3 pitchAxis; - /// /// For a pivoting structure, this is the point it pivots around. /// - public TVector3 Offset { get => offset; set => offset = value; } + public TVector3 Offset { get; set; } /// /// The vector the object yaws clockwise around IE downward facing vector /// - public TVector3 YawAxis { get => yawAxis; set => yawAxis = value; } + public TVector3 YawAxis { get; set; } /// /// The vector the object rolls around (clockwise = roll right) IE forward facting vector /// - public TVector3 RollAxis { get => rollAxis; set => rollAxis = value; } + public TVector3 RollAxis { get; set; } /// /// The vector the object pitches around (clockwise = pitch up) /// - public TVector3 PitchAxis { get => pitchAxis; set => pitchAxis = value; } + public TVector3 PitchAxis { get; set; } - internal RotationInfo(OptReader reader) : base(reader) + internal RotationInfo(OptReader reader, NodeHeader nodeHeader) : base(reader, nodeHeader) { - reader.ReadUnknownUseValue(0, this); - reader.ReadUnknownUseValue(0, this); - reader.ReadUnknownUseValue(1, this); + reader.Seek(nodeHeader.DataAddress); - reader.FollowPointerToNextByte(this); + Offset = reader.ReadVector(); + YawAxis = reader.ReadVector(); + RollAxis = reader.ReadVector(); + PitchAxis = reader.ReadVector(); + } - offset = reader.ReadVector(); - yawAxis = reader.ReadVector(); - rollAxis = reader.ReadVector(); - pitchAxis = reader.ReadVector(); + public RotationInfo(TVector3 up, TVector3 right, TVector3 forwards) : base(string.Empty, Types.NodeType.Pivot) + { + YawAxis = up; + RollAxis = forwards; + PitchAxis = right; } } } diff --git a/XWOpt/OptNode/Types/TextureMinor.cs b/XWOpt/OptNode/SeparatorNode.cs similarity index 80% rename from XWOpt/OptNode/Types/TextureMinor.cs rename to XWOpt/OptNode/SeparatorNode.cs index 1f64590..abd804c 100644 --- a/XWOpt/OptNode/Types/TextureMinor.cs +++ b/XWOpt/OptNode/SeparatorNode.cs @@ -19,11 +19,16 @@ * OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace SchmooTech.XWOpt.OptNode.Types +namespace SchmooTech.XWOpt.OptNode { - public enum TextureMinor + /// + /// Common type for all types of nodes in an OPT file. + /// + public class SeparatorNode : BaseNode { - Texture = 0, - TextureWithAlpha = 1, + internal SeparatorNode(OptReader opt, NodeHeader header) : base(opt, header) + { + + } } } diff --git a/XWOpt/OptNode/SkinCollection.cs b/XWOpt/OptNode/SkinCollection.cs index 881c6ba..f7ea680 100644 --- a/XWOpt/OptNode/SkinCollection.cs +++ b/XWOpt/OptNode/SkinCollection.cs @@ -21,11 +21,11 @@ namespace SchmooTech.XWOpt.OptNode { - public class SkinCollection : NodeCollection + public class SkinCollection : BaseNode { - internal SkinCollection(OptReader reader) : base() + internal SkinCollection(OptReader reader, NodeHeader nodeHeader) : base(reader, nodeHeader) { - ReadChildren(reader); + } } } diff --git a/XWOpt/OptNode/Texture.cs b/XWOpt/OptNode/Texture.cs index 46b709c..e3f49cf 100644 --- a/XWOpt/OptNode/Texture.cs +++ b/XWOpt/OptNode/Texture.cs @@ -26,25 +26,15 @@ namespace SchmooTech.XWOpt.OptNode { public class Texture : BaseNode { - // Not sure what this number means. Seems to be the same on most textures inside of the same OPT. - private int group; - private string name; - private int mipLevels = 0; - - private byte[] texturePalletRefs; private Collection mipPalletRefs = new Collection(); // 16 pallets at decreasing light levels. // colors packed 5-6-5 blue, green, red. private readonly TexturePallet pallet = new TexturePallet(); - private int width; - private int height; - - public int Group { get => group; set => group = value; } - public string Name { get => name; set => name = value; } - public int Width { get => width; set => width = value; } - public int Height { get => height; set => height = value; } - public int MipLevels { get => mipLevels; set => mipLevels = value; } + + public int Width { get; } + public int Height { get; } + public int MipLevels { get; } public Collection MipPalletRefs { get => mipPalletRefs; } [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1814:PreferJaggedArraysOverMultidimensional", MessageId = "Member")] [CLSCompliant(false)] @@ -52,78 +42,56 @@ public class Texture : BaseNode const int Rgb565ByteWidth = 2; - internal Texture(OptReader reader, int textureNameOffset) : base(reader) + internal Texture(OptReader reader, NodeHeader nodeHeader) : base(reader, nodeHeader) { - // TODO: Check for alpha channel 0 26 node. - reader.ReadUnknownUseValue(0, this); - - group = reader.ReadInt32(); - - var palletAddressOffset = reader.ReadInt32(); + reader.Seek(nodeHeader.DataAddress); - reader.Seek(textureNameOffset); - name = reader.ReadString(9); + var paletteOffset = reader.ReadInt32(); + var paletteSize = reader.ReadInt32(); + var textureSizeIncludingMips = reader.ReadInt32(); + var completeSize = reader.ReadInt32(); + + Width = reader.ReadInt32(); + Height = reader.ReadInt32(); - reader.SeekShouldPointHere(palletAddressOffset, this); + var expectedSize = Height * Width; - // Skip pallet data for now to get pallet references. - var palletOffset = reader.ReadInt32(); + var finalPaletteOffset = paletteOffset; - reader.ReadUnknownUseValue(0, this); - - int size = reader.ReadInt32(); - int sizeWithMips = reader.ReadInt32(); + if (paletteSize != 0) + { + if (expectedSize == textureSizeIncludingMips) + { + finalPaletteOffset = nodeHeader.DataAddress + 24 + completeSize; + } + else + { + finalPaletteOffset = nodeHeader.DataAddress + 24 + expectedSize; + } + } - width = reader.ReadInt32(); - height = reader.ReadInt32(); + var nextMipImageSize = Width * Height / 4; + MipLevels = 1; + while (nextMipImageSize != 0) + { + MipLevels++; + nextMipImageSize /= 4; + } - texturePalletRefs = new byte[size]; - reader.Read(texturePalletRefs, 0, size); + var mipSize = Width * Height; - // Read Mip data - // Not sure we need these. - int nextMipWidth = width / 2, nextMipHeight = height / 2; - int mipSize = nextMipWidth * nextMipHeight; - int mipDataToRead = sizeWithMips - size; - while (mipDataToRead >= mipSize && mipSize > 0) + for (var mip = 0; mip < MipLevels; mip++) { byte[] nextMipRefs = new byte[mipSize]; reader.Read(nextMipRefs, 0, mipSize); mipPalletRefs.Add(nextMipRefs); - mipLevels++; - - nextMipWidth = nextMipWidth / 2; - nextMipHeight = nextMipHeight / 2; - mipDataToRead -= mipSize; - mipSize = nextMipWidth * nextMipHeight; } - // Now go back and find the texture pallet. // A few files have invalid palletOffsets. - if (palletOffset > reader.globalOffset) - { - pallet = reader.ReadPalette(palletOffset); - } - } - - /// - /// Generates RGB565 image from pallet and color data. - /// - /// Which pallet to use when generating the image (0-15) - /// byte[] containing the image, in bottom left to top right order. - public byte[] ToRgb565(int palletNumber) - { - var img = new Byte[texturePalletRefs.Length * 2]; - - for (int i = 0; i < texturePalletRefs.Length; i++) + if (finalPaletteOffset > reader.globalOffset) { - ushort color = pallet[palletNumber, texturePalletRefs[i]]; - - img[i * 2] = (byte)(color & 0xFF); // low order byte - img[(i * 2) + 1] = (byte)(color >> 8); // high order byte + pallet = reader.ReadPalette(finalPaletteOffset); } - - return img; } /// @@ -148,35 +116,27 @@ public void BlitRangeInto(byte[] target, int targetWidth, int targetHeight, int { // positive wrap int tY = (targetY + y) % targetHeight; - int sY = (sourceY + y) % height; + int sY = (sourceY + y) % Height; // negative wrap tY = tY < 0 ? targetHeight + tY : tY; - sY = sY < 0 ? height + sY : sY; + sY = sY < 0 ? Height + sY : sY; for (int x = 0; x < sizeX; x++) { // positive wrap int tX = (targetX + x) % targetWidth; - int sX = (sourceX + x) % width; + int sX = (sourceX + x) % Width; // negative wrap tX = tX < 0 ? targetWidth + tX : tX; - sX = sX < 0 ? width + sX : sX; + sX = sX < 0 ? Width + sX : sX; // target byte int t = (Rgb565ByteWidth * tX) + (Rgb565ByteWidth * targetWidth * tY); // source palette ref - int sr = sX + (width * sY); - - ushort color; - if (mipLevel > 0) - { - color = pallet[palletNumber, mipPalletRefs[mipLevel - 1][sr]]; - } - else - { - color = pallet[palletNumber, texturePalletRefs[sr]]; - } + int sr = sX + (Width * sY); + + ushort color = pallet[palletNumber, mipPalletRefs[mipLevel][sr]]; target[t] = (byte)(color & 0xFF); // low order byte target[t + 1] = (byte)(color >> 8); // high order byte diff --git a/XWOpt/OptNode/TextureReferenceByName.cs b/XWOpt/OptNode/TextureReferenceByName.cs index b54cb18..aba6e13 100644 --- a/XWOpt/OptNode/TextureReferenceByName.cs +++ b/XWOpt/OptNode/TextureReferenceByName.cs @@ -23,20 +23,15 @@ namespace SchmooTech.XWOpt.OptNode { public class TextureReferenceByName : BaseNode { - private string name; - private int id; + public string TextureName { get; set; } - public string Name { get => name; set => name = value; } - public int Id { get => id; set => id = value; } - - internal TextureReferenceByName(OptReader reader) : base(reader) + internal TextureReferenceByName(OptReader reader, NodeHeader nodeHeader) : base(reader, nodeHeader) { - reader.ReadUnknownUseValue(0, this); - reader.ReadUnknownUseValue(0, this); - id = reader.ReadInt32(); - - reader.FollowPointerToNextByte(this); - name = reader.ReadString(9); + if (nodeHeader.DataAddress != 0) + { + reader.Seek(nodeHeader.DataAddress); + TextureName = reader.ReadString(50); + } } } } diff --git a/XWOpt/OptNode/Types/Major.cs b/XWOpt/OptNode/Translation.cs similarity index 74% rename from XWOpt/OptNode/Types/Major.cs rename to XWOpt/OptNode/Translation.cs index 34980c2..22b6f3d 100644 --- a/XWOpt/OptNode/Types/Major.cs +++ b/XWOpt/OptNode/Translation.cs @@ -19,11 +19,17 @@ * OR OTHER DEALINGS IN THE SOFTWARE. */ -namespace SchmooTech.XWOpt.OptNode.Types +namespace SchmooTech.XWOpt.OptNode { - public enum Major + public class Translation : BaseNode { - Generic = 0, - Texture = 20, + public TVector3 TranslationVector { get; private set; } + + internal Translation(OptReader reader, NodeHeader nodeHeader) : base(reader, nodeHeader) + { + reader.Seek(nodeHeader.DataAddress); + + TranslationVector = reader.ReadVector(); + } } } diff --git a/XWOpt/OptNode/Types/GenericMinor.cs b/XWOpt/OptNode/Types/NodeType.cs similarity index 69% rename from XWOpt/OptNode/Types/GenericMinor.cs rename to XWOpt/OptNode/Types/NodeType.cs index 17663d6..908109f 100644 --- a/XWOpt/OptNode/Types/GenericMinor.cs +++ b/XWOpt/OptNode/Types/NodeType.cs @@ -21,22 +21,34 @@ namespace SchmooTech.XWOpt.OptNode.Types { - public enum GenericMinor + public enum NodeType { - Branch = 0, - FaceList = 1, - MainJump = 2, - MeshVertex = 3, - Info = 4, - TextureReferenceByName = 7, + Separator = 0, + IndexedFaceSet = 1, + Transform = 2, + VertexPosition = 3, + Translation = 4, + Rotation = 5, + Scale = 6, + UseTexture = 7, + Def = 8, + Material = 9, + MaterialBinding = 10, VertexNormal = 11, + VertexNormalBinding = 12, TextureVertex = 13, - TextureHeader = 20, + TextureVertexBinding = 14, + QuadMesh = 15, + FaceSet = 16, + TriangleStripSet = 17, + Group = 18, + BaseColour = 19, + Texture = 20, MeshLod = 21, Hardpoint = 22, - Transform = 23, - SkinSelector = 24, - MeshDescriptor = 25, + Pivot = 23, + CamoSwitch = 24, + ComponentInfo = 25, EngineGlow = 28, Unknown = 0xff, } diff --git a/XWOpt/OptNode/VertexNormals.cs b/XWOpt/OptNode/VertexNormals.cs index 301a2b4..f637882 100644 --- a/XWOpt/OptNode/VertexNormals.cs +++ b/XWOpt/OptNode/VertexNormals.cs @@ -25,21 +25,13 @@ namespace SchmooTech.XWOpt.OptNode { public class VertexNormals : BaseNode { - private Collection normals; + public Collection Normals { get; private set; } - public Collection Normals { get => normals; } - - internal VertexNormals(OptReader reader) : base(reader) + internal VertexNormals(OptReader reader, NodeHeader nodeHeader) : base(reader, nodeHeader) { - // unknown zeros - reader.ReadUnknownUseValue(0, this); - reader.ReadUnknownUseValue(0, this); - - var count = reader.ReadInt32(); - - reader.FollowPointerToNextByte(this); + reader.Seek(nodeHeader.DataAddress); - normals = reader.ReadVectorCollection(count); + Normals = reader.ReadVectorCollection(nodeHeader.DataCount); } } } diff --git a/XWOpt/OptNode/VertexUV.cs b/XWOpt/OptNode/VertexUV.cs index d584d95..29bba1d 100644 --- a/XWOpt/OptNode/VertexUV.cs +++ b/XWOpt/OptNode/VertexUV.cs @@ -25,21 +25,13 @@ namespace SchmooTech.XWOpt.OptNode { public class VertexUV : BaseNode { - private Collection vertices; + public Collection Vertices { get; } - public Collection Vertices { get => vertices; } - - internal VertexUV(OptReader reader) : base(reader) + internal VertexUV(OptReader reader, NodeHeader nodeHeader) : base(reader, nodeHeader) { - // unknown zeros - reader.ReadUnknownUseValue(0, this); - reader.ReadUnknownUseValue(0, this); - - var count = reader.ReadInt32(); - - reader.FollowPointerToNextByte(this); + reader.Seek(nodeHeader.DataAddress); - vertices = reader.ReadVectorCollection(count); + Vertices = reader.ReadVectorCollection(nodeHeader.DataCount); } } } diff --git a/XWOpt/OptReader.cs b/XWOpt/OptReader.cs index fcd119d..f648c01 100644 --- a/XWOpt/OptReader.cs +++ b/XWOpt/OptReader.cs @@ -26,7 +26,6 @@ using System.Collections.ObjectModel; using System.Globalization; using System.IO; -using System.Reflection; using System.Text; namespace SchmooTech.XWOpt @@ -62,32 +61,6 @@ internal OptReader(Stream stream, Action logger) : base(stream) this.logger = logger; } - internal List ReadChildren(object context) - { - var count = ReadInt32(); - if (0 == count) - { - return new List(); - } - - var offset = ReadInt32(); - - // Reverse jump count and offset? - if (version <= 2) - { - ReadUnknownUseValue(1, context); - } - else - { - ReadUnknownUseValue(0, context); - } - ReadInt32(); // skip reverse jump pointer - - // warn if caller is skipping anything else - SeekShouldPointHere(offset, context); - return ReadChildren(count, offset, context); - } - internal List ReadChildren(int count, int jumpListOffset, object context) { var nodes = new List(); @@ -98,7 +71,7 @@ internal List ReadChildren(int count, int jumpListOffset, object conte int nextNode = ReadInt32(); if (nextNode != 0) { - nodes.Add(ReadNodeAt(nextNode, context)); + nodes.Add(ReadNodeAt(nextNode, context, null)); } } @@ -106,108 +79,66 @@ internal List ReadChildren(int count, int jumpListOffset, object conte } // Instantiates the correct node type based on IDs found. - public BaseNode ReadNodeAt(int offset, object context) + public BaseNode ReadNodeAt(int offset, object context, BaseNode parent) { - int preHeaderOffset = 0; - if (nodeCache.ContainsKey(offset)) { return nodeCache[offset]; } Seek(offset); - int majorId = ReadInt32(); - int minorId = ReadInt32(); - - // Edge case: one block type doesn't start with major/minor type id and actually start with another offset. - // So peek ahead one more long and shuffle numbers where they go. - // This may not work if globalOffset is 0. - // Should be a pointer to an offset containing string "Tex00000" or similar. - int peek = ReadInt32(); - if (majorId > globalOffset && minorId == (long)Major.Texture) - { - preHeaderOffset = majorId; - majorId = minorId; - minorId = peek; - } - else if (majorId > globalOffset && minorId == 0 && peek == 1) - { - // This is a weird subtype found in SHUTTLE.OPT - return new NamedNodeCollection(this, majorId); - } - else - { - BaseStream.Seek(-4, SeekOrigin.Current); - } + + var nodeHeader = new NodeHeader(this, parent); // Figure out the type of node and build appropriate object. BaseNode node; - switch (majorId) + + switch(nodeHeader.NodeType) { - case (int)Major.Generic: - switch (minorId) - { - case (int)GenericMinor.Branch: - node = new NodeCollection(this) as BaseNode; - break; - case (int)GenericMinor.MeshVertex: - node = MakeGenericNode(typeof(MeshVertices<>), new Type[] { Vector3T }); - break; - case (int)GenericMinor.TextureVertex: - node = MakeGenericNode(typeof(VertexUV<>), new Type[] { Vector2T }); - break; - case (int)GenericMinor.TextureReferenceByName: - node = new TextureReferenceByName(this) as BaseNode; - break; - case (int)GenericMinor.VertexNormal: - node = MakeGenericNode(typeof(VertexNormals<>), new Type[] { Vector3T }); - break; - case (int)GenericMinor.Hardpoint: - node = MakeGenericNode(typeof(Hardpoint<>), new Type[] { Vector3T }); - break; - case (int)GenericMinor.Transform: - node = MakeGenericNode(typeof(RotationInfo<>), new Type[] { Vector3T }); - break; - case (int)GenericMinor.MeshLod: - node = new LodCollection(this) as BaseNode; - break; - case (int)GenericMinor.FaceList: - node = MakeGenericNode(typeof(FaceList<>), new Type[] { Vector3T }); - break; - case (int)GenericMinor.SkinSelector: - node = new SkinCollection(this) as BaseNode; - break; - case (int)GenericMinor.MeshDescriptor: - node = MakeGenericNode(typeof(PartDescriptor<>), new Type[] { Vector3T }); - break; - case (int)GenericMinor.EngineGlow: - node = MakeGenericNode(typeof(EngineGlow<>), new Type[] { Vector3T }); - break; - default: - logger?.Invoke("Found unknown node type " + majorId + " " + minorId + " at " + BaseStream.Position + " context:" + context); - node = new BaseNode(this); - break; - } + case NodeType.Separator: + node = new SeparatorNode(this, nodeHeader); break; - - case (int)Major.Texture: - switch (minorId) - { - case (int)TextureMinor.Texture: - node = new Texture(this, preHeaderOffset); - break; - case (int)TextureMinor.TextureWithAlpha: - node = new Texture(this, preHeaderOffset); - break; - default: - logger?.Invoke("Found unknown node type " + majorId + " " + minorId + " at " + BaseStream.Position + " context:" + context); - node = new Texture(this, preHeaderOffset); - break; - } + case NodeType.IndexedFaceSet: + node = MakeGenericNode(typeof(FaceList<>), new Type[] { Vector3T }, nodeHeader); + break; + case NodeType.VertexPosition: + node = MakeGenericNode(typeof(MeshVertices<>), new Type[] { Vector3T }, nodeHeader); + break; + case NodeType.Translation: + node = MakeGenericNode(typeof(Translation<>), new Type[] { Vector3T }, nodeHeader); + break; + case NodeType.UseTexture: + node = new TextureReferenceByName(this, nodeHeader); + break; + case NodeType.VertexNormal: + node = MakeGenericNode(typeof(VertexNormals<>), new Type[] { Vector3T }, nodeHeader); + break; + case NodeType.TextureVertex: + node = MakeGenericNode(typeof(VertexUV<>), new Type[] { Vector2T }, nodeHeader); + break; + case NodeType.Texture: + node = new Texture(this, nodeHeader); + break; + case NodeType.MeshLod: + node = new LodCollection(this, nodeHeader); + break; + case NodeType.Hardpoint: + node = MakeGenericNode(typeof(Hardpoint<>), new Type[] { Vector3T }, nodeHeader); + break; + case NodeType.Pivot: + node = MakeGenericNode(typeof(RotationInfo<>), new Type[] { Vector3T }, nodeHeader); + break; + case NodeType.CamoSwitch: + node = new SkinCollection(this, nodeHeader); + break; + case NodeType.ComponentInfo: + node = MakeGenericNode(typeof(PartDescriptor<>), new Type[] { Vector3T }, nodeHeader); break; default: - node = new BaseNode(this); + logger?.Invoke("Found unknown node type " + nodeHeader.NodeType + " " + nodeHeader.Name + " at " + BaseStream.Position + " context:" + context); + node = new BaseNode(this, nodeHeader); break; + } nodeCache[offset] = node; @@ -277,19 +208,12 @@ internal void ReadUnknownUseValue(int expected, object context) } // Create OptNodes with reflection avoids generic parameter explosion. - BaseNode MakeGenericNode(Type nodeType, Type[] GenericParams, object constructorArgs = null) + BaseNode MakeGenericNode(Type nodeType, Type[] GenericParams, NodeHeader nodeHeader) { var closedGeneric = nodeType.MakeGenericType(GenericParams); - if (null != constructorArgs) - { - return closedGeneric.GetConstructor(new Type[] { this.GetType(), constructorArgs.GetType() }).Invoke(new object[] { this, constructorArgs }) as BaseNode; - } - else - { - var cotr = closedGeneric.GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, null, new Type[] { this.GetType() }, null); - return cotr.Invoke(new object[] { this }) as BaseNode; - } + var ctor = closedGeneric.GetConstructor(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic, null, System.Reflection.CallingConventions.HasThis, new Type[] { this.GetType(), typeof(NodeHeader) }, null); + return ctor.Invoke(new object[] { this, nodeHeader }) as BaseNode; } internal string ReadString(int maxLen) diff --git a/XWOpt/TexturePallet.cs b/XWOpt/TexturePallet.cs index 32b4ea2..bae9963 100644 --- a/XWOpt/TexturePallet.cs +++ b/XWOpt/TexturePallet.cs @@ -35,19 +35,28 @@ public class TexturePallet // Number of colors for each pallet fixed in the format specs. const int ColorCount = 256; + readonly byte[,] eightBitPalettes = new byte[PalletCount, ColorCount]; // Colour Index for 256 Colour [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1814:PreferJaggedArraysOverMultidimensional", MessageId = "Member")] - readonly ushort[,] pallets = new ushort[PalletCount, ColorCount]; + readonly ushort[,] sixteenBitPalettes = new ushort[PalletCount, ColorCount]; // R5G6B5 for 16 bit Colour public TexturePallet() { } internal TexturePallet(OptReader reader) { + for (int i = 0; i < PalletCount; i++) + { + for (int j = 0; j < ColorCount; j++) + { + eightBitPalettes[i, j] = reader.ReadByte(); + } + } + // For some reason pallets 0-7 seem to be padding, 8-15 appear to be increasing brightness for (int i = 0; i < PalletCount; i++) { for (int j = 0; j < ColorCount; j++) { - pallets[i, j] = reader.ReadUInt16(); + sixteenBitPalettes[i, j] = reader.ReadUInt16(); } } } @@ -66,7 +75,7 @@ public void SetValue(int palletNumber, int which, ushort color) { BoundsCheck(palletNumber, which); - pallets[palletNumber, which] = color; + sixteenBitPalettes[palletNumber, which] = color; } [CLSCompliant(false)] @@ -74,7 +83,7 @@ public ushort GetValue(int palletNumber, int which) { BoundsCheck(palletNumber, which); - return pallets[palletNumber, which]; + return sixteenBitPalettes[palletNumber, which]; } private static void BoundsCheck(int palletNumber, int which) diff --git a/XWOpt/XWOpt.csproj b/XWOpt/XWOpt.csproj index 303a683..b9071c8 100644 --- a/XWOpt/XWOpt.csproj +++ b/XWOpt/XWOpt.csproj @@ -45,22 +45,21 @@ - - + + + + - - - diff --git a/XWOptUnity/CraftFactory.cs b/XWOptUnity/CraftFactory.cs index b476635..6ae3671 100644 --- a/XWOptUnity/CraftFactory.cs +++ b/XWOptUnity/CraftFactory.cs @@ -23,6 +23,7 @@ using SchmooTech.XWOpt.OptNode; using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.IO; using System.Linq; using UnityEngine; @@ -36,7 +37,7 @@ namespace SchmooTech.XWOptUnity /// The part object that has been instantiated /// The XWOpt part descriptor associated with the part /// The XWOpt rotation information associated with the part - public delegate void ProcessPartHandler(GameObject part, PartDescriptor descriptor, RotationInfo rotationInfo); + public delegate void ProcessPartHandler(int partIndex, GameObject part, PartDescriptor descriptor, RotationInfo rotationInfo); /// /// Callback for game specific setup of hardpoint objects after instantiation. @@ -57,6 +58,12 @@ namespace SchmooTech.XWOptUnity /// The place on the model that is shown in the targeting window. public delegate void ProcessTargetGroupHandler(GameObject targetGroup, int id, PartType type, Vector3 location); + /// + /// Call for game specific inspection and adjustment of the Root Nodes for this craft. The collection can be adjusted for game specific needs, before the parts are baked into the final craft. + /// + /// The root node collection from the OPT reader + public delegate void ProcessPartHierarchyBeforeBake(Collection rootNodes); + /// /// Reads OPT model and helps instantiate GameObjects based on useful data in the file. /// @@ -104,6 +111,11 @@ public class CraftFactory /// public ProcessTargetGroupHandler ProcessTargetGroup { get; set; } + /// + /// Callback for game specific adjustment or inspection of the OptNode hierarchy. + /// + public ProcessPartHierarchyBeforeBake ProcessHierarchy { get; set; } + /// /// The shader to use on the materials. Default is Unity "XwOptUnity/TextureAtlas" shader. /// Shader must support atlas texture tiling. @@ -208,7 +220,7 @@ public CraftFactory(string fileName) CraftFactoryImpl(); } - public CraftFactory(Stream stream) + public CraftFactory(Stream stream, ProcessPartHierarchyBeforeBake processPartHierarchy = null) { if (stream is null) { @@ -219,11 +231,15 @@ public CraftFactory(Stream stream) Opt.Read(stream); + ProcessHierarchy = processPartHierarchy; + CraftFactoryImpl(); } private void CraftFactoryImpl() { + ProcessHierarchy?.Invoke(Opt.RootNodes); + // Determine total size of the craft. Used for LOD size. bool foundDescriptor = false; Vector3 upperBound = new Vector3(); // upper bound @@ -257,9 +273,17 @@ private void CraftFactoryImpl() Size = float.PositiveInfinity; } - foreach (NodeCollection shipPart in Opt.RootNodes.OfType()) + for (int i = 0, partIndex = 0; i < Opt.RootNodes.Count; i++, partIndex++) { - var factory = new PartFactory(this, shipPart); + var shipPart = Opt.RootNodes[i] as SeparatorNode; + + if (shipPart == null) + { + partIndex--; + continue; + } + + var factory = new PartFactory(this, shipPart, partIndex); if (null == factory.descriptor || null == TargetingGroupBase) { @@ -350,9 +374,9 @@ static Vector3 RotateIntoUnitySpace(Vector3 v) /// /// This will trigger bake operations if any options have been modified that would affect the baking process. /// - public GameObject CreateCraftObject() + public GameObject CreateCraftObject(GameObject craftBase) { - return CreateCraftObject(0); + return CreateCraftObject(0, craftBase); } /// @@ -364,14 +388,17 @@ public GameObject CreateCraftObject() /// Which skin to use. This is usually based on which squadron, EG Red, Blue, Gold, Alpha, Beta, etc. /// If the model has no skins, this is ignored. /// - public GameObject CreateCraftObject(int skin) + /// + /// The override for the root GameObject to Instantiate for the Craft instead of the default CraftBase. + /// + public GameObject CreateCraftObject(int skin, GameObject craftBaseOverride = null) { if (NeedsMainThreadBake) { MainThreadBake(); } - var craft = UnityEngine.Object.Instantiate(CraftBase); + var craft = UnityEngine.Object.Instantiate(craftBaseOverride == null ? CraftBase : craftBaseOverride); foreach (var targetGroup in targetGroups) { diff --git a/XWOptUnity/HardpointFactory.cs b/XWOptUnity/HardpointFactory.cs index 7861dc9..199cc31 100644 --- a/XWOptUnity/HardpointFactory.cs +++ b/XWOptUnity/HardpointFactory.cs @@ -33,11 +33,11 @@ internal HardpointFactory(CraftFactory part) Craft = part; } - internal GameObject MakeHardpoint(GameObject parent, Hardpoint hardpointNode, PartDescriptor partDescriptor) + internal GameObject MakeHardpoint(GameObject parent, Hardpoint hardpointNode, PartDescriptor partDescriptor, RotationInfo rotationInfo) { var hardpointObj = Object.Instantiate(Craft.HardpointBase) as GameObject; hardpointObj.name = hardpointNode.WeaponType.ToString(); - Helpers.AttachTransform(parent, hardpointObj, hardpointNode.Location); + Helpers.AttachTransform(parent, hardpointObj, hardpointNode.Location - rotationInfo.Offset); Craft.ProcessHardpoint?.Invoke(hardpointObj, partDescriptor, hardpointNode); diff --git a/XWOptUnity/LodFactory.cs b/XWOptUnity/LodFactory.cs index dc50514..d24ee34 100644 --- a/XWOptUnity/LodFactory.cs +++ b/XWOptUnity/LodFactory.cs @@ -31,39 +31,39 @@ namespace SchmooTech.XWOptUnity { class LodFactory : IBakeable { - NodeCollection _lodNode; + SeparatorNode _lodNode; readonly int _index; - readonly float _threshold; + float _threshold; + Bounds _bounds; PartFactory Part { get; set; } - Dictionary skinSpecificSubmeshes = new Dictionary(); + List skinSpecificSubmeshes = new List(); - /// - /// Fudge factor for increasing LOD cutover distance based on increased resolution and improved - /// rendering technology on modern computers. - /// - /// 640x480 -> 1920x1080 = ~3x increased resolution - /// - /// Add additional subjective multipliers based on anti-aliasing (4x) and texture filtering (4x). - /// - /// 3 * (4+4) - /// - public const float DetailImprovementFudgeFactor = 24f; + public const float DetailImprovementFudgeFactor = Mathf.PI / 3f; - internal LodFactory(PartFactory part, NodeCollection lodNode, int index, float threshold) + internal LodFactory(PartFactory part, SeparatorNode lodNode, Bounds bounds, int index, float threshold) { Part = part; _lodNode = lodNode; _index = index; + _bounds = bounds; if (threshold > 0 && threshold < float.PositiveInfinity) { - // OPT LOD thresholds are based on distance. Unity is based on screen height. - _threshold = (float)(1 / (DetailImprovementFudgeFactor * Math.Atan(1 / threshold))); + _threshold = threshold; } else { _threshold = 0; } + + if (_threshold != 0f) + { + var distance = (1f / _threshold) * (1600f / 65536f); + + // OPT LOD thresholds are based on distance. Unity is based on screen height. + var screenAngle = Mathf.Atan(_bounds.extents.magnitude / distance); + _threshold = screenAngle / DetailImprovementFudgeFactor; + } } Mesh MakeMesh(FaceList faceList, string textureName) @@ -161,7 +161,7 @@ Mesh MakeMesh(FaceList faceList, string textureName) { Debug.LogError(string.Format(CultureInfo.CurrentCulture, "UV {0}/{4} out of bound {1:X} {2} {3} ", vt.uvId, faceList.OffsetInFile, i, j, optUV.Vertices.Count)); } - meshVerts.Add(optVerts.Vertices[vt.vId]); + meshVerts.Add(optVerts.Vertices[vt.vId] - Part.rotationInfo.Offset); meshNorms.Add(normal.normalized); // translate uv to atlas space @@ -227,13 +227,14 @@ public void MainThreadBake() for (int skin = 0; skin < skinCount; skin++) { var subMeshes = new List(); - foreach (var assoc in WalkTextureAssociations(skin)) { - subMeshes.Add(new CombineInstance() + var combineInstance = new CombineInstance() { mesh = MakeMesh(assoc.faceList, assoc.textureName) - }); + }; + + subMeshes.Add(combineInstance); } var mesh = new Mesh(); @@ -241,7 +242,7 @@ public void MainThreadBake() mesh.RecalculateBounds(); mesh.name = ToString() + "_skin" + skin; - skinSpecificSubmeshes[skin] = mesh; + skinSpecificSubmeshes.Add(mesh); } } @@ -271,11 +272,10 @@ internal LOD MakeLOD(GameObject parent, int skin) lodObj.AddComponent(); Helpers.AttachTransform(parent, lodObj); - // Lower LODs may not have skin specific textures even if higher LODs do. - if (skin >= skinSpecificSubmeshes.Count) - skin = 0; + var skinMeshSwitch = lodObj.AddComponent(); + skinMeshSwitch.Initialise(skinSpecificSubmeshes.ToArray()); - lodObj.GetComponent().sharedMesh = skinSpecificSubmeshes[skin]; + skinMeshSwitch.SwitchSkin(skin); lodObj.GetComponent().sharedMaterial = Part.Craft.TextureAtlas.Material; return new LOD(_threshold, new Renderer[] { lodObj.GetComponent() }); @@ -284,7 +284,7 @@ internal LOD MakeLOD(GameObject parent, int skin) /// /// Which texture to use for which submesh /// - struct TextureMeshAssociation + class TextureMeshAssociation { public string textureName; public FaceList faceList; @@ -296,61 +296,64 @@ IEnumerable WalkTextureAssociations(int skin) // them besides that the texture preceeds the mesh in this list. // So keep track of the last mesh or mesh reference we've seen and apply it to the next mesh. // If there is more than one texture preceding a mesh, the last one must be used. - string previousTexture = null; + var previousTexture = new TextureMeshAssociation + { + faceList = null, + textureName = null + }; - // Workaround for lambda shuttle. Fuselage parts have a weird sub-part wrapper. - var SearchedNodes = new List(); foreach (var child in _lodNode.Children) { - switch (child) + foreach (var assoc in WalkTextureAssociations(child, skin, previousTexture)) { - case NamedNodeCollection named: - SearchedNodes.AddRange((named.Children[0] as NodeCollection).Children); - break; - default: - SearchedNodes.Add(child); - break; + yield return assoc; } } + } - foreach (var child in SearchedNodes) + IEnumerable WalkTextureAssociations(BaseNode child, int skin, TextureMeshAssociation previousTexture) + { + switch (child) { - switch (child) - { - case XWOpt.OptNode.Texture t: - previousTexture = t.Name; - break; - case TextureReferenceByName t: - previousTexture = t.Name; - break; - case SkinCollection selector: - // Workaround: Some training platform parts have varying number of skins. - int usedSkin = skin % selector.Children.Count; - switch (selector.Children[usedSkin]) - { - case XWOpt.OptNode.Texture t: - previousTexture = t.Name; - break; - case TextureReferenceByName t: - previousTexture = t.Name; - break; - } - break; - case FaceList f: - // Some meshes are not preceeded by a texture. In this case use a global default texture. - if (null == previousTexture) - { - previousTexture = "Tex00000"; - } + case XWOpt.OptNode.Texture t: + previousTexture.textureName = t.Name; + break; + case TextureReferenceByName t: + previousTexture.textureName = t.TextureName; + break; + case SkinCollection selector: + // Workaround: Some training platform parts have varying number of skins. + int usedSkin = skin % selector.Children.Count; + switch (selector.Children[usedSkin]) + { + case XWOpt.OptNode.Texture t: + previousTexture.textureName = t.Name; + break; + case TextureReferenceByName t: + previousTexture.textureName = t.TextureName; + break; + } + yield break; + case FaceList f: + // Some meshes are not preceeded by a texture. In this case use a global default texture. + if (null == previousTexture.textureName) + { + previousTexture.textureName = "Tex00000"; + } + + previousTexture.faceList = f; - TextureMeshAssociation association; - association.textureName = previousTexture; - association.faceList = f; + yield return previousTexture; + previousTexture.textureName = null; - yield return association; + break; + } - previousTexture = null; - break; + foreach (var subNode in child.Children) + { + foreach (var assoc in WalkTextureAssociations(subNode, skin, previousTexture)) + { + yield return assoc; } } } diff --git a/XWOptUnity/PartFactory.cs b/XWOptUnity/PartFactory.cs index f5b0f95..d3cd063 100644 --- a/XWOptUnity/PartFactory.cs +++ b/XWOptUnity/PartFactory.cs @@ -19,18 +19,18 @@ * OR OTHER DEALINGS IN THE SOFTWARE. */ -using SchmooTech.XWOpt.OptNode; -using System; using System.Collections.Generic; using System.Linq; +using SchmooTech.XWOpt.OptNode; using UnityEngine; namespace SchmooTech.XWOptUnity { internal class PartFactory { - internal NodeCollection ShipPart { get; set; } + internal SeparatorNode ShipPart { get; set; } internal CraftFactory Craft { get; set; } + internal int PartIndex { get; set; } [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal bool CreateChildTarget @@ -62,19 +62,25 @@ internal bool CreateChildTarget HardpointFactory hardpointFactory; TargetPointFactory targetPoint = null; - internal PartFactory(CraftFactory craft, NodeCollection shipPart) + internal PartFactory(CraftFactory craft, SeparatorNode shipPart, int partIndex) { Craft = craft; ShipPart = shipPart; + PartIndex = partIndex; hardpointFactory = new HardpointFactory(craft); // Fetch ship part top level data descriptor = ShipPart.Children.OfType>().First(); - rotationInfo = ShipPart.Children.OfType>().First(); + rotationInfo = ShipPart.Children.OfType>().FirstOrDefault(); verts = ShipPart.OfType>().First(); vertUV = ShipPart.OfType>().First(); vertNormals = ShipPart.OfType>().First(); + if (rotationInfo == null) + { + rotationInfo = new RotationInfo(Vector3.up, Vector3.right, Vector3.forward); + } + // All meshes are contained inside of a MeshLod // There is only one MeshLod per part. // Each LOD is BranchNode containing a collection of meshes and textures it uses. @@ -89,16 +95,19 @@ internal PartFactory(CraftFactory craft, NodeCollection shipPart) // Out of order LODs are probably broken. See TIE98 CAL.OPT. // If this distance is greater than the previous distance (smaller number means greater render distance), // then this LOD is occluded by the previous LOD. - if (distance > 0 && i > 0 && distance > lodNode.MaxRenderDistance[i - 1]) continue; + if (distance > 0 && i > 0 && distance > lodNode.MaxRenderDistance[i - 1]) + { + continue; + } - if (lodNode.Children[i] is NodeCollection branch) + if (lodNode.Children[i] is SeparatorNode branch) { - _lods.Add(new LodFactory(this, branch, newLodIndex, distance)); + _lods.Add(new LodFactory(this, branch, new Bounds(descriptor.HitboxCenterPoint, descriptor.HitboxSpan), newLodIndex, distance)); newLodIndex++; } else { - Debug.LogError("Skipping LOD" + newLodIndex + " as it is not a NodeCollection"); + Debug.LogError("Skipping LOD" + newLodIndex + " as it is not a Separator Node"); } } } @@ -129,7 +138,7 @@ internal GameObject CreatePart(GameObject parent, int skin) // Generate hardpoints foreach (var hardpoint in ShipPart.OfType>()) { - hardpointFactory.MakeHardpoint(partObj, hardpoint, descriptor); + hardpointFactory.MakeHardpoint(partObj, hardpoint, descriptor, rotationInfo); } if (null != targetPoint) @@ -137,9 +146,9 @@ internal GameObject CreatePart(GameObject parent, int skin) Helpers.AttachTransform(partObj, targetPoint.CreateTargetPoint(), descriptor.HitboxCenterPoint); } - Helpers.AttachTransform(parent, partObj); + Helpers.AttachTransform(parent, partObj, rotationInfo.Offset); - Craft.ProcessPart?.Invoke(partObj, descriptor, rotationInfo); + Craft.ProcessPart?.Invoke(PartIndex, partObj, descriptor, rotationInfo); return partObj; } diff --git a/XWOptUnity/SkinMeshSwitch.cs b/XWOptUnity/SkinMeshSwitch.cs new file mode 100644 index 0000000..fe3177a --- /dev/null +++ b/XWOptUnity/SkinMeshSwitch.cs @@ -0,0 +1,94 @@ +/* + * Copyright 2017 Jason McNew + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this + * software and associated documentation files (the "Software"), to deal in the Software + * without restriction, including without limitation the rights to use, copy, modify, + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be included in all copies + * or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR + * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ + +using UnityEngine; + +#if UNITY_EDITOR +using UnityEditor; +#endif + +namespace SchmooTech.XWOptUnity +{ + [RequireComponent(typeof(MeshFilter))] + public class SkinMeshSwitch : MonoBehaviour + { + public Mesh [] _skins; + internal MeshFilter _meshFilter; + + public void Initialise(Mesh [] skins) + { + _skins = skins; + if (!didAwake) + { + Awake(); + } + } + + public void Awake() + { + if (_meshFilter == null) + { + _meshFilter = GetComponent(); + } + } + + public void SwitchSkin(int skin) + { + if (!didAwake) + { + Awake(); + } + + // Lower LODs may not have skin specific textures even if higher LODs do. + if (skin >= _skins.Length) + { + skin = 0; + } + + _meshFilter.sharedMesh = _skins[skin]; + } + } + +#if UNITY_EDITOR + [CustomEditor(typeof(SkinMeshSwitch))] + public class SkinMeshSwitchEditor : Editor + { + public override void OnInspectorGUI() + { + var skinMeshFilter = target as SkinMeshSwitch; + + if (skinMeshFilter._skins != null) + { + for (var i = 0; i < skinMeshFilter._skins.Length; i++) + { + var skin = skinMeshFilter._skins[i]; + if (GUILayout.Button($"Skin {i}: {skin.name}")) + { + skinMeshFilter.SwitchSkin(i); + } + } + } + + base.OnInspectorGUI(); + } + } +#endif +} diff --git a/XWOptUnity/TextureAtlas.cs b/XWOptUnity/TextureAtlas.cs index 048d45b..221ea31 100644 --- a/XWOptUnity/TextureAtlas.cs +++ b/XWOptUnity/TextureAtlas.cs @@ -111,14 +111,13 @@ int VersionSpecificPaletteNumber(bool emissive) { case 1: case 2: - // TIE98/Xwing98/XvT pallet 0-7 are 0xCDCD paddding. Pallet 8 is very dark, pallet 15 is normal level. if (emissive) { - palette = 8; + palette = 0; } else { - palette = 15; + palette = 8; } break; default: @@ -137,6 +136,30 @@ int VersionSpecificPaletteNumber(bool emissive) return palette; } + void UnpackColourR5G6B5(byte[] data, int offset, out float red, out float green, out float blue) + { + // Unpack RGB565, low order byte first. + red = (data[offset + 1] >> 3) / 31f; + green = (((data[offset + 1] & 7) << 3) | (data[offset] >> 5)) / 63f; + blue = (data[offset] & 0x1F) / 31f; + } + + Color UnpackColourR5G6B5(byte[] data, int offset) + { + UnpackColourR5G6B5(data, offset, out var red, out var green, out var blue); + return new Color(red, green, blue); + } + + void PackColourR5G6B5(Color rgb, byte[] data, int offset) + { + byte a_r = (byte)(rgb.r * 31f); + byte a_g = (byte)(rgb.g * 63f); + byte a_b = (byte)(rgb.b * 31f); + + data[offset + 1] = (byte)((a_r << 3) | (a_g >> 3)); + data[offset] = (byte)(((a_g & 0x1F) << 5) | a_b); + } + // The lowest lighted pallet will have bright areas representing self-lighting. void RebalanceColorsForEmissive() { @@ -145,41 +168,22 @@ void RebalanceColorsForEmissive() if (_emissive[i] == 0 && _emissive[i + 1] == 0) continue; - // Unpack RGB565, low order byte first. - Color albedo = new Color( - (_albido[i + 1] >> 3) / 31f, - (((_albido[i + 1] & 7) << 3) | (_albido[i] >> 5)) / 63f, - (_albido[i] & 0x1F) / 31f - ); - - Color emissive = new Color( - (_emissive[i + 1] >> 3) / 31f, - (((_emissive[i + 1] & 7) << 3) | (_emissive[i] >> 5)) / 63f, - (_emissive[i] & 0x1F) / 31f - ); - - // Lowest brightness palette has too much ambient light to make a good emissive texture. - // So we try to squash the ambient part without reducing brightness of the emissive features too much. - emissive.r = Mathf.Pow(emissive.r, emissiveExponent.Value); - emissive.g = Mathf.Pow(emissive.g, emissiveExponent.Value); - emissive.b = Mathf.Pow(emissive.b, emissiveExponent.Value); - - // The material will be oversaturated if the emissive layer is simply layerd over the main texture. - // So reduce the albedo by the emissive part. - albedo -= emissive; + Color minLighting = UnpackColourR5G6B5(_emissive, i); + Color maxLighting = UnpackColourR5G6B5(_albido, i); // Repack RGB565 - byte a_r = (byte)(albedo.r * 31f); - byte a_g = (byte)(albedo.g * 63f); - byte a_b = (byte)(albedo.b * 31f); - _albido[i + 1] = (byte)((a_r << 3) | (a_g >> 3)); - _albido[i] = (byte)(((a_g & 0x1F) << 5) | a_b); - - byte e_r = (byte)(emissive.r * 31f); - byte e_g = (byte)(emissive.g * 63f); - byte e_b = (byte)(emissive.b * 31f); - _emissive[i + 1] = (byte)((e_r << 3) | (e_g >> 3)); - _emissive[i] = (byte)(((e_g & 0x1F) << 5) | e_b); + // Quick and dirty heuristic to find emissive pixels. Must be 25% lit at maximum lighting (stops shadows being emissive), + // and must not change more than 4% across all light levels (stops normal non-emissive pixels) + if (maxLighting.maxColorComponent > 0.25f && (maxLighting - minLighting).maxColorComponent < 0.04f) + { + PackColourR5G6B5(Color.black, _albido, i); + PackColourR5G6B5(minLighting, _emissive, i); + } + else + { + PackColourR5G6B5(maxLighting, _albido, i); + PackColourR5G6B5(Color.black, _emissive, i); + } } } } diff --git a/XWOptUnity/XwOptUnity.csproj b/XWOptUnity/XwOptUnity.csproj index 60cdec2..43827d7 100644 --- a/XWOptUnity/XwOptUnity.csproj +++ b/XWOptUnity/XwOptUnity.csproj @@ -61,6 +61,7 @@ +