diff --git a/STROOP.Core/GameMemoryAccess/SavestateDecompression.cs b/STROOP.Core/GameMemoryAccess/SavestateDecompression.cs new file mode 100644 index 000000000..fe0597269 --- /dev/null +++ b/STROOP.Core/GameMemoryAccess/SavestateDecompression.cs @@ -0,0 +1,98 @@ +using System.IO.Compression; + +namespace STROOP.Core.GameMemoryAccess; + +public static class SavestateDecompression +{ + static readonly byte[] Lz4Magic = { 0x04, 0x22, 0x4D, 0x18 }; + + public static bool IsLz4(byte[] data) => data.Length >= 4 && data.AsSpan(0, 4).SequenceEqual(Lz4Magic); + + public static byte[] Decompress(byte[] data) + { + if (IsLz4(data)) + return DecompressLz4Frame(data); + + using MemoryStream input = new MemoryStream(data); + using GZipStream gzipStream = new GZipStream(input, CompressionMode.Decompress); + using MemoryStream unzip = new MemoryStream(); + gzipStream.CopyTo(unzip); + return unzip.ToArray(); + } + + static byte[] DecompressLz4Frame(byte[] data) + { + int p = 4; + byte flg = data[p]; + p += 2; + bool blockChecksum = (flg & 0x10) != 0; + bool contentSize = (flg & 0x08) != 0; + bool dictId = (flg & 0x01) != 0; + if (contentSize) p += 8; + if (dictId) p += 4; + p += 1; + + MemoryStream output = new MemoryStream(data.Length * 5); + while (p + 4 <= data.Length) + { + uint blockSize = BitConverter.ToUInt32(data, p); + p += 4; + if (blockSize == 0) + break; + bool uncompressed = (blockSize & 0x80000000) != 0; + int size = (int)(blockSize & 0x7FFFFFFF); + if (uncompressed) + output.Write(data, p, size); + else + DecodeBlock(data, p, size, output); + p += size; + if (blockChecksum) p += 4; + } + + return output.ToArray(); + } + + static void DecodeBlock(byte[] src, int start, int length, MemoryStream output) + { + int p = start, end = start + length; + while (p < end) + { + int token = src[p++]; + int literals = token >> 4; + if (literals == 15) + { + int b; + do literals += b = src[p++]; + while (b == 255); + } + + output.Write(src, p, literals); + p += literals; + if (p >= end) + break; + + int offset = src[p] | src[p + 1] << 8; + p += 2; + int matchLength = token & 15; + if (matchLength == 15) + { + int b; + do matchLength += b = src[p++]; + while (b == 255); + } + + matchLength += 4; + + byte[] buffer = output.GetBuffer(); + int matchStart = (int)output.Length - offset; + if (offset >= matchLength) + output.Write(buffer, matchStart, matchLength); + else + for (int i = 0; i < matchLength; i++) + { + buffer = output.GetBuffer(); + output.WriteByte(buffer[matchStart + i]); + } + } + } +} diff --git a/STROOP.Core/GameMemoryAccess/StFileIO.cs b/STROOP.Core/GameMemoryAccess/StFileIO.cs index c598e135c..df706ef6f 100644 --- a/STROOP.Core/GameMemoryAccess/StFileIO.cs +++ b/STROOP.Core/GameMemoryAccess/StFileIO.cs @@ -28,11 +28,7 @@ public StFileIO(string path) : base() private void LoadMemory() { - using FileStream? fileStream = new FileStream(_path, FileMode.Open); - using GZipStream? gzipStream = new GZipStream(fileStream, CompressionMode.Decompress); - using MemoryStream unzip = new MemoryStream(); - gzipStream.CopyTo(unzip); - _data = unzip.ToArray(); + _data = SavestateDecompression.Decompress(File.ReadAllBytes(_path)); } public void SaveMemory(string path) diff --git a/STROOP.Core/ProcessStream.cs b/STROOP.Core/ProcessStream.cs index 71b4b6fa2..9c7a1325f 100644 --- a/STROOP.Core/ProcessStream.cs +++ b/STROOP.Core/ProcessStream.cs @@ -410,6 +410,13 @@ public bool RefreshRam() } } + public void CopyCachedRam(byte[] destination) + { + var ram = _ram; + lock (ram) + Buffer.BlockCopy(ram, 0, destination, 0, Math.Min(ram.Length, destination.Length)); + } + public bool GetAllRam(out byte[] allRam) { allRam = new byte[MAX_RAM_SIZE]; diff --git a/STROOP/Resources/Shaders/LevelModel.frag.glsl b/STROOP/Resources/Shaders/LevelModel.frag.glsl new file mode 100644 index 000000000..88ff1619e --- /dev/null +++ b/STROOP/Resources/Shaders/LevelModel.frag.glsl @@ -0,0 +1,33 @@ +#version 330 core + +in vec2 fs_texCoord; +in vec4 fs_color; +in float fs_worldY; + +uniform sampler2D tex; +uniform bool useTexture; +uniform bool decal; +uniform float alphaCutoff; +uniform bool opaque; +uniform float maxY; + +layout(location=0) out vec4 color; + +void main() +{ + if (fs_worldY > maxY) + discard; + + vec4 c = fs_color; + if (useTexture) + { + vec4 t = texture(tex, fs_texCoord); + c = decal ? vec4(mix(fs_color.rgb, t.rgb, t.a), fs_color.a) : t * fs_color; + } + + if (c.a < alphaCutoff) + discard; + if (opaque) + c.a = 1; + color = c; +} diff --git a/STROOP/Resources/Shaders/LevelModel.vert.glsl b/STROOP/Resources/Shaders/LevelModel.vert.glsl new file mode 100644 index 000000000..32e8ecf41 --- /dev/null +++ b/STROOP/Resources/Shaders/LevelModel.vert.glsl @@ -0,0 +1,34 @@ +#version 330 core + +layout (location = 0) in vec3 vs_position; +layout (location = 1) in vec2 vs_texCoord; +layout (location = 2) in vec4 vs_color; +layout (location = 3) in vec4 vs_ambient; +layout (location = 4) in vec3 vs_normal; +layout (location = 5) in vec3 vs_lightDir; + +uniform mat4 viewProjection; +uniform mat4 model; +uniform mat3 viewRotation; + +out vec2 fs_texCoord; +out vec4 fs_color; +out float fs_worldY; + +void main() +{ + vec4 world = model * vec4(vs_position, 1); + gl_Position = viewProjection * world; + fs_texCoord = vs_texCoord; + fs_worldY = world.y; + + vec4 color = vs_color; + if (vs_ambient.a > 0.5) + { + vec3 n = viewRotation * (mat3(model) * vs_normal); + float nLength = length(n), lLength = length(vs_lightDir); + float k = nLength > 0.0 && lLength > 0.0 ? max(0.0, dot(n / nLength, vs_lightDir / lLength)) : 0.0; + color.rgb = min(vec3(1.0), vs_ambient.rgb + vs_color.rgb * k); + } + fs_color = color; +} diff --git a/STROOP/Resources/Shaders/Lines.geom.glsl b/STROOP/Resources/Shaders/Lines.geom.glsl index dc7f31e50..bc4fd4523 100644 --- a/STROOP/Resources/Shaders/Lines.geom.glsl +++ b/STROOP/Resources/Shaders/Lines.geom.glsl @@ -1,4 +1,4 @@ -#version 330 core +#version 330 core uniform vec2 viewportSize; @@ -12,28 +12,49 @@ layout (triangle_strip, max_vertices = 4) out; void main() { - vec3 p1 = gl_in[1].gl_Position.xyz / gl_in[1].gl_Position.w; - vec3 p0 = gl_in[0].gl_Position.xyz / gl_in[0].gl_Position.w; + vec4 c0 = gl_in[0].gl_Position, c1 = gl_in[1].gl_Position; + vec4 color0 = gs_color[0], color1 = gs_color[1]; + float width0 = gs_width[0], width1 = gs_width[1]; + + float d0 = c0.z + c0.w, d1 = c1.z + c1.w; + if (d0 < 0 && d1 < 0) + return; + if (d0 < 0) + { + float t = d0 / (d0 - d1); + c0 = mix(c0, c1, t); + color0 = mix(color0, color1, t); + width0 = mix(width0, width1, t); + } + else if (d1 < 0) + { + float t = d1 / (d1 - d0); + c1 = mix(c1, c0, t); + color1 = mix(color1, color0, t); + width1 = mix(width1, width0, t); + } + + float w0 = max(c0.w, 1e-6); + float w1 = max(c1.w, 1e-6); + vec3 p0 = c0.xyz / w0; + vec3 p1 = c1.xyz / w1; vec2 dist = p1.xy - p0.xy; vec2 perpendicular = normalize(vec2(dist.y, -dist.x)) / viewportSize; - - float w0 = gl_in[0].gl_Position.w; - float w1 = gl_in[1].gl_Position.w; - - gl_Position = vec4(vec3(p0.xy + perpendicular * gs_width[0], p0.z) * w0, w0); - fs_color = gs_color[0]; + + gl_Position = vec4(vec3(p0.xy + perpendicular * width0, p0.z) * w0, w0); + fs_color = color0; EmitVertex(); - - gl_Position = vec4(vec3(p0.xy - perpendicular * gs_width[0], p0.z) * w0, w0); - fs_color = gs_color[0]; + + gl_Position = vec4(vec3(p0.xy - perpendicular * width0, p0.z) * w0, w0); + fs_color = color0; EmitVertex(); - - gl_Position = vec4(vec3(p1.xy + perpendicular * gs_width[1], p1.z) * w1, w1); - fs_color = gs_color[1]; + + gl_Position = vec4(vec3(p1.xy + perpendicular * width1, p1.z) * w1, w1); + fs_color = color1; EmitVertex(); - - gl_Position = vec4(vec3(p1.xy - perpendicular * gs_width[1], p1.z) * w1, w1); - fs_color = gs_color[1]; + + gl_Position = vec4(vec3(p1.xy - perpendicular * width1, p1.z) * w1, w1); + fs_color = color1; EmitVertex(); EndPrimitive(); -} \ No newline at end of file +} diff --git a/STROOP/Resources/Shaders/Skybox.frag.glsl b/STROOP/Resources/Shaders/Skybox.frag.glsl new file mode 100644 index 000000000..e38e09d66 --- /dev/null +++ b/STROOP/Resources/Shaders/Skybox.frag.glsl @@ -0,0 +1,23 @@ +#version 330 core + +in vec2 fs_screen; + +uniform sampler2D sky; +uniform bool useTexture; +uniform vec3 color; +uniform vec2 windowOrigin; + +layout(location=0) out vec4 outColor; + +void main() +{ + if (!useTexture) + { + outColor = vec4(color, 1); + return; + } + + float x = windowOrigin.x + fs_screen.x * 320.0; + float y = windowOrigin.y - fs_screen.y * 240.0; + outColor = vec4(texture(sky, vec2(x / 1600.0, (960.0 - y) / 960.0)).rgb, 1); +} diff --git a/STROOP/Resources/Shaders/Skybox.vert.glsl b/STROOP/Resources/Shaders/Skybox.vert.glsl new file mode 100644 index 000000000..c5520016d --- /dev/null +++ b/STROOP/Resources/Shaders/Skybox.vert.glsl @@ -0,0 +1,10 @@ +#version 330 core + +out vec2 fs_screen; + +void main() +{ + vec2 ndc = vec2((gl_VertexID & 1) * 4.0 - 1.0, (gl_VertexID >> 1) * 4.0 - 1.0); + gl_Position = vec4(ndc, 1, 1); + fs_screen = vec2(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5); +} diff --git a/STROOP/Tabs/GhostTab/ColoredHats.cs b/STROOP/Tabs/GhostTab/ColoredHats.cs index 14c01c9fd..f18296f8b 100644 --- a/STROOP/Tabs/GhostTab/ColoredHats.cs +++ b/STROOP/Tabs/GhostTab/ColoredHats.cs @@ -40,6 +40,21 @@ partial class GhostTab uint COLORED_HATS_LIGHTS_ADDR => ghostRegionBase + 0x8300u; + public uint ColoredHatsGeneratorAddress => ghostRegionBase + COLORED_HATS_CODE_OFFSET; + + public uint ColoredHatsLightsAddress => COLORED_HATS_LIGHTS_ADDR; + + public bool ColoredHatsInstalled => + Config.Stream.GetUInt32(ColoredHatsGeneratorAddress) == 0x27BDFFC0 + && Config.Stream.GetUInt32(ColoredHatsGeneratorAddress + 4) == 0xAFBF0014 + && Config.Stream.GetUInt32(ColoredHatsGeneratorAddress + 8) == 0x3C090401; + + public bool GhostHackInstalled => + Config.Stream.GetUInt32(ghostRegionBase + GHOST_LOOP_CODE_OFFSET) == 0x27BDFFC0 + && Config.Stream.GetUInt32(ghostRegionBase + GHOST_LOOP_CODE_OFFSET + 4) == 0x3C088036; + + public bool GhostsDrawnInGame => GhostHackInstalled && Config.Stream.GetByte(numGhostsAddr) > 0; + private void EnableColoredHats() { using (Config.Stream.Suspend()) diff --git a/STROOP/Tabs/MapTab/DataUtil/F3D.cs b/STROOP/Tabs/MapTab/DataUtil/F3D.cs new file mode 100644 index 000000000..e3f4b3504 --- /dev/null +++ b/STROOP/Tabs/MapTab/DataUtil/F3D.cs @@ -0,0 +1,658 @@ +using System; +using System.Collections.Generic; +using OpenTK.Mathematics; + +namespace STROOP.Tabs.MapTab.DataUtil +{ + public class Texture + { + public int width, height; + public byte[] rgba; + public int wrapS, wrapT; + } + + public enum CullMode + { + None, + Back, + Front, + } + + public class Batch + { + public int texture; + public int layer; + public bool decal; + public CullMode cull; + public int first, count; + } + + public class Mesh + { + public readonly List batches = new List(); + public int triangleCount; + } + + public class MeshStore + { + public const int VertexSize = 36; + + public byte[] data = new byte[VertexSize * 4096]; + public int length; + + public Mesh Add(MeshBuilder builder) + { + var mesh = new Mesh { triangleCount = builder.triangleCount }; + var keys = new List<(int texture, int layer, bool decal, CullMode cull)>(builder.meshes.Keys); + keys.Sort((a, b) => a.layer != b.layer ? a.layer.CompareTo(b.layer) : a.texture.CompareTo(b.texture)); + foreach (var key in keys) + { + var vertices = builder.meshes[key]; + if (length + vertices.Count > data.Length) + Array.Resize(ref data, Math.Max(length + vertices.Count, data.Length * 2)); + vertices.CopyTo(data, length); + mesh.batches.Add(new Batch + { + texture = key.texture, + layer = key.layer, + decal = key.decal, + cull = key.cull, + first = length / VertexSize, + count = vertices.Count / VertexSize, + }); + length += vertices.Count; + } + + return mesh; + } + } + + public class MeshBuilder + { + public readonly Dictionary<(int texture, int layer, bool decal, CullMode cull), List> meshes = new Dictionary<(int, int, bool, CullMode), List>(); + public int triangleCount; + } + + public class TextureSet + { + public readonly List textures = new List(); + readonly Dictionary<(uint addr, int fmt, int siz, int w, int h, uint tlut, int pal, int tlutType, int cms, int cmt), int> indices = new Dictionary<(uint, int, int, int, int, uint, int, int, int, int), int>(); + + public int Get(Rdram ram, uint addr, int fmt, int siz, int w, int h, uint? tlut, int pal, int tlutType, int cms, int cmt) + { + var key = (addr, fmt, siz, w, h, tlut ?? 0xFFFFFFFF, pal, tlutType, cms, cmt); + if (!indices.TryGetValue(key, out int index)) + { + textures.Add(new Texture + { + width = w, + height = h, + rgba = DecodeTexture(ram, addr, fmt, siz, w, h, tlut, pal, tlutType), + wrapS = (cms & 1) != 0 ? 1 : (cms & 2) != 0 ? 2 : 0, + wrapT = (cmt & 1) != 0 ? 1 : (cmt & 2) != 0 ? 2 : 0, + }); + indices[key] = index = textures.Count - 1; + } + + return index; + } + + static int C5(int v) => (v << 3) | (v >> 2); + + static void Rgba16(int v, byte[] o, int i) + { + o[i] = (byte)C5((v >> 11) & 31); + o[i + 1] = (byte)C5((v >> 6) & 31); + o[i + 2] = (byte)C5((v >> 1) & 31); + o[i + 3] = (byte)((v & 1) != 0 ? 255 : 0); + } + + public static byte[] DecodeTexture(Rdram ram, uint addr, int fmt, int siz, int w, int h, uint? tlut, int palette, int tlutType) + { + int bits = 4 << siz; + uint src = ram.Phys(addr); + var o = new byte[w * h * 4]; + uint tlutPhys = tlut.HasValue ? ram.Phys(tlut.Value) : 0; + for (int i = 0; i < w * h; i++) + { + int v; + switch (bits) + { + case 4: + int b = ram.U8Phys(src + (uint)(i >> 1)); + v = (i & 1) == 0 ? b >> 4 : b & 15; + break; + case 8: + v = ram.U8Phys(src + (uint)i); + break; + case 16: + v = ram.U16Phys(src + (uint)(2 * i)); + break; + default: + v = (int)ram.U32Phys(src + (uint)(4 * i)); + break; + } + + int k = 4 * i; + switch (fmt) + { + case 0: + if (bits == 16) + Rgba16(v, o, k); + else + { + o[k] = (byte)(v >> 24); + o[k + 1] = (byte)(v >> 16); + o[k + 2] = (byte)(v >> 8); + o[k + 3] = (byte)v; + } + + break; + case 2: + int index = v + (bits == 4 ? palette * 16 : 0); + if (!tlut.HasValue) + { + byte g = (byte)(siz == 0 ? index * 17 : index); + o[k] = o[k + 1] = o[k + 2] = g; + o[k + 3] = 255; + } + else + { + int p = ram.U16Phys(tlutPhys + (uint)(2 * index)); + if (tlutType == 3) + { + o[k] = o[k + 1] = o[k + 2] = (byte)(p >> 8); + o[k + 3] = (byte)p; + } + else + Rgba16(p, o, k); + } + + break; + case 3: + if (bits == 4) + { + int i3 = (v >> 1) & 7; + o[k] = o[k + 1] = o[k + 2] = (byte)((i3 << 5) | (i3 << 2) | (i3 >> 1)); + o[k + 3] = (byte)((v & 1) != 0 ? 255 : 0); + } + else if (bits == 8) + { + o[k] = o[k + 1] = o[k + 2] = (byte)((v >> 4) * 17); + o[k + 3] = (byte)((v & 15) * 17); + } + else + { + o[k] = o[k + 1] = o[k + 2] = (byte)(v >> 8); + o[k + 3] = (byte)v; + } + + break; + default: + o[k] = o[k + 1] = o[k + 2] = o[k + 3] = (byte)(bits == 4 ? v * 17 : v & 255); + break; + } + } + + return o; + } + } + + public class F3D + { + const int G_MOVEMEM = 0x03, G_VTX = 0x04, G_DL = 0x06; + const int G_CLEARGEOMETRYMODE = 0xB6, G_SETGEOMETRYMODE = 0xB7, G_ENDDL = 0xB8, G_SETOTHERMODE_H = 0xBA; + const int G_TEXTURE = 0xBB, G_MOVEWORD = 0xBC, G_TRI1 = 0xBF; + const int G_LOADTLUT = 0xF0, G_SETTILESIZE = 0xF2, G_LOADBLOCK = 0xF3, G_LOADTILE = 0xF4, G_SETTILE = 0xF5; + const int G_SETPRIMCOLOR = 0xFA, G_SETENVCOLOR = 0xFB, G_SETCOMBINE = 0xFC, G_SETTIMG = 0xFD; + const uint G_LIGHTING = 0x00020000, G_CULL_FRONT = 0x00001000, G_CULL_BACK = 0x00002000; + const int G_MW_NUMLIGHT = 0x02; + + public const uint GEO_INIT = 0x00000004 | 0x00000200 | G_CULL_BACK | G_LIGHTING; + + public const uint COMBINE_SHADE_W1 = 4 << 15; + + internal class Tile + { + public int fmt, siz = 2, tmem, pal, cms, cmt, masks, maskt, uls, ult, lrs, lrt; + + public Tile MemberwiseCloneTile() => (Tile)MemberwiseClone(); + } + + struct Vtx + { + public float x, y, z; + public short s, t; + public byte r, g, b, a; + + public bool lit; + public sbyte nx, ny, nz, lx, ly, lz; + public byte ar, ag, ab; + } + + readonly Rdram ram; + readonly TextureSet textureSet; + readonly MeshBuilder output; + + public uint geo = GEO_INIT; + bool texOn; + double texScaleS = 1, texScaleT = 1; + readonly Tile[] tiles = new Tile[8]; + uint timgAddr; + readonly Dictionary tmemSrc = new Dictionary(); + uint? tlut; + int tlutType; + byte[] prim = { 255, 255, 255, 255 }, env = { 255, 255, 255, 255 }; + public uint combine0, combine1; + readonly Dictionary lights = new Dictionary(); + int numLights = 1; + readonly Vtx?[] vbuf = new Vtx?[16]; + readonly byte[] raw = new byte[16]; + + public F3D(Rdram ram, TextureSet textureSet, MeshBuilder output) + { + this.ram = ram; + this.textureSet = textureSet; + this.output = output; + for (int i = 0; i < tiles.Length; i++) + tiles[i] = new Tile(); + } + + public sealed class State + { + internal uint geo, timgAddr, combine0, combine1; + internal bool texOn; + internal double texScaleS, texScaleT; + internal Tile[] tiles; + internal KeyValuePair[] tmemSrc; + internal uint? tlut; + internal int tlutType, numLights; + internal byte[] prim, env; + internal KeyValuePair[] lights; + int hash; + + public int Hash + { + get + { + if (hash != 0) + return hash; + var h = new HashCode(); + h.Add(geo); + h.Add(timgAddr); + h.Add(combine0); + h.Add(combine1); + h.Add(texOn); + h.Add(texScaleS); + h.Add(texScaleT); + foreach (var t in tiles) + h.Add(HashCode.Combine(t.fmt, t.siz, t.tmem, t.pal, t.cms, t.cmt, HashCode.Combine(t.masks, t.maskt), HashCode.Combine(t.uls, t.ult, t.lrs, t.lrt))); + foreach (var kv in tmemSrc) + h.Add(HashCode.Combine(kv.Key, kv.Value)); + h.Add(tlut); + h.Add(tlutType); + h.Add(numLights); + h.Add(BitConverter.ToInt32(prim)); + h.Add(BitConverter.ToInt32(env)); + foreach (var kv in lights) + h.Add(HashCode.Combine(kv.Key, kv.Value)); + return hash = h.ToHashCode() | 1; + } + } + } + + public State Save() + { + var t = new Tile[tiles.Length]; + for (int i = 0; i < t.Length; i++) + t[i] = (Tile)tiles[i].MemberwiseCloneTile(); + var tmem = new List>(tmemSrc); + tmem.Sort((a, b) => a.Key.CompareTo(b.Key)); + var l = new List>(lights); + l.Sort((a, b) => a.Key.CompareTo(b.Key)); + return new State + { + geo = geo, timgAddr = timgAddr, combine0 = combine0, combine1 = combine1, texOn = texOn, + texScaleS = texScaleS, texScaleT = texScaleT, tiles = t, tmemSrc = tmem.ToArray(), tlut = tlut, + tlutType = tlutType, numLights = numLights, prim = (byte[])prim.Clone(), env = (byte[])env.Clone(), + lights = l.ToArray(), + }; + } + + public void Load(State s) + { + geo = s.geo; + timgAddr = s.timgAddr; + combine0 = s.combine0; + combine1 = s.combine1; + texOn = s.texOn; + texScaleS = s.texScaleS; + texScaleT = s.texScaleT; + for (int i = 0; i < tiles.Length; i++) + tiles[i] = s.tiles[i].MemberwiseCloneTile(); + tmemSrc.Clear(); + foreach (var kv in s.tmemSrc) + tmemSrc[kv.Key] = kv.Value; + tlut = s.tlut; + tlutType = s.tlutType; + numLights = s.numLights; + prim = (byte[])s.prim.Clone(); + env = (byte[])s.env.Clone(); + lights.Clear(); + foreach (var kv in s.lights) + lights[kv.Key] = kv.Value; + } + + void Combiner(out int a0, out int b0, out int c0, out int d0) + { + a0 = (int)(combine0 >> 20) & 15; + c0 = (int)(combine0 >> 15) & 31; + b0 = (int)(combine1 >> 28) & 15; + d0 = (int)(combine1 >> 15) & 7; + } + + int ColorSource(out bool usesTex) + { + Combiner(out int a0, out int b0, out int c0, out int d0); + usesTex = a0 == 1 || b0 == 1 || c0 == 1 || d0 == 1; + if (a0 == 4 || b0 == 4 || c0 == 4 || d0 == 4) return 1; + if (a0 == 3 || b0 == 3 || c0 == 3 || d0 == 3) return 2; + if (a0 == 5 || b0 == 5 || c0 == 5 || d0 == 5) return 3; + return 0; + } + + byte AlphaSource(byte shadeAlpha) + { + int a = (int)(combine0 >> 12) & 7, c = (int)(combine0 >> 9) & 7, b = (int)(combine1 >> 12) & 7, d = (int)(combine1 >> 9) & 7; + if (a == 5 || b == 5 || c == 5 || d == 5) return env[3]; + if (a == 3 || b == 3 || c == 3 || d == 3) return prim[3]; + if (a == 4 || b == 4 || c == 4 || d == 4) return shadeAlpha; + return 255; + } + + bool Decal() + { + Combiner(out int a0, out int b0, out int c0, out int d0); + return a0 == 1 && b0 == 4 && c0 == 8 && d0 == 4; + } + + void Shade(ref Vtx v, in Matrix4 m) + { + if ((geo & G_LIGHTING) == 0) + { + v.r = raw[12]; + v.g = raw[13]; + v.b = raw[14]; + v.a = raw[15]; + return; + } + + var n = Vector3.TransformNormal(new Vector3((sbyte)raw[12], (sbyte)raw[13], (sbyte)raw[14]), m); + if (n.LengthSquared > 0) + n = n.Normalized() * 127; + v.lit = true; + v.nx = (sbyte)Math.Round(n.X); + v.ny = (sbyte)Math.Round(n.Y); + v.nz = (sbyte)Math.Round(n.Z); + v.a = raw[15]; + + double ar = 128, ag = 128, ab = 128; + if (lights.TryGetValue(numLights + 1, out var amb)) + (ar, ag, ab) = (amb.r, amb.g, amb.b); + var light = lights.TryGetValue(1, out var first) ? first : ((byte)255, (byte)255, (byte)255, (sbyte)0x28, (sbyte)0x28, (sbyte)0x28); + v.r = light.Item1; + v.g = light.Item2; + v.b = light.Item3; + v.lx = light.Item4; + v.ly = light.Item5; + v.lz = light.Item6; + + for (int i = 2; i <= numLights; i++) + { + var l = lights.TryGetValue(i, out var x) ? x : ((byte)255, (byte)255, (byte)255, (sbyte)0x28, (sbyte)0x28, (sbyte)0x28); + double ln = Math.Sqrt(l.Item4 * l.Item4 + l.Item5 * l.Item5 + l.Item6 * l.Item6); + if (ln == 0) ln = 1; + double k = Math.Max(0, ((sbyte)raw[12] * l.Item4 + (sbyte)raw[13] * l.Item5 + (sbyte)raw[14] * l.Item6) / (127.0 * ln)); + ar += l.Item1 * k; + ag += l.Item2 * k; + ab += l.Item3 * k; + } + + v.ar = (byte)Math.Min(255, (int)ar); + v.ag = (byte)Math.Min(255, (int)ag); + v.ab = (byte)Math.Min(255, (int)ab); + } + + int GetTexture(bool usesTex, out int w, out int h, out int uls, out int ult) + { + var t = tiles[0]; + w = ((t.lrs - t.uls) >> 2) + 1; + h = ((t.lrt - t.ult) >> 2) + 1; + uls = t.uls; + ult = t.ult; + if (!(texOn && usesTex)) + return -1; + if (!tmemSrc.TryGetValue(t.tmem, out uint addr) || !(0 < w && w <= 256 && 0 < h && h <= 256)) + return -1; + return textureSet.Get(ram, addr, t.fmt, t.siz, w, h, t.fmt == 2 ? tlut : null, t.siz == 0 ? t.pal : 0, tlutType, + t.cms | (t.masks == 0 ? 2 : 0), t.cmt | (t.maskt == 0 ? 2 : 0)); + } + + void LoadVertices(uint addr, int n, int v0, in Matrix4 m) + { + uint p = ram.Phys(addr); + for (int i = 0; i < n; i++, p += 16) + { + if (!ram.Ok(p, 16)) + return; + for (uint k = 0; k < 16; k++) + raw[k] = ram.U8Phys(p + k); + var pos = new Vector3((short)(raw[0] << 8 | raw[1]), (short)(raw[2] << 8 | raw[3]), (short)(raw[4] << 8 | raw[5])); + var world = Vector3.TransformPosition(pos, m); + var v = new Vtx + { + x = world.X, + y = world.Y, + z = world.Z, + s = (short)(raw[8] << 8 | raw[9]), + t = (short)(raw[10] << 8 | raw[11]), + }; + Shade(ref v, m); + if (v0 + i < 16) + vbuf[v0 + i] = v; + } + } + + void Triangle(int i0, int i1, int i2, int layer) + { + if (i0 >= 16 || i1 >= 16 || i2 >= 16 || !vbuf[i0].HasValue || !vbuf[i1].HasValue || !vbuf[i2].HasValue) + return; + var cull = (geo & G_CULL_BACK) != 0 ? CullMode.Back : CullMode.None; + if ((geo & G_CULL_FRONT) != 0) + { + if (cull == CullMode.Back) + return; + cull = CullMode.Front; + } + + int src = ColorSource(out bool usesTex); + int texture = GetTexture(usesTex, out int w, out int h, out int uls, out int ult); + bool decal = texture >= 0 && Decal(); + var key = (texture, layer, decal, cull); + if (!output.meshes.TryGetValue(key, out var mesh)) + output.meshes[key] = mesh = new List(MeshStore.VertexSize * 3 * 32); + + float fu = 0, fv = 0, ou = 0, ov = 0; + if (texture >= 0) + { + fu = (float)(texScaleS / (32.0 * w)); + fv = (float)(texScaleT / (32.0 * h)); + ou = uls / (4.0f * w); + ov = ult / (4.0f * h); + } + + Vertex(mesh, vbuf[i0].Value, texture, fu, fv, ou, ov, src); + Vertex(mesh, vbuf[i1].Value, texture, fu, fv, ou, ov, src); + Vertex(mesh, vbuf[i2].Value, texture, fu, fv, ou, ov, src); + output.triangleCount++; + } + + void Vertex(List mesh, in Vtx v, int texture, float fu, float fv, float ou, float ov, int src) + { + AddFloat(mesh, v.x); + AddFloat(mesh, v.y); + AddFloat(mesh, v.z); + AddFloat(mesh, texture >= 0 ? v.s * fu - ou : 0); + AddFloat(mesh, texture >= 0 ? v.t * fv - ov : 0); + switch (src) + { + case 1: + mesh.Add(v.r); + mesh.Add(v.g); + mesh.Add(v.b); + mesh.Add(v.a); + break; + case 2: + mesh.AddRange(prim); + break; + case 3: + mesh.AddRange(env); + break; + default: + mesh.Add(255); + mesh.Add(255); + mesh.Add(255); + mesh.Add(v.a); + break; + } + + mesh[mesh.Count - 1] = AlphaSource(v.a); + + bool lit = src == 1 && v.lit; + mesh.Add(lit ? v.ar : (byte)0); + mesh.Add(lit ? v.ag : (byte)0); + mesh.Add(lit ? v.ab : (byte)0); + mesh.Add(lit ? (byte)255 : (byte)0); + mesh.Add((byte)(lit ? v.nx : 0)); + mesh.Add((byte)(lit ? v.ny : 0)); + mesh.Add((byte)(lit ? v.nz : 0)); + mesh.Add(0); + mesh.Add((byte)(lit ? v.lx : 0)); + mesh.Add((byte)(lit ? v.ly : 0)); + mesh.Add((byte)(lit ? v.lz : 0)); + mesh.Add(0); + } + + static void AddFloat(List list, float f) + { + int b = BitConverter.SingleToInt32Bits(f); + list.Add((byte)b); + list.Add((byte)(b >> 8)); + list.Add((byte)(b >> 16)); + list.Add((byte)(b >> 24)); + } + + public void SetEnvColor(byte r, byte g, byte b, byte a) => env = new[] { r, g, b, a }; + + public void MoveMemLight(int index, uint address) + { + if (index < 0x86 || index > 0x94 || index % 2 != 0) + return; + uint p = ram.Phys(address); + if (ram.Ok(p, 16)) + lights[(index - 0x86) / 2 + 1] = (ram.U8Phys(p), ram.U8Phys(p + 1), ram.U8Phys(p + 2), (sbyte)ram.U8Phys(p + 8), (sbyte)ram.U8Phys(p + 9), (sbyte)ram.U8Phys(p + 10)); + } + + public void Run(uint dl, int layer, in Matrix4 m, int depth = 0) + { + uint pc = dl; + for (int iteration = 0; iteration < 100000; iteration++) + { + if (!ram.Ok(ram.Phys(pc), 8)) + return; + uint w0 = ram.U32(pc), w1 = ram.U32(pc + 4); + int op = (int)(w0 >> 24); + pc += 8; + switch (op) + { + case G_ENDDL: + return; + case G_DL: + if (depth < 16) + Run(w1, layer, m, depth + 1); + if (((w0 >> 16) & 0xFF) != 0) + return; + break; + case G_VTX: + LoadVertices(w1, (int)((w0 >> 20) & 15) + 1, (int)(w0 >> 16) & 15, m); + break; + case G_TRI1: + Triangle((int)((w1 >> 16) & 0xFF) / 10, (int)((w1 >> 8) & 0xFF) / 10, (int)(w1 & 0xFF) / 10, layer); + break; + case G_SETGEOMETRYMODE: + geo |= w1; + break; + case G_CLEARGEOMETRYMODE: + geo &= ~w1; + break; + case G_TEXTURE: + texOn = (w0 & 0xFF) != 0; + uint s = (w1 >> 16) & 0xFFFF, t = w1 & 0xFFFF; + texScaleS = s == 0xFFFF ? (s + 1) / 65536.0 : s / 65536.0; + texScaleT = t == 0xFFFF ? (t + 1) / 65536.0 : t / 65536.0; + break; + case G_MOVEMEM: + MoveMemLight((int)(w0 >> 16) & 0xFF, w1); + break; + case G_MOVEWORD: + if ((w0 & 0xFF) == G_MW_NUMLIGHT) + numLights = (int)Math.Max(1, (((long)w1 - 0x80000000L) >> 5) - 1); + break; + case G_SETOTHERMODE_H: + int shift = (int)(w0 >> 8) & 0xFF, length = (int)w0 & 0xFF; + if (shift <= 14 && 14 < shift + length) + tlutType = (int)(w1 >> 14) & 3; + break; + case G_SETTIMG: + timgAddr = w1; + break; + case G_SETTILE: + var tile = tiles[(w1 >> 24) & 7]; + tile.fmt = (int)(w0 >> 21) & 7; + tile.siz = (int)(w0 >> 19) & 3; + tile.tmem = (int)w0 & 0x1FF; + tile.pal = (int)(w1 >> 20) & 15; + tile.cmt = (int)(w1 >> 18) & 3; + tile.cms = (int)(w1 >> 8) & 3; + tile.maskt = (int)(w1 >> 14) & 15; + tile.masks = (int)(w1 >> 4) & 15; + break; + case G_SETTILESIZE: + var sizedTile = tiles[(w1 >> 24) & 7]; + sizedTile.uls = (int)(w0 >> 12) & 0xFFF; + sizedTile.ult = (int)w0 & 0xFFF; + sizedTile.lrs = (int)(w1 >> 12) & 0xFFF; + sizedTile.lrt = (int)w1 & 0xFFF; + break; + case G_LOADBLOCK: + case G_LOADTILE: + tmemSrc[tiles[(w1 >> 24) & 7].tmem] = timgAddr; + break; + case G_LOADTLUT: + tlut = timgAddr; + break; + case G_SETPRIMCOLOR: + prim = new[] { (byte)(w1 >> 24), (byte)(w1 >> 16), (byte)(w1 >> 8), (byte)w1 }; + break; + case G_SETENVCOLOR: + env = new[] { (byte)(w1 >> 24), (byte)(w1 >> 16), (byte)(w1 >> 8), (byte)w1 }; + break; + case G_SETCOMBINE: + combine0 = w0 & 0xFFFFFF; + combine1 = w1; + break; + } + } + } + } +} diff --git a/STROOP/Tabs/MapTab/DataUtil/LevelModel.cs b/STROOP/Tabs/MapTab/DataUtil/LevelModel.cs new file mode 100644 index 000000000..addf1a731 --- /dev/null +++ b/STROOP/Tabs/MapTab/DataUtil/LevelModel.cs @@ -0,0 +1,740 @@ +using System; +using System.Collections.Generic; +using OpenTK.Mathematics; + +namespace STROOP.Tabs.MapTab.DataUtil +{ + public class LevelModel + { + public class GameAddresses + { + public uint marioObjectPointer, marioState; + + public uint geoSwitchAnimState; + + public uint skyboxTextures; + + public uint geoPaintingDraw, paintingGroups; + + public uint coloredHatsGenerator, coloredHatsLights; + + public bool ghostHack; + + public uint geoSwitchMarioCapEffect, geoMirrorMarioSetAlpha, bodyStates; + + public uint geoUpdateLayerTransparency; + + public static GameAddresses US(uint marioObjectPointer, uint marioState) => new GameAddresses + { + marioObjectPointer = marioObjectPointer, + marioState = marioState, + geoSwitchAnimState = 0x8029DB48, + skyboxTextures = 0x80330F00, + geoPaintingDraw = 0x802D5B98, + paintingGroups = 0x8033134C, + geoSwitchMarioCapEffect = 0x802776D8, + geoMirrorMarioSetAlpha = 0x802770A4, + bodyStates = 0x8033B3B0, + geoUpdateLayerTransparency = 0x8029D924, + }; + } + + public struct Shadow + { + public Vector3 position; + public float size, opacity; + public bool square; + } + + public struct Instance + { + public Mesh mesh; + public Matrix4 matrix; + public bool billboard; + public Vector3 anchor, anchorScale; + } + + public readonly MeshStore store = new MeshStore(); + public readonly TextureSet textures = new TextureSet(); + public Mesh level = new Mesh(); + public readonly List instances = new List(); + public readonly List shadows = new List(); + + public Texture skybox; + public Vector3? backgroundColor; + + public int displayListCount; + public uint area; + + public bool IsEmpty => level.triangleCount == 0; + + public static readonly LevelModel None = new LevelModel(); + + const int GN_ROOT = 0x001, GN_ORTHO = 0x002, GN_LOD = 0x00B, GN_SWITCH = 0x10C, GN_CAMERA = 0x114; + const int GN_TRANSROT = 0x015, GN_TRANS = 0x016, GN_ROT = 0x017, GN_OBJECT = 0x018, GN_ANIMPART = 0x019, GN_BILLBOARD = 0x01A, GN_DL = 0x01B, GN_SCALE = 0x01C; + const int GN_SHADOW = 0x028, GN_OBJPARENT = 0x029, GN_GENLIST = 0x12A, GN_BG = 0x12C, GN_HELD = 0x12E; + + const int GRAPH_RENDER_ACTIVE = 0x01, GRAPH_RENDER_BILLBOARD = 0x04, GRAPH_RENDER_INVISIBLE = 0x10; + + uint rootNode, cameraNode, objectGroup; + + readonly Dictionary<(uint dl, int layer, int state), (Mesh mesh, F3D.State state)> objectMeshes = new Dictionary<(uint, int, int), (Mesh, F3D.State)>(); + + readonly F3D.State[] layerStates = new F3D.State[8]; + + F3D.State defaultState; + + #region matrices (row vectors, v' = v . M: the game's convention) + + static Matrix4 Affine(double m00, double m01, double m02, double m10, double m11, double m12, double m20, double m21, double m22, double tx, double ty, double tz) => + new Matrix4( + (float)m00, (float)m01, (float)m02, 0, + (float)m10, (float)m11, (float)m12, 0, + (float)m20, (float)m21, (float)m22, 0, + (float)tx, (float)ty, (float)tz, 1); + + static Matrix4 RotZXYTrans(double tx, double ty, double tz, short rx, short ry, short rz) + { + double sx = Math.Sin(rx * Math.PI / 32768), cx = Math.Cos(rx * Math.PI / 32768); + double sy = Math.Sin(ry * Math.PI / 32768), cy = Math.Cos(ry * Math.PI / 32768); + double sz = Math.Sin(rz * Math.PI / 32768), cz = Math.Cos(rz * Math.PI / 32768); + return Affine( + cy * cz + sx * sy * sz, cx * sz, -sy * cz + sx * cy * sz, + -cy * sz + sx * sy * cz, cx * cz, sy * sz + sx * cy * cz, + cx * sy, -sx, cx * cy, + tx, ty, tz); + } + + static Matrix4 RotXYZTrans(double tx, double ty, double tz, short rx, short ry, short rz) + { + double sx = Math.Sin(rx * Math.PI / 32768), cx = Math.Cos(rx * Math.PI / 32768); + double sy = Math.Sin(ry * Math.PI / 32768), cy = Math.Cos(ry * Math.PI / 32768); + double sz = Math.Sin(rz * Math.PI / 32768), cz = Math.Cos(rz * Math.PI / 32768); + return Affine( + cy * cz, cy * sz, -sy, + sx * sy * cz - cx * sz, sx * sy * sz + cx * cz, sx * cy, + cx * sy * cz + sx * sz, cx * sy * sz - sx * cz, cx * cy, + tx, ty, tz); + } + + static Matrix4 ScaleRows(Matrix4 m, Vector3 s) + { + m.Row0 *= s.X; + m.Row1 *= s.Y; + m.Row2 *= s.Z; + m.Row0.W = m.Row1.W = m.Row2.W = 0; + return m; + } + + #endregion + + #region the static level + + public static LevelModel Build(Rdram ram, uint currentAreaPointer, GameAddresses addresses) + { + var model = new LevelModel(); + model.area = ram.U32(currentAreaPointer); + if (!Rdram.IsPointer(model.area)) + return model; + uint root = ram.U32(model.area + 4); + if (!Rdram.IsPointer(root)) + return model; + model.rootNode = root; + model.addresses = addresses; + + var dls = new List<(uint dl, int layer, Matrix4 m)>(); + model.CollectDisplayLists(ram, root, Matrix4.Identity, 0, dls); + model.displayListCount = dls.Count; + + var builder = new MeshBuilder(); + var f3d = new F3D(ram, model.textures, builder); + foreach (var (dl, layer, m) in dls) + { + f3d.geo = F3D.GEO_INIT; + f3d.Run(dl, layer, m); + } + + model.level = model.store.Add(builder); + model.ReadBackground(ram, root, addresses); + return model; + } + + const float PAINTING_SIZE = 614; + + void AddPainting(Rdram ram, uint parameter, int layer, Matrix4 m, List<(uint, int, Matrix4)> output) + { + uint group = ram.U32(addresses.paintingGroups + 4 * ((parameter >> 8) & 0xFF)); + if (group == 0) + return; + uint painting = ram.U32(group + 4 * (parameter & 0xFF)); + if (painting == 0 || !ram.Ok(ram.Phys(painting), 0x78)) + return; + uint dl = ram.U32(painting + 0x58); + float size = ram.F32(painting + 0x74); + if (dl == 0 || !(size > 0)) + return; + var transform = Matrix4.CreateScale(size / PAINTING_SIZE) + * Matrix4.CreateRotationX(MathHelper.DegreesToRadians(ram.F32(painting + 0x08))) + * Matrix4.CreateRotationY(MathHelper.DegreesToRadians(ram.F32(painting + 0x0C))) + * Matrix4.CreateTranslation(ram.F32(painting + 0x10), ram.F32(painting + 0x14), ram.F32(painting + 0x18)); + output.Add((dl, layer, transform * m)); + } + + const int SKYBOX_COLS = 10, SKYBOX_ROWS = 8; + + void ReadBackground(Rdram ram, uint root, GameAddresses addresses) + { + var stack = new Stack(); + var seen = new HashSet(); + stack.Push(root); + while (stack.Count > 0 && seen.Count < 4096) + { + uint n = stack.Pop(); + if (!Rdram.IsPointer(n) || !seen.Add(n)) + continue; + if (ram.U16(n) == GN_BG) + { + uint function = ram.U32(n + 0x14), background = ram.U32(n + 0x1C); + if (function == 0) + { + int c = (int)(background & 0xFFFF); + backgroundColor = new Vector3(((c >> 11) & 31) / 31f, ((c >> 6) & 31) / 31f, ((c >> 1) & 31) / 31f); + } + else if (addresses.skyboxTextures != 0) + { + uint tiles = ram.U32(addresses.skyboxTextures + 4 * (background & 0xFF)); + var image = new byte[SKYBOX_COLS * 32 * SKYBOX_ROWS * 32 * 4]; + for (int t = 0; t < SKYBOX_COLS * SKYBOX_ROWS; t++) + { + byte[] tile = TextureSet.DecodeTexture(ram, ram.U32(tiles + (uint)(4 * t)), 0, 2, 32, 32, null, 0, 0); + int x0 = t % SKYBOX_COLS * 32, y0 = t / SKYBOX_COLS * 32; + for (int y = 0; y < 32; y++) + Buffer.BlockCopy(tile, y * 128, image, ((y0 + y) * SKYBOX_COLS * 32 + x0) * 4, 128); + } + + skybox = new Texture { width = SKYBOX_COLS * 32, height = SKYBOX_ROWS * 32, rgba = image, wrapS = 0, wrapT = 2 }; + } + + return; + } + + foreach (uint c in Children(ram, n)) + stack.Push(c); + } + } + + static IEnumerable Children(Rdram ram, uint n) + { + uint first = ram.U32(n + 0x10), c = first; + for (int i = 0; i < 1024 && Rdram.IsPointer(c); i++) + { + yield return c; + c = ram.U32(c + 0x08); + if (c == first) + break; + } + } + + void CollectDisplayLists(Rdram ram, uint n, Matrix4 m, int depth, List<(uint, int, Matrix4)> output) + { + if (depth > 40) + return; + int t = ram.U16(n); + ushort flags = ram.U16(n + 2); + if ((flags & GRAPH_RENDER_ACTIVE) == 0) + return; + if (t == GN_CAMERA) + cameraNode = n; + if (t == GN_OBJPARENT) + objectGroup = ram.U32(n + 0x14); + if (t == GN_GENLIST && addresses.geoPaintingDraw != 0 && ram.U32(n + 0x14) == addresses.geoPaintingDraw) + AddPainting(ram, ram.U32(n + 0x18), (flags >> 8) & 0xFF, m, output); + if (t == GN_OBJPARENT || t == GN_GENLIST || t == GN_BG || t == GN_HELD || t == GN_OBJECT || t == GN_SHADOW || t == GN_ORTHO) + return; + int layer = (flags >> 8) & 0xFF; + uint dl = 0; + switch (t) + { + case GN_DL: + dl = ram.U32(n + 0x14); + break; + case GN_TRANSROT: + m = RotZXYTrans(ram.S16(n + 0x18), ram.S16(n + 0x1A), ram.S16(n + 0x1C), ram.S16(n + 0x1E), ram.S16(n + 0x20), ram.S16(n + 0x22)) * m; + dl = ram.U32(n + 0x14); + break; + case GN_TRANS: + case GN_ANIMPART: + case GN_BILLBOARD: + m = RotZXYTrans(ram.S16(n + 0x18), ram.S16(n + 0x1A), ram.S16(n + 0x1C), 0, 0, 0) * m; + dl = ram.U32(n + 0x14); + break; + case GN_ROT: + m = RotZXYTrans(0, 0, 0, ram.S16(n + 0x18), ram.S16(n + 0x1A), ram.S16(n + 0x1C)) * m; + dl = ram.U32(n + 0x14); + break; + case GN_SCALE: + m = Matrix4.CreateScale(ram.F32(n + 0x18)) * m; + dl = ram.U32(n + 0x14); + break; + } + + if (dl != 0) + output.Add((dl, layer, m)); + if (t == GN_LOD && !(ram.S16(n + 0x14) <= 0 && 0 < ram.S16(n + 0x16))) + return; + + foreach (uint c in Children(ram, n)) + CollectDisplayLists(ram, c, m, depth + 1, output); + } + + #endregion + + #region the objects (Mario included), as rendered on the last frame + + const int ANIM_NONE = 0, ANIM_TRANSLATION = 1, ANIM_VERTICAL_TRANSLATION = 2, ANIM_LATERAL_TRANSLATION = 3, ANIM_NO_TRANSLATION = 4, ANIM_ROTATION = 5; + + struct AnimState + { + public int type, frame; + public uint attributes, values; + public float translationMultiplier; + } + + AnimState anim; + Vector3 gameCamera; + byte areaIndex; + int nodeBudget; + GameAddresses addresses; + uint currentObject, marioObject; + bool inHeldObject; + ISet roomCulledObjects; + + const int ACTIVE_FLAG_ACTIVE = 0x01, ACTIVE_FLAG_IN_DIFFERENT_ROOM = 0x08; + + public void UpdateObjects(Rdram ram, GameAddresses addresses, ISet roomCulledObjects = null) + { + instances.Clear(); + shadows.Clear(); + if (!Rdram.IsPointer(objectGroup)) + return; + this.addresses = addresses; + this.roomCulledObjects = roomCulledObjects; + defaultState ??= new F3D(ram, textures, new MeshBuilder()) { combine1 = F3D.COMBINE_SHADE_W1 }.Save(); + for (int i = 0; i < layerStates.Length; i++) + layerStates[i] = defaultState; + marioObject = ram.U32(addresses.marioObjectPointer); + areaIndex = ram.U8(rootNode + 0x14); + gameCamera = Rdram.IsPointer(cameraNode) + ? new Vector3(ram.F32(cameraNode + 0x1C), ram.F32(cameraNode + 0x20), ram.F32(cameraNode + 0x24)) + : Vector3.Zero; + foreach (uint obj in Children(ram, objectGroup)) + { + nodeBudget = 4096; + DrawObject(ram, obj); + } + } + + void DrawObject(Rdram ram, uint n) + { + if (ram.U16(n) != GN_OBJECT) + return; + ushort flags = ram.U16(n + 2); + if ((flags & GRAPH_RENDER_INVISIBLE) != 0) + return; + if ((flags & GRAPH_RENDER_ACTIVE) == 0) + { + int activeFlags = ram.U16(n + 0x74); + bool roomCulled = (activeFlags & ACTIVE_FLAG_IN_DIFFERENT_ROOM) != 0 || (roomCulledObjects?.Contains(n) ?? false); + if ((activeFlags & ACTIVE_FLAG_ACTIVE) == 0 || !roomCulled) + return; + } + + if (ram.U8(n + 0x18) != areaIndex) + return; + uint sharedChild = ram.U32(n + 0x14); + if (!Rdram.IsPointer(sharedChild)) + return; + + var pos = new Vector3(ram.F32(n + 0x20), ram.F32(n + 0x24), ram.F32(n + 0x28)); + var scale = new Vector3(ram.F32(n + 0x2C), ram.F32(n + 0x30), ram.F32(n + 0x34)); + var frame = new Frame(); + uint throwMatrix = ram.U32(n + 0x50); + if (Rdram.IsPointer(throwMatrix)) + frame.matrix = ScaleRows(ReadMatrix(ram, throwMatrix), scale); + else if ((flags & GRAPH_RENDER_BILLBOARD) != 0) + { + frame.matrix = Matrix4.Identity; + frame.billboard = true; + frame.anchor = pos; + frame.anchorScale = scale; + } + else + { + var angle = (ram.S16(n + 0x1A), ram.S16(n + 0x1C), ram.S16(n + 0x1E)); + frame.matrix = ScaleRows(RotZXYTrans(pos.X, pos.Y, pos.Z, angle.Item1, angle.Item2, angle.Item3), scale); + } + + frame.objectMatrix = frame.matrix; + currentObject = n; + SetAnimation(ram, n); + DrawNodeAndSiblings(ram, sharedChild, frame, 0); + anim.type = ANIM_NONE; + } + + void AddShadow(Rdram ram, uint n, bool held, in Frame frame) + { + float size = ram.S16(n + 0x14); + byte solidity = ram.U8(n + 0x16), type = ram.U8(n + 0x17); + Vector3 position; + if (held) + position = frame.WorldPosition; + else + { + position = new Vector3(ram.F32(currentObject + 0x20), ram.F32(currentObject + 0x24), ram.F32(currentObject + 0x28)); + size *= ram.F32(currentObject + 0x2C); + } + + float floor = currentObject == marioObject && addresses.marioState != 0 + ? ram.F32(addresses.marioState + 0x70) + : ram.F32(currentObject + 0xE8); + float height = position.Y - floor; + if (float.IsNaN(floor) || floor < -10000 || height < -50 || height > 2000) + return; + + float opacity = solidity / 255f; + if (height > 0) + { + size *= height >= 600 ? 0.5f : 1 - 0.5f * height / 600; + if (height > 600) + opacity *= Math.Max(0, 1 - (height - 600) / 600); + } + + if (size > 0 && opacity > 0) + shadows.Add(new Shadow + { + position = new Vector3(position.X, floor, position.Z), + size = size, + opacity = opacity, + square = type >= 10 && type <= 12, + }); + } + + static Matrix4 ReadMatrix(Rdram ram, uint a) + { + var m = new Matrix4(); + for (int r = 0; r < 4; r++) + for (int c = 0; c < 4; c++) + m[r, c] = ram.F32(a + (uint)(16 * r + 4 * c)); + return m; + } + + void SetAnimation(Rdram ram, uint objectNode) + { + anim = default; + uint curAnim = ram.U32(objectNode + 0x3C); + if (!Rdram.IsPointer(curAnim) && (curAnim >> 24) == 0) + return; + int animFlags = ram.U16(curAnim); + short divisor = ram.S16(curAnim + 0x02); + if ((animFlags & (1 << 3)) != 0) + anim.type = ANIM_VERTICAL_TRANSLATION; + else if ((animFlags & (1 << 4)) != 0) + anim.type = ANIM_LATERAL_TRANSLATION; + else if ((animFlags & (1 << 6)) != 0) + anim.type = ANIM_NO_TRANSLATION; + else + anim.type = ANIM_TRANSLATION; + anim.frame = ram.S16(objectNode + 0x40); + anim.values = ram.U32(curAnim + 0x0C); + anim.attributes = ram.U32(curAnim + 0x10); + anim.translationMultiplier = divisor == 0 ? 1 : ram.S16(objectNode + 0x3A) / (float)divisor; + } + + short NextAnimationValue(Rdram ram) + { + int count = ram.U16(anim.attributes), start = ram.U16(anim.attributes + 2); + int index = anim.frame < count ? start + anim.frame : start + count - 1; + anim.attributes += 4; + return ram.S16(anim.values + (uint)(2 * index)); + } + + struct Frame + { + public Matrix4 matrix, objectMatrix; + public bool billboard; + public Vector3 anchor, anchorScale; + + public Vector3 WorldPosition => billboard ? anchor + matrix.Row3.Xyz : matrix.Row3.Xyz; + } + + void DrawNodeAndSiblings(Rdram ram, uint first, Frame frame, int depth) + { + uint c = first; + for (int i = 0; i < 256 && Rdram.IsPointer(c); i++) + { + DrawNode(ram, c, frame, depth); + c = ram.U32(c + 0x08); + if (c == first) + break; + } + } + + void Emit(uint dl, int layer, in Frame frame, Rdram ram) + { + if (!Rdram.IsPointer(dl) && (dl >> 24) == 0) + return; + int slot = layer & 7; + var incoming = layerStates[slot]; + var key = (dl, layer, incoming.Hash); + if (!objectMeshes.TryGetValue(key, out var entry)) + { + var builder = new MeshBuilder(); + var f3d = new F3D(ram, textures, builder); + f3d.Load(incoming); + f3d.Run(dl, layer, Matrix4.Identity); + objectMeshes[key] = entry = (store.Add(builder), f3d.Save()); + } + + layerStates[slot] = entry.state; + if (entry.mesh.triangleCount > 0) + instances.Add(new Instance { mesh = entry.mesh, matrix = frame.matrix, billboard = frame.billboard, anchor = frame.anchor, anchorScale = frame.anchorScale }); + } + + void DrawNode(Rdram ram, uint n, Frame frame, int depth) + { + if (depth > 64 || --nodeBudget < 0) + return; + int t = ram.U16(n); + ushort flags = ram.U16(n + 2); + if ((flags & GRAPH_RENDER_ACTIVE) == 0) + return; + int layer = (flags >> 8) & 0xFF; + uint dl = 0; + switch (t) + { + case GN_DL: + dl = ram.U32(n + 0x14); + break; + case GN_TRANSROT: + frame.matrix = RotZXYTrans(ram.S16(n + 0x18), ram.S16(n + 0x1A), ram.S16(n + 0x1C), ram.S16(n + 0x1E), ram.S16(n + 0x20), ram.S16(n + 0x22)) * frame.matrix; + dl = ram.U32(n + 0x14); + break; + case GN_TRANS: + frame.matrix = RotZXYTrans(ram.S16(n + 0x18), ram.S16(n + 0x1A), ram.S16(n + 0x1C), 0, 0, 0) * frame.matrix; + dl = ram.U32(n + 0x14); + break; + case GN_ROT: + frame.matrix = RotZXYTrans(0, 0, 0, ram.S16(n + 0x18), ram.S16(n + 0x1A), ram.S16(n + 0x1C)) * frame.matrix; + dl = ram.U32(n + 0x14); + break; + case GN_SCALE: + frame.matrix = Matrix4.CreateScale(ram.F32(n + 0x18)) * frame.matrix; + dl = ram.U32(n + 0x14); + break; + case GN_ANIMPART: + frame.matrix = AnimatedPart(ram, n) * frame.matrix; + dl = ram.U32(n + 0x14); + break; + case GN_BILLBOARD: + var position = Vector3.TransformPosition(new Vector3(ram.S16(n + 0x18), ram.S16(n + 0x1A), ram.S16(n + 0x1C)), frame.matrix); + frame.anchor = frame.billboard ? frame.anchor + position : position; + frame.anchorScale = Vector3.One; + frame.billboard = true; + frame.matrix = Matrix4.Identity; + dl = ram.U32(n + 0x14); + break; + case GN_SHADOW: + AddShadow(ram, n, inHeldObject, frame); + break; + case GN_SWITCH: + int selected = ram.S16(n + 0x1E), i = 0; + uint switchFunction = ram.U32(n + 0x14); + if (addresses.geoSwitchAnimState != 0 && switchFunction == addresses.geoSwitchAnimState) + { + selected = ram.S32(currentObject + 0xF0); + if (selected >= ram.S16(n + 0x1C) || selected < 0) + selected = 0; + } + else if (addresses.geoSwitchMarioCapEffect != 0 && switchFunction == addresses.geoSwitchMarioCapEffect) + selected = MarioCapEffect(ram); + + uint selectedChild = ram.U32(n + 0x10); + for (i = 0; Rdram.IsPointer(selectedChild) && selected > i && i < 256; i++) + selectedChild = ram.U32(selectedChild + 0x08); + if (Rdram.IsPointer(selectedChild)) + DrawNode(ram, selectedChild, frame, depth + 1); + return; + case GN_LOD: + float distance = (frame.WorldPosition - gameCamera).Length; + if (!(ram.S16(n + 0x14) <= distance && distance < ram.S16(n + 0x16))) + return; + break; + case GN_HELD: + if (currentObject == marioObject && !inHeldObject) + DrawHeldObject(ram, n, frame, depth); + return; + case GN_GENLIST: + uint generator = ram.U32(n + 0x14); + if (addresses.coloredHatsGenerator != 0 && generator == addresses.coloredHatsGenerator) + EmitColoredHat(ram, layer, frame); + else if (addresses.geoMirrorMarioSetAlpha != 0 && generator == addresses.geoMirrorMarioSetAlpha) + SetMarioAlpha(ram); + else if (addresses.geoUpdateLayerTransparency != 0 && generator == addresses.geoUpdateLayerTransparency) + { + int opacity = ram.S32(currentObject + 0x17C) & 0xFF; + int target = ram.U32(n + 0x18) == 20 ? 6 : opacity == 0xFF ? LAYER_OPAQUE : LAYER_TRANSPARENT; + SetEnvAlpha(ram, target, opacity); + } + + break; + case GN_OBJECT: + case GN_OBJPARENT: + case GN_BG: + return; + } + + if (dl != 0) + Emit(dl, layer, frame, ram); + uint children = ram.U32(n + 0x10); + if (Rdram.IsPointer(children)) + DrawNodeAndSiblings(ram, children, frame, depth + 1); + } + + const int LAYER_OPAQUE = 1, LAYER_TRANSPARENT = 5; + + bool IsGhost => addresses.ghostHack && currentObject != marioObject; + + int MarioCapEffect(Rdram ram) => + IsGhost ? (sbyte)ram.U8(currentObject + 0x61) : ram.U16(addresses.bodyStates + 8) >> 8; + + readonly Dictionary<(int state, int alpha), F3D.State> alphaStates = new Dictionary<(int, int), F3D.State>(); + + void SetMarioAlpha(Rdram ram) + { + int alpha; + if (IsGhost) + alpha = (sbyte)ram.U8(currentObject + 0x61) != 0 ? 0x7F : 0xFF; + else + { + int modelState = ram.U16(addresses.bodyStates + 8); + alpha = (modelState & 0x100) != 0 ? modelState & 0xFF : 0xFF; + } + + SetEnvAlpha(ram, alpha == 0xFF ? LAYER_OPAQUE : LAYER_TRANSPARENT, alpha); + } + + void SetEnvAlpha(Rdram ram, int slot, int alpha) + { + slot &= 7; + var key = (layerStates[slot].Hash, alpha); + if (!alphaStates.TryGetValue(key, out var state)) + { + var f3d = new F3D(ram, textures, new MeshBuilder()); + f3d.Load(layerStates[slot]); + f3d.SetEnvColor(255, 255, 255, (byte)alpha); + alphaStates[key] = state = f3d.Save(); + } + + layerStates[slot] = state; + } + + const uint COLORED_HAT_DL_1 = 0x040119A0, COLORED_HAT_DL_2 = 0x04011978; + + void EmitColoredHat(Rdram ram, int layer, in Frame frame) + { + uint index = currentObject == marioObject ? 0u : ram.U8(currentObject + 0x60); + uint lights = addresses.coloredHatsLights + 0x20 * index; + int slot = layer & 7; + var incoming = layerStates[slot]; + var key = (lights, layer | 0x100, HashCode.Combine(incoming.Hash, ram.U32(lights), ram.U32(lights + 8), ram.U32(lights + 12))); + if (!objectMeshes.TryGetValue(key, out var entry)) + { + var builder = new MeshBuilder(); + var f3d = new F3D(ram, textures, builder); + f3d.Load(incoming); + f3d.MoveMemLight(0x88, lights); + f3d.MoveMemLight(0x86, lights + 8); + f3d.Run(COLORED_HAT_DL_1, layer, Matrix4.Identity); + f3d.MoveMemLight(0x88, lights); + f3d.MoveMemLight(0x86, lights + 8); + f3d.Run(COLORED_HAT_DL_2, layer, Matrix4.Identity); + objectMeshes[key] = entry = (store.Add(builder), f3d.Save()); + } + + layerStates[slot] = entry.state; + if (entry.mesh.triangleCount > 0) + instances.Add(new Instance { mesh = entry.mesh, matrix = frame.matrix, billboard = frame.billboard, anchor = frame.anchor, anchorScale = frame.anchorScale }); + } + + Matrix4 AnimatedPart(Rdram ram, uint n) + { + double tx = ram.S16(n + 0x18), ty = ram.S16(n + 0x1A), tz = ram.S16(n + 0x1C); + short rx = 0, ry = 0, rz = 0; + switch (anim.type) + { + case ANIM_TRANSLATION: + tx += NextAnimationValue(ram) * anim.translationMultiplier; + ty += NextAnimationValue(ram) * anim.translationMultiplier; + tz += NextAnimationValue(ram) * anim.translationMultiplier; + anim.type = ANIM_ROTATION; + break; + case ANIM_LATERAL_TRANSLATION: + tx += NextAnimationValue(ram) * anim.translationMultiplier; + anim.attributes += 4; + tz += NextAnimationValue(ram) * anim.translationMultiplier; + anim.type = ANIM_ROTATION; + break; + case ANIM_VERTICAL_TRANSLATION: + anim.attributes += 4; + ty += NextAnimationValue(ram) * anim.translationMultiplier; + anim.attributes += 4; + anim.type = ANIM_ROTATION; + break; + case ANIM_NO_TRANSLATION: + anim.attributes += 12; + anim.type = ANIM_ROTATION; + break; + } + + if (anim.type == ANIM_ROTATION) + { + rx = NextAnimationValue(ram); + ry = NextAnimationValue(ram); + rz = NextAnimationValue(ram); + } + + return RotXYZTrans(tx, ty, tz, rx, ry, rz); + } + + void DrawHeldObject(Rdram ram, uint n, Frame frame, int depth) + { + uint obj = ram.U32(n + 0x1C); + if (!Rdram.IsPointer(obj)) + return; + uint sharedChild = ram.U32(obj + 0x14); + if (!Rdram.IsPointer(sharedChild)) + return; + + var hand = frame.WorldPosition; + var holder = frame.objectMatrix; + holder.Row3 = new Vector4(hand, 1); + var translation = Matrix4.CreateTranslation(ram.S16(n + 0x20) / 4f, ram.S16(n + 0x22) / 4f, ram.S16(n + 0x24) / 4f); + var scale = new Vector3(ram.F32(obj + 0x2C), ram.F32(obj + 0x30), ram.F32(obj + 0x34)); + var held = new Frame { matrix = ScaleRows(translation * holder, scale) }; + held.objectMatrix = held.matrix; + + var holderAnimation = anim; + uint holderObject = currentObject; + bool wasInHeldObject = inHeldObject; + currentObject = obj; + inHeldObject = true; + SetAnimation(ram, obj); + DrawNodeAndSiblings(ram, sharedChild, held, depth + 1); + anim = holderAnimation; + currentObject = holderObject; + inHeldObject = wasInHeldObject; + } + + #endregion + } +} diff --git a/STROOP/Tabs/MapTab/DataUtil/Rdram.cs b/STROOP/Tabs/MapTab/DataUtil/Rdram.cs new file mode 100644 index 000000000..b17c2fcd0 --- /dev/null +++ b/STROOP/Tabs/MapTab/DataUtil/Rdram.cs @@ -0,0 +1,45 @@ +using System; + +namespace STROOP.Tabs.MapTab.DataUtil +{ + public class Rdram + { + public readonly byte[] data; + readonly uint[] segments = new uint[32]; + + public Rdram(byte[] littleEndianWords) + { + data = littleEndianWords; + } + + public void LoadSegmentTable(uint segmentTable) + { + for (uint i = 0; i < 32; i++) + segments[i] = U32((segmentTable | 0x80000000) + 4 * i); + } + + public uint Phys(uint a) => (a & 0x80000000) != 0 ? a & 0x7FFFFF : (segments[(a >> 24) & 0x1F] + (a & 0xFFFFFF)) & 0x7FFFFF; + + public bool Ok(uint physical, int length) => physical + length <= data.Length; + + public static bool IsPointer(uint a) => a >= 0x80000000 && a < 0x80800000; + + public byte U8Phys(uint p) => p < data.Length ? data[p ^ 3] : (byte)0; + + public ushort U16Phys(uint p) => (ushort)(U8Phys(p) << 8 | U8Phys(p + 1)); + + public uint U32Phys(uint p) + { + if ((p & 3) == 0) + return p + 4 <= data.Length ? BitConverter.ToUInt32(data, (int)p) : 0; + return (uint)(U8Phys(p) << 24 | U8Phys(p + 1) << 16 | U8Phys(p + 2) << 8 | U8Phys(p + 3)); + } + + public byte U8(uint a) => U8Phys(Phys(a)); + public ushort U16(uint a) => U16Phys(Phys(a)); + public short S16(uint a) => (short)U16(a); + public uint U32(uint a) => U32Phys(Phys(a)); + public int S32(uint a) => (int)U32(a); + public float F32(uint a) => BitConverter.Int32BitsToSingle((int)U32(a)); + } +} diff --git a/STROOP/Tabs/MapTab/MapGraphics.cs b/STROOP/Tabs/MapTab/MapGraphics.cs index 362734ab0..229baa42b 100644 --- a/STROOP/Tabs/MapTab/MapGraphics.cs +++ b/STROOP/Tabs/MapTab/MapGraphics.cs @@ -269,6 +269,8 @@ public MapGraphics(MapTab mapTab, GLControl glControl, Func ge List levelTrianglesFor3DMap; + public bool Draw3DCollisionGeometry => view3D.display3DLevelGeometry || !mapTab.IsLevelModelShown; + Control previouslyActiveControl; Control GetActiveLeafControl(ContainerControl root) @@ -457,7 +459,7 @@ private void OnPaint() GL.ClearDepth(1); GL.Clear(ClearBufferMask.DepthBufferBit); - if (view3D.display3DLevelGeometry) + if (Draw3DCollisionGeometry) drawLayers[(int)DrawLayers.FillBuffers].Insert(0, () => { foreach (var t in levelTrianglesFor3DMap) diff --git a/STROOP/Tabs/MapTab/MapObjects/MapDrawingObject.cs b/STROOP/Tabs/MapTab/MapObjects/MapDrawingObject.cs index fd7466df3..adbaee8e1 100644 --- a/STROOP/Tabs/MapTab/MapObjects/MapDrawingObject.cs +++ b/STROOP/Tabs/MapTab/MapObjects/MapDrawingObject.cs @@ -5,6 +5,7 @@ using System.Windows.Forms; using OpenTK; using OpenTK.Mathematics; +using STROOP.Variables.SM64MemoryLayout; namespace STROOP.Tabs.MapTab.MapObjects { @@ -14,10 +15,19 @@ public class MapDrawingObject : MapLineObject class Drawing : IHoverData { MapDrawingObject parent; + readonly bool cursorHasHeight; - public Drawing(MapDrawingObject target) + public Drawing(MapDrawingObject target, MapGraphics graphics) { this.parent = target; + cursorHasHeight = graphics.viewMode != MapGraphics.ViewMode.TopDown; + } + + static Vector3 WithHeight(Vector3 position, bool hasHeight) + { + if (!hasHeight) + position.Y = Config.Stream.GetSingle(MarioConfig.StructAddress + MarioConfig.YOffset); + return position; } public void AddContextMenuItems(MapTab tab, ContextMenuStrip menu) @@ -37,7 +47,7 @@ public void AddContextMenuItems(MapTab tab, ContextMenuStrip menu) public void DragTo(Vector3 newPosition, bool setY) { - Vector3 currentVertex = newPosition; + Vector3 currentVertex = WithHeight(newPosition, setY); if (currentVertex != parent._lastVertex) { parent._vertices.Add(parent._lastVertex); @@ -53,7 +63,7 @@ public void SetLookAt(Vector3 lookAt) public void LeftClick(Vector3 position) { - parent._lastVertex = position; + parent._lastVertex = WithHeight(position, cursorHasHeight); } public void RightClick(Vector3 position) @@ -105,6 +115,6 @@ protected override ContextMenuStrip GetContextMenuStrip(MapTracker targetTracker public void ClearDrawing() => _vertices.Clear(); - public override IHoverData GetHoverData(MapGraphics graphics, ref Vector3 position) => drawingEnabled ? new Drawing(this) : null; + public override IHoverData GetHoverData(MapGraphics graphics, ref Vector3 position) => drawingEnabled ? new Drawing(this, graphics) : null; } } diff --git a/STROOP/Tabs/MapTab/MapObjects/MapGhostObject.cs b/STROOP/Tabs/MapTab/MapObjects/MapGhostObject.cs index 952f15da5..63c276b40 100644 --- a/STROOP/Tabs/MapTab/MapObjects/MapGhostObject.cs +++ b/STROOP/Tabs/MapTab/MapObjects/MapGhostObject.cs @@ -21,10 +21,12 @@ public MapGhostObject() protected override void DrawTopDown(MapGraphics graphics) { + bool onlyHovered = currentMapTab.AreObjectModelsShown(graphics) + && Config.StroopMainForm.GetTab().GhostsDrawnInGame; graphics.drawLayers[(int)MapGraphics.DrawLayers.FillBuffers].Add(() => { foreach (var pa in positionAngleProvider()) - if (pa is GhostTab.Ghost.GhostPositionAngle a) + if (pa is GhostTab.Ghost.GhostPositionAngle a && (!onlyHovered || hoverData.currentPositionAngle == a)) { var transparent = graphics.viewMode == MapGraphics.ViewMode.ThreeDimensional; var alpha = hoverData.currentPositionAngle == a ? ObjectUtilities.HoverAlpha() : 1; diff --git a/STROOP/Tabs/MapTab/MapObjects/MapGridlinesObject.cs b/STROOP/Tabs/MapTab/MapObjects/MapGridlinesObject.cs index b2818b25c..7e7a3ef55 100644 --- a/STROOP/Tabs/MapTab/MapObjects/MapGridlinesObject.cs +++ b/STROOP/Tabs/MapTab/MapObjects/MapGridlinesObject.cs @@ -10,7 +10,7 @@ namespace STROOP.Tabs.MapTab.MapObjects { - [ObjectDescription("Unit Gridlines", "Grid", nameof(CreateCustom))] + [ObjectDescription("Unit Gridlines", "Grid", nameof(CreateUnits))] [ObjectDescription("Custom Gridlines", "Grid", nameof(CreateCustom))] public class MapGridlinesObject : MapLineObject { @@ -131,16 +131,16 @@ protected void AddVerticesToPositionAngle( float minY, maxY; bool is3DGrid = !float.IsNaN(verticalMultiplier); + float verticalIncrement = is3DGrid ? increment * verticalMultiplier : float.PositiveInfinity; if (!is3DGrid) minY = maxY = positionAngle.Y; else { - float verticalIncrement = increment * verticalMultiplier; minY = (int)((positionAngle.Y - vExpanse) / verticalIncrement - 1) * verticalIncrement; maxY = (int)((positionAngle.Y + vExpanse) / verticalIncrement + 1) * verticalIncrement; } - for (float y = minY; y <= maxY; y += verticalMultiplier) + for (float y = minY; y <= maxY; y += verticalIncrement) { for (var x = viewXMin; x <= viewXMax; x += increment) { diff --git a/STROOP/Tabs/MapTab/MapObjects/MapIconPointObject.cs b/STROOP/Tabs/MapTab/MapObjects/MapIconPointObject.cs index 8b34d10e3..d38ca341d 100644 --- a/STROOP/Tabs/MapTab/MapObjects/MapIconPointObject.cs +++ b/STROOP/Tabs/MapTab/MapObjects/MapIconPointObject.cs @@ -11,12 +11,16 @@ protected MapIconPointObject(ObjectCreateParams creationParameters) { } + protected virtual bool IsGameObject => false; + protected override void DrawTopDown(MapGraphics graphics) { + bool onlyHovered = IsGameObject && currentMapTab.AreObjectModelsShown(graphics); graphics.drawLayers[(int)MapGraphics.DrawLayers.FillBuffers].Add(() => { foreach (var a in positionAngleProvider()) - DrawIcon(graphics, + if (!onlyHovered || hoverData.currentPositionAngle == a) + DrawIcon(graphics, graphics.viewMode == MapGraphics.ViewMode.ThreeDimensional, (float)a.X, (float)a.Y, (float)a.Z, Rotates ? (float)a.Angle : 0x8000 - graphics.MapViewAngleValue, diff --git a/STROOP/Tabs/MapTab/MapObjects/MapLevelModelObject.cs b/STROOP/Tabs/MapTab/MapObjects/MapLevelModelObject.cs new file mode 100644 index 000000000..762137d8b --- /dev/null +++ b/STROOP/Tabs/MapTab/MapObjects/MapLevelModelObject.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using OpenTK.Graphics.OpenGL; +using STROOP.Core; +using STROOP.Structs; +using STROOP.Structs.Configurations; +using STROOP.Tabs.MapTab.DataUtil; +using STROOP.Variables.SM64MemoryLayout; + +namespace STROOP.Tabs.MapTab.MapObjects +{ + public class MapLevelModelObject : MapObject + { + const int STABLE_UPDATES_BEFORE_REBUILD = 3; + + readonly MapCurrentMapObject fallback = new MapCurrentMapObject(); + readonly Renderers.LevelModelRenderer renderer = new Renderers.LevelModelRenderer(); + readonly Rdram ram = new Rdram(new byte[Config.RamSize]); + + LevelModel model; + int levelKey; + int stableUpdates = -1; + List<(int y, int xMin, int xMax, int zMin, int zMax)> waters; + + public bool showModel = true; + public bool showObjects = true; + public bool showWater = true; + public bool showSky = true; + public bool cullFaces = true; + public bool hideAboveMario = false; + public float hideAboveMarioOffset = 200; + + public LevelModel Model => model; + + public MapLevelModelObject() : base() + { + } + + public override string GetName() => "Level Model"; + + public override Lazy GetInternalImage() => fallback.GetInternalImage(); + + int ComputeLevelKey() + { + uint area = ram.U32(AreaConfig.CurrentAreaPointerAddress); + var hash = new HashCode(); + hash.Add(ram.U8(MiscConfig.LevelIndexAddress)); + hash.Add(area); + hash.Add(Rdram.IsPointer(area) ? ram.U8(area) : 0); + hash.Add(Rdram.IsPointer(area) ? ram.U32(area + 4) : 0); + hash.Add(ram.S32(TriangleConfig.LevelTriangleCountAddress)); + hash.Add(ram.U32(Config.ObjectAssociations.SegmentTable + 4 * 7)); + return hash.ToHashCode(); + } + + public override void Update() + { + base.Update(); + fallback.Update(); + if (Config.Stream == null || !currentMapTab.IsMapVisible) + return; + + Config.Stream.CopyCachedRam(ram.data); + ram.LoadSegmentTable(Config.ObjectAssociations.SegmentTable); + + int key = ComputeLevelKey(); + if (key != levelKey) + { + levelKey = key; + stableUpdates = 0; + } + else if (stableUpdates >= 0) + stableUpdates++; + else if (model == null) + stableUpdates = 0; + + if (currentMapTab.NeedsGeometryRefresh()) + stableUpdates = STABLE_UPDATES_BEFORE_REBUILD; + + if (stableUpdates >= STABLE_UPDATES_BEFORE_REBUILD) + { + stableUpdates = -1; + Rebuild(); + } + + if (model != null && showObjects) + try + { + model.UpdateObjects(ram, GetGameAddresses(), GetDoors()); + } + catch (Exception e) + { + System.Diagnostics.Debug.WriteLine($"Objects could not be placed: {e}"); + model.instances.Clear(); + } + + waters = showWater ? WaterUtilities.GetWaterLevels() : null; + } + + readonly HashSet doors = new HashSet(); + + static readonly uint[] DoorBehaviors = { 0x13000000, 0x13000AFC, 0x13000B0C }; + + HashSet GetDoors() + { + doors.Clear(); + foreach (var obj in Config.StroopMainForm.ObjectSlotsManager.GetLoadedObjectsWithPredicate( + o => Array.IndexOf(DoorBehaviors, o.BehaviorCriteria.BehaviorAddress) >= 0)) + doors.Add(obj.Address); + return doors; + } + + static LevelModel.GameAddresses GetGameAddresses() + { + var addresses = RomVersionConfig.Version == RomVersion.US + ? LevelModel.GameAddresses.US(MarioObjectConfig.PointerAddress, MarioConfig.StructAddress) + : new LevelModel.GameAddresses { marioObjectPointer = MarioObjectConfig.PointerAddress, marioState = MarioConfig.StructAddress }; + if (RomVersionConfig.Version == RomVersion.US) + { + var ghostTab = Config.StroopMainForm.GetTab(); + addresses.ghostHack = ghostTab.GhostHackInstalled; + if (ghostTab.ColoredHatsInstalled) + { + addresses.coloredHatsGenerator = ghostTab.ColoredHatsGeneratorAddress; + addresses.coloredHatsLights = ghostTab.ColoredHatsLightsAddress; + } + } + + return addresses; + } + + public void Rebuild() + { + try + { + model = LevelModel.Build(ram, AreaConfig.CurrentAreaPointerAddress, GetGameAddresses()); + } + catch (Exception e) + { + System.Diagnostics.Debug.WriteLine($"Level model could not be built: {e}"); + model = LevelModel.None; + } + } + + bool UseFallback => !currentMapTab.IsLevelModelShown; + + float GetMaxY() + { + if (!hideAboveMario) + return float.PositiveInfinity; + return Config.Stream.GetSingle(MarioConfig.StructAddress + MarioConfig.YOffset) + hideAboveMarioOffset; + } + + void DrawModel(MapGraphics graphics) + { + var drawnModel = model; + var drawnWaters = waters; + float maxY = GetMaxY(); + graphics.drawLayers[(int)MapGraphics.DrawLayers.Background].Add(() => + { + renderer.Draw(graphics, drawnModel, maxY, cullFaces, showObjects, drawnWaters, + showSky && graphics.viewMode == MapGraphics.ViewMode.ThreeDimensional); + if (graphics.viewMode != MapGraphics.ViewMode.ThreeDimensional) + { + GL.Clear(ClearBufferMask.DepthBufferBit); + GL.Disable(EnableCap.DepthTest); + } + }); + } + + protected override void DrawTopDown(MapGraphics graphics) + { + if (UseFallback) + fallback.Draw(graphics); + else + DrawModel(graphics); + } + + protected override void DrawOrthogonal(MapGraphics graphics) + { + if (!UseFallback) + DrawModel(graphics); + } + + protected override void Draw3D(MapGraphics graphics) + { + if (UseFallback) + fallback.Draw(graphics); + else + DrawModel(graphics); + } + } +} diff --git a/STROOP/Tabs/MapTab/MapObjects/MapLineSegmentObject.cs b/STROOP/Tabs/MapTab/MapObjects/MapLineSegmentObject.cs index e47a266b3..a9a3f0e1c 100644 --- a/STROOP/Tabs/MapTab/MapObjects/MapLineSegmentObject.cs +++ b/STROOP/Tabs/MapTab/MapObjects/MapLineSegmentObject.cs @@ -51,9 +51,11 @@ protected override List GetVertices(MapGraphics graphics) (double startX, double startZ) = MoreMath.ExtrapolateLine2D(x2, z2, x1, z1, dist + _backwardsSize); (double endX, double endZ) = MoreMath.ExtrapolateLine2D(x1, z1, x2, z2, (_useFixedSize ? 0 : dist) + Size); + double startT = dist == 0 ? 0 : -_backwardsSize / dist; + double endT = dist == 0 ? 0 : ((_useFixedSize ? 0 : dist) + Size) / dist; var vertices = new List(); - vertices.Add(new Vector3((float)startX, 0, (float)startZ)); - vertices.Add(new Vector3((float)endX, 0, (float)endZ)); + vertices.Add(new Vector3((float)startX, (float)(y1 + (y2 - y1) * startT), (float)startZ)); + vertices.Add(new Vector3((float)endX, (float)(y1 + (y2 - y1) * endT), (float)endZ)); return vertices; } diff --git a/STROOP/Tabs/MapTab/MapObjects/MapMapObject.cs b/STROOP/Tabs/MapTab/MapObjects/MapMapObject.cs index 896c7b562..2705bc54b 100644 --- a/STROOP/Tabs/MapTab/MapObjects/MapMapObject.cs +++ b/STROOP/Tabs/MapTab/MapObjects/MapMapObject.cs @@ -2,6 +2,8 @@ using System.Drawing; using OpenTK; using OpenTK.Mathematics; +using STROOP.Structs.Configurations; +using STROOP.Variables.SM64MemoryLayout; namespace STROOP.Tabs.MapTab.MapObjects { @@ -43,6 +45,28 @@ protected override void DrawOrthogonal(MapGraphics graphics) { } + protected override void Draw3D(MapGraphics graphics) + { + renderer.SetDrawCalls(graphics); + var img = GetInternalImage(); + if (img == null) + return; + var flat = new Matrix4(1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1); + float y = Config.Stream.GetSingle(MarioConfig.StructAddress + MarioConfig.YOffset); + graphics.drawLayers[(int)MapGraphics.DrawLayers.FillBuffers].Add(() => + { + renderer.texture = GraphicsUtil.TextureFromImage(img.Value); + foreach (var dim in GetDimensions(graphics)) + renderer.AddInstance( + flat + * Matrix4.CreateScale(dim.size.Width, 1, dim.size.Height) + * Matrix4.CreateTranslation(dim.loc.X, y, dim.loc.Y), + graphics.rendererCollection.GetObjectTextureLayer(GetInternalImage().Value), + new Vector4(1) + ); + }); + } + protected List<(PointF loc, SizeF size)> GetDimensions(MapGraphics graphics) { RectangleF rectangle = GetMapLayout().Coordinates; diff --git a/STROOP/Tabs/MapTab/MapObjects/MapMarioObject.cs b/STROOP/Tabs/MapTab/MapObjects/MapMarioObject.cs index ab4cc4186..1b82b281a 100644 --- a/STROOP/Tabs/MapTab/MapObjects/MapMarioObject.cs +++ b/STROOP/Tabs/MapTab/MapObjects/MapMarioObject.cs @@ -17,6 +17,8 @@ public MapMarioObject() : base(null) public override Lazy GetInternalImage() => Config.ObjectAssociations.MarioMapImage; + protected override bool IsGameObject => true; + public override void InitSubTrackerContextMenuStrip(MapTab mapTab, ContextMenuStrip targetStrip) { base.InitSubTrackerContextMenuStrip(mapTab, targetStrip); diff --git a/STROOP/Tabs/MapTab/MapObjects/MapMultipleObjectsObject.cs b/STROOP/Tabs/MapTab/MapObjects/MapMultipleObjectsObject.cs index f6d20e0f0..e31b7fcff 100644 --- a/STROOP/Tabs/MapTab/MapObjects/MapMultipleObjectsObject.cs +++ b/STROOP/Tabs/MapTab/MapObjects/MapMultipleObjectsObject.cs @@ -76,14 +76,18 @@ public override Lazy GetInternalImage() public override string GetName() => PositionAngle.NameOfMultiple(positionAngleProvider(), $"All {_objName}"); + protected override bool IsGameObject => true; + protected override void DrawTopDown(MapGraphics graphics) { + bool onlyHovered = currentMapTab.AreObjectModelsShown(graphics); graphics.drawLayers[(int)MapGraphics.DrawLayers.FillBuffers].Add(() => { List<(float x, float y, float z, float angle, Lazy tex, float alpha)> data = GetData(); data.Reverse(); foreach (var d in data) - DrawIcon(graphics, graphics.viewMode != MapGraphics.ViewMode.TopDown, d.x, d.y, d.z, d.angle, d.tex.Value, new Vector4(1, 1, 1, d.alpha)); + if (!onlyHovered || d.alpha != 1) + DrawIcon(graphics, graphics.viewMode != MapGraphics.ViewMode.TopDown, d.x, d.y, d.z, d.angle, d.tex.Value, new Vector4(1, 1, 1, d.alpha)); }); } diff --git a/STROOP/Tabs/MapTab/MapObjects/MapObjectObject.cs b/STROOP/Tabs/MapTab/MapObjects/MapObjectObject.cs index 94d1dc38e..4ec36f60c 100644 --- a/STROOP/Tabs/MapTab/MapObjects/MapObjectObject.cs +++ b/STROOP/Tabs/MapTab/MapObjects/MapObjectObject.cs @@ -241,6 +241,8 @@ protected MapObjectObject(ObjectProvider objectProvider) this.objectProvider = objectProvider; } + protected override bool IsGameObject => true; + public override Lazy GetInternalImage() { Lazy result = null; diff --git a/STROOP/Tabs/MapTab/MapObjects/MapSectorObject.cs b/STROOP/Tabs/MapTab/MapObjects/MapSectorObject.cs index fdbe86ef8..5c8d0c3b8 100644 --- a/STROOP/Tabs/MapTab/MapObjects/MapSectorObject.cs +++ b/STROOP/Tabs/MapTab/MapObjects/MapSectorObject.cs @@ -31,20 +31,20 @@ protected override void DrawTopDown(MapGraphics graphics) { graphics.drawLayers[(int)MapGraphics.DrawLayers.FillBuffers].Add(() => { - List<(float centerX, float centerZ, float radius, float angle, float angleRadius)> dimenstionList = GetDimensions(); - foreach ((float centerX, float centerZ, float radius, float angle, float angleRadius) in dimenstionList) + var dimenstionList = GetDimensions(); + foreach ((float centerX, float centerY, float centerZ, float radius, float angle, float angleRadius) in dimenstionList) { var pfft = MoreMath.AngleUnitsToRadians(angle - angleRadius); var pfft2 = MoreMath.AngleUnitsToRadians(angle + angleRadius); graphics.lineRenderer.Add( - new Vector3(centerX, 0, centerZ), - new Vector3(centerX + (float)Math.Sin(pfft) * radius, 0, centerZ + (float)Math.Cos(pfft) * radius), + new Vector3(centerX, centerY, centerZ), + new Vector3(centerX + (float)Math.Sin(pfft) * radius, centerY, centerZ + (float)Math.Cos(pfft) * radius), OpenTKUtilities.ColorToVec4(OutlineColor), OutlineWidth); graphics.lineRenderer.Add( - new Vector3(centerX, 0, centerZ), - new Vector3(centerX + (float)Math.Sin(pfft2) * radius, 0, centerZ + (float)Math.Cos(pfft2) * radius), + new Vector3(centerX, centerY, centerZ), + new Vector3(centerX + (float)Math.Sin(pfft2) * radius, centerY, centerZ + (float)Math.Cos(pfft2) * radius), OpenTKUtilities.ColorToVec4(OutlineColor), OutlineWidth); } @@ -53,13 +53,13 @@ protected override void DrawTopDown(MapGraphics graphics) protected override void DrawOrthogonal(MapGraphics graphics) => DrawTopDown(graphics); - protected List<(float centerX, float centerZ, float radius, float angle, float angleRadius)> GetDimensions() + protected List<(float centerX, float centerY, float centerZ, float radius, float angle, float angleRadius)> GetDimensions() { - var lst = new List<(float centerX, float centerZ, float radius, float angle, float angleRadius)>(); + var lst = new List<(float centerX, float centerY, float centerZ, float radius, float angle, float angleRadius)>(); foreach (var _posAngle in positionAngleProvider()) { (double x, double y, double z, double angle) = _posAngle.GetValues(); - lst.Add(((float)x, (float)z, Size, (float)angle, _angleRadius)); + lst.Add(((float)x, (float)y, (float)z, Size, (float)angle, _angleRadius)); } return lst; diff --git a/STROOP/Tabs/MapTab/MapObjects/MapTriangleObject.cs b/STROOP/Tabs/MapTab/MapObjects/MapTriangleObject.cs index 790a71484..209ca6b6a 100644 --- a/STROOP/Tabs/MapTab/MapObjects/MapTriangleObject.cs +++ b/STROOP/Tabs/MapTab/MapObjects/MapTriangleObject.cs @@ -265,7 +265,7 @@ protected override void Draw3D(MapGraphics graphics) baseColor.W = OpacityByte / 255f; var projectionColor = new Vector4(baseColor.Xyz, _projectionAlphaMultiplier * baseColor.W); - if (!graphics.view3D.display3DLevelGeometry) + if (!graphics.Draw3DCollisionGeometry || tri.AssociatedObject != 0) graphics.triangleRenderer.Add( tri.p1, tri.p2, diff --git a/STROOP/Tabs/MapTab/MapTab.cs b/STROOP/Tabs/MapTab/MapTab.cs index 939ef5865..70cf2d0a3 100644 --- a/STROOP/Tabs/MapTab/MapTab.cs +++ b/STROOP/Tabs/MapTab/MapTab.cs @@ -157,7 +157,7 @@ public void Load2D() graphics.Load(() => new Renderers.RendererCollection()); InitializeControls(); - comboBoxViewMode.SelectedIndex = 0; + comboBoxViewMode.SelectedIndex = (int)MapGraphics.ViewMode.ThreeDimensional; if (!System.IO.File.Exists(DEFAULT_TRACKER_FILE)) flowLayoutPanelMapTrackers.Controls.Clear(); else @@ -175,6 +175,18 @@ public MapLayout GetMapLayout(object mapLayoutChoice = null) => bool displayingExtendedBoundaries = false; bool needsGeometryRefresh, _needsGeometryRefreshInternal; public bool NeedsGeometryRefresh() => needsGeometryRefresh; + + MapLevelModelObject levelModelObject; + + public bool IsMapVisible { get; private set; } = true; + + public bool IsLevelModelShown => levelModelObject != null + && levelModelObject.showModel + && levelModelObject.Model is { IsEmpty: false } + && !(comboBoxMapOptionsLevel.SelectedItem is MapLayout); + + public bool AreObjectModelsShown(MapGraphics graphics) => + graphics.viewMode == MapGraphics.ViewMode.ThreeDimensional && IsLevelModelShown && levelModelObject.showObjects; public void RequireGeometryUpdate() => _needsGeometryRefreshInternal = true; @@ -360,7 +372,7 @@ private void InitializeControls() checkBoxMapOptionsTrackMario.Checked = true; // FlowLayoutPanel - flowLayoutPanelMapTrackers.Initialize(new MapCurrentMapObject(), new MapCurrentBackgroundObject()); + flowLayoutPanelMapTrackers.Initialize(levelModelObject = new MapLevelModelObject(), new MapCurrentBackgroundObject()); // ComboBox for Level List mapLayouts = MapAssociations.GetAllMaps(); @@ -552,11 +564,39 @@ void AddViewModeContextMenu() itemRefreshLevelGeometry.Click += (__, ___) => RequireGeometryUpdate(); ctx.Items.Add(itemRefreshLevelGeometry); + var itemLevelModel = new ToolStripMenuItem("Level Model"); + var itemShowLevelModel = new ToolStripMenuItem("Show Textured Level Model") { Checked = levelModelObject.showModel }; + itemShowLevelModel.Click += (__, ___) => itemShowLevelModel.Checked = levelModelObject.showModel = !levelModelObject.showModel; + var itemShowObjects = new ToolStripMenuItem("Show Mario and Objects") { Checked = levelModelObject.showObjects }; + itemShowObjects.Click += (__, ___) => itemShowObjects.Checked = levelModelObject.showObjects = !levelModelObject.showObjects; + var itemShowWater = new ToolStripMenuItem("Show Water") { Checked = levelModelObject.showWater }; + itemShowWater.Click += (__, ___) => itemShowWater.Checked = levelModelObject.showWater = !levelModelObject.showWater; + var itemShowSky = new ToolStripMenuItem("Show Sky (3D)") { Checked = levelModelObject.showSky }; + itemShowSky.Click += (__, ___) => itemShowSky.Checked = levelModelObject.showSky = !levelModelObject.showSky; + var itemCullFaces = new ToolStripMenuItem("Cull Back Faces (like the game)") { Checked = levelModelObject.cullFaces }; + itemCullFaces.Click += (__, ___) => itemCullFaces.Checked = levelModelObject.cullFaces = !levelModelObject.cullFaces; + var itemHideAboveMario = new ToolStripMenuItem("Hide Geometry Above Mario") { Checked = levelModelObject.hideAboveMario }; + itemHideAboveMario.Click += (__, ___) => itemHideAboveMario.Checked = levelModelObject.hideAboveMario = !levelModelObject.hideAboveMario; + var itemHideAboveMarioOffset = new ToolStripMenuItem("Set Height Above Mario..."); + itemHideAboveMarioOffset.Click += (__, ___) => + levelModelObject.hideAboveMarioOffset = (float)DialogUtilities.GetDoubleFromDialog( + levelModelObject.hideAboveMarioOffset, + levelModelObject.hideAboveMarioOffset.ToString(), + "Hide the level model above Mario's height plus:"); + itemLevelModel.DropDownItems.Add(itemShowLevelModel); + itemLevelModel.DropDownItems.Add(itemShowObjects); + itemLevelModel.DropDownItems.Add(itemShowWater); + itemLevelModel.DropDownItems.Add(itemShowSky); + itemLevelModel.DropDownItems.Add(itemCullFaces); + itemLevelModel.DropDownItems.Add(itemHideAboveMario); + itemLevelModel.DropDownItems.Add(itemHideAboveMarioOffset); + ctx.Items.Add(itemLevelModel); + if (graphics.viewMode == MapGraphics.ViewMode.ThreeDimensional) { ctx.Items.Add(new ToolStripSeparator()); - var itemDisplayLevelGeometry = new ToolStripMenuItem("Display Level Geometry"); + var itemDisplayLevelGeometry = new ToolStripMenuItem("Display Collision Geometry"); itemDisplayLevelGeometry.Checked = graphics.view3D.display3DLevelGeometry; itemDisplayLevelGeometry.Click += (__, ___) => itemDisplayLevelGeometry.Checked = graphics.view3D.display3DLevelGeometry = !graphics.view3D.display3DLevelGeometry; ctx.Items.Add(itemDisplayLevelGeometry); @@ -654,7 +694,11 @@ public override void Update(bool active) } } - graphics.UpdateCursor(); + active |= popouts.Count > 0; + IsMapVisible = active; + + if (active) + graphics.UpdateCursor(); if (displayingExtendedBoundaries != SavedSettingsConfig.UseExtendedLevelBoundaries) { displayingExtendedBoundaries = SavedSettingsConfig.UseExtendedLevelBoundaries; @@ -673,7 +717,6 @@ public override void Update(bool active) { flowLayoutPanelMapTrackers.UpdateControl(); - active |= popouts.Count > 0; if (active) { base.Update(active); diff --git a/STROOP/Tabs/MapTab/Renderers/InstanceRenderer.cs b/STROOP/Tabs/MapTab/Renderers/InstanceRenderer.cs index cea003722..75c3465d1 100644 --- a/STROOP/Tabs/MapTab/Renderers/InstanceRenderer.cs +++ b/STROOP/Tabs/MapTab/Renderers/InstanceRenderer.cs @@ -1,7 +1,8 @@ -using OpenTK.Graphics.OpenGL; +using OpenTK.Graphics.OpenGL; using OpenTK; using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using OpenTK.Mathematics; using STROOP.Core; @@ -14,7 +15,6 @@ public abstract class InstanceRenderer : Renderer where InstanceDa protected List instances = new List(); protected readonly int instanceSize; protected int maxInstances { get; private set; } - protected IntPtr dataPtr { get; private set; } public int uniform_viewProjection { get; private set; } @@ -25,16 +25,14 @@ public abstract class InstanceRenderer : Renderer where InstanceDa public InstanceRenderer() { instanceSize = Marshal.SizeOf(typeof(InstanceData)); + if (instanceSize != Unsafe.SizeOf()) + throw new InvalidOperationException($"{typeof(InstanceData)} is not blittable"); } - protected void WriteDataToBuffer() + void WriteDataToBuffer() { - IntPtr ptr = dataPtr; - foreach (var instance in instances) - { - Marshal.StructureToPtr(instance, ptr, false); - ptr = IntPtr.Add(ptr, instanceSize); - } + if (instances.Count > 0) + GL.BufferSubData(BufferTarget.ArrayBuffer, IntPtr.Zero, instanceSize * instances.Count, ref CollectionsMarshal.AsSpan(instances)[0]); } protected void Init(int maxExpectedInstances) @@ -55,19 +53,12 @@ protected void UpdateBuffer(int maxInstances, bool writeData = true) if (maxInstances > this.maxInstances) { - if (dataPtr != IntPtr.Zero) - Marshal.FreeHGlobal(dataPtr); - dataPtr = Marshal.AllocHGlobal(bufferSize); this.maxInstances = maxInstances; - if (writeData) - WriteDataToBuffer(); - GL.BufferData(BufferTarget.ArrayBuffer, (IntPtr)bufferSize, writeData ? dataPtr : IntPtr.Zero, BufferUsageHint.StreamDraw); + GL.BufferData(BufferTarget.ArrayBuffer, (IntPtr)bufferSize, IntPtr.Zero, BufferUsageHint.StreamDraw); } - else - { + + if (writeData) WriteDataToBuffer(); - GL.BufferSubData(BufferTarget.ArrayBuffer, IntPtr.Zero, (IntPtr)(instanceSize * instances.Count), dataPtr); - } } protected void BeginDraw(MapGraphics graphics, bool updateBuffer = true) diff --git a/STROOP/Tabs/MapTab/Renderers/LevelModelRenderer.cs b/STROOP/Tabs/MapTab/Renderers/LevelModelRenderer.cs new file mode 100644 index 000000000..8084c180c --- /dev/null +++ b/STROOP/Tabs/MapTab/Renderers/LevelModelRenderer.cs @@ -0,0 +1,391 @@ +using System; +using System.Collections.Generic; +using OpenTK.Graphics.OpenGL; +using OpenTK.Mathematics; +using STROOP.Tabs.MapTab.DataUtil; + +namespace STROOP.Tabs.MapTab.Renderers +{ + public class LevelModelRenderer + { + const int LAYER_ALPHA = 4, LAYER_TRANSPARENT = 5; + + const uint WaterColor = 0x66D57B3A; + const int ShadowSegments = 16; + + int shader, vao, buffer, dynamicVao, dynamicBuffer; + int bufferCapacity, uploadedLength; + readonly List textures = new List(); + LevelModel uploaded; + byte[] dynamicVertices = new byte[MeshStore.VertexSize * 1024]; + int dynamicLength; + + int uniform_viewRotation; + int uniform_viewProjection, uniform_model, uniform_tex, uniform_useTexture, uniform_decal, uniform_alphaCutoff, uniform_opaque, uniform_maxY; + + int skyShader, skyVao, skyTexture; + int uniform_sky_useTexture, uniform_sky_color, uniform_sky_windowOrigin; + + static int CreateVertexArray(out int vertexBuffer) + { + int vertexArray = GL.GenVertexArray(); + vertexBuffer = GL.GenBuffer(); + GL.BindVertexArray(vertexArray); + GL.BindBuffer(BufferTarget.ArrayBuffer, vertexBuffer); + for (int i = 0; i < 6; i++) + GL.EnableVertexAttribArray(i); + GL.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, MeshStore.VertexSize, 0); + GL.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, MeshStore.VertexSize, 12); + GL.VertexAttribPointer(2, 4, VertexAttribPointerType.UnsignedByte, true, MeshStore.VertexSize, 20); + GL.VertexAttribPointer(3, 4, VertexAttribPointerType.UnsignedByte, true, MeshStore.VertexSize, 24); + GL.VertexAttribPointer(4, 3, VertexAttribPointerType.Byte, false, MeshStore.VertexSize, 28); + GL.VertexAttribPointer(5, 3, VertexAttribPointerType.Byte, false, MeshStore.VertexSize, 32); + GL.BindVertexArray(0); + return vertexArray; + } + + void Init() + { + shader = GraphicsUtil.GetShaderProgram("Resources/Shaders/LevelModel.vert.glsl", "Resources/Shaders/LevelModel.frag.glsl"); + uniform_viewProjection = GL.GetUniformLocation(shader, "viewProjection"); + uniform_model = GL.GetUniformLocation(shader, "model"); + uniform_viewRotation = GL.GetUniformLocation(shader, "viewRotation"); + uniform_tex = GL.GetUniformLocation(shader, "tex"); + uniform_useTexture = GL.GetUniformLocation(shader, "useTexture"); + uniform_decal = GL.GetUniformLocation(shader, "decal"); + uniform_alphaCutoff = GL.GetUniformLocation(shader, "alphaCutoff"); + uniform_opaque = GL.GetUniformLocation(shader, "opaque"); + uniform_maxY = GL.GetUniformLocation(shader, "maxY"); + vao = CreateVertexArray(out buffer); + dynamicVao = CreateVertexArray(out dynamicBuffer); + + skyShader = GraphicsUtil.GetShaderProgram("Resources/Shaders/Skybox.vert.glsl", "Resources/Shaders/Skybox.frag.glsl"); + uniform_sky_useTexture = GL.GetUniformLocation(skyShader, "useTexture"); + uniform_sky_color = GL.GetUniformLocation(skyShader, "color"); + uniform_sky_windowOrigin = GL.GetUniformLocation(skyShader, "windowOrigin"); + skyVao = GL.GenVertexArray(); + } + + void UploadSkybox(Texture sky) + { + if (skyTexture != 0) + GL.DeleteTexture(skyTexture); + skyTexture = 0; + if (sky == null) + return; + skyTexture = GL.GenTexture(); + GL.BindTexture(TextureTarget.Texture2D, skyTexture); + GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba8, sky.width, sky.height, 0, PixelFormat.Rgba, PixelType.UnsignedByte, sky.rgba); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.ClampToEdge); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.ClampToEdge); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear); + GL.BindTexture(TextureTarget.Texture2D, 0); + } + + void DrawSky(MapGraphics graphics, LevelModel model) + { + if (skyTexture == 0 && !model.backgroundColor.HasValue) + return; + var forward = -graphics.BillboardMatrix.Row2.Xyz; + double yaw = (Math.Atan2(forward.X, forward.Z) * 180 / Math.PI + 360) % 360; + double pitch = Math.Atan2(forward.Y, Math.Sqrt(forward.X * forward.X + forward.Z * forward.Z)) * 180 / Math.PI; + double x = Math.Round(320 * yaw / 90) % 1280; + double y = Math.Min(960, Math.Max(240, Math.Round(4 * pitch) + 600)); + + GL.UseProgram(skyShader); + GL.Uniform1(uniform_sky_useTexture, skyTexture != 0 ? 1 : 0); + var color = model.backgroundColor ?? Vector3.Zero; + GL.Uniform3(uniform_sky_color, ref color); + GL.Uniform2(uniform_sky_windowOrigin, (float)(1280 - x), (float)y); + GL.ActiveTexture(TextureUnit.Texture0); + GL.BindTexture(TextureTarget.Texture2D, skyTexture); + GL.Disable(EnableCap.DepthTest); + GL.DepthMask(false); + GL.Disable(EnableCap.Blend); + GL.BindVertexArray(skyVao); + GL.DrawArrays(PrimitiveType.Triangles, 0, 3); + GL.BindVertexArray(0); + GL.DepthMask(true); + } + + static TextureWrapMode WrapMode(int wrap) => wrap switch + { + 1 => TextureWrapMode.MirroredRepeat, + 2 => TextureWrapMode.ClampToEdge, + _ => TextureWrapMode.Repeat, + }; + + void Upload(LevelModel model) + { + if (model != uploaded) + { + if (textures.Count > 0) + GL.DeleteTextures(textures.Count, textures.ToArray()); + textures.Clear(); + uploadedLength = 0; + UploadSkybox(model.skybox); + uploaded = model; + } + + var modelTextures = model.textures.textures; + for (int i = textures.Count; i < modelTextures.Count; i++) + { + var t = modelTextures[i]; + int texture = GL.GenTexture(); + GL.BindTexture(TextureTarget.Texture2D, texture); + GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba8, t.width, t.height, 0, PixelFormat.Rgba, PixelType.UnsignedByte, t.rgba); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)WrapMode(t.wrapS)); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)WrapMode(t.wrapT)); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.LinearMipmapLinear); + GL.GenerateMipmap(GenerateMipmapTarget.Texture2D); + textures.Add(texture); + } + + GL.BindTexture(TextureTarget.Texture2D, 0); + + var store = model.store; + if (store.length > uploadedLength) + { + GL.BindBuffer(BufferTarget.ArrayBuffer, buffer); + if (store.length > bufferCapacity || uploadedLength == 0) + { + bufferCapacity = store.data.Length; + GL.BufferData(BufferTarget.ArrayBuffer, bufferCapacity, store.data, BufferUsageHint.StaticDraw); + } + else + GL.BufferSubData(BufferTarget.ArrayBuffer, (IntPtr)uploadedLength, store.length - uploadedLength, ref store.data[uploadedLength]); + + uploadedLength = store.length; + } + } + + int UploadDynamic(IReadOnlyList shadows, IReadOnlyList<(int y, int xMin, int xMax, int zMin, int zMax)> waters) + { + dynamicLength = 0; + if (shadows != null) + foreach (var s in shadows) + { + var c = s.position + new Vector3(0, 4, 0); + uint color = (uint)(255 * Math.Min(1, s.opacity)) << 24; + float r = s.size / 2; + if (s.square) + { + Quad(c.X - r, c.X + r, c.Z - r, c.Z + r, c.Y, color); + continue; + } + + for (int i = 0; i < ShadowSegments; i++) + { + DynamicVertex(c.X, c.Y, c.Z, color); + DynamicVertex(c.X + r * CircleSin[i], c.Y, c.Z + r * CircleCos[i], color); + DynamicVertex(c.X + r * CircleSin[i + 1], c.Y, c.Z + r * CircleCos[i + 1], color); + } + } + + if (waters != null) + foreach (var w in waters) + Quad(w.xMin, w.xMax, w.zMin, w.zMax, w.y, WaterColor); + + if (dynamicLength > 0) + { + GL.BindBuffer(BufferTarget.ArrayBuffer, dynamicBuffer); + GL.BufferData(BufferTarget.ArrayBuffer, dynamicLength, dynamicVertices, BufferUsageHint.StreamDraw); + } + + return dynamicLength / MeshStore.VertexSize; + } + + static readonly float[] CircleSin = new float[ShadowSegments + 1], CircleCos = new float[ShadowSegments + 1]; + + static LevelModelRenderer() + { + for (int i = 0; i <= ShadowSegments; i++) + { + CircleSin[i] = (float)Math.Sin(2 * Math.PI * i / ShadowSegments); + CircleCos[i] = (float)Math.Cos(2 * Math.PI * i / ShadowSegments); + } + } + + void Quad(float xMin, float xMax, float zMin, float zMax, float y, uint color) + { + DynamicVertex(xMin, y, zMin, color); + DynamicVertex(xMax, y, zMin, color); + DynamicVertex(xMax, y, zMax, color); + DynamicVertex(xMin, y, zMin, color); + DynamicVertex(xMax, y, zMax, color); + DynamicVertex(xMin, y, zMax, color); + } + + void DynamicVertex(float x, float y, float z, uint color) + { + if (dynamicLength + MeshStore.VertexSize > dynamicVertices.Length) + Array.Resize(ref dynamicVertices, dynamicVertices.Length * 2); + var span = dynamicVertices.AsSpan(dynamicLength, MeshStore.VertexSize); + BitConverter.TryWriteBytes(span, x); + BitConverter.TryWriteBytes(span.Slice(4), y); + BitConverter.TryWriteBytes(span.Slice(8), z); + BitConverter.TryWriteBytes(span.Slice(12), 0f); + BitConverter.TryWriteBytes(span.Slice(16), 0f); + BitConverter.TryWriteBytes(span.Slice(20), color); + span.Slice(24).Clear(); + dynamicLength += MeshStore.VertexSize; + } + + static float Determinant3(in Matrix4 m) => new Matrix3(m).Determinant; + + FrontFaceDirection frontFace; + bool viewMirrors; + + void SetModel(in Matrix4 model) + { + var m = model; + GL.UniformMatrix4(uniform_model, false, ref m); + var face = (Determinant3(model) < 0) != viewMirrors ? FrontFaceDirection.Cw : FrontFaceDirection.Ccw; + if (face != frontFace) + GL.FrontFace(frontFace = face); + } + + int stateTransparent, stateCull, stateDecal, stateTexture, stateCutoffLayer; + + void ResetState() => stateTransparent = stateCull = stateDecal = stateTexture = stateCutoffLayer = int.MinValue; + + void DrawBatch(Batch batch, bool cull) + { + int transparent = batch.layer >= LAYER_TRANSPARENT ? 1 : 0; + if (transparent != stateTransparent) + { + if (transparent == 1) + GL.Enable(EnableCap.Blend); + else + GL.Disable(EnableCap.Blend); + GL.DepthMask(transparent == 0); + GL.Uniform1(uniform_opaque, 1 - transparent); + stateTransparent = transparent; + } + + int cullMode = cull ? (int)batch.cull : (int)CullMode.None; + if (cullMode != stateCull) + { + if (cullMode != (int)CullMode.None) + { + GL.Enable(EnableCap.CullFace); + GL.CullFace(cullMode == (int)CullMode.Back ? TriangleFace.Back : TriangleFace.Front); + } + else + GL.Disable(EnableCap.CullFace); + stateCull = cullMode; + } + + int decal = batch.decal ? 1 : 0; + if (decal != stateDecal) + { + float offset = decal == 1 ? 1 : 2; + GL.PolygonOffset(offset, offset); + GL.Uniform1(uniform_decal, decal); + stateDecal = decal; + } + + int cutoffLayer = batch.layer == LAYER_ALPHA ? 1 : transparent == 1 ? 2 : 0; + if (cutoffLayer != stateCutoffLayer) + { + GL.Uniform1(uniform_alphaCutoff, cutoffLayer == 1 ? 0.5f : cutoffLayer == 2 ? 1 / 255f : 0); + stateCutoffLayer = cutoffLayer; + } + + int texture = batch.texture >= 0 && batch.texture < textures.Count ? textures[batch.texture] : 0; + if (texture != stateTexture) + { + GL.Uniform1(uniform_useTexture, texture != 0 ? 1 : 0); + GL.BindTexture(TextureTarget.Texture2D, texture); + stateTexture = texture; + } + + GL.DrawArrays(PrimitiveType.Triangles, batch.first, batch.count); + } + + static int Phase(Batch batch) => batch.layer >= LAYER_TRANSPARENT ? 2 : batch.layer == LAYER_ALPHA ? 1 : 0; + + public void Draw(MapGraphics graphics, LevelModel model, float maxY, bool cull, bool drawObjects, + IReadOnlyList<(int y, int xMin, int xMax, int zMin, int zMax)> waters, bool drawSky) + { + if (shader == 0) + Init(); + Upload(model); + int dynamicVertexCount = UploadDynamic(drawObjects ? model.shadows : null, waters); + + bool blendWasEnabled = GL.IsEnabled(EnableCap.Blend); + if (drawSky) + DrawSky(graphics, model); + + GL.UseProgram(shader); + ResetState(); + var viewProjection = graphics.ViewMatrix; + GL.UniformMatrix4(uniform_viewProjection, false, ref viewProjection); + var viewRotation = Matrix3.Transpose(new Matrix3(graphics.BillboardMatrix)); + GL.UniformMatrix3(uniform_viewRotation, false, ref viewRotation); + GL.Uniform1(uniform_tex, 0); + GL.Uniform1(uniform_maxY, maxY); + GL.ActiveTexture(TextureUnit.Texture0); + + GL.Enable(EnableCap.DepthTest); + GL.DepthFunc(DepthFunction.Lequal); + viewMirrors = Determinant3(graphics.ViewMatrix) > 0; + frontFace = FrontFaceDirection.Ccw; + GL.FrontFace(frontFace); + GL.Enable(EnableCap.PolygonOffsetFill); + + var billboardRotation = graphics.BillboardMatrix; + billboardRotation.Row3 = Vector4.UnitW; + + GL.BindVertexArray(vao); + for (int phase = 0; phase < 3; phase++) + { + SetModel(Matrix4.Identity); + foreach (var batch in model.level.batches) + if (Phase(batch) == phase) + DrawBatch(batch, cull); + + if (drawObjects) + foreach (var instance in model.instances) + { + bool modelSet = false; + foreach (var batch in instance.mesh.batches) + { + if (Phase(batch) != phase) + continue; + if (!modelSet) + { + SetModel(instance.billboard + ? instance.matrix * Matrix4.CreateScale(instance.anchorScale) * billboardRotation * Matrix4.CreateTranslation(instance.anchor) + : instance.matrix); + modelSet = true; + } + + DrawBatch(batch, cull); + } + } + } + + if (dynamicVertexCount > 0) + { + GL.BindVertexArray(dynamicVao); + SetModel(Matrix4.Identity); + DrawBatch(new Batch { texture = -1, layer = LAYER_TRANSPARENT, decal = true, first = 0, count = dynamicVertexCount }, false); + } + + GL.BindTexture(TextureTarget.Texture2D, 0); + GL.BindVertexArray(0); + GL.DepthMask(true); + GL.Disable(EnableCap.CullFace); + GL.FrontFace(FrontFaceDirection.Ccw); + GL.Disable(EnableCap.PolygonOffsetFill); + GL.PolygonOffset(0, 0); + if (blendWasEnabled) + GL.Enable(EnableCap.Blend); + else + GL.Disable(EnableCap.Blend); + } + } +} diff --git a/STROOP/Tabs/MapTab/Renderers/TriangleRenderer.cs b/STROOP/Tabs/MapTab/Renderers/TriangleRenderer.cs index 98fcd2ae7..0e9b1493d 100644 --- a/STROOP/Tabs/MapTab/Renderers/TriangleRenderer.cs +++ b/STROOP/Tabs/MapTab/Renderers/TriangleRenderer.cs @@ -1,4 +1,4 @@ -using System; +using System; using OpenTK.Graphics.OpenGL; using OpenTK; using System.Runtime.InteropServices; @@ -24,7 +24,7 @@ public TransparentTriangleRenderer(TriangleRenderer parent) : base() public void DrawMask(TransparencyRenderer renderer) { - if (triangles.Count == 0) + if (vertices.Count == 0) return; GL.UseProgram(shader); renderer.SetUniforms(shader); @@ -33,19 +33,20 @@ public void DrawMask(TransparencyRenderer renderer) public void DrawTransparent(TransparencyRenderer renderer) { - if (triangles.Count == 0) + if (vertices.Count == 0) return; DrawTriangles(renderer.graphics, parent.shader); } public void Prepare(TransparencyRenderer renderer) { - if (triangles.Count == 0) + if (vertices.Count == 0) return; WriteDataToBuffer(); } } + [StructLayout(LayoutKind.Sequential)] struct TriangleVertex { public const int Size = sizeof(float) * 4 + sizeof(float) * 4 + sizeof(float) * 4 + sizeof(float) * 3; @@ -56,23 +57,13 @@ struct TriangleVertex internal Vector3 outlineThickness; } - struct Triangle - { - public Vector3[] positions; - public Vector4[] colors; - public bool showUnitSquares; - public Vector4 outlineColor; - public Vector3 outlineThickness; - } - int shader; int buffer; int vao; int bufferSize = 0; - IntPtr dataPtr; - List triangles = new List(); + readonly List vertices = new List(); int uniform_viewProjection, uniform_pixelsPerUnit, uniform_unitShift; @@ -124,16 +115,16 @@ void InitInternal(int maxExpectedTriangles) uniform_unitShift = GL.GetUniformLocation(shader, "unitShift"); }); - dataPtr = Marshal.AllocHGlobal((IntPtr)(bufferSize = expectedSize)); + bufferSize = expectedSize; } public override void SetDrawCalls(MapGraphics graphics) { - triangles.Clear(); - transparentRenderer.triangles.Clear(); + vertices.Clear(); + transparentRenderer.vertices.Clear(); graphics.drawLayers[(int)drawlayer].Add(() => { - if (triangles.Count == 0) + if (vertices.Count == 0) return; WriteDataToBuffer(); @@ -150,7 +141,6 @@ public override void SetDrawCalls(MapGraphics graphics) protected void DrawTriangles(MapGraphics graphics, int program) { Vector2 pixelsPerUnit = graphics.pixelsPerUnit; - var error = GL.GetError(); GL.UseProgram(program); GL.BindVertexArray(vao); var mat = graphics.ViewMatrix; @@ -161,8 +151,7 @@ protected void DrawTriangles(MapGraphics graphics, int program) GL.Uniform2(GL.GetUniformLocation(program, "gridOffset"), new Vector2(0)); GL.Disable(EnableCap.CullFace); - GL.DrawArrays(PrimitiveType.Triangles, 0, triangles.Count * 3); - error = GL.GetError(); + GL.DrawArrays(PrimitiveType.Triangles, 0, vertices.Count); GL.BindVertexArray(0); } @@ -174,45 +163,24 @@ public void Add(Vector3 v1, Vector3 v2, Vector3 v3, bool showTriUnits, Vector4 c public void Add(Vector3 v1, Vector3 v2, Vector3 v3, bool showTriUnits, Vector4 color1, Vector4 color2, Vector4 color3, Vector4 outlineColor, Vector3 outlineThickness, bool transparent) { - (transparent ? transparentRenderer.triangles : triangles).Add(new Triangle - { - positions = new[] { v1, v2, v3 }, - colors = new[] { color1, color2, color3 }, - outlineColor = outlineColor, - showUnitSquares = showTriUnits, - outlineThickness = outlineThickness - }); + var target = transparent ? transparentRenderer.vertices : vertices; + float showUnitSquares = showTriUnits ? 1.0f : 0.0f; + target.Add(new TriangleVertex { position = v1, color = color1, outlineColor = outlineColor, showUnitSquares = showUnitSquares, outlineThickness = outlineThickness }); + target.Add(new TriangleVertex { position = v2, color = color2, outlineColor = outlineColor, showUnitSquares = showUnitSquares, outlineThickness = outlineThickness }); + target.Add(new TriangleVertex { position = v3, color = color3, outlineColor = outlineColor, showUnitSquares = showUnitSquares, outlineThickness = outlineThickness }); } void WriteDataToBuffer() { - var dataSize = triangles.Count * TriangleVertex.Size * 3; + var dataSize = vertices.Count * TriangleVertex.Size; GL.BindBuffer(BufferTarget.ArrayBuffer, buffer); if (dataSize > bufferSize) { - Marshal.FreeHGlobal(dataPtr); - dataPtr = Marshal.AllocHGlobal((IntPtr)(bufferSize = Math.Max(dataSize, bufferSize * 2))); + bufferSize = Math.Max(dataSize, bufferSize * 2); GL.BufferData(BufferTarget.ArrayBuffer, (IntPtr)bufferSize, IntPtr.Zero, BufferUsageHint.DynamicDraw); } - IntPtr ptr = dataPtr; - foreach (var instance in triangles) - { - for (int i = 0; i < 3; i++) - { - Marshal.StructureToPtr(new TriangleVertex - { - position = instance.positions[i], - color = instance.colors[i], - outlineColor = instance.outlineColor, - showUnitSquares = instance.showUnitSquares ? 1.0f : 0.0f, - outlineThickness = instance.outlineThickness - }, ptr, false); - ptr = IntPtr.Add(ptr, TriangleVertex.Size); - } - } - - GL.BufferSubData(BufferTarget.ArrayBuffer, IntPtr.Zero, (IntPtr)(dataSize), dataPtr); + GL.BufferSubData(BufferTarget.ArrayBuffer, IntPtr.Zero, dataSize, ref CollectionsMarshal.AsSpan(vertices)[0]); } } } diff --git a/STROOP/Tabs/MapTab/Views/View3D.cs b/STROOP/Tabs/MapTab/Views/View3D.cs index 7a5f29192..164320310 100644 --- a/STROOP/Tabs/MapTab/Views/View3D.cs +++ b/STROOP/Tabs/MapTab/Views/View3D.cs @@ -13,11 +13,13 @@ public enum Camera3DMode } public Camera3DMode camera3DMode = Camera3DMode.FocusOnPositionAngle; - public float camera3DDistanceController = 50; - public bool display3DLevelGeometry = true; + public float camera3DDistanceController = 75; + public bool display3DLevelGeometry = false; public PositionAngle focusPositionAngle { get; set; } = PositionAngle.Mario; + public View3D() => pitch = 0.6f; + void PivotingView.Pivot(PositionAngle pivotPoint) { focusPositionAngle = pivotPoint;