\n");
- sb.Append(" \n");
- }
- sb.Append(" \n");
- sb.Append(" \n");
- }
- sb.Append(" \n");
- sb.Append(" \n");
- sb.Append(" \n");
- for(int i = 0; i < moby.bangles.Length; i++)
- {
- sb.AppendFormat(" \n", mobyName, i);
- sb.Append(" 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1\n");
- sb.AppendFormat(" \n", mobyName, i);
- sb.Append(" \n");
- sb.Append(" \n");
- List instanceShadersWritten = new List();
- for(int j = 0; j < moby.bangles[i].count; j++)
- {
- int shaderIndex = moby.bangles[i].meshes[j].shaderIndex;
- if(instanceShadersWritten.Contains(shaderIndex)) continue;
- instanceShadersWritten.Add(shaderIndex);
- sb.AppendFormat(" \n", Path.GetFileNameWithoutExtension(moby.shaderDB[shaderIndex].name));
- sb.Append(" \n");
- sb.Append(" \n");
- }
- sb.Append(" \n");
- sb.Append(" \n");
- sb.Append(" \n");
- sb.Append(" \n");
- }
- sb.Append(" \n");
- sb.Append(" \n");
- sb.Append(" \n\n\n");
- sb.Append("\n");
- File.WriteAllText(exportPath, sb.ToString());
- }
- private static void WriteDaeSource(StringBuilder sb, float[][] floats, string name, CMoby moby, int bangleIndex, string mobyName, int stride)
- {
- sb.AppendFormat(" \n", mobyName, bangleIndex, name);
- int bangleVertexCount = 0;
- StringBuilder vertext = new StringBuilder(); //I'm a bit of a commedic genious
- for(uint i = 0; i < floats.Length; i++)
- {
- for(uint j = 0; j < floats[i].Length; j++)
- {
- if(stride == 2 && j % 2 == 1)
- {
- vertext.AppendFormat("{0} ", (1f-floats[i][j]).ToString("F8"));
- }
- else
- {
- vertext.AppendFormat("{0} ", floats[i][j].ToString("F8"));
- }
- }
- bangleVertexCount += floats[i].Length;
- }
- sb.AppendFormat(" ", mobyName, bangleVertexCount, bangleIndex, name);
- sb.Append( vertext.ToString());
- sb.Append( "\n");
- sb.Append(" \n");
- sb.AppendFormat(" \n", mobyName, bangleVertexCount / stride, bangleIndex, stride, name);
- if(stride == 3)
- {
- sb.Append(" \n");
- sb.Append(" \n");
- sb.Append(" \n");
- }
- else if(stride == 2)
- {
- sb.Append(" \n");
- sb.Append(" \n");
- }
- sb.Append(" \n");
- sb.Append(" \n");
- sb.Append(" \n");
- }
- private static void WriteSurfaceBlock(StringBuilder sb, CTexture texture)
- {
- if(texture == null) return;
- string simplifiedTextureName = Path.GetFileName(texture.name);
- sb.AppendFormat(" \n", simplifiedTextureName);
- sb.Append(" \n");
- sb.AppendFormat(" {0}\n", simplifiedTextureName);
- sb.Append(" \n");
- sb.Append(" \n");
- sb.AppendFormat(" \n", simplifiedTextureName);
- sb.Append(" \n");
- sb.AppendFormat(" {0}-surface\n", simplifiedTextureName);
- sb.Append(" \n");
- sb.Append(" \n");
- }
- private static void WriteTextureBlock(StringBuilder sb, CTexture texture, string exportFolder)
- {
- if(texture == null || string.IsNullOrEmpty(texture.name)) return;
- string simplifiedTextureName = Path.GetFileName(texture.name);
- sb.AppendFormat(" \n", simplifiedTextureName);
- sb.AppendFormat(" {0}\n", Path.ChangeExtension(simplifiedTextureName, "dds"));
- sb.Append(" \n");
- Directory.CreateDirectory(exportFolder);
- FileStream destinationTexture = File.Create(exportFolder + "/" + Path.ChangeExtension(simplifiedTextureName, "dds"));
- texture.ExportToDDS(destinationTexture, false);
- }
- }
-}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d7ac17a..59b3d55 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,12 +15,12 @@ Because the European date format is better, please keep date format like this: `
- Added a new logging system, logging everything to a file (except the errors, will be fixed in a future release)
- Added a new `Logs` frame, showing the logs output.
- Created internal Entity types based off their real types, increasing editor flexibility and reliability.
-- [TO BE DONE] Added a new `Asset View` frame, allowing to isolate an entity and its data on a separate frame, showing its model in its own frame. It will also allow to export objects as `.gltf`, `.obj` and `.dae` models in the future.
+- Added a new `Asset View` frame, allowing to isolate an entity and its data on a separate frame, showing its model in its own frame. Objects can now be exported as `.gltf`/`.glb` and `.obj` (`.dae` still not supported).
- Added a new `Texture Explorer` frame, allowing to inspect a texture and its data on a separate frame, and export its raw data or export them as `.bmp` and `.png`
- Added bases for animations and animations viewing in the Asset view.
-- [TO BE DONE] Added basic transform tools to interact with assets directly from the 3D View.
+- Added transform tools (move/rotate/scale gizmos, via ImGuizmo) to interact with assets directly from the 3D View, including translation/rotation/scale snapping settings.
- Added frames rounding and removed frames borders, making the UI more modern and pleasant for the eyes.
-- [TO BE DONE] Edited Update frame, it will now show a frame telling that there is no newer version too.
+- Edited Update frame: it now supports a Stable/Nightly update channel setting and properly notifies when no newer version is available.
- Added a loading modal when loading a level, showing the precise progress of the level loading, with all the detailed steps.
- A few fixes for UFrags on old engine.
- Rewrote Textures reading, it is much faster than before.
@@ -31,6 +31,18 @@ Because the European date format is better, please keep date format like this: `
- Instance Properties frame now shows the vertices of the object (will be moved to Asset View).
- Reducing far clip distance should now increase performance as it now unloads objects that are further this distance (which was not the case before, it was just not showing them but they were still rendering)
- Updated Entity Explorer frame's search bar: It will now update the output only when pressing "enter", and above that the search results are now cached, improving considerably performances.
+- Merged LibLunacy (asset-format library) and its archive I/O directly into ReLunacy.Engine instead of referencing them as separate assemblies, fully retargeted to Bliss 1.6.15/Veldrith; reorganized loading/asset code under clearer `Loading`/`Assets`/`Scene`/`Rendering`/`Games` namespaces and removed unused legacy engine code left over from the old architecture.
+- Added `.gltf`/`.glb` and `.obj` model export (with `.mtl`/`.png`), including a new whole-level export (`Export Level`) that bundles every placed instance into a single scene file. Export runs in the background with a progress modal and an "open containing folder" action when done. Mobys with skeletons now always export as static/rigid meshes at level scope (this avoids a crash from colliding bone names across placed instances) and the exported level file is named after the level's own folder instead of the raw `level_cached`/`level_uncached` archive filename.
+- Added skinned-mesh/skeleton export support (joint extraction, bone-weighted meshes) for single-asset exports.
+- Implemented a custom `DecalAwareForwardRenderer`, fixing z-fighting on alpha-blended decal textures (moss/vines painted onto terrain) by disabling depth *writes* (while keeping depth *testing*) for translucent geometry instead of Bliss's default renderer, which hardcoded depth writes for everything.
+- Implemented Moby per-instance render/display distance culling (reverse-engineered from real gameplay data), toggleable from the Render menu — off by default since the free-fly editor camera doesn't share the game's player-anchored camera assumptions.
+- Added Volume selection in the 3D viewport: proper GPU-buffer picking against the volume's actual wireframe edges (not a solid hitbox, so clicking empty interior space no longer selects a volume), with configurable wire thickness and unselected/selected colors in Editor Settings, and volume metadata (ID/group) now shown in the Property Inspector.
+- Added a configurable Selection Outline color in Editor Settings.
+- Expanded supported texture formats (R8, A1R5G5B5, RGBA4, RGBA16F, BC4, BC5, G8B8), fixed imprecise RGB565 channel expansion, and added linearization handling for Morton-swizzled textures.
+- Native vertex tangent decoding straight from source mesh data, replacing the previous derived/approximated tangents, for more accurate normal-mapped rendering.
+- Added a nightly build pipeline (GitHub Actions, Windows + Linux artifacts, rolling release).
+- Relicensed the project under the GNU GPL v3.
+- README overhaul: replaced the demo GIF with an embedded, autoplaying video and cleaned up the licensing section.
## [v0.03](https://github.com/VELD-Dev/ReLunacy/releases/0.03) - 23-05-2025
diff --git a/LICENSE-LUNALIB.txt b/LICENSE-LUNALIB.txt
deleted file mode 100644
index 3ad4dea..0000000
--- a/LICENSE-LUNALIB.txt
+++ /dev/null
@@ -1,515 +0,0 @@
-
-CeCILL-B FREE SOFTWARE LICENSE AGREEMENT
-
-
- Notice
-
-This Agreement is a Free Software license agreement that is the result
-of discussions between its authors in order to ensure compliance with
-the two main principles guiding its drafting:
-
- * firstly, compliance with the principles governing the distribution
- of Free Software: access to source code, broad rights granted to
- users,
- * secondly, the election of a governing law, French law, with which
- it is conformant, both as regards the law of torts and
- intellectual property law, and the protection that it offers to
- both authors and holders of the economic rights over software.
-
-The authors of the CeCILL-B (for Ce[a] C[nrs] I[nria] L[ogiciel] L[ibre])
-license are:
-
-Commissariat l'Energie Atomique - CEA, a public scientific, technical
-and industrial research establishment, having its principal place of
-business at 25 rue Leblanc, immeuble Le Ponant D, 75015 Paris, France.
-
-Centre National de la Recherche Scientifique - CNRS, a public scientific
-and technological establishment, having its principal place of business
-at 3 rue Michel-Ange, 75794 Paris cedex 16, France.
-
-Institut National de Recherche en Informatique et en Automatique -
-INRIA, a public scientific and technological establishment, having its
-principal place of business at Domaine de Voluceau, Rocquencourt, BP
-105, 78153 Le Chesnay cedex, France.
-
-
- Preamble
-
-This Agreement is an open source software license intended to give users
-significant freedom to modify and redistribute the software licensed
-hereunder.
-
-The exercising of this freedom is conditional upon a strong obligation
-of giving credits for everybody that distributes a software
-incorporating a software ruled by the current license so as all
-contributions to be properly identified and acknowledged.
-
-In consideration of access to the source code and the rights to copy,
-modify and redistribute granted by the license, users are provided only
-with a limited warranty and the software's author, the holder of the
-economic rights, and the successive licensors only have limited liability.
-
-In this respect, the risks associated with loading, using, modifying
-and/or developing or reproducing the software by the user are brought to
-the user's attention, given its Free Software status, which may make it
-complicated to use, with the result that its use is reserved for
-developers and experienced professionals having in-depth computer
-knowledge. Users are therefore encouraged to load and test the
-suitability of the software as regards their requirements in conditions
-enabling the security of their systems and/or data to be ensured and,
-more generally, to use and operate it in the same conditions of
-security. This Agreement may be freely reproduced and published,
-provided it is not altered, and that no provisions are either added or
-removed herefrom.
-
-This Agreement may apply to any or all software for which the holder of
-the economic rights decides to submit the use thereof to its provisions.
-
-
- Article 1 - DEFINITIONS
-
-For the purpose of this Agreement, when the following expressions
-commence with a capital letter, they shall have the following meaning:
-
-Agreement: means this license agreement, and its possible subsequent
-versions and annexes.
-
-Software: means the software in its Object Code and/or Source Code form
-and, where applicable, its documentation, "as is" when the Licensee
-accepts the Agreement.
-
-Initial Software: means the Software in its Source Code and possibly its
-Object Code form and, where applicable, its documentation, "as is" when
-it is first distributed under the terms and conditions of the Agreement.
-
-Modified Software: means the Software modified by at least one
-Contribution.
-
-Source Code: means all the Software's instructions and program lines to
-which access is required so as to modify the Software.
-
-Object Code: means the binary files originating from the compilation of
-the Source Code.
-
-Holder: means the holder(s) of the economic rights over the Initial
-Software.
-
-Licensee: means the Software user(s) having accepted the Agreement.
-
-Contributor: means a Licensee having made at least one Contribution.
-
-Licensor: means the Holder, or any other individual or legal entity, who
-distributes the Software under the Agreement.
-
-Contribution: means any or all modifications, corrections, translations,
-adaptations and/or new functions integrated into the Software by any or
-all Contributors, as well as any or all Internal Modules.
-
-Module: means a set of sources files including their documentation that
-enables supplementary functions or services in addition to those offered
-by the Software.
-
-External Module: means any or all Modules, not derived from the
-Software, so that this Module and the Software run in separate address
-spaces, with one calling the other when they are run.
-
-Internal Module: means any or all Module, connected to the Software so
-that they both execute in the same address space.
-
-Parties: mean both the Licensee and the Licensor.
-
-These expressions may be used both in singular and plural form.
-
-
- Article 2 - PURPOSE
-
-The purpose of the Agreement is the grant by the Licensor to the
-Licensee of a non-exclusive, transferable and worldwide license for the
-Software as set forth in Article 5 hereinafter for the whole term of the
-protection granted by the rights over said Software.
-
-
- Article 3 - ACCEPTANCE
-
-3.1 The Licensee shall be deemed as having accepted the terms and
-conditions of this Agreement upon the occurrence of the first of the
-following events:
-
- * (i) loading the Software by any or all means, notably, by
- downloading from a remote server, or by loading from a physical
- medium;
- * (ii) the first time the Licensee exercises any of the rights
- granted hereunder.
-
-3.2 One copy of the Agreement, containing a notice relating to the
-characteristics of the Software, to the limited warranty, and to the
-fact that its use is restricted to experienced users has been provided
-to the Licensee prior to its acceptance as set forth in Article 3.1
-hereinabove, and the Licensee hereby acknowledges that it has read and
-understood it.
-
-
- Article 4 - EFFECTIVE DATE AND TERM
-
-
- 4.1 EFFECTIVE DATE
-
-The Agreement shall become effective on the date when it is accepted by
-the Licensee as set forth in Article 3.1.
-
-
- 4.2 TERM
-
-The Agreement shall remain in force for the entire legal term of
-protection of the economic rights over the Software.
-
-
- Article 5 - SCOPE OF RIGHTS GRANTED
-
-The Licensor hereby grants to the Licensee, who accepts, the following
-rights over the Software for any or all use, and for the term of the
-Agreement, on the basis of the terms and conditions set forth hereinafter.
-
-Besides, if the Licensor owns or comes to own one or more patents
-protecting all or part of the functions of the Software or of its
-components, the Licensor undertakes not to enforce the rights granted by
-these patents against successive Licensees using, exploiting or
-modifying the Software. If these patents are transferred, the Licensor
-undertakes to have the transferees subscribe to the obligations set
-forth in this paragraph.
-
-
- 5.1 RIGHT OF USE
-
-The Licensee is authorized to use the Software, without any limitation
-as to its fields of application, with it being hereinafter specified
-that this comprises:
-
- 1. permanent or temporary reproduction of all or part of the Software
- by any or all means and in any or all form.
-
- 2. loading, displaying, running, or storing the Software on any or
- all medium.
-
- 3. entitlement to observe, study or test its operation so as to
- determine the ideas and principles behind any or all constituent
- elements of said Software. This shall apply when the Licensee
- carries out any or all loading, displaying, running, transmission
- or storage operation as regards the Software, that it is entitled
- to carry out hereunder.
-
-
- 5.2 ENTITLEMENT TO MAKE CONTRIBUTIONS
-
-The right to make Contributions includes the right to translate, adapt,
-arrange, or make any or all modifications to the Software, and the right
-to reproduce the resulting software.
-
-The Licensee is authorized to make any or all Contributions to the
-Software provided that it includes an explicit notice that it is the
-author of said Contribution and indicates the date of the creation thereof.
-
-
- 5.3 RIGHT OF DISTRIBUTION
-
-In particular, the right of distribution includes the right to publish,
-transmit and communicate the Software to the general public on any or
-all medium, and by any or all means, and the right to market, either in
-consideration of a fee, or free of charge, one or more copies of the
-Software by any means.
-
-The Licensee is further authorized to distribute copies of the modified
-or unmodified Software to third parties according to the terms and
-conditions set forth hereinafter.
-
-
- 5.3.1 DISTRIBUTION OF SOFTWARE WITHOUT MODIFICATION
-
-The Licensee is authorized to distribute true copies of the Software in
-Source Code or Object Code form, provided that said distribution
-complies with all the provisions of the Agreement and is accompanied by:
-
- 1. a copy of the Agreement,
-
- 2. a notice relating to the limitation of both the Licensor's
- warranty and liability as set forth in Articles 8 and 9,
-
-and that, in the event that only the Object Code of the Software is
-redistributed, the Licensee allows effective access to the full Source
-Code of the Software at a minimum during the entire period of its
-distribution of the Software, it being understood that the additional
-cost of acquiring the Source Code shall not exceed the cost of
-transferring the data.
-
-
- 5.3.2 DISTRIBUTION OF MODIFIED SOFTWARE
-
-If the Licensee makes any Contribution to the Software, the resulting
-Modified Software may be distributed under a license agreement other
-than this Agreement subject to compliance with the provisions of Article
-5.3.4.
-
-
- 5.3.3 DISTRIBUTION OF EXTERNAL MODULES
-
-When the Licensee has developed an External Module, the terms and
-conditions of this Agreement do not apply to said External Module, that
-may be distributed under a separate license agreement.
-
-
- 5.3.4 CREDITS
-
-Any Licensee who may distribute a Modified Software hereby expressly
-agrees to:
-
- 1. indicate in the related documentation that it is based on the
- Software licensed hereunder, and reproduce the intellectual
- property notice for the Software,
-
- 2. ensure that written indications of the Software intended use,
- intellectual property notice and license hereunder are included in
- easily accessible format from the Modified Software interface,
-
- 3. mention, on a freely accessible website describing the Modified
- Software, at least throughout the distribution term thereof, that
- it is based on the Software licensed hereunder, and reproduce the
- Software intellectual property notice,
-
- 4. where it is distributed to a third party that may distribute a
- Modified Software without having to make its source code
- available, make its best efforts to ensure that said third party
- agrees to comply with the obligations set forth in this Article .
-
-If the Software, whether or not modified, is distributed with an
-External Module designed for use in connection with the Software, the
-Licensee shall submit said External Module to the foregoing obligations.
-
-
- 5.3.5 COMPATIBILITY WITH THE CeCILL AND CeCILL-C LICENSES
-
-Where a Modified Software contains a Contribution subject to the CeCILL
-license, the provisions set forth in Article 5.3.4 shall be optional.
-
-A Modified Software may be distributed under the CeCILL-C license. In
-such a case the provisions set forth in Article 5.3.4 shall be optional.
-
-
- Article 6 - INTELLECTUAL PROPERTY
-
-
- 6.1 OVER THE INITIAL SOFTWARE
-
-The Holder owns the economic rights over the Initial Software. Any or
-all use of the Initial Software is subject to compliance with the terms
-and conditions under which the Holder has elected to distribute its work
-and no one shall be entitled to modify the terms and conditions for the
-distribution of said Initial Software.
-
-The Holder undertakes that the Initial Software will remain ruled at
-least by this Agreement, for the duration set forth in Article 4.2.
-
-
- 6.2 OVER THE CONTRIBUTIONS
-
-The Licensee who develops a Contribution is the owner of the
-intellectual property rights over this Contribution as defined by
-applicable law.
-
-
- 6.3 OVER THE EXTERNAL MODULES
-
-The Licensee who develops an External Module is the owner of the
-intellectual property rights over this External Module as defined by
-applicable law and is free to choose the type of agreement that shall
-govern its distribution.
-
-
- 6.4 JOINT PROVISIONS
-
-The Licensee expressly undertakes:
-
- 1. not to remove, or modify, in any manner, the intellectual property
- notices attached to the Software;
-
- 2. to reproduce said notices, in an identical manner, in the copies
- of the Software modified or not.
-
-The Licensee undertakes not to directly or indirectly infringe the
-intellectual property rights of the Holder and/or Contributors on the
-Software and to take, where applicable, vis--vis its staff, any and all
-measures required to ensure respect of said intellectual property rights
-of the Holder and/or Contributors.
-
-
- Article 7 - RELATED SERVICES
-
-7.1 Under no circumstances shall the Agreement oblige the Licensor to
-provide technical assistance or maintenance services for the Software.
-
-However, the Licensor is entitled to offer this type of services. The
-terms and conditions of such technical assistance, and/or such
-maintenance, shall be set forth in a separate instrument. Only the
-Licensor offering said maintenance and/or technical assistance services
-shall incur liability therefor.
-
-7.2 Similarly, any Licensor is entitled to offer to its licensees, under
-its sole responsibility, a warranty, that shall only be binding upon
-itself, for the redistribution of the Software and/or the Modified
-Software, under terms and conditions that it is free to decide. Said
-warranty, and the financial terms and conditions of its application,
-shall be subject of a separate instrument executed between the Licensor
-and the Licensee.
-
-
- Article 8 - LIABILITY
-
-8.1 Subject to the provisions of Article 8.2, the Licensee shall be
-entitled to claim compensation for any direct loss it may have suffered
-from the Software as a result of a fault on the part of the relevant
-Licensor, subject to providing evidence thereof.
-
-8.2 The Licensor's liability is limited to the commitments made under
-this Agreement and shall not be incurred as a result of in particular:
-(i) loss due the Licensee's total or partial failure to fulfill its
-obligations, (ii) direct or consequential loss that is suffered by the
-Licensee due to the use or performance of the Software, and (iii) more
-generally, any consequential loss. In particular the Parties expressly
-agree that any or all pecuniary or business loss (i.e. loss of data,
-loss of profits, operating loss, loss of customers or orders,
-opportunity cost, any disturbance to business activities) or any or all
-legal proceedings instituted against the Licensee by a third party,
-shall constitute consequential loss and shall not provide entitlement to
-any or all compensation from the Licensor.
-
-
- Article 9 - WARRANTY
-
-9.1 The Licensee acknowledges that the scientific and technical
-state-of-the-art when the Software was distributed did not enable all
-possible uses to be tested and verified, nor for the presence of
-possible defects to be detected. In this respect, the Licensee's
-attention has been drawn to the risks associated with loading, using,
-modifying and/or developing and reproducing the Software which are
-reserved for experienced users.
-
-The Licensee shall be responsible for verifying, by any or all means,
-the suitability of the product for its requirements, its good working
-order, and for ensuring that it shall not cause damage to either persons
-or properties.
-
-9.2 The Licensor hereby represents, in good faith, that it is entitled
-to grant all the rights over the Software (including in particular the
-rights set forth in Article 5).
-
-9.3 The Licensee acknowledges that the Software is supplied "as is" by
-the Licensor without any other express or tacit warranty, other than
-that provided for in Article 9.2 and, in particular, without any warranty
-as to its commercial value, its secured, safe, innovative or relevant
-nature.
-
-Specifically, the Licensor does not warrant that the Software is free
-from any error, that it will operate without interruption, that it will
-be compatible with the Licensee's own equipment and software
-configuration, nor that it will meet the Licensee's requirements.
-
-9.4 The Licensor does not either expressly or tacitly warrant that the
-Software does not infringe any third party intellectual property right
-relating to a patent, software or any other property right. Therefore,
-the Licensor disclaims any and all liability towards the Licensee
-arising out of any or all proceedings for infringement that may be
-instituted in respect of the use, modification and redistribution of the
-Software. Nevertheless, should such proceedings be instituted against
-the Licensee, the Licensor shall provide it with technical and legal
-assistance for its defense. Such technical and legal assistance shall be
-decided on a case-by-case basis between the relevant Licensor and the
-Licensee pursuant to a memorandum of understanding. The Licensor
-disclaims any and all liability as regards the Licensee's use of the
-name of the Software. No warranty is given as regards the existence of
-prior rights over the name of the Software or as regards the existence
-of a trademark.
-
-
- Article 10 - TERMINATION
-
-10.1 In the event of a breach by the Licensee of its obligations
-hereunder, the Licensor may automatically terminate this Agreement
-thirty (30) days after notice has been sent to the Licensee and has
-remained ineffective.
-
-10.2 A Licensee whose Agreement is terminated shall no longer be
-authorized to use, modify or distribute the Software. However, any
-licenses that it may have granted prior to termination of the Agreement
-shall remain valid subject to their having been granted in compliance
-with the terms and conditions hereof.
-
-
- Article 11 - MISCELLANEOUS
-
-
- 11.1 EXCUSABLE EVENTS
-
-Neither Party shall be liable for any or all delay, or failure to
-perform the Agreement, that may be attributable to an event of force
-majeure, an act of God or an outside cause, such as defective
-functioning or interruptions of the electricity or telecommunications
-networks, network paralysis following a virus attack, intervention by
-government authorities, natural disasters, water damage, earthquakes,
-fire, explosions, strikes and labor unrest, war, etc.
-
-11.2 Any failure by either Party, on one or more occasions, to invoke
-one or more of the provisions hereof, shall under no circumstances be
-interpreted as being a waiver by the interested Party of its right to
-invoke said provision(s) subsequently.
-
-11.3 The Agreement cancels and replaces any or all previous agreements,
-whether written or oral, between the Parties and having the same
-purpose, and constitutes the entirety of the agreement between said
-Parties concerning said purpose. No supplement or modification to the
-terms and conditions hereof shall be effective as between the Parties
-unless it is made in writing and signed by their duly authorized
-representatives.
-
-11.4 In the event that one or more of the provisions hereof were to
-conflict with a current or future applicable act or legislative text,
-said act or legislative text shall prevail, and the Parties shall make
-the necessary amendments so as to comply with said act or legislative
-text. All other provisions shall remain effective. Similarly, invalidity
-of a provision of the Agreement, for any reason whatsoever, shall not
-cause the Agreement as a whole to be invalid.
-
-
- 11.5 LANGUAGE
-
-The Agreement is drafted in both French and English and both versions
-are deemed authentic.
-
-
- Article 12 - NEW VERSIONS OF THE AGREEMENT
-
-12.1 Any person is authorized to duplicate and distribute copies of this
-Agreement.
-
-12.2 So as to ensure coherence, the wording of this Agreement is
-protected and may only be modified by the authors of the License, who
-reserve the right to periodically publish updates or new versions of the
-Agreement, each with a separate number. These subsequent versions may
-address new issues encountered by Free Software.
-
-12.3 Any Software distributed under a given version of the Agreement may
-only be subsequently distributed under the same version of the Agreement
-or a subsequent version.
-
-
- Article 13 - GOVERNING LAW AND JURISDICTION
-
-13.1 The Agreement is governed by French law. The Parties agree to
-endeavor to seek an amicable solution to any disagreements or disputes
-that may arise during the performance of the Agreement.
-
-13.2 Failing an amicable solution within two (2) months as from their
-occurrence, and unless emergency proceedings are necessary, the
-disagreements or disputes shall be referred to the Paris Courts having
-jurisdiction, by the more diligent Party.
-
-
-Version 1.0 dated 2006-09-05.
diff --git a/LICENSE-RELUNACY.txt b/LICENSE-RELUNACY.txt
deleted file mode 100644
index 4e3cf08..0000000
--- a/LICENSE-RELUNACY.txt
+++ /dev/null
@@ -1,519 +0,0 @@
-
- CeCILL FREE SOFTWARE LICENSE AGREEMENT
-
-Version 2.1 dated 2013-06-21
-
-
- Notice
-
-This Agreement is a Free Software license agreement that is the result
-of discussions between its authors in order to ensure compliance with
-the two main principles guiding its drafting:
-
- * firstly, compliance with the principles governing the distribution
- of Free Software: access to source code, broad rights granted to users,
- * secondly, the election of a governing law, French law, with which it
- is conformant, both as regards the law of torts and intellectual
- property law, and the protection that it offers to both authors and
- holders of the economic rights over software.
-
-The authors of the CeCILL (for Ce[a] C[nrs] I[nria] L[ogiciel] L[ibre])
-license are:
-
-Commissariat � l'�nergie atomique et aux �nergies alternatives - CEA, a
-public scientific, technical and industrial research establishment,
-having its principal place of business at 25 rue Leblanc, immeuble Le
-Ponant D, 75015 Paris, France.
-
-Centre National de la Recherche Scientifique - CNRS, a public scientific
-and technological establishment, having its principal place of business
-at 3 rue Michel-Ange, 75794 Paris cedex 16, France.
-
-Institut National de Recherche en Informatique et en Automatique -
-Inria, a public scientific and technological establishment, having its
-principal place of business at Domaine de Voluceau, Rocquencourt, BP
-105, 78153 Le Chesnay cedex, France.
-
-
- Preamble
-
-The purpose of this Free Software license agreement is to grant users
-the right to modify and redistribute the software governed by this
-license within the framework of an open source distribution model.
-
-The exercising of this right is conditional upon certain obligations for
-users so as to preserve this status for all subsequent redistributions.
-
-In consideration of access to the source code and the rights to copy,
-modify and redistribute granted by the license, users are provided only
-with a limited warranty and the software's author, the holder of the
-economic rights, and the successive licensors only have limited liability.
-
-In this respect, the risks associated with loading, using, modifying
-and/or developing or reproducing the software by the user are brought to
-the user's attention, given its Free Software status, which may make it
-complicated to use, with the result that its use is reserved for
-developers and experienced professionals having in-depth computer
-knowledge. Users are therefore encouraged to load and test the
-suitability of the software as regards their requirements in conditions
-enabling the security of their systems and/or data to be ensured and,
-more generally, to use and operate it in the same conditions of
-security. This Agreement may be freely reproduced and published,
-provided it is not altered, and that no provisions are either added or
-removed herefrom.
-
-This Agreement may apply to any or all software for which the holder of
-the economic rights decides to submit the use thereof to its provisions.
-
-Frequently asked questions can be found on the official website of the
-CeCILL licenses family (http://www.cecill.info/index.en.html) for any
-necessary clarification.
-
-
- Article 1 - DEFINITIONS
-
-For the purpose of this Agreement, when the following expressions
-commence with a capital letter, they shall have the following meaning:
-
-Agreement: means this license agreement, and its possible subsequent
-versions and annexes.
-
-Software: means the software in its Object Code and/or Source Code form
-and, where applicable, its documentation, "as is" when the Licensee
-accepts the Agreement.
-
-Initial Software: means the Software in its Source Code and possibly its
-Object Code form and, where applicable, its documentation, "as is" when
-it is first distributed under the terms and conditions of the Agreement.
-
-Modified Software: means the Software modified by at least one
-Contribution.
-
-Source Code: means all the Software's instructions and program lines to
-which access is required so as to modify the Software.
-
-Object Code: means the binary files originating from the compilation of
-the Source Code.
-
-Holder: means the holder(s) of the economic rights over the Initial
-Software.
-
-Licensee: means the Software user(s) having accepted the Agreement.
-
-Contributor: means a Licensee having made at least one Contribution.
-
-Licensor: means the Holder, or any other individual or legal entity, who
-distributes the Software under the Agreement.
-
-Contribution: means any or all modifications, corrections, translations,
-adaptations and/or new functions integrated into the Software by any or
-all Contributors, as well as any or all Internal Modules.
-
-Module: means a set of sources files including their documentation that
-enables supplementary functions or services in addition to those offered
-by the Software.
-
-External Module: means any or all Modules, not derived from the
-Software, so that this Module and the Software run in separate address
-spaces, with one calling the other when they are run.
-
-Internal Module: means any or all Module, connected to the Software so
-that they both execute in the same address space.
-
-GNU GPL: means the GNU General Public License version 2 or any
-subsequent version, as published by the Free Software Foundation Inc.
-
-GNU Affero GPL: means the GNU Affero General Public License version 3 or
-any subsequent version, as published by the Free Software Foundation Inc.
-
-EUPL: means the European Union Public License version 1.1 or any
-subsequent version, as published by the European Commission.
-
-Parties: mean both the Licensee and the Licensor.
-
-These expressions may be used both in singular and plural form.
-
-
- Article 2 - PURPOSE
-
-The purpose of the Agreement is the grant by the Licensor to the
-Licensee of a non-exclusive, transferable and worldwide license for the
-Software as set forth in Article 5 <#scope> hereinafter for the whole
-term of the protection granted by the rights over said Software.
-
-
- Article 3 - ACCEPTANCE
-
-3.1 The Licensee shall be deemed as having accepted the terms and
-conditions of this Agreement upon the occurrence of the first of the
-following events:
-
- * (i) loading the Software by any or all means, notably, by
- downloading from a remote server, or by loading from a physical medium;
- * (ii) the first time the Licensee exercises any of the rights granted
- hereunder.
-
-3.2 One copy of the Agreement, containing a notice relating to the
-characteristics of the Software, to the limited warranty, and to the
-fact that its use is restricted to experienced users has been provided
-to the Licensee prior to its acceptance as set forth in Article 3.1
-<#accepting> hereinabove, and the Licensee hereby acknowledges that it
-has read and understood it.
-
-
- Article 4 - EFFECTIVE DATE AND TERM
-
-
- 4.1 EFFECTIVE DATE
-
-The Agreement shall become effective on the date when it is accepted by
-the Licensee as set forth in Article 3.1 <#accepting>.
-
-
- 4.2 TERM
-
-The Agreement shall remain in force for the entire legal term of
-protection of the economic rights over the Software.
-
-
- Article 5 - SCOPE OF RIGHTS GRANTED
-
-The Licensor hereby grants to the Licensee, who accepts, the following
-rights over the Software for any or all use, and for the term of the
-Agreement, on the basis of the terms and conditions set forth hereinafter.
-
-Besides, if the Licensor owns or comes to own one or more patents
-protecting all or part of the functions of the Software or of its
-components, the Licensor undertakes not to enforce the rights granted by
-these patents against successive Licensees using, exploiting or
-modifying the Software. If these patents are transferred, the Licensor
-undertakes to have the transferees subscribe to the obligations set
-forth in this paragraph.
-
-
- 5.1 RIGHT OF USE
-
-The Licensee is authorized to use the Software, without any limitation
-as to its fields of application, with it being hereinafter specified
-that this comprises:
-
- 1. permanent or temporary reproduction of all or part of the Software
- by any or all means and in any or all form.
-
- 2. loading, displaying, running, or storing the Software on any or all
- medium.
-
- 3. entitlement to observe, study or test its operation so as to
- determine the ideas and principles behind any or all constituent
- elements of said Software. This shall apply when the Licensee
- carries out any or all loading, displaying, running, transmission or
- storage operation as regards the Software, that it is entitled to
- carry out hereunder.
-
-
- 5.2 ENTITLEMENT TO MAKE CONTRIBUTIONS
-
-The right to make Contributions includes the right to translate, adapt,
-arrange, or make any or all modifications to the Software, and the right
-to reproduce the resulting software.
-
-The Licensee is authorized to make any or all Contributions to the
-Software provided that it includes an explicit notice that it is the
-author of said Contribution and indicates the date of the creation thereof.
-
-
- 5.3 RIGHT OF DISTRIBUTION
-
-In particular, the right of distribution includes the right to publish,
-transmit and communicate the Software to the general public on any or
-all medium, and by any or all means, and the right to market, either in
-consideration of a fee, or free of charge, one or more copies of the
-Software by any means.
-
-The Licensee is further authorized to distribute copies of the modified
-or unmodified Software to third parties according to the terms and
-conditions set forth hereinafter.
-
-
- 5.3.1 DISTRIBUTION OF SOFTWARE WITHOUT MODIFICATION
-
-The Licensee is authorized to distribute true copies of the Software in
-Source Code or Object Code form, provided that said distribution
-complies with all the provisions of the Agreement and is accompanied by:
-
- 1. a copy of the Agreement,
-
- 2. a notice relating to the limitation of both the Licensor's warranty
- and liability as set forth in Articles 8 and 9,
-
-and that, in the event that only the Object Code of the Software is
-redistributed, the Licensee allows effective access to the full Source
-Code of the Software for a period of at least three years from the
-distribution of the Software, it being understood that the additional
-acquisition cost of the Source Code shall not exceed the cost of the
-data transfer.
-
-
- 5.3.2 DISTRIBUTION OF MODIFIED SOFTWARE
-
-When the Licensee makes a Contribution to the Software, the terms and
-conditions for the distribution of the resulting Modified Software
-become subject to all the provisions of this Agreement.
-
-The Licensee is authorized to distribute the Modified Software, in
-source code or object code form, provided that said distribution
-complies with all the provisions of the Agreement and is accompanied by:
-
- 1. a copy of the Agreement,
-
- 2. a notice relating to the limitation of both the Licensor's warranty
- and liability as set forth in Articles 8 and 9,
-
-and, in the event that only the object code of the Modified Software is
-redistributed,
-
- 3. a note stating the conditions of effective access to the full source
- code of the Modified Software for a period of at least three years
- from the distribution of the Modified Software, it being understood
- that the additional acquisition cost of the source code shall not
- exceed the cost of the data transfer.
-
-
- 5.3.3 DISTRIBUTION OF EXTERNAL MODULES
-
-When the Licensee has developed an External Module, the terms and
-conditions of this Agreement do not apply to said External Module, that
-may be distributed under a separate license agreement.
-
-
- 5.3.4 COMPATIBILITY WITH OTHER LICENSES
-
-The Licensee can include a code that is subject to the provisions of one
-of the versions of the GNU GPL, GNU Affero GPL and/or EUPL in the
-Modified or unmodified Software, and distribute that entire code under
-the terms of the same version of the GNU GPL, GNU Affero GPL and/or EUPL.
-
-The Licensee can include the Modified or unmodified Software in a code
-that is subject to the provisions of one of the versions of the GNU GPL,
-GNU Affero GPL and/or EUPL and distribute that entire code under the
-terms of the same version of the GNU GPL, GNU Affero GPL and/or EUPL.
-
-
- Article 6 - INTELLECTUAL PROPERTY
-
-
- 6.1 OVER THE INITIAL SOFTWARE
-
-The Holder owns the economic rights over the Initial Software. Any or
-all use of the Initial Software is subject to compliance with the terms
-and conditions under which the Holder has elected to distribute its work
-and no one shall be entitled to modify the terms and conditions for the
-distribution of said Initial Software.
-
-The Holder undertakes that the Initial Software will remain ruled at
-least by this Agreement, for the duration set forth in Article 4.2 <#term>.
-
-
- 6.2 OVER THE CONTRIBUTIONS
-
-The Licensee who develops a Contribution is the owner of the
-intellectual property rights over this Contribution as defined by
-applicable law.
-
-
- 6.3 OVER THE EXTERNAL MODULES
-
-The Licensee who develops an External Module is the owner of the
-intellectual property rights over this External Module as defined by
-applicable law and is free to choose the type of agreement that shall
-govern its distribution.
-
-
- 6.4 JOINT PROVISIONS
-
-The Licensee expressly undertakes:
-
- 1. not to remove, or modify, in any manner, the intellectual property
- notices attached to the Software;
-
- 2. to reproduce said notices, in an identical manner, in the copies of
- the Software modified or not.
-
-The Licensee undertakes not to directly or indirectly infringe the
-intellectual property rights on the Software of the Holder and/or
-Contributors, and to take, where applicable, vis-�-vis its staff, any
-and all measures required to ensure respect of said intellectual
-property rights of the Holder and/or Contributors.
-
-
- Article 7 - RELATED SERVICES
-
-7.1 Under no circumstances shall the Agreement oblige the Licensor to
-provide technical assistance or maintenance services for the Software.
-
-However, the Licensor is entitled to offer this type of services. The
-terms and conditions of such technical assistance, and/or such
-maintenance, shall be set forth in a separate instrument. Only the
-Licensor offering said maintenance and/or technical assistance services
-shall incur liability therefor.
-
-7.2 Similarly, any Licensor is entitled to offer to its licensees, under
-its sole responsibility, a warranty, that shall only be binding upon
-itself, for the redistribution of the Software and/or the Modified
-Software, under terms and conditions that it is free to decide. Said
-warranty, and the financial terms and conditions of its application,
-shall be subject of a separate instrument executed between the Licensor
-and the Licensee.
-
-
- Article 8 - LIABILITY
-
-8.1 Subject to the provisions of Article 8.2, the Licensee shall be
-entitled to claim compensation for any direct loss it may have suffered
-from the Software as a result of a fault on the part of the relevant
-Licensor, subject to providing evidence thereof.
-
-8.2 The Licensor's liability is limited to the commitments made under
-this Agreement and shall not be incurred as a result of in particular:
-(i) loss due the Licensee's total or partial failure to fulfill its
-obligations, (ii) direct or consequential loss that is suffered by the
-Licensee due to the use or performance of the Software, and (iii) more
-generally, any consequential loss. In particular the Parties expressly
-agree that any or all pecuniary or business loss (i.e. loss of data,
-loss of profits, operating loss, loss of customers or orders,
-opportunity cost, any disturbance to business activities) or any or all
-legal proceedings instituted against the Licensee by a third party,
-shall constitute consequential loss and shall not provide entitlement to
-any or all compensation from the Licensor.
-
-
- Article 9 - WARRANTY
-
-9.1 The Licensee acknowledges that the scientific and technical
-state-of-the-art when the Software was distributed did not enable all
-possible uses to be tested and verified, nor for the presence of
-possible defects to be detected. In this respect, the Licensee's
-attention has been drawn to the risks associated with loading, using,
-modifying and/or developing and reproducing the Software which are
-reserved for experienced users.
-
-The Licensee shall be responsible for verifying, by any or all means,
-the suitability of the product for its requirements, its good working
-order, and for ensuring that it shall not cause damage to either persons
-or properties.
-
-9.2 The Licensor hereby represents, in good faith, that it is entitled
-to grant all the rights over the Software (including in particular the
-rights set forth in Article 5 <#scope>).
-
-9.3 The Licensee acknowledges that the Software is supplied "as is" by
-the Licensor without any other express or tacit warranty, other than
-that provided for in Article 9.2 <#good-faith> and, in particular,
-without any warranty as to its commercial value, its secured, safe,
-innovative or relevant nature.
-
-Specifically, the Licensor does not warrant that the Software is free
-from any error, that it will operate without interruption, that it will
-be compatible with the Licensee's own equipment and software
-configuration, nor that it will meet the Licensee's requirements.
-
-9.4 The Licensor does not either expressly or tacitly warrant that the
-Software does not infringe any third party intellectual property right
-relating to a patent, software or any other property right. Therefore,
-the Licensor disclaims any and all liability towards the Licensee
-arising out of any or all proceedings for infringement that may be
-instituted in respect of the use, modification and redistribution of the
-Software. Nevertheless, should such proceedings be instituted against
-the Licensee, the Licensor shall provide it with technical and legal
-expertise for its defense. Such technical and legal expertise shall be
-decided on a case-by-case basis between the relevant Licensor and the
-Licensee pursuant to a memorandum of understanding. The Licensor
-disclaims any and all liability as regards the Licensee's use of the
-name of the Software. No warranty is given as regards the existence of
-prior rights over the name of the Software or as regards the existence
-of a trademark.
-
-
- Article 10 - TERMINATION
-
-10.1 In the event of a breach by the Licensee of its obligations
-hereunder, the Licensor may automatically terminate this Agreement
-thirty (30) days after notice has been sent to the Licensee and has
-remained ineffective.
-
-10.2 A Licensee whose Agreement is terminated shall no longer be
-authorized to use, modify or distribute the Software. However, any
-licenses that it may have granted prior to termination of the Agreement
-shall remain valid subject to their having been granted in compliance
-with the terms and conditions hereof.
-
-
- Article 11 - MISCELLANEOUS
-
-
- 11.1 EXCUSABLE EVENTS
-
-Neither Party shall be liable for any or all delay, or failure to
-perform the Agreement, that may be attributable to an event of force
-majeure, an act of God or an outside cause, such as defective
-functioning or interruptions of the electricity or telecommunications
-networks, network paralysis following a virus attack, intervention by
-government authorities, natural disasters, water damage, earthquakes,
-fire, explosions, strikes and labor unrest, war, etc.
-
-11.2 Any failure by either Party, on one or more occasions, to invoke
-one or more of the provisions hereof, shall under no circumstances be
-interpreted as being a waiver by the interested Party of its right to
-invoke said provision(s) subsequently.
-
-11.3 The Agreement cancels and replaces any or all previous agreements,
-whether written or oral, between the Parties and having the same
-purpose, and constitutes the entirety of the agreement between said
-Parties concerning said purpose. No supplement or modification to the
-terms and conditions hereof shall be effective as between the Parties
-unless it is made in writing and signed by their duly authorized
-representatives.
-
-11.4 In the event that one or more of the provisions hereof were to
-conflict with a current or future applicable act or legislative text,
-said act or legislative text shall prevail, and the Parties shall make
-the necessary amendments so as to comply with said act or legislative
-text. All other provisions shall remain effective. Similarly, invalidity
-of a provision of the Agreement, for any reason whatsoever, shall not
-cause the Agreement as a whole to be invalid.
-
-
- 11.5 LANGUAGE
-
-The Agreement is drafted in both French and English and both versions
-are deemed authentic.
-
-
- Article 12 - NEW VERSIONS OF THE AGREEMENT
-
-12.1 Any person is authorized to duplicate and distribute copies of this
-Agreement.
-
-12.2 So as to ensure coherence, the wording of this Agreement is
-protected and may only be modified by the authors of the License, who
-reserve the right to periodically publish updates or new versions of the
-Agreement, each with a separate number. These subsequent versions may
-address new issues encountered by Free Software.
-
-12.3 Any Software distributed under a given version of the Agreement may
-only be subsequently distributed under the same version of the Agreement
-or a subsequent version, subject to the provisions of Article 5.3.4
-<#compatibility>.
-
-
- Article 13 - GOVERNING LAW AND JURISDICTION
-
-13.1 The Agreement is governed by French law. The Parties agree to
-endeavor to seek an amicable solution to any disagreements or disputes
-that may arise during the performance of the Agreement.
-
-13.2 Failing an amicable solution within two (2) months as from their
-occurrence, and unless emergency proceedings are necessary, the
-disagreements or disputes shall be referred to the Paris Courts having
-jurisdiction, by the more diligent Party.
-
diff --git a/LibLunacy/AssetBuilder.cs b/LibLunacy/AssetBuilder.cs
deleted file mode 100644
index 0e6af6d..0000000
--- a/LibLunacy/AssetBuilder.cs
+++ /dev/null
@@ -1,199 +0,0 @@
-using LibLunacy.Objects;
-using LibLunacy.Shaders;
-using LibLunacy.Textures;
-
-namespace LibLunacy
-{
- ///
- /// Rebuilds asset files using new LibLunacy object model with serialization support.
- /// New engine only.
- ///
- public class AssetBuilder : IDisposable
- {
- private readonly FileManager fm;
-
- // Cache original asset binary data
- private readonly Dictionary originalMobyData = new();
- private readonly Dictionary originalTieData = new();
- private readonly Dictionary originalShaderData = new();
- private readonly Dictionary originalZoneData = new();
- private readonly Dictionary originalTextureData = new();
-
- public enum AssetType
- {
- Moby,
- Tie,
- Shader,
- Texture,
- Zone
- }
-
- public AssetBuilder(FileManager fm)
- {
- this.fm = fm;
- }
-
- ///
- /// Cache original binary data for an asset
- ///
- public void CacheOriginalData(ulong tuid, byte[] data, AssetType type)
- {
- var copy = new byte[data.Length];
- Array.Copy(data, copy, data.Length);
-
- switch (type)
- {
- case AssetType.Moby:
- originalMobyData[tuid] = copy;
- break;
- case AssetType.Tie:
- originalTieData[tuid] = copy;
- break;
- case AssetType.Shader:
- originalShaderData[tuid] = copy;
- break;
- case AssetType.Zone:
- originalZoneData[tuid] = copy;
- break;
- case AssetType.Texture:
- originalTextureData[tuid] = copy;
- break;
- }
- }
-
- ///
- /// Rebuild mobys.dat by serializing modified Moby objects to bytes.
- /// Uses Moby.ToBytes() method to serialize modifications.
- ///
- public void RebuildMobysFile(Dictionary mobys, string outputPath)
- {
- var assetlookup = fm.igfiles["assetlookup.dat"];
- if (assetlookup == null)
- throw new InvalidOperationException("assetlookup.dat required");
-
- var mobyptrSection = assetlookup.QuerySection(0x1D600);
- assetlookup.sh.Seek(mobyptrSection.offset);
- var pointers = FileUtils.ReadStructureArray(assetlookup.sh, mobyptrSection.length / 0x10);
-
- using var outputStream = File.Create(outputPath);
-
- foreach (var ptr in pointers)
- {
- if (mobys.TryGetValue(ptr.TUID, out var moby))
- {
- // Serialize the modified Moby object to bytes
- byte[] mobyBytes = moby.ToBytes();
- outputStream.Write(mobyBytes, 0, mobyBytes.Length);
- }
- else if (originalMobyData.TryGetValue(ptr.TUID, out var originalData))
- {
- // Fallback: write original data if moby wasn't loaded
- outputStream.Write(originalData, 0, originalData.Length);
- }
- }
- }
-
- ///
- /// Rebuild shaders.dat by serializing Shader objects.
- /// TODO: Implement Shader.ToBytes() method.
- ///
- public void RebuildShadersFile(Dictionary shaders, string outputPath)
- {
- var assetlookup = fm.igfiles["assetlookup.dat"];
- if (assetlookup == null)
- throw new InvalidOperationException("assetlookup.dat required");
-
- var shaderptrSection = assetlookup.QuerySection(0x1D100);
- assetlookup.sh.Seek(shaderptrSection.offset);
- var pointers = FileUtils.ReadStructureArray(assetlookup.sh, shaderptrSection.length / 0x10);
-
- using var outputStream = File.Create(outputPath);
-
- foreach (var ptr in pointers)
- {
- if (originalShaderData.TryGetValue(ptr.TUID, out var originalData))
- {
- // TODO: Use shader.ToBytes() when implemented
- outputStream.Write(originalData, 0, originalData.Length);
- }
- }
- }
-
- ///
- /// Rebuild ties.dat by serializing modified Tie objects.
- /// Uses Tie.ToBytes() method to serialize modifications.
- ///
- public void RebuildTiesFile(Dictionary ties, string outputPath)
- {
- var assetlookup = fm.igfiles["assetlookup.dat"];
- if (assetlookup == null)
- throw new InvalidOperationException("assetlookup.dat required");
-
- var tieptrSection = assetlookup.QuerySection(0x1D300);
- assetlookup.sh.Seek(tieptrSection.offset);
- var pointers = FileUtils.ReadStructureArray(assetlookup.sh, tieptrSection.length / 0x10);
-
- using var outputStream = File.Create(outputPath);
-
- foreach (var ptr in pointers)
- {
- if (ties.TryGetValue(ptr.TUID, out var tie))
- {
- // Serialize the modified Tie object to bytes
- byte[] tieBytes = tie.ToBytes();
- outputStream.Write(tieBytes, 0, tieBytes.Length);
- }
- else if (originalTieData.TryGetValue(ptr.TUID, out var originalData))
- {
- // Fallback: write original data if tie wasn't loaded
- outputStream.Write(originalData, 0, originalData.Length);
- }
- }
- }
-
- ///
- /// Rebuild zones.dat by serializing Zone objects.
- /// TODO: Implement Zone.ToBytes() method.
- ///
- public void RebuildZonesFile(Dictionary zones, string outputPath)
- {
- var assetlookup = fm.igfiles["assetlookup.dat"];
- if (assetlookup == null)
- throw new InvalidOperationException("assetlookup.dat required");
-
- var zoneptrSection = assetlookup.QuerySection(0x1DA00);
- assetlookup.sh.Seek(zoneptrSection.offset);
- var pointers = FileUtils.ReadStructureArray(assetlookup.sh, zoneptrSection.length / 0x10);
-
- using var outputStream = File.Create(outputPath);
-
- foreach (var ptr in pointers)
- {
- if (originalZoneData.TryGetValue(ptr.TUID, out var originalData))
- {
- // TODO: Use zone.ToBytes() when implemented
- outputStream.Write(originalData, 0, originalData.Length);
- }
- }
- }
-
- ///
- /// TODO: Implement proper texture rebuilding.
- /// Textures are more complex - they may be stored in highmip/lowmip files
- /// and require special handling for mipmaps and texture streaming.
- ///
- public void RebuildTexturesFile(Dictionary textures, string outputPath)
- {
- throw new NotImplementedException("Texture rebuilding not yet implemented. Requires highmip/lowmip file handling.");
- }
-
- public void Dispose()
- {
- originalMobyData.Clear();
- originalTieData.Clear();
- originalShaderData.Clear();
- originalZoneData.Clear();
- originalTextureData.Clear();
- }
- }
-}
diff --git a/LibLunacy/Globals.cs b/LibLunacy/Globals.cs
deleted file mode 100644
index 61e3098..0000000
--- a/LibLunacy/Globals.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-global using System.Collections;
-global using System.Collections.Generic;
-global using System.IO;
-global using System.Runtime.InteropServices;
-global using System.Linq;
-global using System.Text;
diff --git a/LibLunacy/Legacy/DebugFile.cs b/LibLunacy/Legacy/DebugFile.cs
deleted file mode 100644
index e9555f5..0000000
--- a/LibLunacy/Legacy/DebugFile.cs
+++ /dev/null
@@ -1,73 +0,0 @@
-namespace LibLunacy.Legacy
-{
- public class DebugFile
- {
- IGFile file;
-
- public DebugFile(IGFile file)
- {
- this.file = file;
- }
-
- public DebugInstanceName[] GetMobyInstanceNames()
- {
- IGFile.SectionHeader mobyinstNames = file.QuerySection(0x73C0);
- file.sh.Seek(mobyinstNames.offset);
- return FileUtils.ReadStructureArray(file.sh, mobyinstNames.count);
- }
- public DebugInstanceName[] GetTieInstanceNames()
- {
- IGFile.SectionHeader tieinstNames = file.QuerySection(0x72C0);
- file.sh.Seek(tieinstNames.offset);
- return FileUtils.ReadStructureArray(file.sh, tieinstNames.count);
- }
- public DebugAssetName GetMobyPrototypeName(uint i)
- {
- IGFile.SectionHeader mobyNames = file.QuerySection(0x9480);
- file.sh.Seek(mobyNames.offset + i * 0x10);
- return FileUtils.ReadStructure(file.sh);
- }
- public DebugAssetName GetTiePrototypeName(uint i)
- {
- IGFile.SectionHeader tieNames = file.QuerySection(0x9280);
- file.sh.Seek(tieNames.offset + i * 0x10);
- return FileUtils.ReadStructure(file.sh);
- }
- public DebugShaderName GetShaderName(uint i)
- {
- IGFile.SectionHeader shaderNames = file.QuerySection(0x5D00);
- file.sh.Seek(shaderNames.offset + i * 0x30);
- return FileUtils.ReadStructure(file.sh);
- }
-
- [FileStructure(0x18)]
- public struct DebugInstanceName
- {
- [FileOffset(0x00)] public ulong tuid1;
- [FileOffset(0x08)] public ulong tuid2;
- [FileOffset(0x10), Reference] public string name;
- [FileOffset(0x14)] public uint unk;
- }
-
- [FileStructure(0x10)]
- public struct DebugAssetName
- {
- [FileOffset(0x00)] public ulong tuid;
- [FileOffset(0x08), Reference] public string name;
- }
- [FileStructure(0x30)]
- public struct DebugShaderName
- {
- [FileOffset(0x00)] public ulong shaderTuid;
- [FileOffset(0x08), Reference] public string shaderName;
- [FileOffset(0x10)] public uint albedoTuid;
- [FileOffset(0x14)] public uint normalTuid;
- [FileOffset(0x18)] public uint expensiveTuid;
- [FileOffset(0x1C)] public uint wthTuid;
- [FileOffset(0x20), Reference] public string albedoName;
- [FileOffset(0x24), Reference] public string normalName;
- [FileOffset(0x28), Reference] public string expensiveName;
- [FileOffset(0x2C), Reference] public string wthName;
- }
- }
-}
\ No newline at end of file
diff --git a/LibLunacy/Legacy/Gameplay.cs b/LibLunacy/Legacy/Gameplay.cs
deleted file mode 100644
index 6eb9422..0000000
--- a/LibLunacy/Legacy/Gameplay.cs
+++ /dev/null
@@ -1,203 +0,0 @@
-using System.Numerics;
-
-namespace LibLunacy.Legacy
-{
- public class Gameplay
- {
- IGFile file;
- public Region[] regions;
-
- public Gameplay(AssetLoader al)
- {
- Console.WriteLine("Reading Gameplay.dat.");
- file = al.fm.igfiles["gameplay.dat"];
-
- if (al.fm.isOld)
- {
- regions = new Region[1];
- regions[0] = new Region(file, al);
- }
- else
- {
- //Loading regions
-
- IGFile.SectionHeader stringTableSection = file.QuerySection(0x25000);
-
- //gameplay.dat is a weird file in this version of the engine, the count field of section headers is the length and length field of section headers is 0
-
- file.sh.Seek(stringTableSection.offset + stringTableSection.count - 0x10);
- regions = new Region[file.sh.ReadUInt32()];
-
- uint regionTableOffset = file.sh.ReadUInt32();
-
- for (int i = 0; i < regions.Length; i++)
- {
- file.sh.Seek(regionTableOffset + 0x04 * i);
-
- string regionName = file.sh.ReadString(file.sh.ReadUInt32());
- Console.WriteLine($"Region {i}: {regionName}");
- regions[i] = new Region(al, regionName);
- }
- }
- }
- }
-
- public class Region
- {
- public string name = "default";
-
- public Dictionary mobyInstances = new Dictionary();
- public List volumeInstances = new List();
- public CZone[] zones;
-
- public class CVolumeInstance
- {
- public Vector3 position;
- public Quaternion rotation;
- public Vector3 scale;
- public string name;
- public ulong id;
- public CVolumeInstance(NewVolumeInstance nvolume, NewVolumeInstanceMetadata ni)
- {
- Matrix4x4.Decompose(nvolume.transform, out scale, out rotation, out position);
- name = ni.name;
- id = ni.tuid;
- }
- }
- public class CMobyInstance
- {
- public Vector3 position;
- public Vector3 rotation;
- public float scale;
- public CMoby moby;
- public string name;
-
- public CMobyInstance(OldMobyInstance omi, AssetLoader al)
- {
- position = omi.position;
- rotation = omi.rotation;
- scale = omi.scale;
- moby = al.mobys[omi.mobyIndex];
- }
- public CMobyInstance(NewMobyInstance nmi, NewVolumeInstanceMetadata ni, AssetLoader al, IGFile region)
- {
- position = nmi.position;
- rotation = nmi.rotation;
- scale = nmi.scale;
-
- region.sh.Seek(region.QuerySection(0x1C600).offset + 0x08 * nmi.mobyIndex);
-
- moby = al.mobys[region.sh.ReadUInt64()];
- }
-
- }
-
-
- [FileStructure(0x48)]
- public struct OldMobyInstance
- {
- [FileOffset(0x18)] public Vector3 position;
- [FileOffset(0x24)] public Vector3 rotation; //ZYX euler in radians
- [FileOffset(0x30)] public float scale;
- [FileOffset(0x3C)] public ushort mobyIndex;
- }
-
- [FileStructure(0x50)]
- public struct NewMobyInstance
- {
- [FileOffset(0x00)] public ushort mobyIndex;
- [FileOffset(0x02)] public ushort groupIndex;
- [FileOffset(0x14)] public Vector3 position;
- [FileOffset(0x20)] public Vector3 rotation; //ZYX euler in radians
- [FileOffset(0x2C)] public float scale;
- }
- [FileStructure(0x40)]
- public struct NewVolumeInstance
- {
- [FileOffset(0x00)] public Matrix4x4 transform;
- }
- [FileStructure(0x10)]
- public struct NewVolumeInstanceMetadata
- {
- [FileOffset(0x00)] public ulong tuid;
- [FileOffset(0x08), Reference] public string name;
- [FileOffset(0x0C)] public ushort group;
- }
-
- public Region(IGFile file, AssetLoader al)
- {
- IGFile.SectionHeader mobyInstSections = file.QuerySection(0x7340);
- file.sh.Seek(mobyInstSections.offset);
- OldMobyInstance[] mobys = FileUtils.ReadStructureArray(file.sh, mobyInstSections.count);
-
- zones = new CZone[1];
- zones[0] = new CZone(al.fm.igfiles["main.dat"], al, 0);
- zones[0].name = "art";
-
- DebugFile.DebugInstanceName[] names = null;
- if (al.fm.debug != null)
- {
- names = al.fm.debug.GetMobyInstanceNames();
- }
-
- for (int i = 0; i < mobys.Length; i++)
- {
- mobyInstances.Add((ulong)i, new CMobyInstance(mobys[i], al));
- if (names != null)
- {
- mobyInstances.Last().Value.name = names[i].name;
- }
- else
- {
- mobyInstances.Last().Value.name = $"Moby_{mobys[i].mobyIndex.ToString("X04")}_Instance_{i}";
- }
- }
- }
- public Region(AssetLoader al, string regionName)
- {
- name = regionName;
- IGFile prius = (IGFile)al.fm.LoadFile($"{name}/gp_prius.dat", false);
- IGFile region = (IGFile)al.fm.LoadFile($"{name}/region.dat", false);
-
- IGFile.SectionHeader mobyInstSection = prius.QuerySection(0x25048);
- prius.sh.Seek(mobyInstSection.offset);
- NewMobyInstance[] mobys = FileUtils.ReadStructureArray(prius.sh, mobyInstSection.count);
-
- IGFile.SectionHeader mobyNamesSection = prius.QuerySection(0x2504C);
- prius.sh.Seek(mobyNamesSection.offset);
- NewVolumeInstanceMetadata[] mobyNames = FileUtils.ReadStructureArray(prius.sh, mobyInstSection.count);
-
- for (int i = 0; i < mobys.Length; i++)
- {
- mobyInstances.Add(mobyNames[i].tuid, new CMobyInstance(mobys[i], mobyNames[i], al, region));
- mobyInstances.Last().Value.name = mobyNames[i].name;
- }
-
- IGFile.SectionHeader volumeInstSection = prius.QuerySection(0x2505C);
- prius.sh.Seek(volumeInstSection.offset);
- NewVolumeInstance[] volumes = FileUtils.ReadStructureArray(prius.sh, volumeInstSection.count);
-
- IGFile.SectionHeader volumeNamesSection = prius.QuerySection(0x25060);
- prius.sh.Seek(volumeNamesSection.offset);
- NewVolumeInstanceMetadata[] volumeNames = FileUtils.ReadStructureArray(prius.sh, volumeInstSection.count);
-
- for (int i = 0; i < volumes.Length; i++)
- {
- volumeInstances.Add(new CVolumeInstance(volumes[i], volumeNames[i]));
- }
-
- IGFile.SectionHeader zoneNames = region.QuerySection(0x1C000);
- IGFile.SectionHeader zoneRefs = region.QuerySection(0x1C010);
-
- zones = new CZone[zoneRefs.count];
-
- for (int i = 0; i < zoneRefs.count; i++)
- {
- region.sh.Seek(zoneRefs.offset + i * 8);
- zones[i] = al.zones[region.sh.ReadUInt64()];
- region.sh.Seek(zoneNames.offset + i * 4);
- zones[i].name = region.sh.ReadString(region.sh.ReadUInt32());
- }
- }
- }
-}
\ No newline at end of file
diff --git a/LibLunacy/Legacy/Moby.cs b/LibLunacy/Legacy/Moby.cs
deleted file mode 100644
index cdfaa9b..0000000
--- a/LibLunacy/Legacy/Moby.cs
+++ /dev/null
@@ -1,277 +0,0 @@
-using System.Numerics;
-
-namespace LibLunacy.Legacy
-{
- public class CMoby
- {
- [FileStructure(0x40)]
- public struct MobyMesh
- {
- [FileOffset(0x00)] public uint indexIndex;
- [FileOffset(0x04)] public uint vertexOffset;
- [FileOffset(0x08)] public ushort shaderIndex;
- [FileOffset(0x0A)] public ushort vertexCount;
- [FileOffset(0x0C)] public byte boneMapIndexCount;
- [FileOffset(0x0D)] public byte vertexType;
- [FileOffset(0x0E)] public byte boneMapIndex;
- [FileOffset(0x12)] public ushort indexCount;
- [FileOffset(0x20)] public uint boneMap; //Should turn this into a reference
-
- public CShader shader;
- }
- [FileStructure(0x08)]
- public struct Bangle
- {
- [FileOffset(0x00), Reference("MetadataCount")] public MobyMesh[] meshes;
- [FileOffset(0x04)] public uint count;
-
- public uint MetadataCount => count;
- }
-
- [FileStructure(0xC0)]
- public struct OldMoby
- {
- [FileOffset(0x00)] public Vector3 boundingSpherePosition;
- [FileOffset(0x0C)] public float boundingSphereRotation;
- [FileOffset(0x18)] public ushort bangleCount1;
- [FileOffset(0x1A)] public ushort bangleCount2;
- [FileOffset(0x28), Reference("BangleCount")] public Bangle[] bangles;
- [FileOffset(0x34)] public uint indexOffset;
- [FileOffset(0x38)] public uint vertexOffset;
- [FileOffset(0x3C)] public float scale;
-
- public uint BangleCount => bangleCount1;//(uint)(bangleCount1 * (bangleCount2 + 1));
- }
-
- [FileStructure(0x100)]
- public struct NewMoby
- {
- [FileOffset(0x00)] public Vector3 boundingSpherePosition;
- [FileOffset(0x0C)] public float boundingSphereRadius;
- [FileOffset(0x18)] public ushort bangleCount1;
- [FileOffset(0x1A)] public ushort bangleCount2; //Likely LOD count, unsure tho since they reference the exact same verts and indices, perhaps done dynamically?
- [FileOffset(0x24), Reference("BangleCount")] public Bangle[] bangles;
- [FileOffset(0x70)] public float scale;
- [FileOffset(0xB0)] public ulong tuid;
-
- public uint BangleCount => bangleCount1;//(uint)(bangleCount1 * (bangleCount2 + 1));
- }
-
- public Bangle[] bangles;
- public string name;
- public float scale;
- public IGFile file;
- public ulong id; //Either tuid or index depending on game
- public Vector3 boundingSpherePosition;
- public float boundingSphereRadius;
- StreamHelper vertexStream;
- StreamHelper indexStream;
- public List shaderDB { get; private set; } //On the old engine, there's a global shaderDB. On the new engine, it's per MobyObj/tie/shrubmaybe/zonemaybe
-
- public uint MetadataCount
- {
- get
- {
- uint count = 0;
- for (int i = 0; i < bangles.Length; i++)
- {
- count += (uint)bangles[i].meshes.Length;
- }
- return count;
- }
- }
-
- public CMoby(IGFile file, AssetLoader al, uint index = 0)
- {
- this.file = file;
- IGFile.SectionHeader section = file.QuerySection(0xD100);
- if (section.length == 0x100)
- {
- file.sh.Seek(section.offset);
- NewMoby nmoby = FileUtils.ReadStructure(file.sh);
-
- Console.WriteLine($"nmoby.bangles.Length {nmoby.bangles.Length}");
-
- bangles = nmoby.bangles;
- IGFile.SectionHeader namesection = file.QuerySection(0xD200);
- name = file.sh.ReadString(namesection.offset);
- scale = nmoby.scale;
- boundingSpherePosition = nmoby.boundingSpherePosition;
- boundingSphereRadius = nmoby.boundingSphereRadius;
-
- IGFile.SectionHeader vertexsection = file.QuerySection(0xE200);
- //SubStream vertexms = new SubStream(file.sh.BaseStream, vertexsection.offset, vertexsection.length);
- file.sh.Seek(vertexsection.offset);
- MemoryStream vertexms = new MemoryStream(file.sh.ReadBytes(vertexsection.length));
- vertexStream = new StreamHelper(vertexms, file.sh._endianness);
-
- IGFile.SectionHeader indexsection = file.QuerySection(0xE100);
- //SubStream indexms = new SubStream(file.sh.BaseStream,indexsection.offset,indexsection.length);
- file.sh.Seek(indexsection.offset);
- MemoryStream indexms = new MemoryStream(file.sh.ReadBytes(indexsection.length));
- indexStream = new StreamHelper(indexms, file.sh._endianness);
-
- shaderDB = ReadShaderDB(al);
- if (name.Contains("heckler"))
- Console.Write("");
- id = nmoby.tuid;
- }
- else
- {
- file.sh.Seek(section.offset + 0xC0 * index);
- OldMoby omoby = FileUtils.ReadStructure(file.sh);
-
- bangles = omoby.bangles;
- scale = omoby.scale;
- boundingSpherePosition = omoby.boundingSpherePosition;
- boundingSphereRadius = omoby.boundingSphereRotation;
-
- IGFile vertexFile = al.fm.igfiles["vertices.dat"];
-
- MobyMesh lastMesh = omoby.bangles.Last(x => x.count != 0).meshes.Last();
- Stream vertexFileToUse = null;
- Stream indexFileToUse = null;
-
- if ((omoby.vertexOffset & 0x80000000) != 0)
- {
- vertexFileToUse = vertexFile.sh.BaseStream;
- vertexFileToUse.Seek(vertexFile.QuerySection(0x9000).offset, SeekOrigin.Begin);
- }
- else
- {
- vertexFileToUse = al.fm.rawfiles["textures.dat"];
- vertexFileToUse.Seek(0, SeekOrigin.Begin);
- }
- vertexFileToUse.Seek(omoby.vertexOffset & ~0x80000000, SeekOrigin.Current);
- uint vertexSize = lastMesh.vertexOffset + lastMesh.vertexCount * (lastMesh.vertexType == 1 ? 0x1Cu : 0x14u);
- byte[] vertexdata = new byte[vertexSize];
- vertexFileToUse.Read(vertexdata);
- MemoryStream vertexms = new MemoryStream(vertexdata);
-
- if ((omoby.indexOffset & 0x80000000) != 0)
- {
- indexFileToUse = vertexFile.sh.BaseStream;
- indexFileToUse.Seek(vertexFile.QuerySection(0x9100).offset, SeekOrigin.Begin);
- }
- else
- {
- indexFileToUse = al.fm.rawfiles["textures.dat"];
- indexFileToUse.Seek(0, SeekOrigin.Begin);
- }
- indexFileToUse.Seek(omoby.indexOffset & ~0x80000000, SeekOrigin.Current);
- uint indexSize = (lastMesh.indexIndex + lastMesh.indexCount) * 2;
- byte[] indexdata = new byte[indexSize];
- indexFileToUse.Read(indexdata);
- MemoryStream indexms = new MemoryStream(indexdata);
-
- vertexStream = new StreamHelper(vertexms, file.sh._endianness);
- indexStream = new StreamHelper(indexms, file.sh._endianness);
-
- shaderDB = al.shaderDB;
-
- id = index;
-
- if (al.fm.debug != null) name = al.fm.debug.GetMobyPrototypeName(index).name;
- else name = $"Moby_{index.ToString("X04")}";
- }
-
- LoadDependancies(al);
- }
- //Function should NOT be used on old engine, it would work but it'd waste a lot of memory
- private List ReadShaderDB(AssetLoader al)
- {
- IGFile.SectionHeader shaderSection;
- shaderSection = file.QuerySection(0x5600);
- List shaders = new List((int)shaderSection.count);
-
- for (uint i = 0; i < shaderSection.count; i++)
- {
- file.sh.Seek(shaderSection.offset + i * 8);
- shaders.Add(al.shaders[file.sh.ReadUInt64()]);
- }
- return shaders;
- }
- public void GetBuffers(MobyMesh mesh, out uint[] indices, out float[] vPositions, out float[] vTexCoords)
- {
- indices = new uint[mesh.indexCount];
- indexStream.Seek(mesh.indexIndex * 2);
- for (int k = 0; k < mesh.indexCount; k++)
- {
- indices[k] = indexStream.ReadUInt16();
- }
-
- int stride = mesh.vertexType == 1 ? 0x1C : 0x14;
- vPositions = new float[mesh.vertexCount * 3];
- vTexCoords = new float[mesh.vertexCount * 2];
- //bangles[i].meshes[j].vNormals = new float[bangles[i].meshes[j].vertexCount * 3];
- for (int k = 0; k < mesh.vertexCount; k++)
- {
- vertexStream.Seek(mesh.vertexOffset + stride * k + 0x00);
- vPositions[k * 3 + 0] = vertexStream.ReadInt16() * scale;
- vPositions[k * 3 + 1] = vertexStream.ReadInt16() * scale;
- vPositions[k * 3 + 2] = vertexStream.ReadInt16() * scale;
-
- vertexStream.Seek(mesh.vertexOffset + stride * k + (mesh.vertexType == 1 ? 0x10 : 0x08));
-
- vTexCoords[k * 2 + 0] = (float)vertexStream.ReadHalf();
- vTexCoords[k * 2 + 1] = (float)vertexStream.ReadHalf();
-
- /*vertexStream.Seek(bangles[i].meshes[j].vertexOffset + stride * (k+1) - 0x04);
- vertexStream.bitPosition = 1;
- int vnx = (int)vertexStream.ReadIntN(11);
- int vny = (int)vertexStream.ReadIntN(10);
- int vnz = (int)vertexStream.ReadIntN(10);
- bangles[i].meshes[j].vNormals[k * 3 + 0] = (vnx / 511f) * 2 - 2;
- bangles[i].meshes[j].vNormals[k * 3 + 1] = (vny / 511f) * 2 - 2;
- bangles[i].meshes[j].vNormals[k * 3 + 2] = (vnz / 511f) * 2 - 2;*/
- }
- }
- public void LoadDependancies(AssetLoader al)
- {
- }
-
- public void ExportToObj(string filePath)
- {
- uint maxIndex = 0;
-
- StringBuilder obj = new StringBuilder();
-
- obj.Append($"mtllib unused.mtl\n");
- for (int i = 0; i < bangles.Length; i++)
- {
- obj.Append($"o Bangle_{i}\n");
- for (int j = 0; j < bangles[i].count; j++)
- {
- GetBuffers(bangles[i].meshes[j], out uint[] indices, out float[] vPositions, out float[] vTexCoords);
-
- for (int k = 0; k < bangles[i].meshes[j].vertexCount; k++)
- {
- obj.Append($"v {vPositions[k * 3].ToString("F8")} {vPositions[k * 3 + 1].ToString("F8")} {vPositions[k * 3 + 2].ToString("F8")}\n");
- obj.Append($"vt {vTexCoords[k * 2].ToString("F8")} {vTexCoords[k * 2 + 1].ToString("F8")}\n");
- //obj.Append($"vn {bangles[i].meshes[j].vNormals[k * 3].ToString("F8")} {bangles[i].meshes[j].vNormals[k * 3 + 1].ToString("F8")} {bangles[i].meshes[j].vNormals[k * 3 + 2].ToString("F8")}\n");
- }
-
- obj.Append($"usemtl Shader_{bangles[i].meshes[j].shaderIndex}\n");
-
- for (int k = 0; k < bangles[i].meshes[j].indexCount; k += 3)
- {
- string i1 = (indices[k + 0] + maxIndex + 1).ToString();
- string i2 = (indices[k + 1] + maxIndex + 1).ToString();
- string i3 = (indices[k + 2] + maxIndex + 1).ToString();
- //obj.Append($"f {i1}/{i1}/{i1} {i2}/{i2}/{i2} {i3}/{i3}/{i3}\n");
- obj.Append($"f {i1}/{i1} {i2}/{i2} {i3}/{i3}\n");
- }
-
- maxIndex += bangles[i].meshes[j].vertexCount;
- }
- }
- File.WriteAllText(filePath, obj.ToString());
- }
- public void Dispose()
- {
- vertexStream.Close();
- indexStream.Close();
- file.Dispose();
- }
- }
-}
\ No newline at end of file
diff --git a/LibLunacy/Legacy/Shader.cs b/LibLunacy/Legacy/Shader.cs
deleted file mode 100644
index 23e8f05..0000000
--- a/LibLunacy/Legacy/Shader.cs
+++ /dev/null
@@ -1,132 +0,0 @@
-namespace LibLunacy.Legacy
-{
- public class CShader
- {
- [FileStructure(0x80)]
- public struct OldShader
- {
- //These could be OldTextureReference references but i've instead made them just uints so that it can work better with the CTexture class
- [FileOffset(0x00)] public uint albedoOffset;
- [FileOffset(0x04)] public uint normalOffset;
- [FileOffset(0x08)] public uint expensiveOffset;
- [FileOffset(0x11)] public byte renderingMode;
- [FileOffset(0x20)] public float alphaClip;
- }
- [FileStructure(0x80)]
- public struct NewShader
- {
- [FileOffset(0x00)] public int albedoIndex;
- [FileOffset(0x04)] public int normalIndex;
- [FileOffset(0x08)] public int expensiveIndex;
- [FileOffset(0x21)] public byte renderingMode;
- [FileOffset(0x30)] public float alphaClip;
- }
- [FileStructure(0x40)]
- public struct NewReferences
- {
- [FileOffset(0x00)] public ulong thisTuid;
- [FileOffset(0x08), Reference] public string thisName;
- [FileOffset(0x10)] public uint albedoTuid;
- [FileOffset(0x14)] public uint normalTuid;
- [FileOffset(0x18)] public uint expensiveTuid;
- [FileOffset(0x28), Reference] public string albedoName;
- [FileOffset(0x2C), Reference] public string normalName;
- [FileOffset(0x30), Reference] public string expensiveName;
-
- public uint TextureCount => 3;
- }
-
- public enum RenderingMode : byte
- {
- Opaque = 0,
- AlphaClip = 4,
- AlphaBlend = 6,
- }
- IGFile file;
-
-
- public CTexture? albedo = null;
- public CTexture? normal = null;
- public CTexture? expensive = null;
- public RenderingMode renderingMode;
- public float alphaClip;
- public string name;
-
- public CShader(IGFile file, AssetLoader al, uint index = 0)
- {
- this.file = file;
-
- IGFile.SectionHeader section = file.QuerySection(0x5000);
-
- file.sh.Seek(section.offset + 0x80 * index);
-
- if (al.fm.isOld)
- {
- OldShader oshader = FileUtils.ReadStructure(file.sh);
-
- DebugFile.DebugShaderName shaderName = new DebugFile.DebugShaderName();
-
- if (al.fm.debug != null)
- {
- shaderName = al.fm.debug.GetShaderName(index);
- }
-
- if (oshader.albedoOffset != 0)
- {
- albedo = al.textures[oshader.albedoOffset];
- if (al.fm.debug != null)
- {
- albedo.name = shaderName.albedoName;
- }
- }
- if (oshader.normalOffset != 0)
- {
- normal = al.textures[oshader.normalOffset];
- if (al.fm.debug != null)
- {
- normal.name = shaderName.normalName;
- }
- }
- if (oshader.expensiveOffset != 0)
- {
- expensive = al.textures[oshader.expensiveOffset];
- if (al.fm.debug != null)
- {
- expensive.name = shaderName.expensiveName;
- }
- }
- renderingMode = (RenderingMode)oshader.renderingMode;
- alphaClip = oshader.alphaClip;
- name = shaderName.shaderName;
- }
- else
- {
- file.sh.Seek(section.offset, SeekOrigin.Begin);
- NewShader nshader = FileUtils.ReadStructure(file.sh);
- IGFile.SectionHeader refSection = file.QuerySection(0x5D00);
- file.sh.Seek(refSection.offset, SeekOrigin.Begin);
- NewReferences refs = FileUtils.ReadStructure(file.sh);
-
- if (refs.albedoTuid != 0 && al.textures.ContainsKey(refs.albedoTuid))
- {
- albedo = al.textures[refs.albedoTuid];
- albedo.name = refs.albedoName;
- }
- if (refs.normalTuid != 0 && al.textures.ContainsKey(refs.normalTuid))
- {
- normal = al.textures[refs.normalTuid];
- normal.name = refs.normalName;
- }
- if (refs.expensiveTuid != 0 && al.textures.ContainsKey(refs.expensiveTuid))
- {
- expensive = al.textures[refs.expensiveTuid];
- expensive.name = refs.expensiveName;
- }
- name = refs.thisName;
-
- renderingMode = (RenderingMode)nshader.renderingMode;
- alphaClip = nshader.alphaClip;
- }
- }
- }
-}
\ No newline at end of file
diff --git a/LibLunacy/Legacy/Texture.cs b/LibLunacy/Legacy/Texture.cs
deleted file mode 100644
index fb36d33..0000000
--- a/LibLunacy/Legacy/Texture.cs
+++ /dev/null
@@ -1,335 +0,0 @@
-namespace LibLunacy.Legacy
-{
- public class CTexture
- {
- [FileStructure(0x20)]
- public struct OldTextureReference
- {
- [FileOffset(0x00)] public uint offset; //offsets into textures.dat
- [FileOffset(0x04)] public ushort mipmapCount;
-
- //Bits (0 based, from left to right):
- // shift | bits | desc
- // --------|--------|------------------------------------------------
- // 2 | 1 | if 1 then unswizzled, ignored on DXT formats
- // 4 | 4 | format, see TexFormat enum
- [FileOffset(0x06)] public ushort formatBitField;
- [FileOffset(0x18)] public ushort width;
- [FileOffset(0x1A)] public ushort height;
- }
- [FileStructure(0x10)]
- public struct OldTexstreamReference
- {
- [FileOffset(0x00)] public uint offset; //offsets into textures.dat
- [FileOffset(0x06)] public ushort index;
- }
- [FileStructure(0x04)]
- public struct NewTexMeta
- {
- [FileOffset(0x00)] public byte format;
- [FileOffset(0x01)] public byte mipmapCount;
- [FileOffset(0x02)] public byte widthPow;
- [FileOffset(0x03)] public byte heightPow;
- }
-
- public byte[] data;
- public TexFormat format;
- public int width;
- public int height;
- public int mipmapCount;
- public uint id;
- public string name;
-
- public enum TexFormat
- {
- R5G6B5 = 0x03,
- A8R8G8B8 = 0x05,
- DXT1 = 0x06,
- DXT3 = 0x07,
- DXT5 = 0x08,
- }
-
- public uint HighmipSize
- {
- get
- {
- switch (format)
- {
- case TexFormat.DXT1:
- return (uint)(Math.Max(1, (width + 3) / 4) * Math.Max(1, (height + 3) / 4)) * 8;
- case TexFormat.DXT3:
- case TexFormat.DXT5:
- return (uint)(Math.Max(1, (width + 3) / 4) * Math.Max(1, (height + 3) / 4)) * 16;
- case TexFormat.A8R8G8B8:
- return (uint)(width * height * 4u);
- case TexFormat.R5G6B5:
- return (uint)(width * height * 2u);
- default:
- return 0;
- }
- }
- }
-
- public CTexture(FileManager fm, int index)
- {
- if (fm.isOld)
- {
- IGFile main = fm.igfiles["main.dat"];
- Stream textures = fm.rawfiles["textures.dat"];
- Stream? texstream = fm.rawfiles["texstream.dat"];
-
- IGFile.SectionHeader texrefs = main.QuerySection(0x5200);
- IGFile.SectionHeader texstrrefs = main.QuerySection(0x9800);
-
- main.sh.Seek(texrefs.offset + index * 0x20);
- OldTextureReference otr = FileUtils.ReadStructure(main.sh);
- main.sh.Seek(texstrrefs.offset);
- OldTexstreamReference[] ots = FileUtils.ReadStructureArray(main.sh, texstrrefs.count);
-
- width = otr.width;
- height = otr.height;
- mipmapCount = 0;
- format = (TexFormat)(otr.formatBitField >> 8 & 0xF);
-
- if (texstream != null && ots.Any(x => x.index == index))
- {
- width *= 2;
- height *= 2;
- mipmapCount += 1;
- }
-
- data = new byte[HighmipSize];
-
- if (HighmipSize == 0) return;
-
- if (texstream != null && ots.Any(x => x.index == index))
- {
- texstream.Seek(ots.First(x => x.index == index).offset, SeekOrigin.Begin);
- if (format == TexFormat.DXT1 || format == TexFormat.DXT3 || format == TexFormat.DXT5 /*&& (otr.formatBitField & 0x2000) == 0*/)
- {
- texstream.Read(data);
- }
- else
- {
- //width = 0;
- //height = 0;
- Console.WriteLine($"Unswizzling {((uint)(texrefs.offset + index * 0x20)).ToString("X08")}");
- Unswizzle(texstream, ref data, width, height, format);
- }
- }
- else
- {
- textures.Seek(otr.offset, SeekOrigin.Begin);
- if (format == TexFormat.DXT1 || format == TexFormat.DXT3 || format == TexFormat.DXT5 /*&& (otr.formatBitField & 0x2000) == 0*/)
- {
- textures.Read(data);
- }
- else
- {
- //width = 0;
- //height = 0;
- Console.WriteLine($"Unswizzling {((uint)(texrefs.offset + index * 0x20)).ToString("X08")}");
- Unswizzle(textures, ref data, width, height, format);
- }
- }
-
- id = (uint)(texrefs.offset + index * 0x20);
- name = $"Texture_{index}";
- }
- else
- {
- IGFile assetlookup = fm.igfiles["assetlookup.dat"];
- Stream textures = fm.rawfiles["textures.dat"];
- Stream highmips = fm.rawfiles["highmips.dat"];
- IGFile.SectionHeader highmipPtrs = assetlookup.QuerySection(0x1D1C0);
- IGFile.SectionHeader textureMetas = assetlookup.QuerySection(0x1D140);
-
- assetlookup.sh.Seek(highmipPtrs.offset + index * 0x10);
- AssetLoader.AssetPointer hmipPtr = FileUtils.ReadStructure(assetlookup.sh);
- assetlookup.sh.Seek(textureMetas.offset + index * 0x04);
- NewTexMeta meta = FileUtils.ReadStructure(assetlookup.sh);
-
- width = 1 << meta.widthPow;
- height = 1 << meta.heightPow;
- mipmapCount = 1;//meta.mipmapCount;
- if (meta.format == 6 || meta.format == 7 || meta.format == 8 || meta.format == 5 || meta.format == 0x0B)
- {
- format = (TexFormat)meta.format;
- }
- else
- {
- Console.Error.WriteLine($"WARNING: TEXTURE {hmipPtr.tuid.ToString("X016")} HAS UNKNOWN FORMAT {meta.format.ToString("X02")}, SKIPPING...");
- goto errorcleanup;
- }
-
- if (hmipPtr.length == 0)
- {
- Console.Error.WriteLine($"WARNING: HMIP {hmipPtr.tuid.ToString("X016")} HAS SIZE 0, SKIPPING...");
- goto errorcleanup;
- }
-
- data = new byte[hmipPtr.length];
- highmips.Seek(hmipPtr.offset, SeekOrigin.Begin);
-
- if (format == TexFormat.DXT1 || format == TexFormat.DXT3 || format == TexFormat.DXT5)
- {
- highmips.Read(data);
- }
- else if (format == TexFormat.A8R8G8B8 || format == TexFormat.R5G6B5)
- {
- Console.WriteLine($"Unswizzling {hmipPtr.tuid.ToString("X016")}");
- Unswizzle(highmips, ref data, width, height, format);
- }
-
- goto finish;
-
- errorcleanup:
- width = 0;
- height = 0;
-
- finish:
- id = (uint)hmipPtr.tuid;
- }
- }
-
- private void Unswizzle(Stream s, ref byte[] b, int width, int height, TexFormat format)
- {
- if (format == TexFormat.DXT1 || format == TexFormat.DXT3 || format == TexFormat.DXT5) throw new InvalidOperationException("DXT textures aren't swizzled");
- if (b.Length == 0) throw new ArgumentException("Array too small");
- //if(b.Length == 0) return;
-
- long ogPos = s.Position;
-
- int pixelSize = 0;
- if (format == TexFormat.R5G6B5) pixelSize = 2;
- else if (format == TexFormat.A8R8G8B8) pixelSize = 4;
-
- byte[] pixel = new byte[pixelSize];
-
- for (int t = 0; t < width * height; t++)
- {
- int index = Morton(t, width, height);
- s.Read(pixel);
- if (format == TexFormat.A8R8G8B8)
- {
- Array.Reverse(pixel, 1, 3); // ABGR -> ARGB
- Array.Reverse(pixel, 0, 4); // ARGB -> RGBA
- }
- Array.Copy(pixel, 0, b, index * pixelSize, pixelSize);
- }
- }
-
- //Stolen from RawTex
- private int Morton(int t, int x, int y)
- {
- int num2;
- int num = num2 = 1;
- int num3 = t;
- int num4 = x;
- int num5 = y;
- int num6 = 0;
- int num7 = 0;
- while (num4 > 1 || num5 > 1)
- {
- if (num4 > 1)
- {
- num6 += num2 * (num3 & 1);
- num3 >>= 1;
- num2 *= 2;
- num4 >>= 1;
- }
- if (num5 > 1)
- {
- num7 += num * (num3 & 1);
- num3 >>= 1;
- num *= 2;
- num5 >>= 1;
- }
- }
- return num7 * x + num6;
- }
-
- private static readonly byte[] ddsHeader = new byte[0x80]
- {
- 0x44, 0x44, 0x53, 0x20, // "DDS "
- 0x7C, 0x00, 0x00, 0x00, // Version Info
- 0x07, 0x10, 0x0A, 0x00, // More Version Info
- 0x00, 0x00, 0x00, 0x00, // Height
- 0x00, 0x00, 0x00, 0x00, // Width
- 0x00, 0x00, 0x00, 0x00, // Size
- 0x00, 0x00, 0x00, 0x00, //
- 0x00, 0x00, 0x00, 0x00, // Mipmaps
- 0x00, 0x00, 0x00, 0x00, //
- 0x00, 0x00, 0x00, 0x00, //
- 0x00, 0x00, 0x00, 0x00, //
- 0x00, 0x00, 0x00, 0x00, //
- 0x00, 0x00, 0x00, 0x00, //
- 0x00, 0x00, 0x00, 0x00, //
- 0x00, 0x00, 0x00, 0x00, //
- 0x00, 0x00, 0x00, 0x00, //
- 0x00, 0x00, 0x00, 0x00, //
- 0x00, 0x00, 0x00, 0x00, //
- 0x00, 0x00, 0x00, 0x00, //
- 0x20, 0x00, 0x00, 0x00, //
- 0x04, 0x00, 0x00, 0x00, //
- 0x44, 0x58, 0x54, 0x30, // "DXT0"
- 0x00, 0x00, 0x00, 0x00, //
- 0x00, 0x00, 0x00, 0x00, //
- 0x00, 0x00, 0x00, 0x00, //
- 0x00, 0x00, 0x00, 0x00, //
- 0x00, 0x00, 0x00, 0x00, //
- 0x08, 0x10, 0x40, 0x00, //
- 0x00, 0x00, 0x00, 0x00, //
- 0x00, 0x00, 0x00, 0x00, //
- 0x00, 0x00, 0x00, 0x00, //
- 0x00, 0x00, 0x00, 0x00 //
- };
- public void ExportToDDS(Stream dst, bool leaveOpen = true)
- {
- dst.Write(ddsHeader, 0x00, 0x80);
- dst.Seek(0x0C, SeekOrigin.Begin);
- dst.Write(BitConverter.GetBytes((uint)height), 0x00, 0x04);
- dst.Write(BitConverter.GetBytes((uint)width), 0x00, 0x04);
- dst.Write(BitConverter.GetBytes(HighmipSize), 0x00, 0x04);
- dst.Seek(0x1C, SeekOrigin.Begin);
- dst.Write(BitConverter.GetBytes((uint)1), 0x00, 0x04);
- dst.Seek(0x57, SeekOrigin.Begin);
- switch (format)
- {
- case TexFormat.DXT1:
- dst.Write(BitConverter.GetBytes((byte)0x31), 0x00, 0x01);
- break;
- case TexFormat.DXT3:
- dst.Write(BitConverter.GetBytes((byte)0x33), 0x00, 0x01);
- break;
- case TexFormat.DXT5:
- dst.Write(BitConverter.GetBytes((byte)0x35), 0x00, 0x01);
- break;
- case TexFormat.A8R8G8B8:
- dst.Seek(0x4C, SeekOrigin.Begin);
- dst.Write(new byte[]
- {
- 0x20, 0x00, 0x00, 0x00,
- 0x41, 0x00, 0x00, 0x00,
- 0x00, 0x00, 0x00, 0x00,
- 0x20, 0x00, 0x00, 0x00,
- 0x00, 0x00, 0x00, 0xFF,
- 0x00, 0x00, 0xFF, 0x00,
- 0x00, 0xFF, 0x00, 0x00,
- 0xFF, 0x00, 0x00, 0x00
- }, 0x00, 0x20);
- break;
- default:
- Console.WriteLine($"WARNING: TEXTURE {id.ToString("X08")} aka {name} has unsupported texture format {format.ToString()}");
- break;
- }
- dst.Seek(0x80, SeekOrigin.Begin);
- dst.Write(data, 0x00, (int)HighmipSize);
- dst.Flush();
- if (!leaveOpen)
- {
- dst.Close();
- }
- }
- }
-}
\ No newline at end of file
diff --git a/LibLunacy/Legacy/Tie.cs b/LibLunacy/Legacy/Tie.cs
deleted file mode 100644
index 8f32158..0000000
--- a/LibLunacy/Legacy/Tie.cs
+++ /dev/null
@@ -1,163 +0,0 @@
-using System.Numerics;
-
-namespace LibLunacy.Legacy
-{
- public class CTie
- {
- [FileStructure(0x40)]
- public struct TieMesh
- {
- [FileOffset(0x00)] public uint indexIndex;
- [FileOffset(0x04)] public ushort vertexIndex;
- [FileOffset(0x08)] public ushort vertexCount;
- [FileOffset(0x12)] public ushort indexCount;
- [FileOffset(0x28)] public ushort oldShaderIndex;
- [FileOffset(0x2A)] public byte newShaderIndex;
-
- public CShader shader;
- }
- [FileStructure(0x80)]
- public struct Tie
- {
- [FileOffset(0x00), Reference("MetadataCount")] public TieMesh[] meshes;
- [FileOffset(0x0F)] public byte metadataCount;
- [FileOffset(0x14)] public uint vertexBufferStart;
- [FileOffset(0x18)] public uint vertexBufferSize;
- [FileOffset(0x20)] public Vector3 scale;
- [FileOffset(0x64), Reference] public string name; //nullptr on old engine
- [FileOffset(0x68)] public ulong tuid; //0 on old engine
-
- public uint MetadataCount => metadataCount;
- }
-
- public TieMesh[] meshes;
- public string name;
- public IGFile file;
- public ulong id; //Either tuid or index depending on game
- private Vector3 scale;
-
- IGFile geometryFile;
- StreamHelper vertexStream;
- IGFile.SectionHeader vertexSection;
- IGFile.SectionHeader indexSection;
-
- public CTie(IGFile file, AssetLoader al, uint index = 0)
- {
- this.file = file;
- IGFile.SectionHeader section = file.QuerySection(0x3400);
-
- Console.WriteLine($"Loading Tie {(section.offset + index * 0x80).ToString("X08")}");
- file.sh.Seek(section.offset + index * 0x80);
- Tie tie = FileUtils.ReadStructure(file.sh);
-
- meshes = tie.meshes;
- scale = tie.scale;
-
- //Console.WriteLine($"TieMetadata {(section.offset + index * 0x80).ToString("X08")} first mesh shader index: {meshes[0].newShaderIndex}");
-
- if (al.fm.isOld)
- {
- id = section.offset + index * 0x80;
- geometryFile = al.fm.igfiles["vertices.dat"];
- vertexSection = geometryFile.QuerySection(0x9000);
- indexSection = geometryFile.QuerySection(0x9100);
- if (al.fm.debug != null) name = al.fm.debug.GetTiePrototypeName(index).name;
- else name = $"Tie_{index.ToString("X04")}";
- }
- else
- {
- name = tie.name;
- id = tie.tuid;
- geometryFile = file;
- vertexSection = file.QuerySection(0x3000);
- indexSection = file.QuerySection(0x3200);
- }
- geometryFile.sh.Seek(vertexSection.offset + tie.vertexBufferStart);
- vertexStream = new StreamHelper(new MemoryStream(geometryFile.sh.ReadBytes(tie.vertexBufferSize)), file.sh._endianness);
- LoadDependancies(al);
- }
- public void GetBuffers(TieMesh mesh, out uint[] indices, out float[] vPositions, out float[] vTexCoords)
- {
- indices = new uint[mesh.indexCount];
- geometryFile.sh.Seek(indexSection.offset + mesh.indexIndex * 2);
- for (int j = 0; j < mesh.indexCount; j++)
- {
- indices[j] = geometryFile.sh.ReadUInt16();
- }
-
- vPositions = new float[mesh.vertexCount * 3];
- vTexCoords = new float[mesh.vertexCount * 2];
- for (int j = 0; j < mesh.vertexCount; j++)
- {
- vertexStream.Seek((mesh.vertexIndex + j) * 0x14 + 0x00);
- vPositions[j * 3 + 0] = vertexStream.ReadInt16() * scale.X;
- vPositions[j * 3 + 1] = vertexStream.ReadInt16() * scale.Y;
- vPositions[j * 3 + 2] = vertexStream.ReadInt16() * scale.Z;
-
- vertexStream.Seek((mesh.vertexIndex + j) * 0x14 + 0x08);
- vTexCoords[j * 2 + 0] = (float)vertexStream.ReadHalf();
- vTexCoords[j * 2 + 1] = (float)vertexStream.ReadHalf();
- }
-
- }
- private void LoadDependancies(AssetLoader al)
- {
- IGFile.SectionHeader shaderSection;
- if (al.fm.isOld)
- {
- shaderSection = al.fm.igfiles["main.dat"].QuerySection(0x5000);
- }
- else
- {
- shaderSection = file.QuerySection(0x5600);
- }
- for (int i = 0; i < meshes.Length; i++)
- {
- if (al.fm.isOld)
- {
- meshes[i].shader = al.shaders[meshes[i].oldShaderIndex];
- }
- else
- {
- file.sh.Seek(shaderSection.offset + meshes[i].newShaderIndex * 8);
- meshes[i].shader = al.shaders[file.sh.ReadUInt64()];
- }
- }
- }
- public void ExportToObj(string filePath)
- {
- uint maxIndex = 0;
-
- StringBuilder obj = new StringBuilder();
-
- obj.Append($"mtllib unused.mtl\n");
- for (int i = 0; i < meshes.Length; i++)
- {
- obj.Append($"o Mesh_{i}\n");
-
- GetBuffers(meshes[i], out uint[] indices, out float[] vPositions, out float[] vTexCoords);
-
- for (int k = 0; k < meshes[i].vertexCount; k++)
- {
- obj.Append($"v {vPositions[k * 3].ToString("F8")} {vPositions[k * 3 + 1].ToString("F8")} {vPositions[k * 3 + 2].ToString("F8")}\n");
- obj.Append($"vt {vTexCoords[k * 2].ToString("F8")} {vTexCoords[k * 2 + 1].ToString("F8")}\n");
- //obj.Append($"vn {vNormals[k * 3].ToString("F8")} {vNormals[k * 3 + 1].ToString("F8")} {vNormals[k * 3 + 2].ToString("F8")}\n");
- }
-
- obj.Append($"usemtl Shader_{meshes[i].oldShaderIndex}\n");
-
- for (int k = 0; k < meshes[i].indexCount; k += 3)
- {
- string i1 = (indices[k + 0] + maxIndex + 1).ToString();
- string i2 = (indices[k + 1] + maxIndex + 1).ToString();
- string i3 = (indices[k + 2] + maxIndex + 1).ToString();
- //obj.Append($"f {i1}/{i1}/{i1} {i2}/{i2}/{i2} {i3}/{i3}/{i3}\n");
- obj.Append($"f {i1}/{i1} {i2}/{i2} {i3}/{i3}\n");
- }
-
- maxIndex += meshes[i].vertexCount;
- }
- File.WriteAllText(filePath, obj.ToString());
- }
- }
-}
\ No newline at end of file
diff --git a/LibLunacy/Legacy/Zone.cs b/LibLunacy/Legacy/Zone.cs
deleted file mode 100644
index 455110e..0000000
--- a/LibLunacy/Legacy/Zone.cs
+++ /dev/null
@@ -1,354 +0,0 @@
-using System.Numerics;
-using System.Reflection;
-using System.Runtime.Intrinsics.X86;
-
-namespace LibLunacy.Legacy
-{
- public class CZone
- {
- [FileStructure(0x80)]
- public struct TieInstance
- {
- [FileOffset(0x00)] public Matrix4x4 transformation;
- [FileOffset(0x40)] public Vector3 boundingPosition;
- [FileOffset(0x4C)] public float boundingRadius;
- [FileOffset(0x50)] public uint tie; //Offset but used as k key into the assetloader tieInstancesStructArr dictionary on old engine, otherwise ind into tuid array
- }
-
- // That's sorta like an interface
- public interface UFrag
- {
- public Vector3 GetPosition();
- public void SetPosition(Vector3 value);
- public uint GetIndexOffset();
- public uint GetVertexOffset();
- public ushort GetIndexCount();
- public ushort GetVertexCount();
- public ushort GetShaderIndex();
- public Vector4 GetBoundingSphere();
- public float[] GetVertPositions();
- public void SetVertPositions(float[] vpos);
- public float[] GetUVs();
- public void SetUVs(float[] uvs);
- public uint[] GetIndices();
- public void SetIndices(uint[] ind);
- public CShader GetShader();
- public void SetShader(CShader shad);
- }
-
- [FileStructure(0x80)]
- public struct NewUFrag : UFrag
- {
- [FileOffset(0x30)] public Vector3 position;
- [FileOffset(0x30)] public Vector4 boundingSphere;
- [FileOffset(0x40)] public uint indexOffset;
- [FileOffset(0x44)] public uint vertexOffset;
- [FileOffset(0x48)] public ushort indexCount;
- [FileOffset(0x4A)] public ushort vertexCount;
- [FileOffset(0x50)] public ushort shaderIndex;
- public float[] vPositions;
- public float[] vTexCoords;
- public uint[] indices;
- public CShader shader;
-
- public readonly ushort GetIndexCount() => indexCount;
- public readonly uint GetIndexOffset() => indexOffset;
- public readonly uint[] GetIndices() => indices;
- public readonly Vector3 GetPosition() => position;
- public readonly Vector4 GetBoundingSphere() => boundingSphere;
- public readonly CShader GetShader() => shader;
- public readonly ushort GetShaderIndex() => shaderIndex;
- public readonly float[] GetUVs() => vTexCoords;
- public readonly ushort GetVertexCount() => vertexCount;
- public readonly uint GetVertexOffset() => vertexOffset;
- public readonly float[] GetVertPositions() => vPositions;
-
- public void SetIndices(uint[] ind) => indices = ind;
- public void SetShader(CShader shad) => shader = shad;
- public void SetUVs(float[] uvs) => vTexCoords = uvs;
- public void SetVertPositions(float[] vpos) => vPositions = vpos;
- public void SetPosition(Vector3 value) => position = value;
- }
-
- [FileStructure(0x80)]
- public struct OldUFrag : UFrag
- {
- [FileOffset(0x40)] public uint indexOffset;
- [FileOffset(0x44)] public uint vertexOffset;
- [FileOffset(0x48)] public ushort indexCount;
- [FileOffset(0x4A)] public ushort vertexCount;
- [FileOffset(0x50)] public ushort shaderIndex;
- [FileOffset(0x60)] public Vector3 position;
- [FileOffset(0x60)] public Vector4 boundingSphere;
- [FileOffset(0x10)] public Vector4 rotation; // Further inspection needed.
- public float[] vPositions;
- public float[] vTexCoords;
- public uint[] indices;
- public CShader shader;
-
- public readonly ushort GetIndexCount() => indexCount;
- public readonly uint GetIndexOffset() => indexOffset;
- public readonly uint[] GetIndices() => indices;
- public readonly Vector3 GetPosition() => position;
- public readonly Vector4 GetBoundingSphere() => boundingSphere;
- public readonly CShader GetShader() => shader;
- public readonly ushort GetShaderIndex() => shaderIndex;
- public readonly float[] GetUVs() => vTexCoords;
- public readonly ushort GetVertexCount() => vertexCount;
- public readonly uint GetVertexOffset() => vertexOffset;
- public readonly float[] GetVertPositions() => vPositions;
-
- public void SetIndices(uint[] ind) => indices = ind;
- public void SetShader(CShader shad) => shader = shad;
- public void SetUVs(float[] uvs) => vTexCoords = uvs;
- public void SetVertPositions(float[] vpos) => vPositions = vpos;
- public void SetPosition(Vector3 value) => position = value;
- }
-
- [FileStructure(0x18)]
- public struct UFragVertex
- {
- [FileOffset(0x00)] public short x;
- [FileOffset(0x02)] public short y;
- [FileOffset(0x04)] public short z;
- [FileOffset(0x06)] public ushort unkConst; // Not an interesting thing afaik
- [FileOffset(0x08)] public Half UVx;
- [FileOffset(0x0A)] public Half UVy;
- [FileOffset(0x0C)] public Half UV2x;
- [FileOffset(0x0E)] public Half UV2y;
- [FileOffset(0x10)] public uint normal;
- [FileOffset(0x14)] public uint tangent;
- }
-
- [FileStructure(0x18)]
- public struct OldUFragVertex
- {
- [FileOffset(0x00)] public short x;
- [FileOffset(0x02)] public short y;
- [FileOffset(0x04)] public short z;
- [FileOffset(0x06)] public short divider;
- [FileOffset(0x08)] public Half UVx;
- [FileOffset(0x0A)] public Half UVy;
- [FileOffset(0x0C)] public Half UV2x;
- [FileOffset(0x0E)] public Half UV2y;
- [FileOffset(0x10)] public uint normal;
- [FileOffset(0x14)] public uint tangent;
- }
-
- ///
- /// For debug purposes.
- ///
- /// Any object
- /// An accurate string representation of the object
- public static string ToString(object obj)
- {
- var fields = obj.GetType().GetFields();
- var sb = new StringBuilder();
- sb.AppendLine($"{obj.GetType().Name} {{");
- foreach (var field in fields)
- {
- var val = field.GetValue(obj);
- sb.AppendLine($"\t{field.FieldType.Name} {field.Name}: {val};");
- }
- sb.AppendLine("}");
- return sb.ToString();
- }
-
-
- public int index;
- public Dictionary tieInstances = new Dictionary();
- public UFrag[] ufrags;
- public string name;
-
- public class CTieInstance
- {
- public string name = string.Empty;
- public Matrix4x4 transformation;
- public CTie tie;
- public Vector3 boundingPosition;
- public float boundingRadius;
- public CTieInstance(TieInstance instance, AssetLoader al, IGFile file)
- {
- transformation = instance.transformation;
- if (al.fm.isOld)
- {
- tie = al.ties[instance.tie];
- }
- else
- {
- file.sh.Seek(file.QuerySection(0x7200).offset + 0x08 * instance.tie);
-
- tie = al.ties[file.sh.ReadUInt64()];
- }
- boundingPosition = instance.boundingPosition;
- boundingRadius = instance.boundingRadius;
- }
- }
-
- public CZone(IGFile file, AssetLoader al, int ind)
- {
- index = ind;
- Console.WriteLine($"ZONE INDEX: {index}");
- IGFile.SectionHeader tieInstSection;
- AssetLoader.AssetPointer[] newnames = null;
- DebugFile.DebugInstanceName[] oldnames = null;
-
- if (al.fm.isOld)
- {
- tieInstSection = file.QuerySection(0x9240);
- if (al.fm.debug != null)
- {
- oldnames = al.fm.debug.GetTieInstanceNames();
- }
- }
- else
- {
- IGFile.SectionHeader tieNameSection = file.QuerySection(0x72C0);
- file.sh.Seek(tieNameSection.offset);
- Console.WriteLine($"names @ {tieNameSection.offset}");
- newnames = FileUtils.ReadStructureArray(file.sh, tieNameSection.count);
-
- tieInstSection = file.QuerySection(0x7240);
- }
-
- file.sh.Seek(tieInstSection.offset);
- TieInstance[] tieInstancesStructArr = FileUtils.ReadStructureArray(file.sh, tieInstSection.count);
-
- for (int i = 0; i < tieInstancesStructArr.Length; i++)
- {
- tieInstances.Add((ulong)i, new CTieInstance(tieInstancesStructArr[i], al, file));
- if (al.fm.isOld)
- {
- if (al.fm.debug != null) tieInstances.Last().Value.name = oldnames[i].name;
- else tieInstances.Last().Value.name = $"Tie_{i:X}";
- }
- else
- {
- tieInstances.Last().Value.name = file.sh.ReadString(newnames[i].offset);
- }
- }
-
- //ufrags = new NewUFrag[0];
-
- ufrags = Array.Empty();
- //if (al.fm.isOld) return;
- if (!al.fm.isOld || index < 1)
- LoadUFrags(file, al);
- }
-
- private void LoadUFrags(IGFile file, AssetLoader al)
- {
- IGFile.SectionHeader ufragSection;
-
- IGFile geometryFile;
-
- IGFile.SectionHeader vertexSection;
- IGFile.SectionHeader indexSection;
- IGFile.SectionHeader shaderSection;
-
- NewUFrag[] newUfrags;
- OldUFrag[] oldUfrags;
-
- if (al.fm.isOld)
- {
- ufragSection = file.QuerySection(0x6200);
- geometryFile = al.fm.igfiles["vertices.dat"];
- vertexSection = geometryFile.QuerySection(0x9000);
- indexSection = geometryFile.QuerySection(0x9100);
- shaderSection = file.QuerySection(0x71A0);
- }
- else
- {
- ufragSection = file.QuerySection(0x6200);
- geometryFile = file;
- vertexSection = file.QuerySection(0x6000);
- indexSection = file.QuerySection(0x6100);
- shaderSection = file.QuerySection(0x71A0);
- }
-
- file.sh.Seek(shaderSection.offset);
- ulong[] shaders = file.sh.ReadStructArray(shaderSection.count);
-
- ufrags = new UFrag[ufragSection.count];
- file.sh.Seek(ufragSection.offset);
- if (al.fm.isOld)
- {
- oldUfrags = FileUtils.ReadStructureArray(file.sh, ufragSection.count);
- for (int i = 0; i < oldUfrags.Length; i++)
- {
- // Transforming the indexOffset because it's actually a count rather than an offset. Bit weird yeah.
- var oldUFrag = oldUfrags[i];
- oldUFrag.indexOffset *= sizeof(ushort);
- ufrags[i] = oldUFrag;
- }
- }
- else
- {
- newUfrags = FileUtils.ReadStructureArray(file.sh, ufragSection.count);
- for (int i = 0; i < newUfrags.Length; i++)
- {
- ufrags[i] = newUfrags[i];
- }
- }
-
- for (int i = 0; i < ufragSection.count; i++)
- {
- float[] vpos = new float[ufrags[i].GetVertexCount() * 3];
- float[] uvs = new float[ufrags[i].GetVertexCount() * 2];
- uint[] ind = new uint[ufrags[i].GetIndexCount()];
- var shadInd = ufrags[i].GetShaderIndex();
- if (al.fm.isOld)
- {
- ufrags[i].SetShader(al.shaders[shadInd]);
- }
- else
- {
- ufrags[i].SetShader(al.shaders[shaders[shadInd]]);
- }
-
- geometryFile.sh.Seek(vertexSection.offset + ufrags[i].GetVertexOffset());
- if (al.fm.isOld)
- {
- var uFragVertices = FileUtils.ReadStructureArray(geometryFile.sh, ufrags[i].GetVertexCount());
- for (int j = 0; j < ufrags[i].GetVertexCount(); j++)
- {
- var uFragVertex = uFragVertices[j];
- geometryFile.sh.Seek(vertexSection.offset + ufrags[i].GetVertexOffset() + 0x18 * j);
- vpos[j * 3 + 2] = uFragVertex.z;
- vpos[j * 3 + 0] = uFragVertex.x;
- vpos[j * 3 + 1] = uFragVertex.y;
- geometryFile.sh.Seek(0x08, SeekOrigin.Current);
- uvs[j * 2 + 0] = (float)geometryFile.sh.ReadHalf();
- uvs[j * 2 + 1] = (float)geometryFile.sh.ReadHalf();
- }
- }
- else
- {
- var uFragVertices = FileUtils.ReadStructureArray(geometryFile.sh, ufrags[i].GetVertexCount());
- for (int j = 0; j < ufrags[i].GetVertexCount(); j++)
- {
- var uFragVertex = uFragVertices[j];
- geometryFile.sh.Seek(vertexSection.offset + ufrags[i].GetVertexOffset() + 0x18 * j);
- vpos[j * 3 + 0] = uFragVertex.x;
- vpos[j * 3 + 1] = uFragVertex.y;
- vpos[j * 3 + 2] = uFragVertex.z;
- geometryFile.sh.Seek(0x08, SeekOrigin.Current);
- uvs[j * 2 + 0] = (float)geometryFile.sh.ReadHalf();
- uvs[j * 2 + 1] = (float)geometryFile.sh.ReadHalf();
- }
- }
-
-
- geometryFile.sh.Seek(indexSection.offset + ufrags[i].GetIndexOffset());
- for (int j = 0; j < ufrags[i].GetIndexCount(); j++)
- {
- ind[j] = geometryFile.sh.ReadUInt16();
- }
-
- ufrags[i].SetVertPositions(vpos);
- ufrags[i].SetUVs(uvs);
- ufrags[i].SetIndices(ind);
- }
- }
- }
-}
diff --git a/LibLunacy/LibLunacy.csproj b/LibLunacy/LibLunacy.csproj
deleted file mode 100644
index cb69c16..0000000
--- a/LibLunacy/LibLunacy.csproj
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
- net6.0
- 12.0
- enable
- enable
- true
-
-
-
-
-
-
-
diff --git a/Lunacy/AssetManager.cs b/Lunacy/AssetManager.cs
deleted file mode 100644
index 5bf3fbd..0000000
--- a/Lunacy/AssetManager.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-using LibLunacy.Legacy;
-
-namespace Lunacy
-{
- //This class converts the games assets into things that OpenGL can deal with, along with caching them to cut down on memory and loads
- public class AssetManager
- {
- private static readonly Lazy lazy = new Lazy(() => new AssetManager());
- public static AssetManager Singleton => lazy.Value;
-
- public Dictionary mobys = new Dictionary();
- public Dictionary ties = new Dictionary();
- public Dictionary textures = new Dictionary();
-
- public void Initialize(AssetLoader al)
- {
- //TODO: cache materials
- foreach(KeyValuePair ctex in al.textures)
- {
- textures.Add(ctex.Key, new Texture(ctex.Value));
- }
- foreach(KeyValuePair moby in al.mobys)
- {
- mobys.Add(moby.Key, new DrawableListList(moby.Value));
- }
- foreach(KeyValuePair tie in al.ties)
- {
- ties.Add(tie.Key, new DrawableList(tie.Value));
- }
- }
-
- public void ConsolidateMobys()
- {
- foreach(KeyValuePair moby in AssetManager.Singleton.mobys)
- {
- moby.Value.ConsolidateDrawCalls();
- }
- }
- public void ConsolidateTies()
- {
- foreach(KeyValuePair tie in AssetManager.Singleton.ties)
- {
- tie.Value.ConsolidateDrawCalls();
- }
- }
- }
-}
\ No newline at end of file
diff --git a/Lunacy/Camera.cs b/Lunacy/Camera.cs
deleted file mode 100644
index 5b56003..0000000
--- a/Lunacy/Camera.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-namespace Lunacy
-{
- public static class Camera
- {
- public static Transform transform = new Transform();
-
- public static Matrix4 WorldToView
- {
- get
- {
- return Matrix4.CreateTranslation(transform.position) * Matrix4.CreateFromQuaternion(transform.rotation);
- }
- }
- public static Matrix4 ViewToClip;
-
- public static void CreatePerspective(float fov, float aspect)
- {
- ViewToClip = Matrix4.CreatePerspectiveFieldOfView(fov, aspect, 0.1f, 10000f);
- }
- }
-}
\ No newline at end of file
diff --git a/Lunacy/Drawable.cs b/Lunacy/Drawable.cs
deleted file mode 100644
index b6821c0..0000000
--- a/Lunacy/Drawable.cs
+++ /dev/null
@@ -1,263 +0,0 @@
-using LibLunacy.Legacy;
-
-namespace Lunacy
-{
- //Buffers are split up due to how not all vertex attributes are currently known.
- //Creating one single buffer structure where the data is interweaved could lead to issues with excess memory usage for meshes where those extra vertex attributes aren't in use.
- //One example of such is blending, only some MobyObj meshes have this and it would be a waste of memory to store this for ties, zone meshes, and shrubs.
- public class Drawable
- {
- public List transforms = new List();
- int VwBO;
- int VpBO;
- int VtcBO;
- int VAO;
- int EBO;
- int indexCount;
- public Material material { get; private set; }
-
- public Drawable()
- {
- Prepare();
- }
- public Drawable(CMoby moby, CMoby.MobyMesh mesh)
- {
- Prepare();
- moby.GetBuffers(mesh, out uint[] indices, out float[] vPositions, out float[] vTexCoords);
- SetVertexPositions(vPositions);
- SetVertexTexCoords(vTexCoords);
- SetIndices(indices);
- SetMaterial(new Material(moby.shaderDB[mesh.shaderIndex]));
- }
- public Drawable(CTie tie, CTie.TieMesh mesh)
- {
- Prepare();
-
- tie.GetBuffers(mesh, out uint[] indices, out float[] vPositions, out float[] vTexCoords);
-
- SetVertexPositions(vPositions);
- SetVertexTexCoords(vTexCoords);
- SetIndices(indices);
- SetMaterial(new Material(mesh.shader));
- }
- public Drawable(ref CZone.UFrag mesh)
- {
- Prepare();
- SetVertexPositions(mesh.GetVertPositions());
- SetVertexTexCoords(mesh.GetUVs());
- SetIndices(mesh.GetIndices());
- //Texture? tex = (mesh.shader.albedo == null ? null : new Texture(mesh.shader.albedo));
- SetMaterial(new Material(mesh.GetShader()));
- }
-
- public void Prepare()
- {
- VAO = GL.GenVertexArray();
- EBO = GL.GenBuffer();
- }
-
- public void SetIndices(uint[] indices)
- {
- GL.BindVertexArray(VAO);
- GL.BindBuffer(BufferTarget.ElementArrayBuffer, EBO);
- GL.BufferData(BufferTarget.ElementArrayBuffer, indices.Length * sizeof(uint), indices, BufferUsageHint.StaticDraw);
- indexCount = indices.Length;
- }
-
- //It goes:
- // 0: vertex positions (3 floats)
- // 1: vertex tex coords (2 floats)
- public void SetVertexPositions(float[] vpositions)
- {
- VpBO = GL.GenBuffer();
- GL.BindBuffer(BufferTarget.ArrayBuffer, VpBO);
- GL.BufferData(BufferTarget.ArrayBuffer, vpositions.Length * sizeof(float), vpositions, BufferUsageHint.StaticDraw);
-
- GL.BindVertexArray(VAO);
- GL.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, 3 * sizeof(float), 0);
- GL.EnableVertexAttribArray(0);
- }
- public void SetVertexTexCoords(float[] vtexcoords)
- {
- VtcBO = GL.GenBuffer();
- GL.BindBuffer(BufferTarget.ArrayBuffer, VtcBO);
- GL.BufferData(BufferTarget.ArrayBuffer, vtexcoords.Length * sizeof(float), vtexcoords, BufferUsageHint.StaticDraw);
-
- GL.BindVertexArray(VAO);
- GL.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, 2 * sizeof(float), 0);
- GL.EnableVertexAttribArray(1);
- }
- public void SetMaterial(Material mat)
- {
- material = mat;
- }
-
- public void AddDrawCall(Transform transform)
- {
- transforms.Add(transform);
- }
-
- public void ConsolidateDrawCalls()
- {
- Matrix4[] transformMatrices = new Matrix4[transforms.Count];
- for(int i = 0; i < transformMatrices.Length; i++)
- {
- transformMatrices[i] = Matrix4.Transpose(transforms[i].GetLocalToWorldMatrix());
- }
-
- VwBO = GL.GenBuffer();
- GL.BindBuffer(BufferTarget.ArrayBuffer, VwBO);
- GL.BufferData(BufferTarget.ArrayBuffer, transformMatrices.Length * sizeof(float) * 16, transformMatrices, BufferUsageHint.DynamicDraw); //Note: this should be edited in the future so things can be moved
-
- GL.BindVertexArray(VAO);
-
- for(int i = 0; i < 4; i++)
- {
- GL.VertexAttribPointer(4+i, 4, VertexAttribPointerType.Float, false, sizeof(float) * 16, sizeof(float) * 4 * i);
- GL.VertexAttribDivisor(4+i, 1);
- GL.EnableVertexAttribArray(4+i);
- }
- }
-
- public void Draw()
- {
- material.Use();
- material.SetMatrix4x4("worldToClip", Camera.WorldToView * Camera.ViewToClip);
-
- GL.BindVertexArray(VAO);
- GL.DrawElementsInstanced(PrimitiveType.Triangles, indexCount, DrawElementsType.UnsignedInt, IntPtr.Zero, transforms.Count);
- }
-
- public void Draw(Transform transform)
- {
- material.Use();
- material.SetMatrix4x4("world", transform.GetLocalToWorldMatrix() * Camera.WorldToView * Camera.ViewToClip);
-
- GL.BindVertexArray(VAO);
- GL.DrawElements(PrimitiveType.Triangles, indexCount, DrawElementsType.UnsignedInt, IntPtr.Zero);
- }
-
- public void SimpleDraw()
- {
- material.SimpleUse();
- GL.BindVertexArray(VAO);
- GL.DrawElements(PrimitiveType.Triangles, indexCount, DrawElementsType.UnsignedInt, IntPtr.Zero);
- }
-
- public void UpdateTransform(Transform transform)
- {
- int index = transforms.FindIndex(0, transforms.Count, x => x == transform);
-
- GL.BindBuffer(BufferTarget.ArrayBuffer, VwBO);
- Matrix4[] matrix = new Matrix4[1] { Matrix4.Transpose(transform.GetLocalToWorldMatrix()) };
-
- GL.BufferSubData(BufferTarget.ArrayBuffer, (IntPtr)(sizeof(float) * 16 * index), sizeof(float) * 16, matrix);
- }
- }
-
- public class DrawableList : List
- {
- public DrawableList(CMoby moby, CMoby.Bangle bangle)
- {
- this.Capacity = (int)bangle.count;
- for(int i = 0; i < bangle.count; i++)
- {
- this.Add(new Drawable(moby, bangle.meshes[i]));
- }
- }
- public DrawableList(CTie tie)
- {
- this.Capacity = (int)tie.meshes.Length;
- for(int i = 0; i < tie.meshes.Length; i++)
- {
- this.Add(new Drawable(tie, tie.meshes[i]));
- }
- }
- public void AddDrawCall(Transform transform)
- {
- for(int i = 0; i < Count; i++)
- {
- this[i].AddDrawCall(transform);
- }
- }
- public void ConsolidateDrawCalls()
- {
- for(int i = 0; i < Count; i++)
- {
- this[i].ConsolidateDrawCalls();
- }
- }
-
- public void Draw()
- {
- for(int i = 0; i < Count; i++)
- {
- this[i].Draw();
- }
- }
-
- public void Draw(Transform transform)
- {
- for(int i = 0; i < Count; i++)
- {
- this[i].Draw(transform);
- }
- }
-
- public void UpdateTransform(Transform transform)
- {
- for(int i = 0; i < Count; i++)
- {
- this[i].UpdateTransform(transform);
- }
- }
-
- }
-
- public class DrawableListList : List
- {
- public DrawableListList(CMoby moby)
- {
- this.Capacity = (int)moby.bangles.Length;
- for(int i = 0; i < moby.bangles.Length; i++)
- {
- this.Add(new DrawableList(moby, moby.bangles[i]));
- }
- }
- public void AddDrawCall(Transform transform)
- {
- for(int i = 0; i < Count; i++)
- {
- this[i].AddDrawCall(transform);
- }
- }
- public void ConsolidateDrawCalls()
- {
- for(int i = 0; i < Count; i++)
- {
- this[i].ConsolidateDrawCalls();
- }
- }
- public void Draw()
- {
- for(int i = 0; i < Count; i++)
- {
- this[i].Draw();
- }
- }
- public void Draw(Transform transform)
- {
- for(int i = 0; i < Count; i++)
- {
- this[i].Draw(transform);
- }
- }
- public void UpdateTransform(Transform transform)
- {
- for(int i = 0; i < Count; i++)
- {
- this[i].UpdateTransform(transform);
- }
- }
- }
-}
\ No newline at end of file
diff --git a/Lunacy/EntityManager.cs b/Lunacy/EntityManager.cs
deleted file mode 100644
index c4fbcd0..0000000
--- a/Lunacy/EntityManager.cs
+++ /dev/null
@@ -1,249 +0,0 @@
-using System.Linq;
-using LibLunacy.Legacy;
-
-namespace Lunacy
-{
- public class EntityManager
- {
- static Lazy lazy = new Lazy(() => new EntityManager());
-
- public static EntityManager Singleton => lazy.Value;
- public bool loadUfrags = false;
-
- public List regions = new List();
- public List zones = new List();
- public Dictionary> MobyHandles = new Dictionary>();
- public List> TieInstances = new List>();
- public List> TFrags = new List>();
-
- internal List mobys = new List();
-
- internal List transparentDrawables = new List();
- internal List opaqueDrawables = new List();
- public void LoadGameplay(Gameplay gp)
- {
- for(int i = 0; i < gp.regions.Length; i++)
- {
- regions.Add(gp.regions[i]);
- MobyHandles.Add(gp.regions[i].name, new List());
- KeyValuePair[] mobys = gp.regions[i].mobyInstances.ToArray();
- for(ulong j = 0; j < (ulong)mobys.Length; j++)
- {
- MobyHandles[gp.regions[i].name].Add(new Entity(mobys[j].Value));
- }
- for(int j = 0; j < gp.regions[i].zones.Length; j++)
- {
- if(zones.Contains(gp.regions[i].zones[j])) continue;
-
- CZone zone = gp.regions[i].zones[j];
- zones.Add(zone);
-
- TieInstances.Add(new List());
- KeyValuePair[] ties = zone.tieInstances.ToArray();
- for(uint k = 0; k < ties.Length; k++)
- {
- TieInstances.Last().Add(new Entity(ties[k].Value));
- }
- TFrags.Add(new List());
- if(loadUfrags)
- {
- for(uint k = 0; k < gp.regions[i].zones[j].ufrags.Length; k++)
- {
- var ufrag = new Entity(gp.regions[i].zones[j].ufrags[k], (ulong)j, (int)k);
- TFrags.Last().Add(ufrag);
- }
- }
- }
- }
-
- AssetManager.Singleton.ConsolidateMobys();
- AssetManager.Singleton.ConsolidateTies();
-
- ReallocDrawableLists();
-
- /*for(int i = 0; i < gp.zones.Length; i++)
- {
- TFrags.Add(new List());
- if(loadUfrags)
- {
- for(uint j = 0; j < gp.zones[i].ufrags.Length; j++)
- {
- TFrags[i].Add(new Entity(gp.zones[i].ufrags[j]));
- }
- }
- }*/
- }
-
- private void ReallocEntities()
- {
- foreach(KeyValuePair> region in MobyHandles)
- {
- mobys.AddRange(region.Value);
- }
- }
-
- private void ReallocDrawableLists()
- {
- transparentDrawables.Clear();
- opaqueDrawables.Clear();
-
- KeyValuePair[] mobys = AssetManager.Singleton.mobys.ToArray();
- Console.WriteLine($"Reallocating {mobys.Length} mobys");
- for(int i = 0; i < mobys.Length; i++)
- {
- List drawableLists = mobys[i].Value;
- for(int j = 0; j < drawableLists.Count; j++)
- {
- for(int k = 0; k < drawableLists[j].Count; k++)
- {
- if(drawableLists[j][k].material.asset.renderingMode != CShader.RenderingMode.AlphaBlend)
- {
- opaqueDrawables.Add(drawableLists[j][k]);
- }
- else
- {
- transparentDrawables.Add(drawableLists[j][k]);
- }
- }
- }
- }
-
- KeyValuePair[] ties = AssetManager.Singleton.ties.ToArray();
- Console.WriteLine($"Reallocating {ties.Length} ties");
- for(int i = 0; i < ties.Length; i++)
- {
- List drawables = ties[i].Value;
- for(int j = 0; j < drawables.Count; j++)
- {
- if(drawables[j].material.asset.renderingMode != CShader.RenderingMode.AlphaBlend)
- {
- opaqueDrawables.Add(drawables[j]);
- }
- else
- {
- transparentDrawables.Add(drawables[j]);
- }
- }
- }
-
- if(loadUfrags)
- {
- foreach (var z in TFrags)
- {
- Console.WriteLine($"Reallocating {ties.Length} ties");
- foreach(var uf in z)
- {
- var ufragdrawable = uf.drawable as Drawable;
- if(ufragdrawable == null) continue;
- if(ufragdrawable.material.asset.renderingMode != CShader.RenderingMode.AlphaBlend)
- {
- opaqueDrawables.Add(ufragdrawable);
- }
- else
- {
- transparentDrawables.Add(ufragdrawable);
- }
- }
- }
- }
- }
-
- public void RenderOpaque()
- {
- for(int i = 0; i < opaqueDrawables.Count; i++)
- {
- opaqueDrawables[i].Draw();
- }
- }
- public void RenderTransparent()
- {
- for(int i = 0; i < transparentDrawables.Count; i++)
- {
- transparentDrawables[i].Draw();
- }
- }
- }
-
- public class Entity
- {
- public object instance; //Is either a Region.CMobyInstance or a TieInstance depending on if it's a MobyObj or tie repsectively
- public object drawable; //Is either a DrawableListList or a DrawableList depending on if it's a MobyObj or tie respectively
- public int id;
- public string name = string.Empty;
-
- public Transform transform;
-
- //xyz is pos, w is radius
- public Vector4 boundingSphere;
-
- public Entity(Region.CMobyInstance mobyInstance)
- {
- instance = mobyInstance;
- drawable = AssetManager.Singleton.mobys[mobyInstance.moby.id];
- transform = new Transform(
- new Vector3(mobyInstance.position.X, mobyInstance.position.Y, mobyInstance.position.Z),
- new Vector3(mobyInstance.rotation.X, mobyInstance.rotation.Y, mobyInstance.rotation.Z),
- Vector3.One * mobyInstance.scale
- );
- name = mobyInstance.name;
- (drawable as DrawableListList).AddDrawCall(transform);
- boundingSphere = new Vector4(Utils.ToOpenTK(mobyInstance.moby.boundingSpherePosition) + transform.position, mobyInstance.moby.boundingSphereRadius * mobyInstance.scale);
- }
- public Entity(CZone.CTieInstance tieInstance)
- {
- instance = tieInstance;
- drawable = AssetManager.Singleton.ties[tieInstance.tie.id];
- transform = new Transform(tieInstance.transformation.ToOpenTK());
- name = tieInstance.name;
- (drawable as DrawableList).AddDrawCall(transform);
- boundingSphere = new Vector4(Utils.ToOpenTK(tieInstance.boundingPosition), tieInstance.boundingRadius);
- }
- public Entity(CZone.UFrag ufrag, ulong zoneIndex, int ufragIndex)
- {
- instance = ufrag;
- drawable = new Drawable(ref ufrag);
- name = $"UFrag_{zoneIndex}_{ufragIndex}";
- transform = new Transform(ufrag.GetPosition().ToOpenTK() / 0x100, Vector3.Zero, Vector3.One / 0x100);
-
- ((Drawable)drawable).AddDrawCall(transform);
- ((Drawable)drawable).ConsolidateDrawCalls();
- }
-
- public void SetPosition(Vector3 position)
- {
- transform.position = position;
- if(drawable is DrawableListList dll) dll.UpdateTransform(transform);
- else if(drawable is DrawableList dl) dl.UpdateTransform(transform);
- }
- public void SetRotation(Vector3 rotation)
- {
- transform.SetRotation(rotation);
- if(drawable is DrawableListList dll) dll.UpdateTransform(transform);
- else if(drawable is DrawableList dl) dl.UpdateTransform(transform);
- }
- public void SetScale(Vector3 scale)
- {
- transform.scale = scale;
- if(drawable is DrawableListList dll) dll.UpdateTransform(transform);
- else if(drawable is DrawableList dl) dl.UpdateTransform(transform);
- }
- public void Draw()
- {
- if(drawable is DrawableListList dll) dll.Draw();
- else if(drawable is DrawableList dl) dl.Draw();
- else if(drawable is Drawable d) d.Draw(transform);
- }
- public bool IntersectsRay(Vector3 dir, Vector3 position)
- {
- Vector3 m = position - boundingSphere.Xyz;
- float b = Vector3.Dot(m, dir);
- float c = Vector3.Dot(m, m) - boundingSphere.W * boundingSphere.W;
-
- if(c > 0 && b > 0) return false;
-
- float discriminant = b*b - c;
-
- return discriminant >= 0;
- }
- }
-}
\ No newline at end of file
diff --git a/Lunacy/GUI.cs b/Lunacy/GUI.cs
deleted file mode 100644
index c450813..0000000
--- a/Lunacy/GUI.cs
+++ /dev/null
@@ -1,353 +0,0 @@
-using ImGuiNET;
-using System.Collections.Specialized;
-using System.Text.RegularExpressions;
-using Matrix4 = OpenTK.Mathematics.Matrix4;
-using Vector2 = System.Numerics.Vector2;
-using Vector3 = System.Numerics.Vector3;
-using Vector4 = System.Numerics.Vector4;
-
-namespace Lunacy
-{
- public class GUI
- {
- ImGuiController controller;
- Window wnd;
- int RegionsCount { get => EntityManager.Singleton.regions.Count; }
- int ZonesCount { get => EntityManager.Singleton.TFrags.Count; }
- int MobyHandleCount
- {
- get
- {
- var c = 0;
- foreach (var ti in EntityManager.Singleton.MobyHandles)
- c += ti.Value.Count;
- return c;
- }
- }
- int TieInstancesCount
- {
- get
- {
- var c = 0;
- foreach (var ti in EntityManager.Singleton.TieInstances)
- c += ti.Count;
- return c;
- }
- }
- int UFragsCount
- {
- get
- {
- var c = 0;
- foreach (var uf in EntityManager.Singleton.TFrags)
- c += uf.Count;
- return c;
- }
- }
- int ShadersCount;
-
- Entity selectedEntity = null;
-
- bool raycast = false;
-
- public GUI(Window wnd)
- {
- controller = new ImGuiController(wnd.ClientSize.X, wnd.ClientSize.Y);
- this.wnd = wnd;
- }
-
- public void Resize()
- {
- controller.WindowResized(wnd.ClientSize.X, wnd.ClientSize.Y);
- }
-
- public void FrameBegin(double delta)
- {
- controller.Update(wnd, (float)delta);
- }
-
- public void ShowRegionsWindow()
- {
- RenderDockspace();
-
- //ImGui.SetNextWindowViewport(ImGui.GetWindowViewport().PointerID);
- //RenderRegionsExplorer();
- //ImGui.SetNextWindowViewport(ImGui.GetWindowViewport().PointerID);
- //RenderZonesExplorer();
-
- if(true)
- {
- RenderInfoOverlay();
- }
-
- if(selectedEntity != null)
- {
- ShowEntityInfo();
- }
- }
-
- public void Tick()
- {
- if(wnd.KeyboardState.IsKeyPressed(Keys.P)) raycast = !raycast;
-
- if(raycast)
- {
-
- OpenTK.Mathematics.Vector2 mouse = wnd.MouseState.Position;
- OpenTK.Mathematics.Vector3 viewport = new Vector3(
- (2 * mouse.X) / wnd.ClientSize.X - 1,
- 1 - (2 * mouse.Y) / wnd.ClientSize.Y,
- 1
- ).ToOpenTK();
- OpenTK.Mathematics.Vector4 homogeneousClip = new(viewport.X, viewport.Y, -1, 1);
- OpenTK.Mathematics.Vector4 eye = Matrix4.Invert(Matrix4.Transpose(Camera.ViewToClip)) * homogeneousClip;
- eye.Z = -1;
- eye.W = 0;
- OpenTK.Mathematics.Vector3 world = (Matrix4.Invert(Matrix4.Transpose(Camera.WorldToView)) * eye).Xyz;
- world.Normalize();
- string entityNames = string.Empty;
- for(int i = 0; i < EntityManager.Singleton.MobyHandles.Count; i++)
- {
- for(int j = 0; j < EntityManager.Singleton.MobyHandles.ElementAt(i).Value.Count; j++)
- {
- if(EntityManager.Singleton.MobyHandles.ElementAt(i).Value[j].IntersectsRay(world, -Camera.transform.position))
- {
- entityNames += $"{EntityManager.Singleton.MobyHandles.ElementAt(i).Value[j].name}\n";
- }
- }
- }
- for(int i = 0; i < EntityManager.Singleton.TieInstances.Count; i++)
- {
- for(int j = 0; j < EntityManager.Singleton.TieInstances[i].Count; j++)
- {
- if(EntityManager.Singleton.TieInstances[i][j].IntersectsRay(world, -Camera.transform.position))
- {
- entityNames += $"{EntityManager.Singleton.TieInstances[i][j].name}\n";
- }
- }
- }
- ImGui.SetTooltip(entityNames);
- }
- }
-
- public void KeyPress(int c)
- {
- controller.PressChar((char)c);
- }
-
- public void RenderDockspace()
- {
- ImGuiWindowFlags winflags = ImGuiWindowFlags.NoDocking
- | ImGuiWindowFlags.NoTitleBar
- | ImGuiWindowFlags.NoCollapse
- | ImGuiWindowFlags.NoResize
- | ImGuiWindowFlags.NoMove
- | ImGuiWindowFlags.NoBringToFrontOnFocus
- | ImGuiWindowFlags.NoNavFocus;
- ImGui.SetNextWindowViewport(ImGui.GetWindowViewport().ID);
- ImGui.SetNextWindowPos(ImGui.GetMainViewport().WorkPos);
- ImGui.SetNextWindowSize(ImGui.GetMainViewport().WorkSize);
-
- ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, 0);
- ImGui.PushStyleVar(ImGuiStyleVar.WindowBorderSize, 0);
- ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0);
- ImGui.Begin("dockspace", winflags);
-
- uint dockspaceId = ImGui.GetID("dockspace");
- ImGui.DockSpace(dockspaceId, new(0,0), ImGuiDockNodeFlags.None);
- ImGui.DockSpaceOverViewport();
- ImGui.PopStyleVar();
- }
-
- void SearchEntities(in Dictionary> dict, string args, out Dictionary> searchResult)
- {
- searchResult = new();
- foreach (var kvp in dict)
- {
- var catResults = new List();
- foreach(var entity in kvp.Value)
- {
- string name = entity.name;
- string searchRegex = string.Join("|", Regex.Escape(args.ToLower()).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries));
- if(Regex.IsMatch(name.ToLower(), searchRegex))
- {
- catResults.Add(entity);
- }
- }
-
- if (catResults.Count == 0)
- continue;
-
- searchResult.Add(kvp.Key, catResults);
- }
- }
-
- Dictionary> FilteredMobyHandles;
- string mobysSearchArgs = string.Empty;
- bool mobysDictInitialized = false;
- public void RenderRegionsExplorer()
- {
- if(!mobysDictInitialized)
- {
- FilteredMobyHandles = EntityManager.Singleton.MobyHandles;
- mobysDictInitialized = true;
- }
-
- uint dockspaceId = ImGui.GetID("dockspace");
- ImGui.SetNextWindowDockID(dockspaceId, ImGuiCond.Once);
- ImGui.SetNextWindowPos(ImGui.GetMainViewport().GetWorkCenter(), ImGuiCond.FirstUseEver);
- ImGui.Begin("Regions", ImGuiWindowFlags.AlwaysVerticalScrollbar);
- if(ImGui.InputTextWithHint("Search", "blob_small, QWARK_NURSE, etc...", ref mobysSearchArgs, 0xFF, ImGuiInputTextFlags.EnterReturnsTrue))
- {
- if (mobysSearchArgs.Length < 3)
- {
- FilteredMobyHandles = EntityManager.Singleton.MobyHandles;
- }
- else
- {
- SearchEntities(in EntityManager.Singleton.MobyHandles, mobysSearchArgs, out FilteredMobyHandles);
- }
- }
- ImGui.Separator();
- foreach (var mobys in FilteredMobyHandles)
- {
- if (ImGui.CollapsingHeader(mobys.Key))
- {
- for (int i = 0; i < mobys.Value.Count; i++)
- {
- ImGui.PushID($"{mobys.Key}:{i}:{mobys.Value[i].name}");
- if (ImGui.Button(mobys.Value[i].name))
- {
- Camera.transform.position = -mobys.Value[i].transform.position;
- selectedEntity = mobys.Value[i];
- }
- ImGui.PopID();
- }
- }
- }
- ImGui.End();
- }
-
- string tiesSearchArgs = string.Empty;
- readonly Dictionary> TieInstances = new();
- Dictionary> TieInstancesFiltered;
- bool tiesDictInitialized = false;
- public void RenderZonesExplorer()
- {
- if(tiesDictInitialized == false)
- {
- for (int i = 0; i < EntityManager.Singleton.TieInstances.Count; i++)
- {
- var l = new List();
- foreach(var tie in EntityManager.Singleton.TieInstances[i])
- {
- l.Add(tie);
- }
- TieInstances.Add(EntityManager.Singleton.zones[i].name, l);
- }
- tiesDictInitialized = true;
- TieInstancesFiltered = TieInstances;
- }
-
- uint dockspaceId = ImGui.GetID("dockspace");
- ImGui.SetNextWindowDockID(dockspaceId, ImGuiCond.Once);
- ImGui.SetNextWindowPos(ImGui.GetMainViewport().GetWorkCenter(), ImGuiCond.FirstUseEver);
- ImGui.Begin("Zones", ImGuiWindowFlags.AlwaysVerticalScrollbar);
- if(ImGui.InputTextWithHint("Search", "terrain, host, etc...", ref tiesSearchArgs, 0xFF, ImGuiInputTextFlags.EnterReturnsTrue))
- {
- if(tiesSearchArgs.Length < 3)
- {
- TieInstancesFiltered = TieInstances;
- }
- else
- {
- SearchEntities(in TieInstances, tiesSearchArgs, out TieInstancesFiltered);
- }
- }
- ImGui.Separator();
- foreach(var ties in TieInstancesFiltered)
- {
- if (ImGui.CollapsingHeader(ties.Key))
- {
- for(int i = 0; i < ties.Value.Count; i++)
- {
- string tieName = ties.Value[i].name;
-
- ImGui.PushID($"{ties.Key}:{i}:{tieName}");
- if (ImGui.Button(tieName))
- {
- Camera.transform.position = -ties.Value[i].transform.position;
- selectedEntity = ties.Value[i];
- }
- ImGui.PopID();
- }
- }
- }
- ImGui.End();
- }
-
- public void RenderInfoOverlay()
- {
- ImGuiIOPtr io = ImGui.GetIO();
- ImGuiWindowFlags windowFlags = ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoNav;
- float padding = 10f;
- ImGuiViewportPtr viewport = ImGui.GetMainViewport();
- Vector2 work_pos = viewport.WorkPos;
- Vector2 work_size = viewport.WorkSize;
- Vector2 win_pos, win_pos_pivot;
- win_pos.X = work_pos.X + padding;
- win_pos.Y = work_pos.Y + work_size.Y - padding;
- win_pos_pivot.X = 0;
- win_pos_pivot.Y = 1f;
- ImGui.SetNextWindowPos(win_pos, ImGuiCond.Always, win_pos_pivot);
- windowFlags |= ImGuiWindowFlags.NoMove; // Locks the overlay;
-
- ImGui.SetNextWindowBgAlpha(0.4f);
- if (ImGui.Begin("Stats", windowFlags))
- {
- ImGui.Text("Camera info");
- ImGui.Separator();
- ImGui.Text($"Pos: {-Camera.transform.position}");
- ImGui.Text($"Rot: {Camera.transform.eulerRotation}");
- ImGui.Spacing();
- ImGui.Text("Statistics");
- ImGui.Separator();
- ImGui.Text("Framerate: ");
- ImGui.SameLine();
- ImGui.TextColored(Window.framerate > 25 ? new Vector4(0.15f, 1f, 0.15f, 1f) : new Vector4(1f, 0.15f, 0.15f, 1f), $"{Math.Round(Window.framerate)}FPS");
- ImGui.Text($"Regions: {RegionsCount}");
- ImGui.Text($"Zones: {ZonesCount}");
- ImGui.Text($"FilteredMobyHandles: {MobyHandleCount}");
- ImGui.Text($"Ties: {TieInstancesCount}");
- ImGui.Text($"UFrags: {UFragsCount}");
- ImGui.Text($"Shaders: {ShadersCount}");
- ImGui.Text($"Drawables: {EntityManager.Singleton.opaqueDrawables.Count + EntityManager.Singleton.transparentDrawables.Count}");
- }
- ImGui.End();
- }
-
- private void ShowEntityInfo()
- {
- ImGui.Begin($"{selectedEntity.name} Properties");
- bool posChanged = false;
- bool rotChanged = false;
- bool scaleChanged = false;
- System.Numerics.Vector3 position = selectedEntity.transform.position.ToNumerics();
- System.Numerics.Vector3 rotation = (selectedEntity.transform.eulerRotation * (180f / MathHelper.Pi)).ToNumerics();
- System.Numerics.Vector3 scale = selectedEntity.transform.scale.ToNumerics();
- if(ImGui.InputFloat3("position", ref position)) posChanged = true;
- if(ImGui.InputFloat3("Rotation", ref rotation)) rotChanged = true;
- if(ImGui.InputFloat3("Scale", ref scale)) scaleChanged = true;
- if(posChanged) selectedEntity.SetPosition(Utils.ToOpenTK(position));
- if(rotChanged) selectedEntity.SetRotation(Utils.ToOpenTK(rotation / (180f / MathHelper.Pi)));
- if(scaleChanged) selectedEntity.SetScale(Utils.ToOpenTK(scale));
- ImGui.End();
- //ImGui.ShowDemoWindow();
- }
-
- public void FrameEnd()
- {
- controller.Render();
- }
- }
-}
\ No newline at end of file
diff --git a/Lunacy/Globals.cs b/Lunacy/Globals.cs
deleted file mode 100644
index 7cb6185..0000000
--- a/Lunacy/Globals.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-global using OpenTK;
-global using OpenTK.Graphics.OpenGL4;
-global using OpenTK.Windowing.Desktop;
-global using OpenTK.Windowing.Common;
-global using OpenTK.Mathematics;
-global using OpenTK.Windowing.GraphicsLibraryFramework;
-global using LibLunacy;
\ No newline at end of file
diff --git a/Lunacy/ImGuiController.cs b/Lunacy/ImGuiController.cs
deleted file mode 100644
index 0b3db12..0000000
--- a/Lunacy/ImGuiController.cs
+++ /dev/null
@@ -1,557 +0,0 @@
-using ImGuiNET;
-using System;
-using System.Collections.Generic;
-using System.Runtime.CompilerServices;
-using OpenTK.Graphics.OpenGL4;
-using OpenTK.Mathematics;
-using OpenTK.Windowing.Common.Input;
-using OpenTK.Windowing.Desktop;
-using OpenTK.Windowing.GraphicsLibraryFramework;
-using System.Diagnostics;
-using ErrorCode = OpenTK.Graphics.OpenGL4.ErrorCode;
-
-//Credits: https://github.com/NogginBops/ImGui.NET_OpenTK_Sample
-
-namespace Lunacy
-{
- public class ImGuiController : IDisposable
- {
- private bool _frameBegun;
-
- private int _vertexArray;
- private int _vertexBuffer;
- private int _vertexBufferSize;
- private int _indexBuffer;
- private int _indexBufferSize;
-
- //private Texture _fontTexture;
-
- private int _fontTexture;
-
- private int _shader;
- private int _shaderFontTextureLocation;
- private int _shaderProjectionMatrixLocation;
-
- private int _windowWidth;
- private int _windowHeight;
-
- private System.Numerics.Vector2 _scaleFactor = System.Numerics.Vector2.One;
-
- private static bool KHRDebugAvailable = false;
-
- ///
- /// Constructs a new ImGuiController.
- ///
- public ImGuiController(int width, int height)
- {
- _windowWidth = width;
- _windowHeight = height;
-
- int major = GL.GetInteger(GetPName.MajorVersion);
- int minor = GL.GetInteger(GetPName.MinorVersion);
-
- KHRDebugAvailable = (major == 4 && minor >= 3) || IsExtensionSupported("KHR_debug");
-
- IntPtr context = ImGui.CreateContext();
- ImGui.SetCurrentContext(context);
- var io = ImGui.GetIO();
- io.Fonts.AddFontDefault();
-
- io.BackendFlags |= ImGuiBackendFlags.RendererHasVtxOffset;
-
- CreateDeviceResources();
- SetKeyMappings();
-
- SetPerFrameImGuiData(1f / 60f);
-
- ImGui.NewFrame();
- _frameBegun = true;
- }
-
- public void WindowResized(int width, int height)
- {
- _windowWidth = width;
- _windowHeight = height;
- }
-
- public void DestroyDeviceObjects()
- {
- Dispose();
- }
-
- public void CreateDeviceResources()
- {
- _vertexBufferSize = 10000;
- _indexBufferSize = 2000;
-
- int prevVAO = GL.GetInteger(GetPName.VertexArrayBinding);
- int prevArrayBuffer = GL.GetInteger(GetPName.ArrayBufferBinding);
-
- _vertexArray = GL.GenVertexArray();
- GL.BindVertexArray(_vertexArray);
- LabelObject(ObjectLabelIdentifier.VertexArray, _vertexArray, "ImGui");
-
- _vertexBuffer = GL.GenBuffer();
- GL.BindBuffer(BufferTarget.ArrayBuffer, _vertexBuffer);
- LabelObject(ObjectLabelIdentifier.Buffer, _vertexBuffer, "VBO: ImGui");
- GL.BufferData(BufferTarget.ArrayBuffer, _vertexBufferSize, IntPtr.Zero, BufferUsageHint.DynamicDraw);
-
- _indexBuffer = GL.GenBuffer();
- GL.BindBuffer(BufferTarget.ElementArrayBuffer, _indexBuffer);
- LabelObject(ObjectLabelIdentifier.Buffer, _indexBuffer, "EBO: ImGui");
- GL.BufferData(BufferTarget.ElementArrayBuffer, _indexBufferSize, IntPtr.Zero, BufferUsageHint.DynamicDraw);
-
- RecreateFontDeviceTexture();
-
- string VertexSource = @"#version 330 core
-
-uniform mat4 projection_matrix;
-
-layout(location = 0) in vec2 in_position;
-layout(location = 1) in vec2 in_texCoord;
-layout(location = 2) in vec4 in_color;
-
-out vec4 color;
-out vec2 texCoord;
-
-void main()
-{
- gl_Position = projection_matrix * vec4(in_position, 0, 1);
- color = in_color;
- texCoord = in_texCoord;
-}";
- string FragmentSource = @"#version 330 core
-
-uniform sampler2D in_fontTexture;
-
-in vec4 color;
-in vec2 texCoord;
-
-out vec4 outputColor;
-
-void main()
-{
- outputColor = color * texture(in_fontTexture, texCoord);
-}";
-
- _shader = CreateProgram("ImGui", VertexSource, FragmentSource);
- _shaderProjectionMatrixLocation = GL.GetUniformLocation(_shader, "projection_matrix");
- _shaderFontTextureLocation = GL.GetUniformLocation(_shader, "in_fontTexture");
-
- int stride = Unsafe.SizeOf();
- GL.VertexAttribPointer(0, 2, VertexAttribPointerType.Float, false, stride, 0);
- GL.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, stride, 8);
- GL.VertexAttribPointer(2, 4, VertexAttribPointerType.UnsignedByte, true, stride, 16);
-
- GL.EnableVertexAttribArray(0);
- GL.EnableVertexAttribArray(1);
- GL.EnableVertexAttribArray(2);
-
- GL.BindVertexArray(prevVAO);
- GL.BindBuffer(BufferTarget.ArrayBuffer, prevArrayBuffer);
-
- CheckGLError("End of ImGui setup");
- }
-
- ///
- /// Recreates the device texture used to render text.
- ///
- public void RecreateFontDeviceTexture()
- {
- ImGuiIOPtr io = ImGui.GetIO();
- io.Fonts.GetTexDataAsRGBA32(out IntPtr pixels, out int width, out int height, out int bytesPerPixel);
-
- int mips = (int)Math.Floor(Math.Log(Math.Max(width, height), 2));
-
- int prevActiveTexture = GL.GetInteger(GetPName.ActiveTexture);
- GL.ActiveTexture(TextureUnit.Texture0);
- int prevTexture2D = GL.GetInteger(GetPName.TextureBinding2D);
-
- _fontTexture = GL.GenTexture();
- GL.BindTexture(TextureTarget.Texture2D, _fontTexture);
- GL.TexStorage2D(TextureTarget2d.Texture2D, mips, SizedInternalFormat.Rgba8, width, height);
- LabelObject(ObjectLabelIdentifier.Texture, _fontTexture, "ImGui Text Atlas");
-
- GL.TexSubImage2D(TextureTarget.Texture2D, 0, 0, 0, width, height, PixelFormat.Bgra, PixelType.UnsignedByte, pixels);
-
- GL.GenerateMipmap(GenerateMipmapTarget.Texture2D);
-
- GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat);
- GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat);
-
- GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMaxLevel, mips - 1);
-
- GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
- GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
-
- // Restore state
- GL.BindTexture(TextureTarget.Texture2D, prevTexture2D);
- GL.ActiveTexture((TextureUnit)prevActiveTexture);
-
- io.Fonts.SetTexID((IntPtr)_fontTexture);
-
- io.Fonts.ClearTexData();
- }
-
- ///
- /// Renders the ImGui draw list data.
- ///
- public void Render()
- {
- if (_frameBegun)
- {
- _frameBegun = false;
- ImGui.Render();
- RenderImDrawData(ImGui.GetDrawData());
- }
- }
-
- ///
- /// Updates ImGui input and IO configuration state.
- ///
- public void Update(GameWindow wnd, float deltaSeconds)
- {
- if (_frameBegun)
- {
- ImGui.Render();
- }
-
- SetPerFrameImGuiData(deltaSeconds);
- UpdateImGuiInput(wnd);
-
- _frameBegun = true;
- ImGui.NewFrame();
- }
-
- ///
- /// Sets per-frame data based on the associated window.
- /// This is called by Update(float).
- ///
- private void SetPerFrameImGuiData(float deltaSeconds)
- {
- ImGuiIOPtr io = ImGui.GetIO();
- io.DisplaySize = new System.Numerics.Vector2(
- _windowWidth / _scaleFactor.X,
- _windowHeight / _scaleFactor.Y);
- io.DisplayFramebufferScale = _scaleFactor;
- io.DeltaTime = deltaSeconds; // DeltaTime is in seconds.
- }
-
- readonly List PressedChars = new List();
-
- private void UpdateImGuiInput(GameWindow wnd)
- {
- ImGuiIOPtr io = ImGui.GetIO();
-
- MouseState MouseState = wnd.MouseState;
- KeyboardState KeyboardState = wnd.KeyboardState;
-
- io.MouseDown[0] = MouseState[MouseButton.Left];
- io.MouseDown[1] = MouseState[MouseButton.Right];
- io.MouseDown[2] = MouseState[MouseButton.Middle];
-
- var screenPoint = new Vector2i((int)MouseState.X, (int)MouseState.Y);
- var point = screenPoint;//wnd.PointToClient(screenPoint);
- io.MousePos = new System.Numerics.Vector2(point.X, point.Y);
-
- foreach (Keys key in Enum.GetValues(typeof(Keys)))
- {
- if (key == Keys.Unknown)
- {
- continue;
- }
- io.KeysDown[(int)key] = KeyboardState.IsKeyDown(key);
- }
-
- foreach (var c in PressedChars)
- {
- io.AddInputCharacter(c);
- }
- PressedChars.Clear();
-
- io.KeyCtrl = KeyboardState.IsKeyDown(Keys.LeftControl) || KeyboardState.IsKeyDown(Keys.RightControl);
- io.KeyAlt = KeyboardState.IsKeyDown(Keys.LeftAlt) || KeyboardState.IsKeyDown(Keys.RightAlt);
- io.KeyShift = KeyboardState.IsKeyDown(Keys.LeftShift) || KeyboardState.IsKeyDown(Keys.RightShift);
- io.KeySuper = KeyboardState.IsKeyDown(Keys.LeftSuper) || KeyboardState.IsKeyDown(Keys.RightSuper);
- }
-
- internal void PressChar(char keyChar)
- {
- PressedChars.Add(keyChar);
- }
-
- internal void MouseScroll(Vector2 offset)
- {
- ImGuiIOPtr io = ImGui.GetIO();
-
- io.MouseWheel = offset.Y;
- io.MouseWheelH = offset.X;
- }
-
- private static void SetKeyMappings()
- {
- ImGuiIOPtr io = ImGui.GetIO();
- io.KeyMap[(int)ImGuiKey.Tab] = (int)Keys.Tab;
- io.KeyMap[(int)ImGuiKey.LeftArrow] = (int)Keys.Left;
- io.KeyMap[(int)ImGuiKey.RightArrow] = (int)Keys.Right;
- io.KeyMap[(int)ImGuiKey.UpArrow] = (int)Keys.Up;
- io.KeyMap[(int)ImGuiKey.DownArrow] = (int)Keys.Down;
- io.KeyMap[(int)ImGuiKey.PageUp] = (int)Keys.PageUp;
- io.KeyMap[(int)ImGuiKey.PageDown] = (int)Keys.PageDown;
- io.KeyMap[(int)ImGuiKey.Home] = (int)Keys.Home;
- io.KeyMap[(int)ImGuiKey.End] = (int)Keys.End;
- io.KeyMap[(int)ImGuiKey.Delete] = (int)Keys.Delete;
- io.KeyMap[(int)ImGuiKey.Backspace] = (int)Keys.Backspace;
- io.KeyMap[(int)ImGuiKey.Enter] = (int)Keys.Enter;
- io.KeyMap[(int)ImGuiKey.Escape] = (int)Keys.Escape;
- io.KeyMap[(int)ImGuiKey.A] = (int)Keys.A;
- io.KeyMap[(int)ImGuiKey.C] = (int)Keys.C;
- io.KeyMap[(int)ImGuiKey.V] = (int)Keys.V;
- io.KeyMap[(int)ImGuiKey.X] = (int)Keys.X;
- io.KeyMap[(int)ImGuiKey.Y] = (int)Keys.Y;
- io.KeyMap[(int)ImGuiKey.Z] = (int)Keys.Z;
- }
-
- private void RenderImDrawData(ImDrawDataPtr draw_data)
- {
- if (draw_data.CmdListsCount == 0)
- {
- return;
- }
-
- // Get intial state.
- int prevVAO = GL.GetInteger(GetPName.VertexArrayBinding);
- int prevArrayBuffer = GL.GetInteger(GetPName.ArrayBufferBinding);
- int prevProgram = GL.GetInteger(GetPName.CurrentProgram);
- bool prevBlendEnabled = GL.GetBoolean(GetPName.Blend);
- bool prevScissorTestEnabled = GL.GetBoolean(GetPName.ScissorTest);
- int prevBlendEquationRgb = GL.GetInteger(GetPName.BlendEquationRgb);
- int prevBlendEquationAlpha = GL.GetInteger(GetPName.BlendEquationAlpha);
- int prevBlendFuncSrcRgb = GL.GetInteger(GetPName.BlendSrcRgb);
- int prevBlendFuncSrcAlpha = GL.GetInteger(GetPName.BlendSrcAlpha);
- int prevBlendFuncDstRgb = GL.GetInteger(GetPName.BlendDstRgb);
- int prevBlendFuncDstAlpha = GL.GetInteger(GetPName.BlendDstAlpha);
- bool prevCullFaceEnabled = GL.GetBoolean(GetPName.CullFace);
- bool prevDepthTestEnabled = GL.GetBoolean(GetPName.DepthTest);
- int prevActiveTexture = GL.GetInteger(GetPName.ActiveTexture);
- GL.ActiveTexture(TextureUnit.Texture0);
- int prevTexture2D = GL.GetInteger(GetPName.TextureBinding2D);
- Span prevScissorBox = stackalloc int[4];
- unsafe
- {
- fixed (int* iptr = &prevScissorBox[0])
- {
- GL.GetInteger(GetPName.ScissorBox, iptr);
- }
- }
-
- // Bind the element buffer (thru the VAO) so that we can resize it.
- GL.BindVertexArray(_vertexArray);
- // Bind the vertex buffer so that we can resize it.
- GL.BindBuffer(BufferTarget.ArrayBuffer, _vertexBuffer);
- for (int i = 0; i < draw_data.CmdListsCount; i++)
- {
- ImDrawListPtr cmd_list = draw_data.CmdListsRange[i];
-
- int vertexSize = cmd_list.VtxBuffer.Size * Unsafe.SizeOf();
- if (vertexSize > _vertexBufferSize)
- {
- int newSize = (int)Math.Max(_vertexBufferSize * 1.5f, vertexSize);
-
- GL.BufferData(BufferTarget.ArrayBuffer, newSize, IntPtr.Zero, BufferUsageHint.DynamicDraw);
- _vertexBufferSize = newSize;
-
- Console.WriteLine($"Resized dear imgui vertex buffer to new size {_vertexBufferSize}");
- }
-
- int indexSize = cmd_list.IdxBuffer.Size * sizeof(ushort);
- if (indexSize > _indexBufferSize)
- {
- int newSize = (int)Math.Max(_indexBufferSize * 1.5f, indexSize);
- GL.BufferData(BufferTarget.ElementArrayBuffer, newSize, IntPtr.Zero, BufferUsageHint.DynamicDraw);
- _indexBufferSize = newSize;
-
- Console.WriteLine($"Resized dear imgui index buffer to new size {_indexBufferSize}");
- }
- }
-
- // Setup orthographic projection matrix into our constant buffer
- ImGuiIOPtr io = ImGui.GetIO();
- Matrix4 mvp = Matrix4.CreateOrthographicOffCenter(
- 0.0f,
- io.DisplaySize.X,
- io.DisplaySize.Y,
- 0.0f,
- -1.0f,
- 1.0f);
-
- GL.UseProgram(_shader);
- GL.UniformMatrix4(_shaderProjectionMatrixLocation, false, ref mvp);
- GL.Uniform1(_shaderFontTextureLocation, 0);
- CheckGLError("Projection");
-
- GL.BindVertexArray(_vertexArray);
- CheckGLError("VAO");
-
- draw_data.ScaleClipRects(io.DisplayFramebufferScale);
-
- GL.Enable(EnableCap.Blend);
- GL.Enable(EnableCap.ScissorTest);
- GL.BlendEquation(BlendEquationMode.FuncAdd);
- GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
- GL.Disable(EnableCap.CullFace);
- GL.Disable(EnableCap.DepthTest);
-
- // Render command lists
- for (int n = 0; n < draw_data.CmdListsCount; n++)
- {
- ImDrawListPtr cmd_list = draw_data.CmdListsRange[n];
-
- GL.BufferSubData(BufferTarget.ArrayBuffer, IntPtr.Zero, cmd_list.VtxBuffer.Size * Unsafe.SizeOf(), cmd_list.VtxBuffer.Data);
- CheckGLError($"Data Vert {n}");
-
- GL.BufferSubData(BufferTarget.ElementArrayBuffer, IntPtr.Zero, cmd_list.IdxBuffer.Size * sizeof(ushort), cmd_list.IdxBuffer.Data);
- CheckGLError($"Data Idx {n}");
-
- for (int cmd_i = 0; cmd_i < cmd_list.CmdBuffer.Size; cmd_i++)
- {
- ImDrawCmdPtr pcmd = cmd_list.CmdBuffer[cmd_i];
- if (pcmd.UserCallback != IntPtr.Zero)
- {
- throw new NotImplementedException();
- }
- else
- {
- GL.ActiveTexture(TextureUnit.Texture0);
- GL.BindTexture(TextureTarget.Texture2D, (int)pcmd.TextureId);
- CheckGLError("Texture");
-
- // We do _windowHeight - (int)clip.W instead of (int)clip.Y because gl has flipped Y when it comes to these coordinates
- var clip = pcmd.ClipRect;
- GL.Scissor((int)clip.X, _windowHeight - (int)clip.W, (int)(clip.Z - clip.X), (int)(clip.W - clip.Y));
- CheckGLError("Scissor");
-
- if ((io.BackendFlags & ImGuiBackendFlags.RendererHasVtxOffset) != 0)
- {
- GL.DrawElementsBaseVertex(PrimitiveType.Triangles, (int)pcmd.ElemCount, DrawElementsType.UnsignedShort, (IntPtr)(pcmd.IdxOffset * sizeof(ushort)), unchecked((int)pcmd.VtxOffset));
- }
- else
- {
- GL.DrawElements(BeginMode.Triangles, (int)pcmd.ElemCount, DrawElementsType.UnsignedShort, (int)pcmd.IdxOffset * sizeof(ushort));
- }
- CheckGLError("Draw");
- }
- }
- }
-
- GL.Disable(EnableCap.Blend);
- GL.Disable(EnableCap.ScissorTest);
-
- // Reset state
- GL.BindTexture(TextureTarget.Texture2D, prevTexture2D);
- GL.ActiveTexture((TextureUnit)prevActiveTexture);
- GL.UseProgram(prevProgram);
- GL.BindVertexArray(prevVAO);
- GL.Scissor(prevScissorBox[0], prevScissorBox[1], prevScissorBox[2], prevScissorBox[3]);
- GL.BindBuffer(BufferTarget.ArrayBuffer, prevArrayBuffer);
- GL.BlendEquationSeparate((BlendEquationMode)prevBlendEquationRgb, (BlendEquationMode)prevBlendEquationAlpha);
- GL.BlendFuncSeparate(
- (BlendingFactorSrc)prevBlendFuncSrcRgb,
- (BlendingFactorDest)prevBlendFuncDstRgb,
- (BlendingFactorSrc)prevBlendFuncSrcAlpha,
- (BlendingFactorDest)prevBlendFuncDstAlpha);
- if (prevBlendEnabled) GL.Enable(EnableCap.Blend); else GL.Disable(EnableCap.Blend);
- if (prevDepthTestEnabled) GL.Enable(EnableCap.DepthTest); else GL.Disable(EnableCap.DepthTest);
- if (prevCullFaceEnabled) GL.Enable(EnableCap.CullFace); else GL.Disable(EnableCap.CullFace);
- if (prevScissorTestEnabled) GL.Enable(EnableCap.ScissorTest); else GL.Disable(EnableCap.ScissorTest);
- }
-
- ///
- /// Frees all graphics resources used by the renderer.
- ///
- public void Dispose()
- {
- GL.DeleteVertexArray(_vertexArray);
- GL.DeleteBuffer(_vertexBuffer);
- GL.DeleteBuffer(_indexBuffer);
-
- GL.DeleteTexture(_fontTexture);
- GL.DeleteProgram(_shader);
- }
-
- public static void LabelObject(ObjectLabelIdentifier objLabelIdent, int glObject, string name)
- {
- if (KHRDebugAvailable)
- GL.ObjectLabel(objLabelIdent, glObject, name.Length, name);
- }
-
- static bool IsExtensionSupported(string name)
- {
- int n = GL.GetInteger(GetPName.NumExtensions);
- for (int i = 0; i < n; i++)
- {
- string extension = GL.GetString(StringNameIndexed.Extensions, i);
- if (extension == name) return true;
- }
-
- return false;
- }
-
- public static int CreateProgram(string name, string vertexSource, string fragmentSoruce)
- {
- int program = GL.CreateProgram();
- LabelObject(ObjectLabelIdentifier.Program, program, $"Program: {name}");
-
- int vertex = CompileShader(name, ShaderType.VertexShader, vertexSource);
- int fragment = CompileShader(name, ShaderType.FragmentShader, fragmentSoruce);
-
- GL.AttachShader(program, vertex);
- GL.AttachShader(program, fragment);
-
- GL.LinkProgram(program);
-
- GL.GetProgram(program, GetProgramParameterName.LinkStatus, out int success);
- if (success == 0)
- {
- string info = GL.GetProgramInfoLog(program);
- Debug.WriteLine($"GL.LinkProgram had info log [{name}]:\n{info}");
- }
-
- GL.DetachShader(program, vertex);
- GL.DetachShader(program, fragment);
-
- GL.DeleteShader(vertex);
- GL.DeleteShader(fragment);
-
- return program;
- }
-
- private static int CompileShader(string name, ShaderType type, string source)
- {
- int shader = GL.CreateShader(type);
- LabelObject(ObjectLabelIdentifier.Shader, shader, $"Shader: {name}");
-
- GL.ShaderSource(shader, source);
- GL.CompileShader(shader);
-
- GL.GetShader(shader, ShaderParameter.CompileStatus, out int success);
- if (success == 0)
- {
- string info = GL.GetShaderInfoLog(shader);
- Debug.WriteLine($"GL.CompileShader for shader '{name}' [{type}] had info log:\n{info}");
- }
-
- return shader;
- }
-
- public static void CheckGLError(string title)
- {
- ErrorCode error;
- int i = 1;
- while ((error = GL.GetError()) != ErrorCode.NoError)
- {
- Debug.Print($"{title} ({i++}): {error}");
- }
- }
- }
-}
\ No newline at end of file
diff --git a/Lunacy/Lunacy.csproj b/Lunacy/Lunacy.csproj
deleted file mode 100644
index 0e6dccd..0000000
--- a/Lunacy/Lunacy.csproj
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
-
-
-
-
- PreserveNewest
-
-
-
-
- Exe
- net6.0
- enable
- enable
- true
-
-
diff --git a/Lunacy/Material.cs b/Lunacy/Material.cs
deleted file mode 100644
index ba3abb5..0000000
--- a/Lunacy/Material.cs
+++ /dev/null
@@ -1,97 +0,0 @@
-using LibLunacy.Legacy;
-
-namespace Lunacy
-{
- public class Material
- {
- public int programId;
- Texture? albedo;
- public PrimitiveType drawType;
- public uint numUsing = 0;
- public CShader.RenderingMode renderingMode = CShader.RenderingMode.Opaque;
- public CShader asset;
-
- Dictionary uniforms = new Dictionary();
-
- public bool HasTransparency
- {
- get
- {
- if(albedo == null) return false;
- return albedo.format == CTexture.TexFormat.DXT3 || albedo.format == CTexture.TexFormat.DXT5 || albedo.format == CTexture.TexFormat.A8R8G8B8;
- }
- }
-
- public Material(int handle, Texture? albedo = null, CShader.RenderingMode renderingMode = CShader.RenderingMode.Opaque, PrimitiveType primitiveType = PrimitiveType.Triangles)
- {
- this.albedo = albedo;
- this.programId = handle;
- this.drawType = primitiveType;
- this.renderingMode = renderingMode;
- }
- public Material(CShader asset)
- {
- this.asset = asset;
- Texture? tex = (asset.albedo == null ? null : AssetManager.Singleton.textures[asset.albedo.id]);
- if(tex == null && asset.albedo != null) Console.Error.WriteLine($"WARNING: FAILED TO FIND TEXTURE {asset.albedo.id.ToString("X08")} AKA {asset.albedo.name}");
- if(asset.renderingMode != CShader.RenderingMode.AlphaBlend)
- {
- this.programId = MaterialManager.materials["stdv;solidf"];
- }
- else
- {
- this.programId = MaterialManager.materials["stdv;transparentf"];
- }
- this.albedo = tex;
- this.drawType = PrimitiveType.Triangles;
- }
-
- public void Use()
- {
- SimpleUse();
- if(albedo != null)
- {
- albedo.Use();
- SetInt("albedo", 0);
- SetBool("useTexture", true);
- if(asset.renderingMode == CShader.RenderingMode.AlphaClip)
- {
- SetFloat("alphaClip", asset.alphaClip);
- }
- else
- {
- SetFloat("alphaClip", 0);
- }
- }
- else
- {
- SetBool("useTexture", false);
- }
- }
- public void SimpleUse()
- {
- GL.UseProgram(programId);
- }
-
- public void SetMatrix4x4(string name, Matrix4 data) => GL.UniformMatrix4(GetUniformLocation(name), true, ref data);
-
- public void SetBool(string name, bool data) => SetInt(name, data ? 1 : 0);
-
- public void SetFloat(string name, float data) => GL.Uniform1(GetUniformLocation(name), data);
- public void SetInt(string name, int data) => GL.Uniform1(GetUniformLocation(name), data);
-
- private int GetUniformLocation(string name)
- {
- if(!uniforms.ContainsKey(name))
- {
- uniforms.Add(name, GL.GetUniformLocation(programId, name));
- }
- return uniforms[name];
- }
-
- public void Dispose()
- {
- GL.DeleteProgram(programId);
- }
- }
-}
\ No newline at end of file
diff --git a/Lunacy/MaterialManager.cs b/Lunacy/MaterialManager.cs
deleted file mode 100644
index 182a946..0000000
--- a/Lunacy/MaterialManager.cs
+++ /dev/null
@@ -1,66 +0,0 @@
-using System.Reflection;
-
-namespace Lunacy
-{
- public static class MaterialManager
- {
- public static Dictionary materials = new Dictionary();
-
- public static int LoadMaterial(string name, string vertexShaderPath, string fragmentShaderPath)
- {
- if(materials.Any(x => x.Key == name))
- {
- int shaderID = materials.First(x => x.Key == name).Value;
- return shaderID;
- }
- string vertexSource = File.ReadAllText(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + "/" + vertexShaderPath);
- string fragmentSource = File.ReadAllText(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + "/" + fragmentShaderPath);
-
- int vertexProgramId = GL.CreateShader(ShaderType.VertexShader);
- int fragmentProgramId = GL.CreateShader(ShaderType.FragmentShader);
-
- GL.ShaderSource(vertexProgramId, vertexSource);
- GL.CompileShader(vertexProgramId);
-
- GL.ShaderSource(fragmentProgramId, fragmentSource);
- GL.CompileShader(fragmentProgramId);
-
- GL.GetShader(vertexProgramId, ShaderParameter.CompileStatus, out int res);
- if(res != (int)All.True)
- {
- string infoLog = GL.GetShaderInfoLog(vertexProgramId);
- throw new Exception($"Error when compiling vertex shader at {vertexShaderPath}.\nError: {infoLog}");
- }
-
- GL.GetShader(fragmentProgramId, ShaderParameter.CompileStatus, out res);
- if(res != (int)All.True)
- {
- string infoLog = GL.GetShaderInfoLog(fragmentProgramId);
- throw new Exception($"Error when compiling fragment shader at {fragmentShaderPath}.\nError: {infoLog}");
- }
-
- int programId = GL.CreateProgram();
- GL.AttachShader(programId, vertexProgramId);
- GL.AttachShader(programId, fragmentProgramId);
-
- GL.LinkProgram(programId);
-
- GL.GetProgram(programId, GetProgramParameterName.LinkStatus, out res);
- if(res != (int)All.True)
- {
- string infoLog = GL.GetProgramInfoLog(programId);
- throw new Exception($"Error when linking program.\nError Code {GL.GetError()}.\nError Log: {infoLog}");
- }
-
- GL.DetachShader(programId, vertexProgramId);
- GL.DetachShader(programId, fragmentProgramId);
-
- GL.DeleteShader(vertexProgramId);
- GL.DeleteShader(fragmentProgramId);
-
- materials.Add(name, programId);
-
- return programId;
- }
- }
-}
\ No newline at end of file
diff --git a/Lunacy/Program.cs b/Lunacy/Program.cs
deleted file mode 100644
index 2a477dd..0000000
--- a/Lunacy/Program.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-using LibLunacy;
-
-using OpenTK;
-
-namespace Lunacy
-{
- public static class Program
- {
- public static void Main(string[] args)
- {
- Window wnd = new Window(
- new GameWindowSettings()
- {
- IsMultiThreaded = false
- },
- new NativeWindowSettings()
- {
- Size = new Vector2i(1280, 720),
- Title = "Lunacy Level Editor",
- Flags = ContextFlags.ForwardCompatible
- },
- args
- );
-
- wnd.Run();
- }
- }
-}
\ No newline at end of file
diff --git a/Lunacy/Texture.cs b/Lunacy/Texture.cs
deleted file mode 100644
index 312a7c0..0000000
--- a/Lunacy/Texture.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-using LibLunacy.Legacy;
-
-namespace Lunacy
-{
- public class Texture
- {
- public int textureId;
- public CTexture.TexFormat format;
-
- public unsafe Texture(CTexture ctex)
- {
- textureId = GL.GenTexture();
-
- GL.ActiveTexture(TextureUnit.Texture0);
- GL.BindTexture(TextureTarget.Texture2D, textureId);
-
- format = ctex.format;
-
- fixed (byte* b = ctex.data)
- {
- uint offset = 0;
- for(int i = 0; i < ctex.mipmapCount; i++)
- {
- if(format == CTexture.TexFormat.DXT1)
- {
- int size = (Math.Max( 1, ((ctex.width / (int)Math.Pow(2, i))+3)/4) * Math.Max(1, ((ctex.height / (int)Math.Pow(2, i)) +3)/4)) * 8;
- GL.CompressedTexImage2D(TextureTarget.Texture2D, i, InternalFormat.CompressedRgbS3tcDxt1Ext, ctex.width, ctex.height, 0, size, (IntPtr)(b + offset));
- offset += (uint)size;
- }
- else if (format == CTexture.TexFormat.DXT3)
- {
- int size = (Math.Max( 1, ((ctex.width / (int)Math.Pow(2, i))+3)/4) * Math.Max(1, ((ctex.height / (int)Math.Pow(2, i)) +3)/4)) * 16;
- GL.CompressedTexImage2D(TextureTarget.Texture2D, i, InternalFormat.CompressedRgbaS3tcDxt3Ext, ctex.width, ctex.height, 0, size, (IntPtr)(b + offset));
- offset += (uint)size;
- }
- else if (format == CTexture.TexFormat.DXT5)
- {
- int size = (Math.Max( 1, ((ctex.width / (int)Math.Pow(2, i))+3)/4) * Math.Max(1, ((ctex.height / (int)Math.Pow(2, i)) +3)/4)) * 16;
- GL.CompressedTexImage2D(TextureTarget.Texture2D, i, InternalFormat.CompressedRgbaS3tcDxt5Ext, ctex.width, ctex.height, 0, size, (IntPtr)(b + offset));
- offset += (uint)size;
- }
- else if(format == CTexture.TexFormat.A8R8G8B8)
- {
- int size = 4 * ctex.width * ctex.height;
- GL.TexImage2D(TextureTarget.Texture2D, i, PixelInternalFormat.Rgba, ctex.width, ctex.height, 0, PixelFormat.Rgba, PixelType.UnsignedByte, (IntPtr)(b + offset));
- }
- else if(format == CTexture.TexFormat.R5G6B5)
- {
- int size = 2 * ctex.width * ctex.height;
- GL.TexImage2D(TextureTarget.Texture2D, i, PixelInternalFormat.R5G6B5IccSgix, ctex.width, ctex.height, 0, PixelFormat.R5G6B5IccSgix, PixelType.UnsignedShort565, (IntPtr)(b + offset));
- }
- }
- }
-
- GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat);
- GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat);
- GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
- GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.LinearMipmapLinear);
-
- GL.GenerateMipmap(GenerateMipmapTarget.Texture2D);
-
- GL.BindTexture(TextureTarget.Texture2D, 0);
- }
-
- public void Use()
- {
- GL.ActiveTexture(TextureUnit.Texture0);
- GL.BindTexture(TextureTarget.Texture2D, textureId);
- }
- }
-}
\ No newline at end of file
diff --git a/Lunacy/Transform.cs b/Lunacy/Transform.cs
deleted file mode 100644
index 020b4a3..0000000
--- a/Lunacy/Transform.cs
+++ /dev/null
@@ -1,81 +0,0 @@
-namespace Lunacy
-{
- public class Transform
- {
- public Vector3 position = Vector3.Zero;
- public Quaternion rotation { get; private set; }
- public Vector3 eulerRotation { get; private set; }
- public Vector3 scale = Vector3.One;
-
- private Matrix4 modelMatrix;
- public bool useMatrix = false;
-
- public bool updated = false;
-
- public Vector3 Forward
- {
- get
- {
- return Quaternion.Invert(rotation) * Vector3.UnitZ;
- }
- }
-
- public Vector3 Up
- {
- get
- {
- return Quaternion.Invert(rotation) * Vector3.UnitY;
- }
- }
-
- public Vector3 Right
- {
- get
- {
- return Quaternion.Invert(rotation) * Vector3.UnitX;
- }
- }
-
- public Transform()
- {
- position = Vector3.Zero;
- SetRotation(Vector3.Zero);
- scale = Vector3.One;
- }
-
- public Transform(Vector3 position, Vector3 rotation, Vector3 scale)
- {
- this.position = position;
- SetRotation(rotation);
- this.scale = scale;
- }
- public Transform(Matrix4 mat)
- {
- useMatrix = true;
- modelMatrix = mat;
- position = mat.ExtractTranslation();
- scale = mat.ExtractScale();
- Quaternion quatRotation = mat.ExtractRotation();
- quatRotation.ToEulerAngles(out Vector3 tempEulers);
- SetRotation(tempEulers);
- }
-
- public void SetRotation(Quaternion quaternion)
- {
- rotation = quaternion;
- rotation.ToEulerAngles(out Vector3 tempEulers);
- eulerRotation = tempEulers;
- }
- public void SetRotation(Vector3 eulers)
- {
- eulerRotation = eulers;
- rotation = Quaternion.FromAxisAngle(Vector3.UnitZ, eulerRotation.Z) * Quaternion.FromAxisAngle(Vector3.UnitY, eulerRotation.Y) * Quaternion.FromAxisAngle(Vector3.UnitX, eulerRotation.X);
- }
-
- public Matrix4 GetLocalToWorldMatrix()
- {
- if(useMatrix) return modelMatrix;
- return Matrix4.Identity * Matrix4.CreateScale(scale) * Matrix4.CreateFromQuaternion(rotation) * Matrix4.CreateTranslation(position);
- }
- }
-}
\ No newline at end of file
diff --git a/Lunacy/Utils.cs b/Lunacy/Utils.cs
deleted file mode 100644
index 1be57da..0000000
--- a/Lunacy/Utils.cs
+++ /dev/null
@@ -1,99 +0,0 @@
-using System.Numerics;
-using System.Text;
-using Vector3 = System.Numerics.Vector3;
-using Quaternion = System.Numerics.Quaternion;
-
-namespace LibLunacy
-{
- public static class Utils
- {
- public static Vector3 ToNumerics(this in OpenTK.Mathematics.Vector3 input)
- {
- return new Vector3(input.X, input.Y, input.Z);
- }
- public static OpenTK.Mathematics.Vector3 ToOpenTK(this in Vector3 input)
- {
- return new OpenTK.Mathematics.Vector3(input.X, input.Y, input.Z);
- }
-
- public static System.Numerics.Quaternion ToNumerics(this in OpenTK.Mathematics.Quaternion input)
- {
- return new Quaternion(input.X, input.Y, input.Z, input.W);
- }
-
- public static OpenTK.Mathematics.Quaternion ToOpenTK(this in Quaternion input)
- {
- return new OpenTK.Mathematics.Quaternion(input.X, input.Y, input.Z, input.W);
- }
-
- public static Matrix4x4 ToNumerics(this in Matrix4 input)
- {
- return new Matrix4x4(
- input.M11, input.M12, input.M13, input.M14,
- input.M21, input.M22, input.M23, input.M24,
- input.M31, input.M32, input.M33, input.M34,
- input.M41, input.M42, input.M43, input.M44
- );
- }
-
- public static Matrix4 ToOpenTK(this in Matrix4x4 input)
- {
- return new OpenTK.Mathematics.Matrix4(
- input.M11, input.M12, input.M13, input.M14,
- input.M21, input.M22, input.M23, input.M24,
- input.M31, input.M32, input.M33, input.M34,
- input.M41, input.M42, input.M43, input.M44
- );
- }
-
- public static string ToString(in object? obj)
- {
- if (obj is null)
- return "null";
-
- var fields = obj.GetType().GetFields();
- var sb = new StringBuilder();
- sb.AppendLine($"{obj.GetType().Name} {{");
- foreach ( var field in fields )
- {
- var val = field.GetValue(obj);
- sb.AppendLine($"\t{field.FieldType.Name} {field.Name}: {val};");
- }
- sb.AppendLine("}");
- return sb.ToString();
- }
-
- public static void DecomposeMatrix4(this in Matrix4 matrix, out Vector3 pos, out Quaternion rot, out Vector3 scale)
- {
- pos = matrix.ExtractTranslation().ToNumerics();
- rot = matrix.ExtractRotation().ToNumerics();
- scale = matrix.ExtractScale().ToNumerics();
- }
-
- public static int LevenshteinDistance(string s, string t)
- {
- int n = s.Length;
- int m = t.Length;
- int[,] d = new int[n + 1, m + 1];
-
- if (n == 0)
- return m;
- if (m == 0)
- return n;
-
- for (int i = 0; i <= n; d[i, 0] = i++) ;
- for (int j = 0; j <= m; d[0, j] = j++) ;
-
- for (int i = 1; i <= n; i++)
- {
- for (int j = 1; j <= m; j++)
- {
- int cost = (t[j - 1] == s[i - 1]) ? 0 : 1;
- d[i, j] = Math.Min(Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1), d[i - 1, j - 1] + cost);
- }
- }
-
- return d[n, m];
- }
- }
-}
\ No newline at end of file
diff --git a/Lunacy/Window.cs b/Lunacy/Window.cs
deleted file mode 100644
index c706c77..0000000
--- a/Lunacy/Window.cs
+++ /dev/null
@@ -1,290 +0,0 @@
-using LibLunacy.Legacy;
-using LibLunacy.Numerics;
-using System.ComponentModel;
-
-namespace Lunacy;
-
-public class Window : GameWindow
-{
- FileManager fm;
- internal static AssetLoader? al; // handles loading assets from files
- Gameplay gp;
-
- GUI gui;
-
- public Vec2 freecamLocal;
-
- Drawable quad;
- Material composite;
- Material screen;
-
- internal static float framerate;
- int opaqueFbo;
- int transFbo;
- int opaqueTex;
- int depthTex;
- int accumTex;
- int revealTex;
- float[] cClearBuf = new float[4]{0, 0, 0, 1};
- float[] dClearBuf = new float[4]{1, 1, 1, 1};
-
- public Window(GameWindowSettings gws, NativeWindowSettings nws, string[] args) : base(gws, nws)
- {
- LoadFolder(args[0]);
- EntityManager.Singleton.loadUfrags = args.Any(x => x == "--load-ufrags");
- }
-
- public void LoadFolder(string folderPath)
- {
- fm = new FileManager();
- fm.LoadFolder(folderPath);
- al = new AssetLoader(fm);
- var prog = new System.Numerics.Vector2();
- var tot = 0f;
- var stat = "";
- al.LoadAssets(ref prog, ref tot, ref stat);
- }
-
- protected override void OnLoad()
- {
- base.OnLoad();
-
- GL.Enable(EnableCap.DepthTest);
- GL.Enable(EnableCap.Texture2D);
- //GL.Enable(EnableCap.StencilTest);
- //GL.Enable(EnableCap.CullFace);
- //GL.Enable(EnableCap.Blend);
- //GL.BlendFuncSeparate(BlendingFactorSrc.SrcAlpha, BlendingFactorDest.OneMinusSrcAlpha, BlendingFactorSrc.One, BlendingFactorDest.Zero);
-
- MaterialManager.LoadMaterial("stdv;transparentf", "shaders/stdv.glsl", "shaders/transparentf.glsl");
- MaterialManager.LoadMaterial("stdv;solidf", "shaders/stdv.glsl", "shaders/solidf.glsl");
- MaterialManager.LoadMaterial("stdv;whitef", "shaders/stdvsingle.glsl", "shaders/whitef.glsl");
- MaterialManager.LoadMaterial("stdv;volumef", "shaders/stdv.glsl", "shaders/volumef.glsl");
- MaterialManager.LoadMaterial("stdv;pickingf", "shaders/stdv.glsl", "shaders/pickingf.glsl");
- MaterialManager.LoadMaterial("screenv;compositef", "shaders/screenv.glsl", "shaders/compositef.glsl");
- MaterialManager.LoadMaterial("screenv;screenf", "shaders/screenv.glsl", "shaders/screenf.glsl");
-
- opaqueFbo = GL.GenFramebuffer();
- transFbo = GL.GenFramebuffer();
-
- opaqueTex = GL.GenTexture();
- GL.BindTexture(TextureTarget.Texture2D, opaqueTex);
- GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba16f, ClientSize.X, ClientSize.Y, 0, PixelFormat.Rgba, PixelType.HalfFloat, IntPtr.Zero);
- GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
- GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
- GL.BindTexture(TextureTarget.Texture2D, 0);
-
- depthTex = GL.GenTexture();
- GL.BindTexture(TextureTarget.Texture2D, depthTex);
- GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.DepthComponent, ClientSize.X, ClientSize.Y, 0, PixelFormat.DepthComponent, PixelType.Float, IntPtr.Zero);
- GL.BindTexture(TextureTarget.Texture2D, 0);
-
- GL.BindFramebuffer(FramebufferTarget.Framebuffer, opaqueFbo);
- GL.FramebufferTexture2D(FramebufferTarget.Framebuffer, FramebufferAttachment.ColorAttachment0, TextureTarget.Texture2D, opaqueTex, 0);
- GL.FramebufferTexture2D(FramebufferTarget.Framebuffer, FramebufferAttachment.DepthAttachment, TextureTarget.Texture2D, depthTex, 0);
-
- FramebufferErrorCode fbec = GL.CheckFramebufferStatus(FramebufferTarget.Framebuffer);
- if(fbec != FramebufferErrorCode.FramebufferComplete)
- {
- throw new Exception($"opaqueFbo incomplete, error {fbec.ToString()}");
- }
-
- accumTex = GL.GenTexture();
- GL.BindTexture(TextureTarget.Texture2D, accumTex);
- GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba16f, ClientSize.X, ClientSize.Y, 0, PixelFormat.Rgba, PixelType.HalfFloat, IntPtr.Zero);
- GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
- GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
- GL.BindTexture(TextureTarget.Texture2D, 0);
-
- revealTex = GL.GenTexture();
- GL.BindTexture(TextureTarget.Texture2D, revealTex);
- GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.R8, ClientSize.X, ClientSize.Y, 0, PixelFormat.Red, PixelType.Float, IntPtr.Zero);
- GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
- GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
- GL.BindTexture(TextureTarget.Texture2D, 0);
-
- GL.BindFramebuffer(FramebufferTarget.Framebuffer, transFbo);
- GL.FramebufferTexture2D(FramebufferTarget.Framebuffer, FramebufferAttachment.ColorAttachment0, TextureTarget.Texture2D, accumTex, 0);
- GL.FramebufferTexture2D(FramebufferTarget.Framebuffer, FramebufferAttachment.ColorAttachment1, TextureTarget.Texture2D, revealTex, 0);
- GL.FramebufferTexture2D(FramebufferTarget.Framebuffer, FramebufferAttachment.DepthAttachment, TextureTarget.Texture2D, depthTex, 0);
-
- DrawBuffersEnum[] transDrawBuffers = new DrawBuffersEnum[]{ DrawBuffersEnum.ColorAttachment0, DrawBuffersEnum.ColorAttachment1 };
- GL.DrawBuffers(2, transDrawBuffers);
-
- fbec = GL.CheckFramebufferStatus(FramebufferTarget.Framebuffer);
- if(fbec != FramebufferErrorCode.FramebufferComplete)
- {
- throw new Exception($"transFbo incomplete, error {fbec.ToString()}");
- }
-
- GL.BindFramebuffer(FramebufferTarget.Framebuffer, 0);
-
- quad = new Drawable();
- quad.SetVertexPositions(new float[]
- {
- -1, -1, 0,
- -1, 1, 0,
- 1, 1, 0,
- 1, -1, 0,
- });
- quad.SetVertexTexCoords(new float[]
- {
- 0, 0,
- 0, 1,
- 1, 1,
- 1, 0
- });
- quad.SetIndices(new uint[]
- {
- 0, 1, 2,
- 2, 3, 0
- });
- composite = new Material(MaterialManager.materials["screenv;compositef"]);
- screen = new Material(MaterialManager.materials["screenv;screenf"]);
-
- Camera.CreatePerspective(MathHelper.PiOver2, ClientSize.X / (float)ClientSize.Y);
-
- gui = new GUI(this);
-
- AssetManager.Singleton.Initialize(al);
-
- gp = new Gameplay(al);
-
- EntityManager.Singleton.LoadGameplay(gp);
- }
-
- protected override void OnRenderFrame(FrameEventArgs args)
- {
- base.OnRenderFrame(args);
-
- GL.BindFramebuffer(FramebufferTarget.Framebuffer, opaqueFbo);
- GL.ClearColor(0.1f, 0.1f, 0.1f, 0.0f);
- GL.Enable(EnableCap.DepthTest);
- GL.DepthFunc(DepthFunction.Less);
- GL.DepthMask(true);
- GL.Disable(EnableCap.Blend);
- GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
-
- EntityManager.Singleton.RenderOpaque();
-
- GL.BindFramebuffer(FramebufferTarget.Framebuffer, transFbo);
- GL.DepthMask(false);
- GL.Enable(EnableCap.Blend);
- GL.BlendFunc(0, BlendingFactorSrc.One, BlendingFactorDest.One);
- GL.BlendFunc(1, BlendingFactorSrc.Zero, BlendingFactorDest.OneMinusSrcColor);
- GL.BlendEquation(BlendEquationMode.FuncAdd);
- GL.ClearBuffer(ClearBuffer.Color, 0, cClearBuf);
- GL.ClearBuffer(ClearBuffer.Color, 1, dClearBuf);
-
- EntityManager.Singleton.RenderTransparent();
-
- GL.BindFramebuffer(FramebufferTarget.Framebuffer, opaqueFbo);
- GL.DepthFunc(DepthFunction.Always);
- GL.Enable(EnableCap.Blend);
- GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
-
- quad.SetMaterial(composite);
- quad.material.SimpleUse();
- GL.ActiveTexture(TextureUnit.Texture0);
- GL.BindTexture(TextureTarget.Texture2D, accumTex);
- GL.ActiveTexture(TextureUnit.Texture1);
- GL.BindTexture(TextureTarget.Texture2D, revealTex);
- quad.material.SetInt("accum", 0);
- quad.material.SetInt("reveal", 1);
- quad.SimpleDraw();
-
- GL.BindFramebuffer(FramebufferTarget.Framebuffer, 0);
- GL.Disable(EnableCap.DepthTest);
- GL.DepthMask(true);
- GL.Disable(EnableCap.Blend);
- GL.ClearColor(0, 0, 0, 0);
-
- quad.SetMaterial(screen);
- GL.ActiveTexture(TextureUnit.Texture0);
- GL.BindTexture(TextureTarget.Texture2D, opaqueTex);
- quad.material.SetInt("screen", 0);
- quad.SimpleDraw();
-
- gui.FrameBegin(args.Time);
-
- gui.ShowRegionsWindow();
-
- gui.Tick();
-
- gui.FrameEnd();
-
- SwapBuffers();
- }
-
- protected override void OnUpdateFrame(FrameEventArgs args)
- {
- framerate = (float)(1 / args.Time);
-
- if (KeyboardState.IsKeyDown(Keys.Escape)) Close();
-
- float moveSpeed = 5;
- float sensitivity = 0.01f;
-
- if(KeyboardState.IsKeyDown(Keys.LeftShift)) moveSpeed *= 10;
-
- if(KeyboardState.IsKeyDown(Keys.W)) Camera.transform.position += Camera.transform.Forward * (float)args.Time * moveSpeed;
- if(KeyboardState.IsKeyDown(Keys.A)) Camera.transform.position += Camera.transform.Right * (float)args.Time * moveSpeed;
- if(KeyboardState.IsKeyDown(Keys.S)) Camera.transform.position -= Camera.transform.Forward * (float)args.Time * moveSpeed;
- if(KeyboardState.IsKeyDown(Keys.D)) Camera.transform.position -= Camera.transform.Right * (float)args.Time * moveSpeed;
-
- CursorGrabbed = MouseState.IsButtonDown(MouseButton.Right);
-
- if(CursorGrabbed)
- {
- freecamLocal += (Vec2)MouseState.Delta.Yx * sensitivity;
-
- freecamLocal.X = MathHelper.Clamp(freecamLocal.X, -MathHelper.PiOver2 + 0.0001f, MathHelper.PiOver2 - 0.0001f);
-
- Camera.transform.SetRotation(Quat.FromAxisAngle(Vec3.UnitX, freecamLocal.X) * Quat.FromAxisAngle(Vec3.UnitY, freecamLocal.Y));
- }
- else
- {
- CursorGrabbed = false;
- CursorVisible = true;
- }
-
- Title = $"Lunacy Level Editor | {framerate}";
-
- base.OnUpdateFrame(args);
- }
-
- protected override void OnTextInput(TextInputEventArgs e)
- {
- base.OnTextInput(e);
-
- gui.KeyPress(e.Unicode);
- }
-
- protected override void OnResize(ResizeEventArgs e)
- {
- base.OnResize(e);
-
- GL.Viewport(0, 0, ClientSize.X, ClientSize.Y);
- Camera.CreatePerspective(MathHelper.PiOver2, ClientSize.X / (float)ClientSize.Y);
- gui.Resize();
-
- GL.BindTexture(TextureTarget.Texture2D, opaqueTex);
- GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba16f, ClientSize.X, ClientSize.Y, 0, PixelFormat.Rgba, PixelType.HalfFloat, IntPtr.Zero);
-
- GL.BindTexture(TextureTarget.Texture2D, depthTex);
- GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.DepthComponent, ClientSize.X, ClientSize.Y, 0, PixelFormat.DepthComponent, PixelType.Float, IntPtr.Zero);
-
- GL.BindTexture(TextureTarget.Texture2D, accumTex);
- GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba16f, ClientSize.X, ClientSize.Y, 0, PixelFormat.Rgba, PixelType.HalfFloat, IntPtr.Zero);
-
- GL.BindTexture(TextureTarget.Texture2D, revealTex);
- GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.R8, ClientSize.X, ClientSize.Y, 0, PixelFormat.Red, PixelType.Float, IntPtr.Zero);
-
- GL.BindTexture(TextureTarget.Texture2D, 0);
- }
-
- protected override void OnClosing(CancelEventArgs e)
- {
- base.OnClosing(e);
- }
-}
\ No newline at end of file
diff --git a/Lunacy/shaders/compositef.glsl b/Lunacy/shaders/compositef.glsl
deleted file mode 100644
index 7717026..0000000
--- a/Lunacy/shaders/compositef.glsl
+++ /dev/null
@@ -1,47 +0,0 @@
-#version 440 core
-
-layout(location = 0) out vec4 color;
-
-layout(binding = 0) uniform sampler2D accum;
-layout(binding = 1) uniform sampler2D reveal;
-
-in vec2 UVs;
-
-const float EPSILON = 0.00001f;
-
-bool isApproximatelyEqual(float a, float b)
-{
- return abs(a - b) <= (abs(a) < abs(b) ? abs(b) : abs(a)) * EPSILON;
-}
-
-float max3(vec3 v)
-{
- return max(max(v.x, v.y), v.z);
-}
-
-void main()
-{
- // fragment coordination
- ivec2 coords = ivec2(gl_FragCoord.xy);
-
- // fragment revealage
- float revealage = texelFetch(reveal, coords, 0).r;
-
- // save the blending and color texture fetch cost if there is not a transparent fragment
- if (isApproximatelyEqual(revealage, 1.0f))
- discard;
-
- // fragment color
- vec4 accumulation = texelFetch(accum, coords, 0);
-
- // suppress overflow
- if (isinf(max3(abs(accumulation.rgb))))
- accumulation.rgb = vec3(accumulation.a);
-
- // prevent floating point precision bug
- vec3 average_color = accumulation.rgb / max(accumulation.a, EPSILON);
-
- // blend pixels
- color = vec4(average_color, 1.0f - revealage);
- //color = vec4(average_color, revealage);
-}
\ No newline at end of file
diff --git a/Lunacy/shaders/pickingf.glsl b/Lunacy/shaders/pickingf.glsl
deleted file mode 100644
index 8450dc5..0000000
--- a/Lunacy/shaders/pickingf.glsl
+++ /dev/null
@@ -1,18 +0,0 @@
-#version 440 core
-
-out vec4 color;
-
-in vec2 UVs;
-flat in uint iID;
-
-uniform float picking;
-uniform float maxInst;
-uniform float type;
-
-void main()
-{
- //picking is the drawable index
- //iID is the instance id, maxInst is the total number of instances
- //type is 0.05 if it's a moby, 0.1 if it's a tie, tfrags unimplemented
- color = vec4(picking, iID / maxInst, iID / maxInst, 1);
-}
\ No newline at end of file
diff --git a/Lunacy/shaders/screenf.glsl b/Lunacy/shaders/screenf.glsl
deleted file mode 100644
index af8eeee..0000000
--- a/Lunacy/shaders/screenf.glsl
+++ /dev/null
@@ -1,15 +0,0 @@
-#version 440 core
-
-// shader inputs
-in vec2 UVs;
-
-// shader outputs
-layout (location = 0) out vec4 frag;
-
-// screen image
-uniform sampler2D screen;
-
-void main()
-{
- frag = vec4(texture(screen, UVs).rgb, 1.0f);
-}
\ No newline at end of file
diff --git a/Lunacy/shaders/screenv.glsl b/Lunacy/shaders/screenv.glsl
deleted file mode 100644
index 2b7ab88..0000000
--- a/Lunacy/shaders/screenv.glsl
+++ /dev/null
@@ -1,12 +0,0 @@
-#version 440 core
-
-layout(location = 0) in vec3 aPosition;
-layout(location = 1) in vec2 aTexCoord;
-
-out vec2 UVs;
-
-void main()
-{
- UVs = aTexCoord;
- gl_Position = vec4(aPosition, 1.0);
-}
\ No newline at end of file
diff --git a/Lunacy/shaders/solidf.glsl b/Lunacy/shaders/solidf.glsl
deleted file mode 100644
index 02d2250..0000000
--- a/Lunacy/shaders/solidf.glsl
+++ /dev/null
@@ -1,22 +0,0 @@
-#version 440 core
-
-out vec4 color;
-
-in vec2 UVs;
-
-uniform sampler2D albedo;
-uniform bool useTexture;
-uniform float alphaClip;
-
-void main()
-{
- if(useTexture)
- {
- color = texture(albedo, UVs);
- if(color.a < alphaClip) discard;
- }
- else
- {
- color = vec4(1.0, 0.0, 1.0, 1.0);
- }
-}
\ No newline at end of file
diff --git a/Lunacy/shaders/stdv.glsl b/Lunacy/shaders/stdv.glsl
deleted file mode 100644
index 69f6d63..0000000
--- a/Lunacy/shaders/stdv.glsl
+++ /dev/null
@@ -1,17 +0,0 @@
-#version 440 core
-
-layout(location = 0) in vec3 aPosition;
-layout(location = 1) in vec2 aTexCoord;
-layout(location = 4) in mat4 aModel;
-
-out vec2 UVs;
-out uint iID;
-
-uniform mat4 worldToClip;
-
-void main()
-{
- UVs = aTexCoord;
- iID = gl_InstanceID;
- gl_Position = vec4(aPosition, 1.0) * aModel * worldToClip;
-}
\ No newline at end of file
diff --git a/Lunacy/shaders/stdvsingle.glsl b/Lunacy/shaders/stdvsingle.glsl
deleted file mode 100644
index 144b5e0..0000000
--- a/Lunacy/shaders/stdvsingle.glsl
+++ /dev/null
@@ -1,14 +0,0 @@
-#version 440 core
-
-layout(location = 0) in vec3 aPosition;
-layout(location = 1) in vec2 aTexCoord;
-
-out vec2 UVs;
-
-uniform mat4 world;
-
-void main()
-{
- UVs = aTexCoord;
- gl_Position = vec4(aPosition, 1.0) * world;
-}
\ No newline at end of file
diff --git a/Lunacy/shaders/transparentf.glsl b/Lunacy/shaders/transparentf.glsl
deleted file mode 100644
index 5ace50b..0000000
--- a/Lunacy/shaders/transparentf.glsl
+++ /dev/null
@@ -1,26 +0,0 @@
-#version 440 core
-
-layout(location = 0) out vec4 accum;
-layout(location = 1) out float reveal;
-
-in vec2 UVs;
-
-uniform sampler2D albedo;
-uniform bool useTexture;
-
-void main()
-{
- vec4 color;
- if(useTexture)
- {
- color = texture(albedo, UVs);
- }
- else
- {
- color = vec4(1.0, 0.0, 1.0, 1.0);
- }
-
- reveal = color.a;
- float weight = clamp(pow(min(1.0, color.a * 10.0) + 0.01, 3.0) * 1e8 * pow(1.0 - gl_FragCoord.z * 0.9, 3.0), 1e-2, 3e3);
- accum = vec4(color.rgb * color.a, color.a);
-}
\ No newline at end of file
diff --git a/Lunacy/shaders/volumef.glsl b/Lunacy/shaders/volumef.glsl
deleted file mode 100644
index 5b083a8..0000000
--- a/Lunacy/shaders/volumef.glsl
+++ /dev/null
@@ -1,13 +0,0 @@
-#version 440 core
-
-out vec4 color;
-
-in vec2 UVs;
-
-uniform sampler2D albedo;
-uniform bool useTexture;
-
-void main()
-{
- color = vec4(1, 1, 0, 0.5);
-}
\ No newline at end of file
diff --git a/Lunacy/shaders/whitef.glsl b/Lunacy/shaders/whitef.glsl
deleted file mode 100644
index a897278..0000000
--- a/Lunacy/shaders/whitef.glsl
+++ /dev/null
@@ -1,13 +0,0 @@
-#version 440 core
-
-out vec4 color;
-
-in vec2 UVs;
-
-uniform sampler2D albedo;
-uniform bool useTexture;
-
-void main()
-{
- color = vec4(1.0);
-}
\ No newline at end of file
diff --git a/README.md b/README.md
index c806543..10eb329 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,7 @@
-ReLunacy is still in early development, but it comes with very interesting features, modders and speedrunners will really like it.
-
-- Visualize levels
- - Mobys, Ties and UFrags (terrain)
- - Triggers & Volumes
- - Light sources (WIP)
-- Editing (WIP)
-- Export (WIP)
- - Thanks to [@NefariousTechSupport](https://github.com/NefariousTechSupport?tab=repositories), the AssetExtractor tool works for the essential things, but I will soon be implementing a better exportation tool directly in the level editor.
-- Flexible editor
- - Almost everything in the editor can be edited in Editor settings; camera speed, field of view, overlay, stats profiler, renderer...
- - Dockable UI made for people who like to organise their work.
- - Comfortable and user-friendly; the editor is made to be easy to use.
- - (WIP) Customize the rendering by adding your own shaders to the editor.
+ReLunacy is still in early development, but it already comes with a solid set of features, modders and speedrunners will really like it.
+
+- **Level viewing**
+ - Renders Mobys, Ties, UFrags (terrain) and Volumes, each independently toggleable from the `Render` menu.
+ - Experimental backface culling and experimental decal-aware translucent rendering (depth-tested but not depth-written, so decals like moss/vines don't z-fight with the terrain underneath).
+ - Frustum culling with an optional bounding-sphere debug overlay.
+- **Asset browsing & inspection**
+ - **Asset Viewer**: a dedicated 3D preview for individual Mobys/Ties, with a searchable/filterable asset tree, per-bangle visibility toggles, skeleton overlay for skinned Mobys, GPU mesh/bangle picking, a read-only per-vertex data inspector, and one-click "Find Usages" to jump to placed instances in the level.
+ - **Textures Explorer**: browse every loaded texture, isolate individual R/G/B/A channels, inspect format/dimensions, and find which shaders/assets use a given texture.
+ - **Shader Browser**: inspect every parsed material — decoded texture references (Albedo/Normal/Expensive/Detail Map), rendering mode, and a hex dump of still-unidentified metadata for reverse-engineering.
+ - **PSARC Explorer**: browse, search and extract files directly out of `.psarc` archives, without extracting the whole archive first.
+ - **Game Browser**: scans a game install (folder or `.psarc` archives) and lists every level it finds, auto-detecting old-engine (Tools of Destruction, Quest for Booty) vs new-engine (A Crack in Time, Full Frontal Assault, All 4 One, Into the Nexus) titles.
+- **Editing**
+ - Translate/Rotate/Scale gizmo (world or local space, with configurable snapping) for placed Mobys, Ties and Volumes.
+ - Direct numeric Position/Rotation/Scale editing from the Property Inspector.
+ - This is placement editing for the current session/export, not a level format writer yet. There's no "Save Level" that writes changes back into the game's own files.
+- **Export**
+ - Export individual Mobys/Ties, or a whole level, to **glTF** (`.glb`), whole-level export uses true mesh instancing (each unique asset's geometry is stored once, referenced by every placed instance reducing file size) and includes skeletons/skinning for animated Mobys, as well as pre-configured materials for exportation.
+ - Export individual Mobys/Ties to **Wavefront OBJ** (`.obj`) (with materials and textures; no skeleton support).
+ - Export individual textures as PNG or raw pixel data.
+- **Format support**
+ - Both old-engine and new-engine PS3 titles, loaded either from a pre-extracted folder or directly out of `.psarc` archives.
+ - Broad texture format coverage: R8, R5G6B5, A1R5G5B5, A8R8G8B8, DXT1, DXT3, DXT5, BC4, BC5, G8B8, RGBA4 and RGBA16F.
+ - Optional `texstream.dat`/`debug.dat` side-loading for higher-resolution textures and asset/instance names (see [Notes](#notes)).
+- **Flexible editor**
+ - Dockable UI you can rearrange to fit how you work.
+ - Deep Editor Settings: graphics backend (thanks to Bliss framework), VSync/framerate cap, MSAA, camera speed/FOV/sensitivity, gizmo size/snap, stats overlay (FPS, profiler, level/camera info), and more.
+ - Localization-ready (currently ships with English).
+ - Built-in update checker with Stable and Nightly channels.
⚠️ Prerequisites
Here are essential things you need to run **ReLunacy**, without those, the app might be slow or could just not run at all.
-- (Windows) [**.NET 9.0 Desktop Runtime**](https://builds.dotnet.microsoft.com/dotnet/WindowsDesktop/9.0.1/windowsdesktop-runtime-9.0.1-win-x64.exe)
-- (Linux/Mac) [**.NET 9.0 Runtime**](https://dotnet.microsoft.com/fr-fr/download/dotnet/9.0#runtime-9.0.1)
-- A 64bits (x64) OS... I mean... who uses a x32 device in 2025 ?
+- (Windows) [**.NET 10.0 Desktop Runtime**](https://dotnet.microsoft.com/fr-fr/download/dotnet/thank-you/runtime-desktop-10.0.10-windows-x64-installer)
+- (Linux/Mac) [**.NET 10.0 Runtime**](https://dotnet.microsoft.com/fr-fr/download/dotnet/10.0#runtime-10.0.10)
+- A 64bits (x64) OS... I mean... who uses a x32 device in 2026 ?
⌨️ Usage
-In first place, you need to extract the game you want to inspect the level of, with a tool like **PS3GameExtractor** or simply by using **RPCS3**.
+In first place, you need to extract the game you want to inspect the level of, with a tool like **PS3GameExtractor** or simply by using **RPCS3**. This gives you the game's `USRDIR` folder.
-Then, you need a tool to extract the `.psarc` files, **PS3GameExtractor** can do it, but otherwise use [**PSArcTool**](https://github.com/periander/PSArcTool) by Periander.
+From there, you don't need to extract anything else by hand anymore. Open ReLunacy, go to `File > Game Browser`, paste (or browse to) the path to the `USRDIR` folder and click `Scan`. It reads levels straight out of the packed `.psarc` archives (or from an already extracted folder, both work), lists everything it finds, and you just click `Load` next to the level you want.
-Reach the level of your choice inside `/packed/levels//` and extract `level_cached.psarc` and `level_uncached.psarc` with one of the previous tools.
+If you know exactly which level you want and prefer to type a path yourself, `File > Open level` still works too, it takes a path to a level's `main.dat` (old engine) or `assetlookup.dat` (new engine), inside an extracted folder or a `.psarc` archive.
-Finally, go to the extracted files `/packed/levels//built/levels//` and copy the full address, and paste it in the `File > Open File` dialog frame.
-
Controls:
- Keybindings:
- `[W][A][S][D]` / `[Z][Q][S][D]` to move around, depending on your keyboard.
- `[E][Q]` / `[E][A]` to go up and down, depending on your keyboard.
- `[SHIFT]` to move faster (sets move speed to Editor Settings's max speed).
- `[RMB]`+`[Move Mouse]` to look around.
+ - `[MMB]`+`[Move Mouse]` to pan the camera around (drags the orbit point with it).
+ - `[W]` `[E]` `[R]` to switch between the Translation, Rotation and Scale tools.
+ - `[ESC]` to deselect the current object(s).
- Miscellaneous:
- - `File > Open Level` to open a level.
- - `File > Close Level` to close a level.
+ - `File > Game Browser` to scan a game's `USRDIR` and pick a level to load from the list.
+ - `File > Open level` to load a level by typing or pasting its path directly.
+ - `File > Export Level...` to export the whole loaded level to glTF.
+ - `File > Close level` to close the level.
- `Edit > Editor Settings` to open settings of the editor.
- - `Tools > Translation` to select the translation tool (move objects).
- - `Tools > Rotation` to select the rotation tool (rotate objects).
- - `Tools > Scale` to select the scale tool (rescale objects).
+ - `Tools > Translation` / `Rotation` / `Scale` to select the transform tool for the gizmo.
- `Tools > Deselect Object(s)` to deselect all the selected objects.
- `View > Show Overlay` to show the stats overlay (FPS, level stats, camera info...).
- - `View > View 3D` to open or close the 3D View window.
+ - `View > 3D View` to open or close the 3D View window.
+ - `View > Entity Explorer`, `Asset Viewer`, `Textures Explorer`, `Shader Browser`, `Properties Inspector`, `PSArc Explorer` and `Logs` to open the other editor windows.
- `Render > Mobys` to render or not Mobys.
- `Render > Ties` to render or not Ties.
- `Render > UFrags` to render or not UFrags.
- `Render > Volumes` to render or not volumes.
+ - `Render > Bounding Spheres` to show or hide the debug bounding spheres.
- `About > Official Github` to reach this github page.
- `About > Check for update` to check for updates. If nothing pops up, then you're up to date.
- The Stats Overlay can be customized inside `Edit > Editor Settings > Overlay settings`.
@@ -92,34 +133,17 @@ Controls:
- Clone the repo with `git clone https://github.com/VELD-Dev/ReLunacy.git --recursive` (add `-b dev` if you want to use branch dev)
- After cloning, make sure to run `git submodules update --recursive` to update the external dependencies (LibreFios).
-- cd into the directory with the `Lunacy.sln` file
-- Run `dotnet build ReLunacy` (Lunacy probably won't build anymore, I did not try to fix its errors or adapt it to the new LunaLib)
+- cd into the directory with the `ReLunacy.sln` file
+- Run `dotnet build ReLunacy`
## Running
-### ReLunacy
-
-- Run `ReLunacy.exe` by double-clicking it, or execute `./ReLunacy.exe `.
-- From the menu bar, access `File > Load Level` and enter the path to the level folder.
- - The folder is the folder that contains either `main.dat` or `assetlookup.dat` file.
- - If `assetlookup.dat` is there, `highmips.dat` from `level_uncached.psarc` must be included as well.
+- Run `ReLunacy.exe` by double-clicking it, or execute `./ReLunacy.exe `, it will load that level right on startup.
+- Or just run it with no argument and use `File > Game Browser` or `File > Open level` from the menu bar, as described above in Usage.
+ - If you point it at a folder, that folder needs to contain either `main.dat` or `assetlookup.dat`.
+ - If `assetlookup.dat` is there, `highmips.dat` from `level_uncached.psarc` must be included as well, unless you're loading the level's `.psarc` directly, in which case ReLunacy picks up `level_uncached.psarc` on its own.
-### Lunacy [Deprecated]
-
-- Run `Lunacy.exe `, where the folder contains either the `main.dat` file or the `assetlookup.dat` file
-- if `assetlookup.dat` is there, `highmips.dat` from `level_uncached.psarc` must be included as well
-- Controls are as such:
- - `[RMB]`+`[Move]` to look around
- - `[W][A][S][D]` or `[Z][Q][S][D]`to move, `[SHIFT]` to move faster
- - `[P]` shows the names of all objects that the mouse is hovering over
- - Select an object in either the regions or zones windows to teleport to that object
-- Add the command argument `--load-ufrags` to load UFrags in Lunacy Legacy.
-
-### AssetExtractor
-
-* Run `AssetExtractor.exe `, where the folder contains either the `main.dat` file or the `assetlookup.dat` file
-* If `assetlookup.dat` is there, `highmips.dat` from `level_uncached.psarc` must be included as well
-* Assets will be found in the inputted folder
+Lunacy (the old standalone viewer) and AssetExtractor have both been retired. Everything they used to do now lives in ReLunacy itself.
## Notes
@@ -134,3 +158,13 @@ Of course, I haven't been working alone on this, I wrote ReLunacy, but it would
- [**@MilchRatchet**](https://github.com/MilchRatchet): I strongly inspired the global level editor from [Replanetizer](https://github.com/RatchetModding/Replanetizer), of which they are the maintainer, mostly for the Frames systems.
- [**@PredatorCZ**](https://github.com/PredatorCZ) is one of the pioneer of Ratchet & Clank: Future Series reverse engineer, it makes sense that a lot of the code is based on their [InsomniaToolset](https://github.com/PredatorCZ/InsomniaToolset).
- **@Nooga** is the artist that made ReLunacy's Logo
+- [**@Neigy**](https://github.com/Neigy) Made huge reverse engineering progresses especially regarding textures, volumes and triggers. Also helped a lot for ReLunacy's ultimate rewrite.
+
+## Contributing
+
+Contributions are welcome but they must follow a few rules !
+1. Follow the repository codestyle
+2. Make pull requests with small features !
+3. Rewrites, "19k+ edited lines" and such pull requests will be **INSTANTLY REJECTED**
+4. If code is written by AI, control it enough to avoid making breaking changes, and **DO NOT MAKE HUGE CHANGES**. Those will be instantly rejected as well.
+5. AI is okay as long as **you know what you're doing**. If you have 0 knowledge of the codebase or of the game architecture, please **refrain from making pull requests**. We do not need sloppy code that none's able to debug.
\ No newline at end of file
diff --git a/ReLunacy.Engine/Assets/Geometry/GeometryData.cs b/ReLunacy.Engine/Assets/Geometry/GeometryData.cs
index 5d6748a..1b84fa8 100644
--- a/ReLunacy.Engine/Assets/Geometry/GeometryData.cs
+++ b/ReLunacy.Engine/Assets/Geometry/GeometryData.cs
@@ -9,6 +9,7 @@ public sealed class GeometryData : IGeometry
private readonly float[] _positions;
private readonly float[] _uvs;
private readonly float[]? _normals;
+ private readonly float[] _tangents;
private readonly float[]? _vertexAlphaCandidates;
private readonly uint[] _indices;
private readonly int[]? _jointIndices;
@@ -20,7 +21,7 @@ public sealed class GeometryData : IGeometry
public bool IsLoaded => true;
public GeometryData(ulong id, float[] positions, float[] uvs, uint[] indices, float[]? normals = null, BoundingSphere? boundingSphere = null,
- int[]? jointIndices = null, float[]? jointWeights = null, float[]? vertexAlphaCandidates = null)
+ int[]? jointIndices = null, float[]? jointWeights = null, float[]? vertexAlphaCandidates = null, float[]? tangents = null)
{
if (positions.Length % 3 != 0)
throw new ArgumentException("Positions must be in groups of 3 (x,y,z)", nameof(positions));
@@ -28,6 +29,8 @@ public GeometryData(ulong id, float[] positions, float[] uvs, uint[] indices, fl
throw new ArgumentException("UVs must be in groups of 2 (u,v)", nameof(uvs));
if (normals != null && normals.Length % 3 != 0)
throw new ArgumentException("Normals must be in groups of 3 (nx,ny,nz)", nameof(normals));
+ if (tangents != null && tangents.Length % 3 != 0)
+ throw new ArgumentException("Tangents must be in groups of 3 (tx,ty,tz)", nameof(tangents));
int vertexCount = positions.Length / 3;
@@ -35,6 +38,8 @@ public GeometryData(ulong id, float[] positions, float[] uvs, uint[] indices, fl
throw new ArgumentException("UV count must match vertex count");
if (normals != null && normals.Length / 3 != vertexCount)
throw new ArgumentException("Normal count must match vertex count");
+ if (tangents != null && tangents.Length / 3 != vertexCount)
+ throw new ArgumentException("Tangent count must match vertex count");
if (jointIndices != null && jointIndices.Length != vertexCount * 4)
throw new ArgumentException("Joint index count must be vertex count * 4", nameof(jointIndices));
if (jointWeights != null && jointWeights.Length != vertexCount * 4)
@@ -48,7 +53,12 @@ public GeometryData(ulong id, float[] positions, float[] uvs, uint[] indices, fl
// formats that don't carry real normals at all (UFrags currently don't plumb theirs
// through either) — computed from the triangle data itself rather than guessed, so it's
// still a reasonable substitute where no real data is available.
- _normals = normals ?? ComputeNormals(positions, indices);
+ _normals = normals ?? GeometryMath.ComputeNormals(positions, indices);
+ // Same idea for tangents: readers pass in the packed tangent word's decode (real
+ // tangent-space data) when they have it, and GeometryMath falls back to deriving one from
+ // UV gradients (and always derives the handedness sign, since the source format never
+ // carries one either way — see GeometryMath.ComputeTangents).
+ _tangents = GeometryMath.ComputeTangents(positions, uvs, _normals, indices, tangents);
_vertexAlphaCandidates = vertexAlphaCandidates;
_indices = indices;
_jointIndices = jointIndices;
@@ -56,44 +66,10 @@ public GeometryData(ulong id, float[] positions, float[] uvs, uint[] indices, fl
_boundingSphere = boundingSphere ?? CalculateBoundingSphere(positions);
}
- // Standard area-weighted vertex normal generation: accumulate each triangle's (unnormalized,
- // so larger triangles contribute more) face normal onto its three vertices, then normalize.
- // Triangle winding (and therefore which way "outward" ends up pointing) isn't independently
- // confirmed against these files — if a Decal offset ends up pushing into the surface instead
- // of away from it, that's the first thing to flip (negate the result here), not the offset
- // magnitude in EditorSettings.
- private static float[] ComputeNormals(float[] positions, uint[] indices)
- {
- int vertexCount = positions.Length / 3;
- var accum = new Vector3[vertexCount];
-
- for (int i = 0; i + 2 < indices.Length; i += 3)
- {
- uint i0 = indices[i], i1 = indices[i + 1], i2 = indices[i + 2];
- var p0 = new Vector3(positions[i0 * 3], positions[i0 * 3 + 1], positions[i0 * 3 + 2]);
- var p1 = new Vector3(positions[i1 * 3], positions[i1 * 3 + 1], positions[i1 * 3 + 2]);
- var p2 = new Vector3(positions[i2 * 3], positions[i2 * 3 + 1], positions[i2 * 3 + 2]);
- var faceNormal = Vector3.Cross(p1 - p0, p2 - p0);
-
- accum[i0] += faceNormal;
- accum[i1] += faceNormal;
- accum[i2] += faceNormal;
- }
-
- var result = new float[vertexCount * 3];
- for (int v = 0; v < vertexCount; v++)
- {
- var n = accum[v].LengthSquared() > 1e-12f ? Vector3.Normalize(accum[v]) : Vector3.UnitY;
- result[v * 3] = n.X;
- result[v * 3 + 1] = n.Y;
- result[v * 3 + 2] = n.Z;
- }
- return result;
- }
-
public float[] GetVertexPositions() => _positions;
public float[] GetTextureCoordinates() => _uvs;
public float[]? GetNormals() => _normals;
+ public float[]? GetTangents() => _tangents;
public float[]? GetVertexAlphaCandidates() => _vertexAlphaCandidates;
public uint[] GetIndices() => _indices;
public int[]? GetJointIndices() => _jointIndices;
diff --git a/ReLunacy.Engine/Assets/Geometry/GeometryMath.cs b/ReLunacy.Engine/Assets/Geometry/GeometryMath.cs
new file mode 100644
index 0000000..28c8712
--- /dev/null
+++ b/ReLunacy.Engine/Assets/Geometry/GeometryMath.cs
@@ -0,0 +1,158 @@
+using System.Numerics;
+
+namespace ReLunacy.Engine.Assets.Geometry;
+
+/// Per-vertex normal/tangent derivation shared by GeometryData (the normal path, for
+/// readers that decode real vertex attributes) and export-only adapters that don't go through
+/// GeometryData at all (LevelExporter's UFrag adapter, which has no baked normal/tangent data to
+/// hand over).
+internal static class GeometryMath
+{
+ // Standard area-weighted vertex normal generation: accumulate each triangle's (unnormalized,
+ // so larger triangles contribute more) face normal onto its three vertices, then normalize.
+ // Triangle winding (and therefore which way "outward" ends up pointing) isn't independently
+ // confirmed against these files — if a Decal offset ends up pushing into the surface instead
+ // of away from it, that's the first thing to flip (negate the result here), not the offset
+ // magnitude in EditorSettings.
+ public static float[] ComputeNormals(float[] positions, uint[] indices)
+ {
+ int vertexCount = positions.Length / 3;
+ var accum = new Vector3[vertexCount];
+
+ for (int i = 0; i + 2 < indices.Length; i += 3)
+ {
+ uint i0 = indices[i], i1 = indices[i + 1], i2 = indices[i + 2];
+ var p0 = Get3(positions, i0);
+ var p1 = Get3(positions, i1);
+ var p2 = Get3(positions, i2);
+ var faceNormal = Vector3.Cross(p1 - p0, p2 - p0);
+
+ accum[i0] += faceNormal;
+ accum[i1] += faceNormal;
+ accum[i2] += faceNormal;
+ }
+
+ var result = new float[vertexCount * 3];
+ for (int v = 0; v < vertexCount; v++)
+ {
+ var n = accum[v].LengthSquared() > 1e-12f ? Vector3.Normalize(accum[v]) : Vector3.UnitY;
+ result[v * 3 + 0] = n.X;
+ result[v * 3 + 1] = n.Y;
+ result[v * 3 + 2] = n.Z;
+ }
+ return result;
+ }
+
+ /// Builds a per-vertex glTF-style tangent (Vector4: xyz direction, w = the +-1
+ /// bitangent handedness sign), 4 floats per vertex. Uses `realTangents` (3 floats/vertex,
+ /// decoded straight from VertexFormat0/1's packed tangent word — see PackedNormal) when
+ /// supplied, since that's the game's actual tangent-space basis rather than an approximation;
+ /// falls back to the standard UV-gradient method (Lengyel) when the source format doesn't
+ /// carry tangent data at all (currently only UFrag terrain). Either way `w` is derived here
+ /// from UV winding relative to the (real or derived) tangent — the source files don't carry a
+ /// stored bitangent/handedness bit at all (confirmed: the raw normal/tangent words are
+ /// fully-consumed pure 11:11:10 direction data, zero spare bits — see PackedNormal), so this
+ /// isn't a shortcut taken only in the fallback case, it's the only way to get `w` regardless
+ /// of where the tangent itself came from.
+ public static float[] ComputeTangents(float[] positions, float[] uvs, float[] normals, uint[] indices, float[]? realTangents)
+ {
+ int vertexCount = positions.Length / 3;
+ var tangentAccum = new Vector3[vertexCount];
+ var bitangentAccum = new Vector3[vertexCount];
+ bool hasReal = realTangents != null && realTangents.Length == vertexCount * 3;
+
+ for (int i = 0; i + 2 < indices.Length; i += 3)
+ {
+ uint i0 = indices[i], i1 = indices[i + 1], i2 = indices[i + 2];
+ var p0 = Get3(positions, i0);
+ var p1 = Get3(positions, i1);
+ var p2 = Get3(positions, i2);
+ var uv0 = Get2(uvs, i0);
+ var uv1 = Get2(uvs, i1);
+ var uv2 = Get2(uvs, i2);
+
+ var edge1 = p1 - p0;
+ var edge2 = p2 - p0;
+ var duv1 = uv1 - uv0;
+ var duv2 = uv2 - uv0;
+
+ float det = duv1.X * duv2.Y - duv2.X * duv1.Y;
+ if (MathF.Abs(det) < 1e-12f)
+ continue; // degenerate UV triangle (zero UV area) — no usable tangent/handedness info
+
+ float r = 1f / det;
+ var bitangent = (duv1.X * edge2 - duv2.X * edge1) * r;
+ bitangentAccum[i0] += bitangent; bitangentAccum[i1] += bitangent; bitangentAccum[i2] += bitangent;
+
+ // Accumulated even when hasReal, purely for TangentDecodeSanityCheck below — the real
+ // per-vertex path never reads tangentAccum for its own output in that case.
+ var tangent = (duv2.Y * edge1 - duv1.Y * edge2) * r;
+ tangentAccum[i0] += tangent; tangentAccum[i1] += tangent; tangentAccum[i2] += tangent;
+ }
+
+ var result = new float[vertexCount * 4];
+ float dotSum = 0f, orthoSum = 0f;
+ int sampleCount = 0;
+
+ for (int v = 0; v < vertexCount; v++)
+ {
+ var n = Get3(normals, (uint)v);
+ n = n.LengthSquared() > 1e-12f ? Vector3.Normalize(n) : Vector3.UnitY;
+
+ var t = hasReal ? Get3(realTangents!, (uint)v) : tangentAccum[v];
+
+ if (hasReal && !_tangentDiagnosticLogged && t.LengthSquared() > 1e-12f && tangentAccum[v].LengthSquared() > 1e-12f)
+ {
+ dotSum += Vector3.Dot(Vector3.Normalize(t), Vector3.Normalize(tangentAccum[v]));
+ orthoSum += Vector3.Dot(n, Vector3.Normalize(t));
+ sampleCount++;
+ }
+
+ t -= n * Vector3.Dot(n, t);
+ t = t.LengthSquared() > 1e-12f ? Vector3.Normalize(t) : ArbitraryPerpendicular(n);
+
+ float w = Vector3.Dot(Vector3.Cross(n, t), bitangentAccum[v]) < 0f ? -1f : 1f;
+
+ result[v * 4 + 0] = t.X;
+ result[v * 4 + 1] = t.Y;
+ result[v * 4 + 2] = t.Z;
+ result[v * 4 + 3] = w;
+ }
+
+ if (hasReal && !_tangentDiagnosticLogged && sampleCount > 0)
+ LogTangentDiagnostic(dotSum / sampleCount, orthoSum / sampleCount, sampleCount);
+
+ return result;
+ }
+
+ // Fires once, on the first mesh loaded with real decoded tangent data — the meaning of
+ // VertexFormat0/1's second packed word as specifically a *tangent* (not e.g. a bitangent, and
+ // with the assumed handedness) was never independently verified. InsomniaToolset's own
+ // extract_gltf.cpp only decodes the adjacent word as Normal and never touches this one at all,
+ // so there's no second source in this ecosystem to cross-check against; the only real
+ // confirmation of "packed 11:11:10 unit direction" (see PackedNormal) covers the bit layout,
+ // not which of tangent/bitangent this specific word is. Compares it against a tangent derived
+ // independently from this same triangle's UV gradients (the standard Lengyel method) to close
+ // that gap empirically the first time real level data is available.
+ private static bool _tangentDiagnosticLogged;
+
+ private static void LogTangentDiagnostic(float meanCos, float meanOrtho, int sampleCount)
+ {
+ _tangentDiagnosticLogged = true;
+ Console.WriteLine(
+ $"[GeometryMath] Tangent decode check ({sampleCount} vertices): " +
+ $"mean cos(decoded tangent, UV-derived tangent) = {meanCos:0.###} " +
+ $"(near +1 = tangent, correct handedness, as currently assumed; near -1 = tangent but " +
+ $"needs negating; near 0 = this word is likely the bitangent, not the tangent). " +
+ $"mean dot(normal, tangent) = {meanOrtho:0.###} (should be near 0).");
+ }
+
+ private static Vector3 Get3(float[] arr, uint i) => new(arr[i * 3], arr[i * 3 + 1], arr[i * 3 + 2]);
+ private static Vector2 Get2(float[] arr, uint i) => new(arr[i * 2], arr[i * 2 + 1]);
+
+ private static Vector3 ArbitraryPerpendicular(Vector3 n)
+ {
+ var fallback = MathF.Abs(n.X) < 0.9f ? Vector3.UnitX : Vector3.UnitY;
+ return Vector3.Normalize(Vector3.Cross(n, fallback));
+ }
+}
diff --git a/ReLunacy.Engine/Assets/Geometry/PlacedInstance.cs b/ReLunacy.Engine/Assets/Geometry/PlacedInstance.cs
index a8efc4f..ade3253 100644
--- a/ReLunacy.Engine/Assets/Geometry/PlacedInstance.cs
+++ b/ReLunacy.Engine/Assets/Geometry/PlacedInstance.cs
@@ -13,10 +13,11 @@ public sealed class PlacedInstance : IPlacedInstance where TAsse
public Vector3 Rotation { get; init; }
public float Scale { get; init; }
public ushort Group { get; init; }
+ public float DisplayDistance { get; init; } = -1f;
private readonly Matrix4x4? _rawMatrix;
- public PlacedInstance(TAsset asset, Transform3D transform, ulong tuid, ushort group = 0, string name = "")
+ public PlacedInstance(TAsset asset, Transform3D transform, ulong tuid, ushort group = 0, string name = "", float displayDistance = -1f)
{
Asset = asset ?? throw new ArgumentNullException(nameof(asset));
Position = transform.Position;
@@ -25,6 +26,7 @@ public PlacedInstance(TAsset asset, Transform3D transform, ulong tuid, ushort gr
Group = group;
Name = name;
ID = tuid;
+ DisplayDistance = displayDistance;
}
public PlacedInstance(TAsset asset, Vector3 position, Vector3 rotation, float scale, ulong tuid, ushort group = 0, string name = "")
diff --git a/ReLunacy.Engine/Assets/Interfaces/IGeometry.cs b/ReLunacy.Engine/Assets/Interfaces/IGeometry.cs
index 2722a72..7ed1285 100644
--- a/ReLunacy.Engine/Assets/Interfaces/IGeometry.cs
+++ b/ReLunacy.Engine/Assets/Interfaces/IGeometry.cs
@@ -8,6 +8,11 @@ public interface IGeometry : IAsset
float[] GetTextureCoordinates();
float[]? GetNormals();
+ /// Per-vertex tangent, 4 floats per vertex (xyz direction + w bitangent-handedness
+ /// sign, matching glTF's TANGENT accessor convention) — see GeometryMath.ComputeTangents for
+ /// how it's derived, including why `w` is always computed rather than read from source data.
+ float[]? GetTangents();
+
/// Per-vertex decode of VertexFormat0's boneIndex-as-alpha candidate (see
/// Material.UsesVertexAlphaCandidate) — null for geometry that doesn't carry it. Not
/// necessarily meaningful data even when non-null; callers gate use on the material flag.
diff --git a/ReLunacy.Engine/Assets/Interfaces/IMaterial.cs b/ReLunacy.Engine/Assets/Interfaces/IMaterial.cs
index 62a84d2..c92bbfe 100644
--- a/ReLunacy.Engine/Assets/Interfaces/IMaterial.cs
+++ b/ReLunacy.Engine/Assets/Interfaces/IMaterial.cs
@@ -13,6 +13,10 @@ public interface IMaterial : IAsset
ITexture? AlbedoTexture { get; }
ITexture? NormalTexture { get; }
ITexture? PropertiesTexture { get; }
+ // Confirmed layout (see Shader.DetailMap): B = roughness, R/G = a second, higher-frequency
+ // tangent-space normal map. Tiling scale not yet identified in ShaderMetadata's unknown byte
+ // ranges — consumers use a placeholder tiling factor until it's found.
+ ITexture? DetailTexture { get; }
RenderMode RenderMode { get; }
float AlphaClipThreshold { get; }
diff --git a/ReLunacy.Engine/Assets/Interfaces/IPlacedInstance.cs b/ReLunacy.Engine/Assets/Interfaces/IPlacedInstance.cs
index 4d3437f..b41b4d8 100644
--- a/ReLunacy.Engine/Assets/Interfaces/IPlacedInstance.cs
+++ b/ReLunacy.Engine/Assets/Interfaces/IPlacedInstance.cs
@@ -14,5 +14,7 @@ public interface IPlacedInstance where TAsset : IAsset
public string Name { get; set; }
/// 0 on old engine.
public ushort Group { get; init; }
+ /// Distance (in-game units) beyond which the game itself stops rendering this instance. < 0 means unlimited. Only Mobys carry this from the file; other instance types default to unlimited.
+ public float DisplayDistance { get; init; }
Matrix4x4 GetTransformMatrix();
}
diff --git a/ReLunacy.Engine/Assets/Interfaces/ITexture.cs b/ReLunacy.Engine/Assets/Interfaces/ITexture.cs
index 9651f96..f5e417c 100644
--- a/ReLunacy.Engine/Assets/Interfaces/ITexture.cs
+++ b/ReLunacy.Engine/Assets/Interfaces/ITexture.cs
@@ -7,7 +7,14 @@ public enum TextureFormat
A8R8G8B8 = 2,
DXT1 = 3,
DXT3 = 4,
- DXT5 = 5
+ DXT5 = 5,
+ R8 = 6,
+ A1R5G5B5 = 7,
+ BC4 = 8,
+ BC5 = 9,
+ G8B8 = 10,
+ RGBA4 = 11,
+ RGBA16F = 12,
}
public interface ITexture : IAsset
diff --git a/ReLunacy.Engine/Assets/LevelElements/Volume.cs b/ReLunacy.Engine/Assets/LevelElements/Volume.cs
index f053686..d0fbcbc 100644
--- a/ReLunacy.Engine/Assets/LevelElements/Volume.cs
+++ b/ReLunacy.Engine/Assets/LevelElements/Volume.cs
@@ -17,5 +17,6 @@ public Volume(ulong id, Matrix4x4 transform, string? name = null, ushort group =
Name = name;
this.transform = transform;
IsLoaded = true;
+ this.group = group;
}
}
diff --git a/ReLunacy.Engine/Assets/Materials/Material.cs b/ReLunacy.Engine/Assets/Materials/Material.cs
index fe84e64..196f1a5 100644
--- a/ReLunacy.Engine/Assets/Materials/Material.cs
+++ b/ReLunacy.Engine/Assets/Materials/Material.cs
@@ -11,6 +11,7 @@ public sealed class Material : IMaterial
public ITexture? AlbedoTexture { get; init; }
public ITexture? NormalTexture { get; init; }
public ITexture? PropertiesTexture { get; init; }
+ public ITexture? DetailTexture { get; init; }
public RenderMode RenderMode { get; init; }
public float AlphaClipThreshold { get; init; }
@@ -30,13 +31,14 @@ public Material(ulong id)
AlphaClipThreshold = 0.5f;
}
- public static Material Create(ulong id, ITexture? albedo = null, ITexture? normal = null, ITexture? properties = null, RenderMode renderMode = RenderMode.Opaque, float alphaClipThreshold = 0.01f, bool usesVertexAlphaCandidate = false)
+ public static Material Create(ulong id, ITexture? albedo = null, ITexture? normal = null, ITexture? properties = null, ITexture? detail = null, RenderMode renderMode = RenderMode.Opaque, float alphaClipThreshold = 0.01f, bool usesVertexAlphaCandidate = false)
{
return new Material(id)
{
AlbedoTexture = albedo,
NormalTexture = normal,
PropertiesTexture = properties,
+ DetailTexture = detail,
RenderMode = renderMode,
AlphaClipThreshold = alphaClipThreshold,
UsesVertexAlphaCandidate = usesVertexAlphaCandidate
diff --git a/ReLunacy.Engine/Assets/Textures/Texture.cs b/ReLunacy.Engine/Assets/Textures/Texture.cs
index 04bc886..4f0accc 100644
--- a/ReLunacy.Engine/Assets/Textures/Texture.cs
+++ b/ReLunacy.Engine/Assets/Textures/Texture.cs
@@ -71,10 +71,12 @@ private static int CalculateMipmapSize(int width, int height, TextureFormat form
{
return format switch
{
- TextureFormat.R5G6B5 => width * height * 2,
+ TextureFormat.R5G6B5 or TextureFormat.A1R5G5B5 or TextureFormat.G8B8 or TextureFormat.RGBA4 => width * height * 2,
TextureFormat.A8R8G8B8 => width * height * 4,
- TextureFormat.DXT1 => Math.Max(1, (width + 3) / 4) * Math.Max(1, (height + 3) / 4) * 8,
- TextureFormat.DXT3 or TextureFormat.DXT5 => Math.Max(1, (width + 3) / 4) * Math.Max(1, (height + 3) / 4) * 16,
+ TextureFormat.RGBA16F => width * height * 8,
+ TextureFormat.R8 => width * height,
+ TextureFormat.DXT1 or TextureFormat.BC4 => Math.Max(1, (width + 3) / 4) * Math.Max(1, (height + 3) / 4) * 8,
+ TextureFormat.DXT3 or TextureFormat.DXT5 or TextureFormat.BC5 => Math.Max(1, (width + 3) / 4) * Math.Max(1, (height + 3) / 4) * 16,
_ => throw new NotSupportedException($"Format {format} not supported")
};
}
diff --git a/ReLunacy.Engine/Export/GltfExporter.cs b/ReLunacy.Engine/Export/GltfExporter.cs
index 7f43822..2b88023 100644
--- a/ReLunacy.Engine/Export/GltfExporter.cs
+++ b/ReLunacy.Engine/Export/GltfExporter.cs
@@ -10,10 +10,10 @@
namespace ReLunacy.Engine.Export;
-using MeshBuilder = MeshBuilder;
-using Vertex = VertexBuilder;
-using SkinnedMeshBuilder = MeshBuilder;
-using SkinnedVertex = VertexBuilder;
+using MeshBuilder = MeshBuilder;
+using Vertex = VertexBuilder;
+using SkinnedMeshBuilder = MeshBuilder;
+using SkinnedVertex = VertexBuilder;
/// Exports engine meshes as a single-file .glb — one group (e.g. a Moby's bangle, or a
/// Tie's whole mesh list) becomes one glTF mesh/node, so bangles stay distinct submeshes instead
@@ -31,17 +31,16 @@ public static void Export(string filePath, string modelName, IReadOnlyList onProgress?.Invoke(++processedMeshes / (float)totalMeshes);
- if (skeleton != null && joints != null)
+ if (skeleton != null && jointBindings != null)
{
var meshBuilder = BuildSkinnedMeshBuilder(name, group.Meshes, materialCache, skeleton.RootBoneIndex, ReportProgress);
- var jointBindings = joints.Select((node, i) => (node, EnsureAffine(skeleton.Bones[i].InverseBindPose))).ToArray();
sceneBuilder.AddSkinnedMesh(meshBuilder, jointBindings);
}
else
@@ -74,6 +73,7 @@ public static IMeshBuilder BuildMeshBuilder(string name, IReadO
var positions = mesh.Geometry.GetVertexPositions();
var uvs = mesh.Geometry.GetTextureCoordinates();
var normals = mesh.Geometry.GetNormals();
+ var tangents = mesh.Geometry.GetTangents();
var indices = mesh.Geometry.GetIndices();
int vertexCount = positions.Length / 3;
@@ -84,9 +84,12 @@ public static IMeshBuilder BuildMeshBuilder(string name, IReadO
var normal = normals != null && normals.Length >= i * 3 + 3
? new Vector3(normals[i * 3], normals[i * 3 + 1], normals[i * 3 + 2])
: Vector3.UnitY;
+ var tangent = tangents != null && tangents.Length >= i * 4 + 4
+ ? new Vector4(tangents[i * 4], tangents[i * 4 + 1], tangents[i * 4 + 2], tangents[i * 4 + 3])
+ : new Vector4(1f, 0f, 0f, 1f);
var uv = new Vector2(uvs[i * 2], uvs[i * 2 + 1]);
- vertices[i] = new Vertex(new VertexPositionNormal(position, normal), new VertexTexture1(uv));
+ vertices[i] = new Vertex(new VertexPositionNormalTangent(position, normal, tangent), new VertexTexture1(uv));
}
// Winding is passed through as-is: the renderer draws these with backface culling
@@ -120,6 +123,7 @@ public static IMeshBuilder BuildSkinnedMeshBuilder(string name,
var positions = mesh.Geometry.GetVertexPositions();
var uvs = mesh.Geometry.GetTextureCoordinates();
var normals = mesh.Geometry.GetNormals();
+ var tangents = mesh.Geometry.GetTangents();
var indices = mesh.Geometry.GetIndices();
var jointIndices = mesh.Geometry.GetJointIndices();
var jointWeights = mesh.Geometry.GetJointWeights();
@@ -132,10 +136,13 @@ public static IMeshBuilder BuildSkinnedMeshBuilder(string name,
var normal = normals != null && normals.Length >= i * 3 + 3
? new Vector3(normals[i * 3], normals[i * 3 + 1], normals[i * 3 + 2])
: Vector3.UnitY;
+ var tangent = tangents != null && tangents.Length >= i * 4 + 4
+ ? new Vector4(tangents[i * 4], tangents[i * 4 + 1], tangents[i * 4 + 2], tangents[i * 4 + 3])
+ : new Vector4(1f, 0f, 0f, 1f);
var uv = new Vector2(uvs[i * 2], uvs[i * 2 + 1]);
var joints = BuildJoints(jointIndices, jointWeights, i, rootBoneIndex);
- vertices[i] = new SkinnedVertex(new VertexPositionNormal(position, normal), new VertexTexture1(uv), joints);
+ vertices[i] = new SkinnedVertex(new VertexPositionNormalTangent(position, normal, tangent), new VertexTexture1(uv), joints);
}
for (int i = 0; i + 2 < indices.Length; i += 3)
@@ -172,13 +179,21 @@ private static VertexJoints4 BuildJoints(int[]? jointIndices, float[]? jointWeig
///
/// One NodeBuilder per bone, parented to mirror the skeleton hierarchy, with each node's
/// LocalTransform set to that bone's transform relative to its parent — computed as
- /// `parent.InverseBindPose * bone.WorldBindPose`, transliterated exactly (same operand order)
- /// from InsomniaToolset's GenerateSkeleton (extract_gltf.cpp), not independently re-derived.
+ /// `bone.WorldBindPose * parent.InverseBindPose`. An earlier version had this transliterated
+ /// from InsomniaToolset's GenerateSkeleton (extract_gltf.cpp) with the operands reversed
+ /// (`parent.InverseBindPose * bone.WorldBindPose`); these matrices are the row-vector
+ /// convention System.Numerics.Matrix4x4 always uses (confirmed via MobySkeletonReader/
+ /// RegionReader's identical sequential-float fill, and that other consumers of these same
+ /// matrices Decompose them correctly elsewhere), so composing local-then-parent transforms
+ /// for a row vector (`v' = v * Local * ParentWorld`) means the correct parent-relative
+ /// transform is `WorldBindPose * ParentInverseBindPose`, not the reverse — the reversed order
+ /// silently produced a conjugated (wrong) rotation for any bone whose orientation doesn't
+ /// commute with its parent's, deforming/exploding the exported mesh without any error.
/// Returned in skeleton bone-index order so glTF's JOINTS_0 vertex indices (already resolved
/// to skeleton-global bone indices at read time — see MobyReader.ExtractSkinData) can be used
/// directly as indices into this array with no further remapping.
///
- private static NodeBuilder[] BuildJointNodes(ISkeleton skeleton)
+ private static NodeBuilder[] BuildJointNodes(ISkeleton skeleton, NodeBuilder? rootParent = null)
{
var nodes = new NodeBuilder[skeleton.Bones.Count];
@@ -189,7 +204,7 @@ void CreateNode(int index, NodeBuilder? parent)
var local = index == skeleton.RootBoneIndex
? bone.WorldBindPose
- : skeleton.Bones[bone.ParentIndex].InverseBindPose * bone.WorldBindPose;
+ : bone.WorldBindPose * skeleton.Bones[bone.ParentIndex].InverseBindPose;
node.LocalTransform = new AffineTransform(EnsureAffine(local));
nodes[index] = node;
@@ -199,10 +214,23 @@ void CreateNode(int index, NodeBuilder? parent)
CreateNode(i, node);
}
- CreateNode(skeleton.RootBoneIndex, null);
+ CreateNode(skeleton.RootBoneIndex, rootParent);
return nodes;
}
+ /// Builds a fresh joint hierarchy plus its glTF skin bindings (joint node, inverse
+ /// bind matrix). A skinned mesh is positioned in the scene by its joint nodes' world
+ /// transforms rather than by a rigid mesh-attach transform, so every placed instance of a
+ /// skinned asset needs its own joint hierarchy — pass `rootParent` (an instance's own
+ /// transform node) so multiple instances of the same skeleton don't collapse onto the same
+ /// placement. Only the mesh/skin-weight data (built separately) is safe to share across
+ /// instances.
+ internal static (NodeBuilder Node, Matrix4x4 InverseBindMatrix)[] BuildSkinnedJoints(ISkeleton skeleton, NodeBuilder? rootParent = null)
+ {
+ var joints = BuildJointNodes(skeleton, rootParent);
+ return joints.Select((node, i) => (node, EnsureAffine(skeleton.Bones[i].InverseBindPose))).ToArray();
+ }
+
/// Zeroes the W column of the top 3 rows and forces M44=1 — cheap defensive cleanup
/// against non-affine drift in source matrices, mirrored from the same cleanup InsomniaToolset
/// applies before every Decompose/AffineTransform use of these bind-pose matrices.
@@ -234,15 +262,16 @@ private static MaterialBuilder GetOrBuildMaterial(IMaterial material, Dictionary
builder.WithBaseColor(TextureEncoding.EncodeRgbaToPng(albedoRgba, albedoWidth, albedoHeight));
}
- if (material.NormalTexture != null)
- {
- var png = TextureEncoding.DecodeToPng(material.NormalTexture);
- if (png != null)
- builder.WithNormal(png, 1.0f);
- }
+ byte[]? detailRgba = null;
+ int detailWidth = 0, detailHeight = 0;
+ if (material.DetailTexture != null)
+ detailRgba = TextureUtils.DecodeToRgba8888(material.DetailTexture, out detailWidth, out detailHeight);
+
+ if (material.NormalTexture != null || detailRgba != null)
+ ApplyNormalWithDetail(material.NormalTexture, detailRgba, detailWidth, detailHeight, builder);
- if (material.PropertiesTexture != null)
- ApplyExpensiveChannels(material.PropertiesTexture, albedoRgba, albedoWidth, albedoHeight, builder);
+ if (material.PropertiesTexture != null || detailRgba != null)
+ ApplyExpensiveChannels(material.PropertiesTexture, albedoRgba, albedoWidth, albedoHeight, detailRgba, detailWidth, detailHeight, builder);
var alphaMode = material.RenderMode switch
{
@@ -263,22 +292,35 @@ private static MaterialBuilder GetOrBuildMaterial(IMaterial material, Dictionary
/// The "expensive"/properties texture packs specular (R), metallic (G) and emissive intensity
/// (B) into one image — not a layout any glTF texture slot accepts directly, so each channel
/// gets split out into its own properly-shaped image: metallic into a synthesized
- /// metallicRoughnessTexture (metallic in B per glTF convention; there's no source roughness
- /// data, so G is filled with a constant mid-value rather than invented per-pixel data),
- /// specular into KHR_materials_specular's specularTexture (strength in A), and emissive —
- /// the B channel is only ever an *intensity*, the actual glow color is the material's own
- /// albedo — into an RGB texture built by scaling each albedo texel by its co-located
- /// intensity texel (nearest-neighbor if the two textures aren't the same resolution).
+ /// metallicRoughnessTexture (metallic in B per glTF convention; roughness in G is now sourced
+ /// from the detail map's confirmed B channel, tiled — see —
+ /// falling back to a constant mid-value only when no detail map is present at all), specular
+ /// into KHR_materials_specular's specularTexture (strength in A), and emissive — the B channel
+ /// is only ever an *intensity*, the actual glow color is the material's own albedo — into an
+ /// RGB texture built by scaling each albedo texel by its co-located intensity texel
+ /// (nearest-neighbor if the two textures aren't the same resolution). Specular/emissive are
+ /// skipped entirely when there's no properties texture (a detail-only material has no source
+ /// data for either).
///
- private static void ApplyExpensiveChannels(ITexture propertiesTexture, byte[]? albedoRgba, int albedoWidth, int albedoHeight, MaterialBuilder builder)
+ private static void ApplyExpensiveChannels(ITexture? propertiesTexture, byte[]? albedoRgba, int albedoWidth, int albedoHeight, byte[]? detailRgba, int detailWidth, int detailHeight, MaterialBuilder builder)
{
- byte[]? rgba = TextureUtils.DecodeToRgba8888(propertiesTexture, out int width, out int height);
- if (rgba == null)
+ byte[]? rgba = null;
+ int width = 0, height = 0;
+ if (propertiesTexture != null)
+ rgba = TextureUtils.DecodeToRgba8888(propertiesTexture, out width, out height);
+
+ if (rgba == null && detailRgba == null)
return;
+ if (rgba == null)
+ {
+ width = detailWidth;
+ height = detailHeight;
+ }
+
var metallicRoughness = new byte[width * height * 4];
- var specular = new byte[width * height * 4];
- var emissive = new byte[width * height * 4];
+ byte[]? specular = rgba != null ? new byte[width * height * 4] : null;
+ byte[]? emissive = rgba != null ? new byte[width * height * 4] : null;
bool hasAlbedo = albedoRgba != null && albedoWidth > 0 && albedoHeight > 0;
for (int y = 0; y < height; y++)
@@ -286,45 +328,146 @@ private static void ApplyExpensiveChannels(ITexture propertiesTexture, byte[]? a
for (int x = 0; x < width; x++)
{
int i = (y * width + x) * 4;
- byte specularValue = rgba[i + 0];
- byte metallicValue = rgba[i + 1];
- byte emissiveIntensity = rgba[i + 2];
+ byte metallicValue = rgba != null ? rgba[i + 1] : (byte)0;
+
+ byte roughnessValue = 128; // no detail map at all — constant mid-value fallback
+ if (detailRgba != null)
+ {
+ float u = (x + 0.5f) / width;
+ float v = (y + 0.5f) / height;
+ var (_, _, db) = SampleDetailTiled(detailRgba, detailWidth, detailHeight, u, v);
+ roughnessValue = db;
+ }
metallicRoughness[i + 0] = 0;
- metallicRoughness[i + 1] = 128; // no source roughness data — constant mid-value fallback
+ metallicRoughness[i + 1] = roughnessValue;
metallicRoughness[i + 2] = metallicValue;
metallicRoughness[i + 3] = 255;
- specular[i + 0] = 255;
- specular[i + 1] = 255;
- specular[i + 2] = 255;
- specular[i + 3] = specularValue;
+ if (rgba != null)
+ {
+ byte specularValue = rgba[i + 0];
+ byte emissiveIntensity = rgba[i + 2];
+
+ specular![i + 0] = 255;
+ specular[i + 1] = 255;
+ specular[i + 2] = 255;
+ specular[i + 3] = specularValue;
+
+ byte albedoR = 255, albedoG = 255, albedoB = 255;
+ if (hasAlbedo)
+ {
+ int ai = ((y * albedoHeight / height) * albedoWidth + x * albedoWidth / width) * 4;
+ albedoR = albedoRgba![ai + 0];
+ albedoG = albedoRgba[ai + 1];
+ albedoB = albedoRgba[ai + 2];
+ }
+
+ emissive![i + 0] = (byte)(albedoR * emissiveIntensity / 255);
+ emissive[i + 1] = (byte)(albedoG * emissiveIntensity / 255);
+ emissive[i + 2] = (byte)(albedoB * emissiveIntensity / 255);
+ emissive[i + 3] = 255;
+ }
+ }
+ }
+
+ builder.WithMetallicRoughness(TextureEncoding.EncodeRgbaToPng(metallicRoughness, width, height), metallic: null, roughness: null);
- byte albedoR = 255, albedoG = 255, albedoB = 255;
- if (hasAlbedo)
+ if (specular != null && emissive != null)
+ {
+ builder.WithSpecularFactor(TextureEncoding.EncodeRgbaToPng(specular, width, height), 1.0f);
+ // rgb must be an explicit Vector3.One, not null: MaterialBuilder.WithEmissive(image, rgb:
+ // null, ...) never calls the rgb-factor overload at all (see its source — it's guarded by
+ // `if (rgb.HasValue)`), so glTF's emissiveFactor is left at its spec default of (0,0,0).
+ // That means finalEmissive = emissiveTexture * emissiveFactor = emissiveTexture * 0 — the
+ // baked albedo-times-intensity texture below was correct but had zero visible effect in
+ // the actual exported file. Verified empirically (decompiled + reproduced with a synthetic
+ // export/reload round-trip) before fixing, not assumed from the method signature.
+ builder.WithEmissive(TextureEncoding.EncodeRgbaToPng(emissive, width, height), rgb: Vector3.One, strength: 1.0f);
+ }
+ }
+
+ /// Combines the base NormalTexture with DetailTexture's R/G channels (a second,
+ /// tangent-space normal map sampled at a tiled UV — see ) into
+ /// one glTF normal texture, since glTF has no native slot for a second normal map. Uses a UDN
+ /// (partial-derivative) blend — the two normals' XY components add, Z is taken from the base
+ /// normal, and the result is renormalized — cheap and close enough for a detail-scale effect
+ /// given the tiling factor itself is already a placeholder. Baked at the base NormalTexture's
+ /// resolution when present, otherwise at DetailTexture's own resolution with a flat "up" base
+ /// normal.
+ private static void ApplyNormalWithDetail(ITexture? normalTexture, byte[]? detailRgba, int detailWidth, int detailHeight, MaterialBuilder builder)
+ {
+ byte[]? baseRgba = null;
+ int width = 0, height = 0;
+ if (normalTexture != null)
+ baseRgba = TextureUtils.DecodeToRgba8888(normalTexture, out width, out height);
+
+ if (baseRgba == null && detailRgba == null)
+ return;
+
+ if (baseRgba == null)
+ {
+ width = detailWidth;
+ height = detailHeight;
+ }
+
+ var combined = new byte[width * height * 4];
+
+ for (int y = 0; y < height; y++)
+ {
+ for (int x = 0; x < width; x++)
+ {
+ int i = (y * width + x) * 4;
+
+ Vector3 baseNormal = Vector3.UnitZ;
+ if (baseRgba != null)
+ {
+ baseNormal = new Vector3(
+ baseRgba[i + 0] / 255f * 2f - 1f,
+ baseRgba[i + 1] / 255f * 2f - 1f,
+ baseRgba[i + 2] / 255f * 2f - 1f);
+ }
+
+ Vector3 result = baseNormal;
+ if (detailRgba != null)
{
- int ai = ((y * albedoHeight / height) * albedoWidth + x * albedoWidth / width) * 4;
- albedoR = albedoRgba![ai + 0];
- albedoG = albedoRgba[ai + 1];
- albedoB = albedoRgba[ai + 2];
+ float u = (x + 0.5f) / width;
+ float v = (y + 0.5f) / height;
+ var (dr, dg, _) = SampleDetailTiled(detailRgba, detailWidth, detailHeight, u, v);
+ float dnx = dr / 255f * 2f - 1f;
+ float dny = dg / 255f * 2f - 1f;
+
+ result = baseRgba != null
+ ? Vector3.Normalize(new Vector3(baseNormal.X + dnx, baseNormal.Y + dny, baseNormal.Z))
+ : Vector3.Normalize(new Vector3(dnx, dny, MathF.Sqrt(MathF.Max(0f, 1f - dnx * dnx - dny * dny))));
}
- emissive[i + 0] = (byte)(albedoR * emissiveIntensity / 255);
- emissive[i + 1] = (byte)(albedoG * emissiveIntensity / 255);
- emissive[i + 2] = (byte)(albedoB * emissiveIntensity / 255);
- emissive[i + 3] = 255;
+ combined[i + 0] = (byte)((result.X * 0.5f + 0.5f) * 255f);
+ combined[i + 1] = (byte)((result.Y * 0.5f + 0.5f) * 255f);
+ combined[i + 2] = (byte)((result.Z * 0.5f + 0.5f) * 255f);
+ combined[i + 3] = 255;
}
}
- builder.WithMetallicRoughness(TextureEncoding.EncodeRgbaToPng(metallicRoughness, width, height), metallic: null, roughness: null);
- builder.WithSpecularFactor(TextureEncoding.EncodeRgbaToPng(specular, width, height), 1.0f);
- // rgb must be an explicit Vector3.One, not null: MaterialBuilder.WithEmissive(image, rgb:
- // null, ...) never calls the rgb-factor overload at all (see its source — it's guarded by
- // `if (rgb.HasValue)`), so glTF's emissiveFactor is left at its spec default of (0,0,0).
- // That means finalEmissive = emissiveTexture * emissiveFactor = emissiveTexture * 0 — the
- // baked albedo-times-intensity texture below was correct but had zero visible effect in
- // the actual exported file. Verified empirically (decompiled + reproduced with a synthetic
- // export/reload round-trip) before fixing, not assumed from the method signature.
- builder.WithEmissive(TextureEncoding.EncodeRgbaToPng(emissive, width, height), rgb: Vector3.One, strength: 1.0f);
+ builder.WithNormal(TextureEncoding.EncodeRgbaToPng(combined, width, height), 1.0f);
+ }
+
+ // Real per-shader tiling scale hasn't been located in ShaderMetadata's still-unidentified byte
+ // ranges — this is a placeholder repeat factor (a common in-engine detail-map tiling order of
+ // magnitude) used only so the confirmed channel layout can be baked in now rather than left
+ // unused. Replace once the real value is found.
+ private const float PlaceholderDetailTiling = 4.0f;
+
+ private static (byte r, byte g, byte b) SampleDetailTiled(byte[] detailRgba, int detailWidth, int detailHeight, float u, float v)
+ {
+ u = (u * PlaceholderDetailTiling) % 1f;
+ v = (v * PlaceholderDetailTiling) % 1f;
+ if (u < 0f) u += 1f;
+ if (v < 0f) v += 1f;
+
+ int x = Math.Clamp((int)(u * detailWidth), 0, detailWidth - 1);
+ int y = Math.Clamp((int)(v * detailHeight), 0, detailHeight - 1);
+ int i = (y * detailWidth + x) * 4;
+ return (detailRgba[i + 0], detailRgba[i + 1], detailRgba[i + 2]);
}
}
diff --git a/ReLunacy.Engine/Export/LevelExporter.cs b/ReLunacy.Engine/Export/LevelExporter.cs
index f0f9788..da3f207 100644
--- a/ReLunacy.Engine/Export/LevelExporter.cs
+++ b/ReLunacy.Engine/Export/LevelExporter.cs
@@ -1,4 +1,5 @@
using System.Numerics;
+using ReLunacy.Engine.Assets.Geometry;
using ReLunacy.Engine.Assets.Interfaces;
using ReLunacy.Engine.Scene;
using SharpGLTF.Geometry;
@@ -51,6 +52,15 @@ public static void Export(string filePath, string levelName, EntityManager entit
foreach (var instance in assetGroup)
{
+ // Mobys are always exported as static (rigid) meshes at whole-level scope, even
+ // when their asset has a skeleton — a shared skeletal asset placed more than once
+ // would need one fresh joint hierarchy per instance, all parented under the same
+ // level-wide root, and SharpGLTF's armature validation rejects that as soon as two
+ // instances' bone nodes collide by name (NodeBuilder.IsValidArmature walks the
+ // whole scene graph under the shared root, not just one instance's joints),
+ // throwing "Export failed: (Parameter 'joints')" on any level with a skinned Moby
+ // placed more than once. Single-asset export (AssetViewer) is unaffected — each
+ // export there gets its own standalone scene/root.
AddInstanceNode(sceneBuilder, assetNode, instance.Name, instance.Transform.GetMatrix(), assetMeshes);
anyContentAdded = true;
ReportProgress();
@@ -142,6 +152,8 @@ private static void AddInstanceNode(SceneBuilder sceneBuilder, NodeBuilder asset
if (cache.TryGetValue(moby.Id, out var cached))
return cached;
+ // Always the rigid (unskinned) builder — see the comment at this method's call site for why
+ // whole-level export never uses skeletal data, even for Mobys that have one.
var result = moby.Bangles
.Select((bangle, i) => string.IsNullOrEmpty(bangle.Name) ? $"Bangle_{i}" : bangle.Name)
.Zip(moby.Bangles, (name, bangle) => (name, (IMeshBuilder)GltfExporter.BuildMeshBuilder(name, bangle.Meshes, materialCache)))
@@ -181,6 +193,18 @@ private sealed class UFragMeshAdapter(IUFrag ufrag, string name) : IMesh, IGeome
public float[] GetVertexPositions() => ufrag.GetVertexPositions();
public float[] GetTextureCoordinates() => ufrag.GetTextureCoordinates();
public float[]? GetNormals() => ufrag.GetNormals();
+
+ // UFrag terrain carries no baked tangent (or, on some readers, even normal) data — derive
+ // both from the triangle/UV data itself via the same fallback GeometryData uses for
+ // formats that don't decode real vertex attributes.
+ public float[]? GetTangents()
+ {
+ var positions = ufrag.GetVertexPositions();
+ var indices = ufrag.GetIndices();
+ var normals = ufrag.GetNormals() ?? GeometryMath.ComputeNormals(positions, indices);
+ return GeometryMath.ComputeTangents(positions, ufrag.GetTextureCoordinates(), normals, indices, null);
+ }
+
public float[]? GetVertexAlphaCandidates() => null;
public uint[] GetIndices() => ufrag.GetIndices();
public Vector3 GetBoundingCenter() => ufrag.GetBoundingCenter();
diff --git a/ReLunacy.Engine/Games/GameLibraryScanner.cs b/ReLunacy.Engine/Games/GameLibraryScanner.cs
index f301298..cfd5ea9 100644
--- a/ReLunacy.Engine/Games/GameLibraryScanner.cs
+++ b/ReLunacy.Engine/Games/GameLibraryScanner.cs
@@ -84,6 +84,21 @@ private static IEnumerable ScanPsarcLevels(string rootPath)
return File.Exists(candidate) ? candidate : null;
}
+ ///
+ /// Derives a level's display name from its own source path, for callers that only have
+ /// something like Program.ProvidedPath and never went through (so never got
+ /// a proper .Name). Same rule as : a .psarc's own
+ /// filename is always one of a fixed handful (level_cached, level_uncached, level_textures), so
+ /// for a file path the actual level name is its containing folder, not the file itself — a
+ /// folder path (old engine, or a pre-extracted new-engine level) is already the level name.
+ ///
+ public static string GetLevelNameFromPath(string path)
+ {
+ string trimmed = path.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
+ string levelDir = File.Exists(trimmed) ? Path.GetDirectoryName(trimmed) ?? trimmed : trimmed;
+ return Path.GetFileName(levelDir);
+ }
+
///
/// Same lookup as , but starting from a level's own folder or
/// .psarc file path instead of an already-known root — for callers (manual "Open level" file
diff --git a/ReLunacy.Engine/Loading/Interfaces/IMobyInstance.cs b/ReLunacy.Engine/Loading/Interfaces/IMobyInstance.cs
index 77c2afb..04b0059 100644
--- a/ReLunacy.Engine/Loading/Interfaces/IMobyInstance.cs
+++ b/ReLunacy.Engine/Loading/Interfaces/IMobyInstance.cs
@@ -8,4 +8,6 @@ public interface IMobyInstance
public Vector3 Rotation { get; set; }
public float Scale { get; set; }
public ushort MobyIndex { get; set; }
+ /// Distance (in-game units) beyond which the game itself stops rendering this instance. Raw file value; <= 0 means unlimited.
+ public float DisplayDistance { get; set; }
}
diff --git a/ReLunacy.Engine/Loading/Interfaces/ITextureMetadata.cs b/ReLunacy.Engine/Loading/Interfaces/ITextureMetadata.cs
index b0999fd..67ec625 100644
--- a/ReLunacy.Engine/Loading/Interfaces/ITextureMetadata.cs
+++ b/ReLunacy.Engine/Loading/Interfaces/ITextureMetadata.cs
@@ -8,4 +8,10 @@ public interface ITextureMetadata
public uint Height { get; }
public TextureFormat Format { get; }
public ushort MipmapCount { get; }
+
+ /// True if this texture's pixel data is stored linearly (not Morton-swizzled). Always
+ /// true for block-compressed formats (DXT/BC) regardless of any per-instance flag. Otherwise
+ /// per-instance: old engine reads it from a bit in formatBitfield, new engine derives it from
+ /// the raw format byte's 0x8X (swizzled) / 0xAX (linear) prefix — see TextureMetadataOld/New.
+ public bool IsLinear { get; }
}
diff --git a/ReLunacy.Engine/Loading/Meshes/MobyMesh.cs b/ReLunacy.Engine/Loading/Meshes/MobyMesh.cs
index 2ed0aad..191c40c 100644
--- a/ReLunacy.Engine/Loading/Meshes/MobyMesh.cs
+++ b/ReLunacy.Engine/Loading/Meshes/MobyMesh.cs
@@ -181,7 +181,7 @@ public void ReadBoneMap(StreamHelper sh)
sh.Seek(savedPosition);
}
- public readonly void GetBuffers(float scalar, out float[] vpos, out uint[] ind, out float[] uvcoords, out float[] normals, out float[] vertexAlphaCandidates)
+ public readonly void GetBuffers(float scalar, out float[] vpos, out uint[] ind, out float[] uvcoords, out float[] normals, out float[] tangents, out float[] vertexAlphaCandidates)
{
ind = new uint[indicesCount];
for (int k = 0; k < indicesCount; k++) ind[k] = indices[k];
@@ -189,14 +189,16 @@ public readonly void GetBuffers(float scalar, out float[] vpos, out uint[] ind,
vpos = new float[verticesCount * 3];
uvcoords = new float[verticesCount * 2];
normals = new float[verticesCount * 3];
+ tangents = new float[verticesCount * 3];
vertexAlphaCandidates = new float[verticesCount];
for (int k = 0; k < verticesCount; k++)
{
- // Mobys scale uniformly (single scalar, unlike Ties' per-axis Vector3), so a decoded
- // normal doesn't need the inverse-transpose treatment Ties do — direction is unaffected
- // by uniform scale, only renormalized since the packed decode isn't exactly unit length.
- Vector3 n;
+ // Mobys scale uniformly (single scalar, unlike Ties' per-axis Vector3), so neither a
+ // decoded normal nor tangent needs any axis-dependent correction — direction is
+ // unaffected by uniform scale, only renormalized since the packed decode isn't exactly
+ // unit length.
+ Vector3 n, t;
if (verticesType == 0)
{
vpos[k * 3 + 0] = vertices0[k].position.Item1 * scalar;
@@ -205,6 +207,7 @@ public readonly void GetBuffers(float scalar, out float[] vpos, out uint[] ind,
uvcoords[k * 2 + 0] = (float)vertices0[k].UVs.Item1;
uvcoords[k * 2 + 1] = (float)vertices0[k].UVs.Item2;
n = vertices0[k].Normal;
+ t = vertices0[k].Tangent;
vertexAlphaCandidates[k] = vertices0[k].VertexAlphaCandidate;
}
else
@@ -215,6 +218,7 @@ public readonly void GetBuffers(float scalar, out float[] vpos, out uint[] ind,
uvcoords[k * 2 + 0] = (float)vertices1[k].UVs.Item1;
uvcoords[k * 2 + 1] = (float)vertices1[k].UVs.Item2;
n = vertices1[k].Normal;
+ t = vertices1[k].Tangent;
// VertexFormat1's Unk1 is the skinned equivalent of VertexFormat0.boneIndex, but
// unlike boneIndex it's confirmed to carry tangible (bone-related) data on boned
// meshes — not a vertex alpha candidate, so no decode applies here.
@@ -225,6 +229,11 @@ public readonly void GetBuffers(float scalar, out float[] vpos, out uint[] ind,
normals[k * 3 + 0] = n.X;
normals[k * 3 + 1] = n.Y;
normals[k * 3 + 2] = n.Z;
+
+ t = t.LengthSquared() > 1e-12f ? Vector3.Normalize(t) : Vector3.UnitX;
+ tangents[k * 3 + 0] = t.X;
+ tangents[k * 3 + 1] = t.Y;
+ tangents[k * 3 + 2] = t.Z;
}
}
diff --git a/ReLunacy.Engine/Loading/Meshes/TieMesh.cs b/ReLunacy.Engine/Loading/Meshes/TieMesh.cs
index 765dfa7..e5e866a 100644
--- a/ReLunacy.Engine/Loading/Meshes/TieMesh.cs
+++ b/ReLunacy.Engine/Loading/Meshes/TieMesh.cs
@@ -86,7 +86,7 @@ public void ReadIndicesBuffer(StreamHelper sh)
}
}
- public readonly void GetBuffers(Vector3 scale, out float[] vpos, out uint[] ind, out float[] uvcoords, out float[] normals, out float[] vertexAlphaCandidates)
+ public readonly void GetBuffers(Vector3 scale, out float[] vpos, out uint[] ind, out float[] uvcoords, out float[] normals, out float[] tangents, out float[] vertexAlphaCandidates)
{
ind = new uint[indicesCount];
for (int k = 0; k < indicesCount; k++) ind[k] = indices[k];
@@ -94,6 +94,7 @@ public readonly void GetBuffers(Vector3 scale, out float[] vpos, out uint[] ind,
vpos = new float[verticesCount * 3];
uvcoords = new float[verticesCount * 2];
normals = new float[verticesCount * 3];
+ tangents = new float[verticesCount * 3];
vertexAlphaCandidates = new float[verticesCount];
for (int k = 0; k < verticesCount; k++)
@@ -115,6 +116,16 @@ public readonly void GetBuffers(Vector3 scale, out float[] vpos, out uint[] ind,
normals[k * 3 + 0] = scaledN.X;
normals[k * 3 + 1] = scaledN.Y;
normals[k * 3 + 2] = scaledN.Z;
+
+ // Unlike the normal, a tangent lies IN the surface (it's an edge/gradient direction,
+ // not a perpendicular) — under non-uniform scale it transforms with the scale
+ // directly, the same as a position, not with the inverse-transpose.
+ Vector3 t = vertices[k].Tangent;
+ Vector3 scaledT = new(t.X * scale.X, t.Y * scale.Y, t.Z * scale.Z);
+ scaledT = scaledT.LengthSquared() > 1e-12f ? Vector3.Normalize(scaledT) : Vector3.UnitX;
+ tangents[k * 3 + 0] = scaledT.X;
+ tangents[k * 3 + 1] = scaledT.Y;
+ tangents[k * 3 + 2] = scaledT.Z;
}
}
diff --git a/ReLunacy.Engine/Loading/Objects/Instances/MobyInstanceNew.cs b/ReLunacy.Engine/Loading/Objects/Instances/MobyInstanceNew.cs
index a20c1e7..6e91450 100644
--- a/ReLunacy.Engine/Loading/Objects/Instances/MobyInstanceNew.cs
+++ b/ReLunacy.Engine/Loading/Objects/Instances/MobyInstanceNew.cs
@@ -12,7 +12,9 @@ public record struct MobyInstanceNew : ILunaSerializable, IMobyInstance
[FileOffset(0x00)] public ushort mobyIndex;
[FileOffset(0x02)] public ushort groupIndex;
- [FileOffset(0x04), Reference(0x10)] public byte[] Unk1;
+ [FileOffset(0x04)] public float displayDist;
+ [FileOffset(0x08)] public float updateDist;
+ [FileOffset(0x0C), Reference(0x08)] public byte[] Unk1;
[FileOffset(0x14)] public Vector3 position;
[FileOffset(0x20)] public Vector3 rotation;
[FileOffset(0x2C)] public float scale;
@@ -22,6 +24,8 @@ public record struct MobyInstanceNew : ILunaSerializable, IMobyInstance
public Vector3 Rotation { readonly get => rotation; set => rotation = value; }
public float Scale { readonly get => scale; set => scale = value; }
public ushort MobyIndex { readonly get => mobyIndex; set => mobyIndex = value; }
+ public float DisplayDistance { readonly get => displayDist; set => displayDist = value; }
+ public float UpdateDistance { readonly get => updateDist; set => updateDist = value; }
public static MobyInstanceNew Read(StreamHelper sh) => FileUtils.ReadStructure(sh);
diff --git a/ReLunacy.Engine/Loading/Objects/Instances/MobyInstanceOld.cs b/ReLunacy.Engine/Loading/Objects/Instances/MobyInstanceOld.cs
index 631185a..1a3470b 100644
--- a/ReLunacy.Engine/Loading/Objects/Instances/MobyInstanceOld.cs
+++ b/ReLunacy.Engine/Loading/Objects/Instances/MobyInstanceOld.cs
@@ -10,19 +10,24 @@ public record struct MobyInstanceOld : ILunaSerializable, IMobyInstance
public const uint ID = 0x7340;
public const uint Size = 0x48;
- [FileOffset(0x00), Reference(0x18)] public byte[] Unk1;
+ [FileOffset(0x00), Reference(0x08)] public byte[] Unk1;
+ [FileOffset(0x08)] public float displayDist;
+ [FileOffset(0x0C)] public float updateDist;
+ [FileOffset(0x10), Reference(0x08)] public byte[] Unk2;
[FileOffset(0x18)] public Vector3 position;
[FileOffset(0x24)] public Vector3 rotation;
[FileOffset(0x30)] public float scale;
- [FileOffset(0x34)] public ulong Unk2;
+ [FileOffset(0x34)] public ulong Unk3;
[FileOffset(0x3C)] public ushort mobyIndex;
- [FileOffset(0x3E)] public ushort Unk3;
- [FileOffset(0x40)] public ulong Unk4;
+ [FileOffset(0x3E)] public ushort Unk4;
+ [FileOffset(0x40)] public ulong Unk5;
public Vector3 Position { get => position; set => position = value; }
public Vector3 Rotation { get => rotation; set => rotation = value; }
public float Scale { get => scale; set => scale = value; }
public ushort MobyIndex { get => mobyIndex; set => mobyIndex = value; }
+ public float DisplayDistance { get => displayDist; set => displayDist = value; }
+ public float UpdateDistance { get => updateDist; set => updateDist = value; }
public static MobyInstanceOld Read(StreamHelper sh) => FileUtils.ReadStructure(sh);
diff --git a/ReLunacy.Engine/Loading/Objects/Moby.cs b/ReLunacy.Engine/Loading/Objects/Moby.cs
index 3e6cb0c..a60e0e1 100644
--- a/ReLunacy.Engine/Loading/Objects/Moby.cs
+++ b/ReLunacy.Engine/Loading/Objects/Moby.cs
@@ -85,6 +85,22 @@ public class Moby : IDisposable
if (MobyObj is not OldMoby omoby)
return;
+ // Some old-engine mobys (logic-only props: triggers, camera targets, path markers,
+ // etc. — confirmed present in Tools of Destruction's meridian_city) have zero bangles,
+ // or a bangle with zero meshes: no visual geometry at all. bangles/meshes are
+ // [Reference(...)]-deserialized arrays that stay null when their count is zero, so
+ // blindly indexing bangles[^1].meshes[^1] (as this used to, four times below) threw a
+ // NullReferenceException for any such moby instead of just... having no mesh data.
+ if (!TryGetLastMesh(omoby.bangles, out var lastMesh))
+ {
+ // Empty, not left null: MobyReader.ReadMobyBanglesMeshes unconditionally seeks
+ // these streams before checking bangle/mesh counts, so a null stream here would
+ // just move the same crash one call further down instead of fixing it.
+ verticesStream = new StreamHelper(new MemoryStream(), StreamHelper.Endianness.Big);
+ indicesStream = new StreamHelper(new MemoryStream(), StreamHelper.Endianness.Big);
+ return;
+ }
+
// Old engine: geometry lives in either vertices.dat or textures.dat, selected by
// the high bit of the offset field itself.
if ((omoby.verticesOffset & 0x80000000) != 0)
@@ -92,7 +108,6 @@ public class Moby : IDisposable
var vertigfile = fm.igfiles["vertices.dat"]!;
var vertSec = vertigfile.QuerySection(0x9000);
vertigfile.sh.Seek(vertSec.offset + (omoby.verticesOffset & ~0x80000000));
- var lastMesh = omoby.bangles[^1].meshes[^1];
var length = lastMesh.verticesOffset + lastMesh.verticesCount * (lastMesh.verticesType == 0 ? VertexFormat0.Size : VertexFormat1.Size);
verticesStream = new StreamHelper(new MemoryStream(vertigfile.sh.ReadBytes(length)), StreamHelper.Endianness.Big);
}
@@ -103,7 +118,6 @@ public class Moby : IDisposable
omoby.verticesOffset &= ~0x80000000;
txstream.Seek(omoby.verticesOffset, SeekOrigin.Begin);
- var lastMesh = omoby.bangles[^1].meshes[^1];
var length = lastMesh.verticesOffset + lastMesh.verticesCount * (lastMesh.verticesType == 0 ? VertexFormat0.Size : VertexFormat1.Size);
byte[] verticesData = new byte[length];
txstream.Read(verticesData, 0, (int)length);
@@ -115,7 +129,6 @@ public class Moby : IDisposable
var indigfile = fm.igfiles["vertices.dat"]!;
var indSec = indigfile.QuerySection(0x9100);
indigfile.sh.Seek(indSec.offset + (omoby.indicesOffset & ~0x80000000));
- var lastMesh = omoby.bangles[^1].meshes[^1];
var length = lastMesh.indicesOffset * sizeof(ushort) + lastMesh.indicesCount * (uint)sizeof(ushort);
indicesStream = new StreamHelper(new MemoryStream(indigfile.sh.ReadBytes(length)), StreamHelper.Endianness.Big);
}
@@ -126,7 +139,6 @@ public class Moby : IDisposable
omoby.indicesOffset &= ~0x80000000;
txstream.Seek(omoby.indicesOffset, SeekOrigin.Begin);
- var lastMesh = omoby.bangles[^1].meshes[^1];
var length = lastMesh.indicesOffset * sizeof(ushort) + lastMesh.indicesCount * (uint)sizeof(ushort);
byte[] indexData = new byte[length];
txstream.Read(indexData, 0, (int)length);
@@ -135,6 +147,28 @@ public class Moby : IDisposable
}
}
+ // Searches backward for the last bangle that actually has meshes (not necessarily the very
+ // last bangle — a moby could have trailing empty bangles too), since the whole point is
+ // finding the true final mesh's offset/count to compute the total buffer length. Returns
+ // false if this moby has no mesh data anywhere (null/empty bangles, or every bangle empty).
+ private static bool TryGetLastMesh(MobyBangle[]? bangles, out MobyMesh lastMesh)
+ {
+ lastMesh = default;
+ if (bangles == null)
+ return false;
+
+ for (int i = bangles.Length - 1; i >= 0; i--)
+ {
+ if (bangles[i].meshes != null && bangles[i].meshes.Length > 0)
+ {
+ lastMesh = bangles[i].meshes[^1];
+ return true;
+ }
+ }
+
+ return false;
+ }
+
public void ReadMoby(bool isOld, int index = 0) // index only for old mobys
{
MobyObj = isOld ? OldMoby.Read(mobyStream, index) : NewMoby.Read(mobyStream);
@@ -144,16 +178,24 @@ public class Moby : IDisposable
public void Dispose()
{
- for (int i = 0; i < MobyObj.bangles.Length; i++)
+ // Same null-bangles/null-meshes possibility as the constructor guards against above (a
+ // moby with no visual geometry) — nothing was rented from either pool in that case, so
+ // there's nothing to return either.
+ if (MobyObj.bangles != null)
{
- for (int j = 0; j < MobyObj.bangles[i].meshes.Length; j++)
+ for (int i = 0; i < MobyObj.bangles.Length; i++)
{
- ref var mesh = ref MobyObj.bangles[i].meshes[j];
- if (mesh.verticesType == 0) ArrayPool.Shared.Return(mesh.vertices0);
- if (mesh.verticesType == 1) ArrayPool.Shared.Return(mesh.vertices1);
+ if (MobyObj.bangles[i].meshes == null) continue;
+
+ for (int j = 0; j < MobyObj.bangles[i].meshes.Length; j++)
+ {
+ ref var mesh = ref MobyObj.bangles[i].meshes[j];
+ if (mesh.verticesType == 0) ArrayPool.Shared.Return(mesh.vertices0);
+ if (mesh.verticesType == 1) ArrayPool.Shared.Return(mesh.vertices1);
+ }
}
+ ArrayPool.Shared.Return(MobyObj.bangles);
}
- ArrayPool.Shared.Return(MobyObj.bangles);
verticesStream?.Close();
indicesStream?.Close();
diff --git a/ReLunacy.Engine/Loading/Readers/DebugReader.cs b/ReLunacy.Engine/Loading/Readers/DebugReader.cs
index 930b56f..341d6f4 100644
--- a/ReLunacy.Engine/Loading/Readers/DebugReader.cs
+++ b/ReLunacy.Engine/Loading/Readers/DebugReader.cs
@@ -14,7 +14,11 @@ public sealed class DebugReader
// names from gp_prius.dat (mobys) or the zone's own file (ties), not debug.dat.
private readonly List _mobyInstanceNames = [];
private readonly List _tieInstanceNames = [];
- private readonly List _volumeNames = [];
+ // Index-aligned with the old-engine volume transform array (section 0x7740 in gameplay.dat —
+ // see RegionReader.ReadVolumesOld), same as _mobyInstanceNames/_tieInstanceNames above. Must
+ // stay List with an unconditional Add per entry, not List skipping empties —
+ // skipping any entry desyncs every name after it from its actual volume index.
+ private readonly List _volumeNames = [];
private readonly bool _isOld;
public DebugReader(FileManager fileManager)
@@ -133,12 +137,16 @@ private void LoadVolumeNames()
var section = _debugFile!.QuerySection(0x7760);
if (section.count == 0) return;
- for (int i = 0; i < section.count; i++)
- {
- var volumeName = _debugFile.sh.ReadString();
- if (!string.IsNullOrEmpty(volumeName))
- _volumeNames.Add(volumeName);
- }
+ // Previously read via a bare loop of sh.ReadString() calls with no seek to section.offset
+ // first — it read from wherever the stream happened to be left by LoadShaderNames() just
+ // before it, not this section's actual data, and skipped adding an entry at all for empty
+ // names instead of preserving the slot — desyncing every name after the first gap from its
+ // real volume index. Same struct/pattern as moby/tie instance names fixes both.
+ _debugFile.sh.Seek(section.offset);
+ var names = FileUtils.ReadStructureArray(_debugFile.sh, section.count);
+
+ foreach (var item in names)
+ _volumeNames.Add(string.IsNullOrEmpty(item.name) ? null : item.name);
}
public string? GetMobyPrototypeName(ulong tuid) => _mobyPrototypeNames.TryGetValue(tuid, out var name) ? name : null;
@@ -146,6 +154,7 @@ private void LoadVolumeNames()
public string? GetShaderName(ulong tuid) => _shaderNames.TryGetValue(tuid, out var name) ? name : null;
public string? GetMobyInstanceName(int index) => index >= 0 && index < _mobyInstanceNames.Count ? _mobyInstanceNames[index] : null;
public string? GetTieInstanceName(int index) => index >= 0 && index < _tieInstanceNames.Count ? _tieInstanceNames[index] : null;
+ public string? GetVolumeName(int index) => index >= 0 && index < _volumeNames.Count ? _volumeNames[index] : null;
public string GetSummary()
{
diff --git a/ReLunacy.Engine/Loading/Readers/MaterialReader.cs b/ReLunacy.Engine/Loading/Readers/MaterialReader.cs
index aec7e75..5845d75 100644
--- a/ReLunacy.Engine/Loading/Readers/MaterialReader.cs
+++ b/ReLunacy.Engine/Loading/Readers/MaterialReader.cs
@@ -62,6 +62,7 @@ public IMaterial GetMaterialByTuid(ulong tuid)
albedo: albedo,
normal: shader.Normal != null ? WrapTexture(shader.Normal) : null,
properties: shader.Expensive != null ? WrapTexture(shader.Expensive) : null,
+ detail: shader.DetailMap != null ? WrapTexture(shader.DetailMap) : null,
renderMode: ToRenderMode(shader.RenderingMode),
alphaClipThreshold: GetAlphaClip(shader),
usesVertexAlphaCandidate: UsesVertexAlphaCandidate(shader.RenderingMode, albedo));
@@ -160,8 +161,12 @@ private static float GetAlphaClip(Shader shader) =>
private static bool UsesVertexAlphaCandidate(RenderingMode mode, ITexture? albedo) =>
mode is RenderingMode.Overlay or RenderingMode.SoftEdge or RenderingMode.Blended && !HasAlphaChannel(albedo);
+ // A1R5G5B5/RGBA4 carry real (if low-precision) alpha bits, same as A8R8G8B8/DXT3/DXT5 —
+ // included here for the same reason those are: UsesVertexAlphaCandidate should only kick in
+ // when the albedo genuinely has nowhere else to source transparency from.
private static bool HasAlphaChannel(ITexture? texture) =>
- texture?.Format is TextureFormat.A8R8G8B8 or TextureFormat.DXT3 or TextureFormat.DXT5;
+ texture?.Format is TextureFormat.A8R8G8B8 or TextureFormat.DXT3 or TextureFormat.DXT5
+ or TextureFormat.A1R5G5B5 or TextureFormat.RGBA4;
private static TextureFormat ToTextureFormat(Textures.TextureFormat format) => format switch
{
@@ -170,6 +175,13 @@ private static bool HasAlphaChannel(ITexture? texture) =>
Textures.TextureFormat.DXT1 => TextureFormat.DXT1,
Textures.TextureFormat.DXT3 => TextureFormat.DXT3,
Textures.TextureFormat.DXT5 => TextureFormat.DXT5,
+ Textures.TextureFormat.R8 => TextureFormat.R8,
+ Textures.TextureFormat.A1R5G5B5 => TextureFormat.A1R5G5B5,
+ Textures.TextureFormat.BC4 => TextureFormat.BC4,
+ Textures.TextureFormat.BC5 => TextureFormat.BC5,
+ Textures.TextureFormat.G8B8 => TextureFormat.G8B8,
+ Textures.TextureFormat.RGBA4 => TextureFormat.RGBA4,
+ Textures.TextureFormat.RGBA16F => TextureFormat.RGBA16F,
_ => TextureFormat.Unknown,
};
}
diff --git a/ReLunacy.Engine/Loading/Readers/MobyReader.cs b/ReLunacy.Engine/Loading/Readers/MobyReader.cs
index b68655b..b3b90a2 100644
--- a/ReLunacy.Engine/Loading/Readers/MobyReader.cs
+++ b/ReLunacy.Engine/Loading/Readers/MobyReader.cs
@@ -178,10 +178,10 @@ private IMesh ConvertMobyMesh(MobyMesh legacyMesh, Objects.Moby moby)
{
// Positions are fixed-point int16 in bangle-local space; the moby's own scale must be
// applied here, matching what MobyMesh.GetBuffers already does for the legacy renderer.
- legacyMesh.GetBuffers(moby.Scale, out var positions, out var indices, out var uvs, out var normals, out var vertexAlphaCandidates);
+ legacyMesh.GetBuffers(moby.Scale, out var positions, out var indices, out var uvs, out var normals, out var tangents, out var vertexAlphaCandidates);
- var (jointIndices, jointWeights) = ExtractSkinData(legacyMesh);
- var geometry = new GeometryData(id: 0, positions: positions, uvs: uvs, indices: indices, normals: normals, jointIndices: jointIndices, jointWeights: jointWeights, vertexAlphaCandidates: vertexAlphaCandidates);
+ var (jointIndices, jointWeights) = ExtractSkinData(legacyMesh, (int)(moby.Skeleton?.NumBones ?? 0));
+ var geometry = new GeometryData(id: 0, positions: positions, uvs: uvs, indices: indices, normals: normals, tangents: tangents, jointIndices: jointIndices, jointWeights: jointWeights, vertexAlphaCandidates: vertexAlphaCandidates);
IMaterial material = moby.IsOld
? _materialReader.GetMaterialByIndex(legacyMesh.shaderIndex)
@@ -203,7 +203,7 @@ private IMesh ConvertMobyMesh(MobyMesh legacyMesh, Objects.Moby moby)
/// feature.
/// Returns (null, null) if this mesh has no joint palette (no skin data).
///
- private static (int[]? jointIndices, float[]? jointWeights) ExtractSkinData(MobyMesh mesh)
+ private static (int[]? jointIndices, float[]? jointWeights) ExtractSkinData(MobyMesh mesh, int skeletonBoneCount)
{
if (mesh.boneMap.Length == 0)
return (null, null);
@@ -218,10 +218,10 @@ private static (int[]? jointIndices, float[]? jointWeights) ExtractSkinData(Moby
for (int v = 0; v < vertexCount; v++)
{
var vertex = mesh.vertices1[v];
- SetBinding(jointIndices, jointWeights, mesh.boneMap, v, 0, vertex.bones.Item1, vertex.weights.Item1);
- SetBinding(jointIndices, jointWeights, mesh.boneMap, v, 1, vertex.bones.Item2, vertex.weights.Item2);
- SetBinding(jointIndices, jointWeights, mesh.boneMap, v, 2, vertex.bones.Item3, vertex.weights.Item3);
- SetBinding(jointIndices, jointWeights, mesh.boneMap, v, 3, vertex.bones.Item4, vertex.weights.Item4);
+ SetBinding(jointIndices, jointWeights, mesh.boneMap, skeletonBoneCount, v, 0, vertex.bones.Item1, vertex.weights.Item1);
+ SetBinding(jointIndices, jointWeights, mesh.boneMap, skeletonBoneCount, v, 1, vertex.bones.Item2, vertex.weights.Item2);
+ SetBinding(jointIndices, jointWeights, mesh.boneMap, skeletonBoneCount, v, 2, vertex.bones.Item3, vertex.weights.Item3);
+ SetBinding(jointIndices, jointWeights, mesh.boneMap, skeletonBoneCount, v, 3, vertex.bones.Item4, vertex.weights.Item4);
}
}
else if (mesh.verticesType == 0)
@@ -229,19 +229,29 @@ private static (int[]? jointIndices, float[]? jointWeights) ExtractSkinData(Moby
for (int v = 0; v < vertexCount; v++)
{
int localIndex = Math.Abs((mesh.vertices0[v].boneIndex + 1) / 3);
- SetBinding(jointIndices, jointWeights, mesh.boneMap, v, 0, localIndex, 255);
+ SetBinding(jointIndices, jointWeights, mesh.boneMap, skeletonBoneCount, v, 0, localIndex, 255);
}
}
return (jointIndices, jointWeights);
}
- private static void SetBinding(int[] jointIndices, float[] jointWeights, ushort[] boneMap, int vertex, int slot, int localIndex, byte weightByte)
+ // skeletonBoneCount bounds-checks boneMap's resolved value too, not just the local palette
+ // index into boneMap itself — boneMap[localIndex] is a skeleton-global bone index, and nothing
+ // previously verified it was actually within the skeleton before it reached GltfExporter's
+ // joint-node array (built with exactly skeleton.Bones.Count entries), where an out-of-range
+ // value would throw. Treated the same as an unweighted slot (skipped) rather than clamped, so
+ // a corrupt/misread binding silently drops that influence instead of binding to a wrong bone.
+ private static void SetBinding(int[] jointIndices, float[] jointWeights, ushort[] boneMap, int skeletonBoneCount, int vertex, int slot, int localIndex, byte weightByte)
{
if (weightByte == 0 || localIndex < 0 || localIndex >= boneMap.Length)
return;
- jointIndices[vertex * 4 + slot] = boneMap[localIndex];
+ int globalIndex = boneMap[localIndex];
+ if (skeletonBoneCount > 0 && globalIndex >= skeletonBoneCount)
+ return;
+
+ jointIndices[vertex * 4 + slot] = globalIndex;
jointWeights[vertex * 4 + slot] = weightByte / 255f;
}
}
diff --git a/ReLunacy.Engine/Loading/Readers/RegionReader.cs b/ReLunacy.Engine/Loading/Readers/RegionReader.cs
index 508f92f..6ece5dd 100644
--- a/ReLunacy.Engine/Loading/Readers/RegionReader.cs
+++ b/ReLunacy.Engine/Loading/Readers/RegionReader.cs
@@ -31,7 +31,7 @@ private Assets.Levels.Region ReadRegionOld()
IGFile gameplayFile = _fileManager.igfiles["gameplay.dat"]!;
var mobyInstances = ReadMobyInstancesOld(gameplayFile);
- var volumes = ReadVolumesOld(gameplayFile);
+ var volumes = ReadVolumesOld(gameplayFile, _debugReader);
return new Assets.Levels.Region(
id: 0,
@@ -153,7 +153,10 @@ private List> ReadMobyInstancesOld(IGFile gameplayFile)
// Matches Legacy's Region(IGFile, AssetLoader): debug.dat instance names (when
// present) are matched purely by array position, not by any tuid.
string name = _debugReader.GetMobyInstanceName(i) ?? $"Moby_{legacyInstance.mobyIndex:X4}_Instance_{i}";
- mobyInstances.Add(new PlacedInstance(moby, transform, (ulong)i, 0, name));
+ // 0 or negative in the file means unlimited — normalize to -1 so callers only
+ // ever need to check "< 0 = unlimited".
+ float displayDistance = legacyInstance.displayDist <= 0 ? -1f : legacyInstance.displayDist;
+ mobyInstances.Add(new PlacedInstance(moby, transform, (ulong)i, 0, name, displayDistance));
}
}
@@ -222,7 +225,8 @@ private List> ReadMobyInstancesNew(IGFile prius, IGFile r
? metadataNames[i]!
: $"Moby_{legacyInstance.mobyIndex:X4}_Instance_{i}";
- mobyInstances.Add(new PlacedInstance(moby, transform, instanceTUID, group, name));
+ float displayDistance = legacyInstance.displayDist <= 0 ? -1f : legacyInstance.displayDist;
+ mobyInstances.Add(new PlacedInstance(moby, transform, instanceTUID, group, name, displayDistance));
}
}
@@ -235,14 +239,26 @@ private List> ReadMobyInstancesNew(IGFile prius, IGFile r
sh.ReadSingle(), sh.ReadSingle(), sh.ReadSingle(), sh.ReadSingle(),
sh.ReadSingle(), sh.ReadSingle(), sh.ReadSingle(), sh.ReadSingle());
- private static List ReadVolumesOld(IGFile gameplayFile)
+ private static List ReadVolumesOld(IGFile gameplayFile, DebugReader debugReader)
{
var volumeSection = gameplayFile.QuerySection(0x7740);
var volumes = new Volume[volumeSection.count];
- gameplayFile.sh.Seek(volumeSection.offset);
for (int i = 0; i < volumeSection.count; i++)
{
- volumes[i] = new Volume((ulong)i, ReadMatrix4x4(gameplayFile.sh));
+ // Old-engine volume entries are 0x90 bytes each: a 0x40-byte (16-float, row-major,
+ // same convention as TieBound/new-engine volumes) transform matrix followed by 0x50
+ // bytes of still-unidentified trailing data — confirmed against ReLunacy-Ymir's own
+ // OldVolumeInstance, which documents this exact layout and explicitly warns against
+ // reading it as a packed array of bare matrices. Reading with no stride skip (what
+ // this used to do — sequential 0x40-byte reads with no gap) meant every entry after
+ // the first started inside the PREVIOUS entry's unknown trailing bytes instead of at
+ // its own real matrix: since gcd(0x40, 0x90) leaves a common period of 9 iterations
+ // (9 * 0x40 == 4 * 0x90), only every 9th "volume" happened to land back on a genuine
+ // entry boundary and decode correctly — everything else decomposed into a garbled
+ // scale/rotation, which reads as a visibly wrong-shaped/wrong-proportioned volume.
+ gameplayFile.sh.Seek(volumeSection.offset + i * 0x90);
+ string name = debugReader.GetVolumeName(i) ?? $"Volume_{i}";
+ volumes[i] = new Volume((ulong)i, ReadMatrix4x4(gameplayFile.sh), name);
}
return [.. volumes];
}
@@ -250,32 +266,43 @@ private static List ReadVolumesOld(IGFile gameplayFile)
private static List ReadVolumesNew(IGFile prius)
{
// Same file-location correction as moby instances: volumes and their names live in
- // gp_prius.dat, not region.dat.
+ // gp_prius.dat, not region.dat. Metadata (TUID/name/group) is read first into arrays,
+ // same two-pass shape as ReadMobyInstancesNew, so each Volume can be constructed with its
+ // real identity/group instead of a loop-index placeholder that a later pass can't fix up
+ // (Volume.Id is init-only).
+ var volumeMetaSection = prius.QuerySection(InstanceMetadata.VolumeMetadataID);
+ var metadatas = new InstanceMetadata[volumeMetaSection.count];
+ var metadataNames = new string?[volumeMetaSection.count];
+ if (volumeMetaSection.count > 0)
+ {
+ prius.sh.Seek(volumeMetaSection.offset);
+ for (int i = 0; i < volumeMetaSection.count; i++)
+ {
+ metadatas[i] = new InstanceMetadata(prius.sh);
+ if (metadatas[i].namePointer != 0)
+ {
+ // Same position-drift hazard as the moby metadata loop above: save/restore
+ // around the string-pool seek so the next sequential InstanceMetadata read
+ // stays correct.
+ long nextRecordPos = prius.sh.BaseStream.Position;
+ metadataNames[i] = prius.sh.ReadString(metadatas[i].namePointer);
+ prius.sh.Seek(nextRecordPos);
+ }
+ }
+ }
+
var volumeSection = prius.QuerySection(0x2505C);
var volumes = new Volume[volumeSection.count];
prius.sh.Seek(volumeSection.offset);
for (int i = 0; i < volumeSection.count; i++)
{
- volumes[i] = new Volume((ulong)i, ReadMatrix4x4(prius.sh));
- }
-
- var volumeMetaSection = prius.QuerySection(InstanceMetadata.VolumeMetadataID);
- prius.sh.Seek(volumeMetaSection.offset);
- for (int i = 0; i < volumeMetaSection.count && i < volumes.Length; i++)
- {
- var volumeMeta = new InstanceMetadata(prius.sh);
- if (volumeMeta.namePointer != 0)
- {
- // Same position-drift hazard as the moby metadata loop above: save/restore around
- // the string-pool seek so the next sequential InstanceMetadata read stays correct.
- long nextRecordPos = prius.sh.BaseStream.Position;
- volumes[i].Name = prius.sh.ReadString(volumeMeta.namePointer);
- prius.sh.Seek(nextRecordPos);
- }
- else
- {
- volumes[i].Name = $"Volume_{i}";
- }
+ var transform = ReadMatrix4x4(prius.sh);
+ ulong tuid = i < metadatas.Length ? metadatas[i].TUID : (ulong)i;
+ ushort group = i < metadatas.Length ? metadatas[i].group : (ushort)0;
+ string name = i < metadataNames.Length && metadataNames[i] != null
+ ? metadataNames[i]!
+ : $"Volume_{i}";
+ volumes[i] = new Volume(tuid, transform, name, group);
}
return [.. volumes];
diff --git a/ReLunacy.Engine/Loading/Readers/TieReader.cs b/ReLunacy.Engine/Loading/Readers/TieReader.cs
index fc463b7..30b4792 100644
--- a/ReLunacy.Engine/Loading/Readers/TieReader.cs
+++ b/ReLunacy.Engine/Loading/Readers/TieReader.cs
@@ -115,9 +115,9 @@ private void ReadTieMeshes(Objects.Tie tie)
private IMesh ConvertTieMesh(TieMesh legacyMesh, System.Numerics.Vector3 scale, Objects.Tie tie)
{
- legacyMesh.GetBuffers(scale, out var positions, out var indices, out var uvs, out var normals, out var vertexAlphaCandidates);
+ legacyMesh.GetBuffers(scale, out var positions, out var indices, out var uvs, out var normals, out var tangents, out var vertexAlphaCandidates);
- var geometry = new GeometryData(id: 0, positions: positions, uvs: uvs, indices: indices, normals: normals, vertexAlphaCandidates: vertexAlphaCandidates);
+ var geometry = new GeometryData(id: 0, positions: positions, uvs: uvs, indices: indices, normals: normals, tangents: tangents, vertexAlphaCandidates: vertexAlphaCandidates);
IMaterial material = legacyMesh.isOld
? _materialReader.GetMaterialByIndex(legacyMesh.oldShaderIndex)
diff --git a/ReLunacy.Engine/Loading/Shaders/Shader.cs b/ReLunacy.Engine/Loading/Shaders/Shader.cs
index 9da5a9c..1fa5108 100644
--- a/ReLunacy.Engine/Loading/Shaders/Shader.cs
+++ b/ReLunacy.Engine/Loading/Shaders/Shader.cs
@@ -20,8 +20,10 @@ public class Shader
public Texture? Normal;
public Texture? Expensive;
// Old engine only so far (ShaderMetadataOld.detailMap, offset 0x0C) — ShaderMetadataNew
- // hasn't had its equivalent identified yet. Unconfirmed what this actually holds; being
- // wired through so it can be inspected in the Shader Browser rather than guessed at blind.
+ // hasn't had its equivalent identified yet. Confirmed layout: B = roughness, R/G = a second,
+ // higher-frequency tangent-space normal map, sampled at a tiled UV. The tiling scale itself
+ // hasn't been located in ShaderMetadata's still-unidentified byte ranges — consumers
+ // (GltfExporter) use a placeholder constant until it's found.
public Texture? DetailMap;
public RenderingMode RenderingMode => (RenderingMode)(isOld ? metadataOld!.Value.renderingMode : metadataNew!.Value.renderingMode);
diff --git a/ReLunacy.Engine/Loading/TextureShaderLoader.cs b/ReLunacy.Engine/Loading/TextureShaderLoader.cs
index e5a968b..8f54060 100644
--- a/ReLunacy.Engine/Loading/TextureShaderLoader.cs
+++ b/ReLunacy.Engine/Loading/TextureShaderLoader.cs
@@ -50,9 +50,15 @@ private void LoadTexturesNew()
var alstream = assetlookup.sh;
var hmstream = new StreamHelper(highmipstream, StreamHelper.Endianness.Big);
+ var texstream = new StreamHelper(texturestream, StreamHelper.Endianness.Big);
var highmipsPtrSec = assetlookup.QuerySection(Texture.HighmipsPointerID);
var textureMetaSec = assetlookup.QuerySection(TextureMetadataNew.ID);
+ // Lower-resolution single-mip fallback copies, embedded directly in textures.dat,
+ // index-aligned with the metadata/highmip-pointer tables above — see
+ // Texture.ReadTexture's lowres fallback branch. Absent on some levels (QuerySection
+ // returns a zero-length default header when the section doesn't exist at all).
+ var textureRefSec = assetlookup.QuerySection(0x1D180);
alstream.Seek(highmipsPtrSec.offset);
var highmipsPtrs = AssetPointer.ReadArray(alstream, highmipsPtrSec.length / 0x10);
@@ -71,7 +77,15 @@ private void LoadTexturesNew()
// above made this loop run more than once (id=0 duplicate on the 2nd texture).
var tex = new Texture(alstream) { highmipsRef = highmipsPtrs[i], id = highmipsPtrs[i].TUID };
Textures.Add(tex.id, tex);
- tex.ReadTexture(hmstream);
+
+ AssetPointer? lowresRef = null;
+ if (textureRefSec.length >= (i + 1) * 0x10)
+ {
+ alstream.Seek(textureRefSec.offset + i * 0x10);
+ lowresRef = new AssetPointer(alstream);
+ }
+
+ tex.ReadTexture(hmstream, texstream, lowresRef);
}
}
@@ -112,21 +126,33 @@ private void LoadTexturesOld()
texstreamReferences.Add(TexstreamReference.Read(mainStream));
}
+ // texstreamReferences.index is the TARGET texture's index, not a 1:1 position in this
+ // list — a texstream override only exists for a subset of textures. The previous loop
+ // used its own counter `i` as both the reference-list position AND the texture-array
+ // index, which are different things: any reference whose own .index was >=
+ // texstreamRefSection.count (entirely plausible — the ref list only has as many
+ // entries as overridden textures, which can be indexed anywhere in the full texture
+ // table) was silently skipped, and the reference actually found at position i was
+ // applied to the wrong texture whenever the two diverged.
var textureList = Textures.Values.ToArray();
- for (uint i = 0; i < texstreamRefSection.count; i++)
+ foreach (var texstreamref in texstreamReferences)
{
- if (!texstreamReferences.Any(otr => otr.index == i))
- continue;
-
- var texstreamref = texstreamReferences.Find(otr => otr.index == i);
- textureList[i].highmipsMetadatasOld?.Add(texstreamref);
+ if (texstreamref.index < textureList.Length)
+ textureList[texstreamref.index].highmipsMetadatasOld?.Add(texstreamref);
}
}
- var streamToRead = texstream ?? textures;
+ // Per texture, not a single stream for the whole level: only textures with their own
+ // texstream override (highmipsMetadatasOld non-empty) read from texstream.dat — its
+ // offsets are meaningless against textures.dat and vice versa. The previous single
+ // `streamToRead = texstream ?? textures` read EVERY texture from texstream.dat whenever
+ // that file existed at all, even textures with no override entry, seeking to garbage
+ // offsets for all of them — texstream.dat only overrides a subset of textures on levels
+ // that have one at all (e.g. Tools of Destruction's meridian_city).
foreach (var tex in Textures.Values)
{
- tex.ReadTexture(streamToRead);
+ bool hasOverride = (tex.highmipsMetadatasOld?.Count ?? 0) > 0;
+ tex.ReadTexture(hasOverride && texstream is not null ? texstream : textures);
}
}
diff --git a/ReLunacy.Engine/Loading/Textures/Texture.cs b/ReLunacy.Engine/Loading/Textures/Texture.cs
index 4903804..38fe314 100644
--- a/ReLunacy.Engine/Loading/Textures/Texture.cs
+++ b/ReLunacy.Engine/Loading/Textures/Texture.cs
@@ -23,16 +23,26 @@ public class Texture
public ITextureMetadata textureMetadata;
public bool isOld;
+ private static readonly HashSet _loggedSuspiciousTextures = [];
+
+ // Block-compressed formats are always read/stored linear regardless of the per-instance
+ // linear bit/prefix (see TextureMetadataOld/New.IsLinear) — DXT/BC compression has no
+ // swizzled-on-disk variant in this format family.
+ private static bool IsBlockCompressed(TextureFormat format) =>
+ format is TextureFormat.DXT1 or TextureFormat.DXT3 or TextureFormat.DXT5 or TextureFormat.BC4 or TextureFormat.BC5;
+
public uint HighmipSize
{
get
{
return TexFormat switch
{
- TextureFormat.DXT1 => Math.Max(1, (Width + 3) / 4) * Math.Max(1, (Height + 3) / 4) * 8,
- TextureFormat.DXT3 or TextureFormat.DXT5 => Math.Max(1, (Width + 3) / 4) * Math.Max(1, (Height + 3) / 4) * 16,
+ TextureFormat.DXT1 or TextureFormat.BC4 => Math.Max(1, (Width + 3) / 4) * Math.Max(1, (Height + 3) / 4) * 8,
+ TextureFormat.DXT3 or TextureFormat.DXT5 or TextureFormat.BC5 => Math.Max(1, (Width + 3) / 4) * Math.Max(1, (Height + 3) / 4) * 16,
TextureFormat.A8R8G8B8 => Width * Height * 4u,
- TextureFormat.R5G6B5 => Width * Height * 2u,
+ TextureFormat.RGBA16F => Width * Height * 8u,
+ TextureFormat.R5G6B5 or TextureFormat.A1R5G5B5 or TextureFormat.G8B8 or TextureFormat.RGBA4 => Width * Height * 2u,
+ TextureFormat.R8 => Width * Height,
_ => 0,
};
}
@@ -69,10 +79,13 @@ public void ReadHighmipsPtr(StreamHelper sh)
id = highmipsRef.Value.TUID;
}
- /// In new engine, stream must be the highmips stream.
- public void ReadTexture(StreamHelper sh)
+ /// In new engine, must be the highmips stream. /
+ /// (new engine only) are the assetlookup 0x1D180 fallback — a single-mip copy
+ /// embedded directly in textures.dat, used when this texture has no highmip data at all.
+ public void ReadTexture(StreamHelper sh, StreamHelper? lowresStream = null, AssetPointer? lowresRef = null)
{
int offset;
+ StreamHelper source = sh;
if (isOld)
{
if ((highmipsMetadatasOld?.Count ?? 0) > 0)
@@ -94,37 +107,70 @@ public void ReadTexture(StreamHelper sh)
throw new InvalidOperationException("Highmips reference is null. It must be read before reading the texture in new engine!");
var hmref = highmipsRef.Value;
- offset = (int)hmref.offset;
- if (hmref.length == 0)
+ if (hmref.length > 0)
+ {
+ offset = (int)hmref.offset;
+ data = new byte[hmref.length];
+
+ // Diagnostic: the highmip entry's own length should match what the block decoder
+ // will actually expect for this texture's Width/Height/format — if it doesn't,
+ // the decoder gets handed a buffer that's the wrong size for the dimensions it's
+ // told to decode, which for a short buffer reads as flat/degenerate output (the
+ // reported new-engine DXT1/DXT5 "unicolor" symptom) without throwing anything.
+ if (IsBlockCompressed(TexFormat) && _loggedSuspiciousTextures.Add(id) && hmref.length != HighmipSize)
+ Console.WriteLine($"Diagnostic: texture {id:X} ('{name}') is {TexFormat} at {Width}x{Height} — highmip entry is {hmref.length} bytes but decoding at these dimensions expects {HighmipSize} bytes.");
+ }
+ else if (lowresStream is not null && lowresRef is { length: > 0 } lref && HighmipSize > 0)
+ {
+ // No highmip data for this texture — fall back to the lower-resolution single-mip
+ // copy embedded directly in textures.dat (assetlookup section 0x1D180), same as
+ // ReLunacy-Ymir's `useLowres` path. Previously this case just returned with `data`
+ // left at its default `[]`, which decodes to null and renders as
+ // GlobalResource.DefaultModelTexture — a flat placeholder that looks exactly like
+ // the reported "unicolor" bug, for any texture whose highmip entry is legitimately
+ // empty (a normal, common case on new engine, not corruption).
+ source = lowresStream;
+ offset = (int)lref.offset;
+ data = new byte[HighmipSize];
+ }
+ else
{
return;
}
- data = new byte[hmref.length];
}
- if (offset > sh.BaseStream.Length || offset < 0)
- throw new IndexOutOfRangeException($"Offset is out of bounds: {offset:X}/{sh.BaseStream.Length:X}");
+ if (offset > source.BaseStream.Length || offset < 0)
+ throw new IndexOutOfRangeException($"Offset is out of bounds: {offset:X}/{source.BaseStream.Length:X}");
- if (TexFormat > TextureFormat.A8R8G8B8)
+ // Whether to unswizzle is a per-instance property (see ITextureMetadata.IsLinear), not
+ // something derivable from the format alone — the previous `TexFormat > A8R8G8B8` check
+ // only worked by coincidence for the 5 formats that existed before this format list was
+ // expanded (every "> A8R8G8B8" format happened to also be DXT). It breaks for RGBA4/G8B8,
+ // which the new-engine prefix scheme can mark either swizzled OR linear per texture.
+ if (IsBlockCompressed(TexFormat) || textureMetadata.IsLinear)
{
- sh.Seek(offset);
- sh.Read(data);
+ source.Seek(offset);
+ source.Read(data);
}
else
{
- sh.Seek(offset);
- Unswizzle(sh);
+ source.Seek(offset);
+ Unswizzle(source);
}
}
public void Unswizzle(StreamHelper sh)
{
- if ((int)TexFormat > (int)TextureFormat.A8R8G8B8) throw new InvalidOperationException("DXT formats aren't swizzled.");
+ if (IsBlockCompressed(TexFormat)) throw new InvalidOperationException("DXT/BC formats aren't swizzled.");
if (data.Length <= 1) return;
- int pixelSize = 0;
- if (TexFormat == TextureFormat.R5G6B5) pixelSize = 2;
- else if (TexFormat == TextureFormat.A8R8G8B8) pixelSize = 4;
+ int pixelSize = TexFormat switch
+ {
+ TextureFormat.R8 => 1,
+ TextureFormat.R5G6B5 or TextureFormat.A1R5G5B5 or TextureFormat.G8B8 or TextureFormat.RGBA4 => 2,
+ TextureFormat.A8R8G8B8 => 4,
+ _ => throw new ArgumentOutOfRangeException(nameof(TexFormat), TexFormat, "Unsupported format for unswizzle"),
+ };
Span pixel = stackalloc byte[pixelSize];
@@ -138,6 +184,17 @@ public void Unswizzle(StreamHelper sh)
private static int MortonSwizzle(int index, int width, int height)
{
+ // The row-stride multiplier below must be the ORIGINAL width, not the loop-shifted copy —
+ // `width` gets shifted down to 1 by the end of the loop below (that's how it tracks when
+ // to stop consuming bits for the X axis), so using the parameter directly in the final
+ // `yMortonValue * width + xMortonValue` silently used a stride of 1 instead of the real
+ // row width. That collapses most (x,y) pairs onto the same handful of destination indices
+ // instead of spreading them across the full width*height buffer — every swizzled texture
+ // (every non-DXT, non-linear one — DXT/linear textures are read raw and never call this)
+ // came out scrambled, while unswizzled reads looked fine, matching the reported symptom of
+ // some textures being broken and others not. ReLunacy-Ymir's equivalent Morton() takes the
+ // same approach but keeps the original `x` parameter untouched for exactly this reason.
+ int originalWidth = width;
int bitPositionMultiplierY, bitPositionMultiplierX = bitPositionMultiplierY = 1;
int yMortonValue, xMortonValue = yMortonValue = 0;
@@ -159,6 +216,6 @@ private static int MortonSwizzle(int index, int width, int height)
}
}
- return yMortonValue * width + xMortonValue;
+ return yMortonValue * originalWidth + xMortonValue;
}
}
diff --git a/ReLunacy.Engine/Loading/Textures/TextureFormat.cs b/ReLunacy.Engine/Loading/Textures/TextureFormat.cs
index d4c5c8c..a232c5b 100644
--- a/ReLunacy.Engine/Loading/Textures/TextureFormat.cs
+++ b/ReLunacy.Engine/Loading/Textures/TextureFormat.cs
@@ -1,10 +1,24 @@
namespace ReLunacy.Engine.Loading.Textures;
+// Values match the raw 4-bit old-engine format code (TextureMetadataOld's (formatBitfield >> 8) &
+// 0xF) directly, ported from ReLunacy-Ymir's CTexture.TexFormat — that fork's texture handling is
+// confirmed working across formats this enum previously didn't even have members for (R8,
+// A1R5G5B5, BC4, BC5, G8B8), which is why old-engine levels using those formats (e.g. Tools of
+// Destruction's meridian_city) failed to decode. RGBA4/RGBA16F can't come from the old-engine 4-bit
+// mask at all (new-engine only, selected by TextureMetadataNew's format-byte prefix instead), so
+// they keep the fork's original raw byte values (0x83/0x9A) to avoid colliding with 0x00-0x0F.
public enum TextureFormat
{
+ R8 = 0x01,
R5G6B5 = 0x03,
+ A1R5G5B5 = 0x04,
A8R8G8B8 = 0x05,
DXT1 = 0x06,
DXT3 = 0x07,
- DXT5 = 0x08
+ DXT5 = 0x08,
+ BC4 = 0x09,
+ BC5 = 0x0A,
+ G8B8 = 0x0B,
+ RGBA4 = 0x83,
+ RGBA16F = 0x9A,
}
diff --git a/ReLunacy.Engine/Loading/Textures/TextureMetadataNew.cs b/ReLunacy.Engine/Loading/Textures/TextureMetadataNew.cs
index 3fc1d94..e9ccda1 100644
--- a/ReLunacy.Engine/Loading/Textures/TextureMetadataNew.cs
+++ b/ReLunacy.Engine/Loading/Textures/TextureMetadataNew.cs
@@ -16,9 +16,55 @@ public record struct TextureMetadataNew : ILunaSerializable, ITextureMetadata
public readonly uint Width => (uint)1 << widthPow;
public readonly uint Height => (uint)1 << heightPow;
- public readonly TextureFormat Format => (TextureFormat)format;
+
+ // New engine's raw format byte is NOT the same numbering as old engine's 4-bit code — it's
+ // prefixed 0x8X (Morton-swizzled) or 0xAX (linear), plus a couple of bare/special values
+ // (0x01-0x0B, 0x9A). The previous `(TextureFormat)format` cast skipped this normalization
+ // entirely, so every new-engine texture whose format byte didn't happen to equal one of
+ // TextureFormat's raw old-engine values (3/5/6/7/8) decoded to a garbage enum value instead —
+ // ported from ReLunacy-Ymir's CTexture.NormalizeNewEngineFormat, which is confirmed working.
+ public readonly TextureFormat Format => NormalizeFormat(format);
+
+ // 0xAX prefix and 0x9A (RGBA16F) are always linear; DXT/BC are always linear regardless of
+ // prefix; everything else (0x8X prefix, or a bare unprefixed byte) is swizzled. Ported from
+ // CTexture.NewEngineFormatIsLinear.
+ public readonly bool IsLinear => FormatIsLinear(format);
+
public readonly ushort MipmapCount => mipmapCount;
+ private static readonly HashSet _loggedFormatBytes = [];
+
+ private static TextureFormat NormalizeFormat(byte raw)
+ {
+ if (_loggedFormatBytes.Add(raw))
+ Console.WriteLine($"Diagnostic: new-engine texture format byte 0x{raw:X2} seen (normalizes to {NormalizeFormatCore(raw)}).");
+
+ return NormalizeFormatCore(raw);
+ }
+
+ private static TextureFormat NormalizeFormatCore(byte raw) => raw switch
+ {
+ 0x81 or 0xA1 or 0x01 => TextureFormat.R8,
+ 0x82 or 0xA2 or 0x04 => TextureFormat.A1R5G5B5,
+ 0x83 or 0xA3 => TextureFormat.RGBA4,
+ 0x84 or 0xA4 or 0x03 => TextureFormat.R5G6B5,
+ 0x85 or 0xA5 or 0x05 => TextureFormat.A8R8G8B8,
+ 0x86 or 0xA6 or 0x06 => TextureFormat.DXT1,
+ 0x87 or 0xA7 or 0x07 => TextureFormat.DXT3,
+ 0x88 or 0xA8 or 0x08 => TextureFormat.DXT5,
+ 0x8B or 0xAB or 0x0B => TextureFormat.G8B8,
+ 0x9A => TextureFormat.RGBA16F,
+ _ => (TextureFormat)0xFF, // unrecognized — Texture.ReadTexture must treat this as unreadable
+ };
+
+ private static bool FormatIsLinear(byte raw)
+ {
+ if (raw is >= 0xA0 and <= 0xAF) return true;
+ if (raw == 0x9A) return true;
+ var fmt = NormalizeFormatCore(raw);
+ return fmt is TextureFormat.DXT1 or TextureFormat.DXT3 or TextureFormat.DXT5;
+ }
+
public static TextureMetadataNew Read(StreamHelper sh) => FileUtils.ReadStructure(sh);
public byte[] ToBytes(bool isOld, params object[]? additionalParams) => throw new NotImplementedException();
diff --git a/ReLunacy.Engine/Loading/Textures/TextureMetadataOld.cs b/ReLunacy.Engine/Loading/Textures/TextureMetadataOld.cs
index 66e6f9d..58195d2 100644
--- a/ReLunacy.Engine/Loading/Textures/TextureMetadataOld.cs
+++ b/ReLunacy.Engine/Loading/Textures/TextureMetadataOld.cs
@@ -22,6 +22,13 @@ public record struct TextureMetadataOld : ILunaSerializable, ITextureMetadata
public readonly TextureFormat Format => (TextureFormat)((formatBitfield >> 8) & 0x0F);
public readonly ushort MipmapCount => mipmapCount;
+ // Bit 2 of formatBitfield, per ReLunacy-Ymir's CTexture (OldTextureReference doc comment:
+ // "shift 2 | bits 1 | if 1 then unswizzled (linear), ignored on DXT formats"). DXT/BC formats
+ // are always linear regardless of this bit, same as new engine.
+ public readonly bool IsLinear =>
+ Format is TextureFormat.DXT1 or TextureFormat.DXT3 or TextureFormat.DXT5 or TextureFormat.BC4 or TextureFormat.BC5
+ || ((formatBitfield >> 2) & 1) != 0;
+
// Unk1 (0x08-0x18) has never been decoded — it's raw, unexamined bytes. This struct's ID
// (0x5200) and total size (0x20) match InsomniaToolset's PS3 "NV4097_SET_*TEXTURE_*
// registry dump" Texture struct field-for-field where it's been verified (offset/numMips at
@@ -35,7 +42,19 @@ public record struct TextureMetadataOld : ILunaSerializable, ITextureMetadata
// nothing reads this for actual rendering decisions yet.
public readonly bool AlphaKillCandidate => Unk1 != null && Unk1.Length > 7 && (Unk1[7] & 0x20) != 0;
- public static TextureMetadataOld Read(StreamHelper sh) => FileUtils.ReadStructure(sh);
+ // One-shot diagnostic: which old-engine formatBitfield values actually appear in real level
+ // data, and whether the per-instance linear bit (bit 2) is ever set. Neither the format-code
+ // range nor that bit has been confirmed against real data before now — this settles both from
+ // the next level load instead of assuming the ReLunacy-Ymir port's decode is exhaustive.
+ private static readonly HashSet _loggedFormatBitfields = [];
+
+ public static TextureMetadataOld Read(StreamHelper sh)
+ {
+ var meta = FileUtils.ReadStructure(sh);
+ if (_loggedFormatBitfields.Add(meta.formatBitfield))
+ Console.WriteLine($"Diagnostic: old-engine texture formatBitfield 0x{meta.formatBitfield:X4} seen (format={meta.Format}, linearBit={((meta.formatBitfield >> 2) & 1) != 0}).");
+ return meta;
+ }
public byte[] ToBytes(bool isOld, params object[]? additionalParams) => throw new NotImplementedException();
}
diff --git a/ReLunacy.Engine/Rendering/AssetManager.cs b/ReLunacy.Engine/Rendering/AssetManager.cs
index a4e7448..925e9a6 100644
--- a/ReLunacy.Engine/Rendering/AssetManager.cs
+++ b/ReLunacy.Engine/Rendering/AssetManager.cs
@@ -32,6 +32,13 @@ public sealed class AssetManager : IDisposable
private bool _backfaceCulling;
private Effect? _vertexAlphaModelEffect;
+ // Veldrith's RasterizerStateDescription.DEFAULT assumes a clockwise front face; this game's
+ // meshes wind the opposite way, so using DEFAULT as-is culled the near side of every triangle
+ // and left the far side visible (backface culling looked "inside out" — confirmed by the user
+ // after enabling it). Same CullMode.Back as DEFAULT, just the winding flipped.
+ private static readonly RasterizerStateDescription BackfaceCullState = new(
+ FaceCullMode.Back, PolygonFillMode.Solid, FrontFace.CounterClockwise, true, false);
+
public IReadOnlyDictionary BuiltTextures => _textureCache;
public IReadOnlyDictionary SourceTextures => _sourceTextures;
@@ -119,7 +126,7 @@ public Material GetOrBuildMaterial(IMaterial material)
var bMat = new Material(
material.UsesVertexAlphaCandidate ? GetVertexAlphaModelEffect() : GlobalResource.DefaultModelEffect,
- _backfaceCulling ? RasterizerStateDescription.DEFAULT : RasterizerStateDescription.CULL_NONE,
+ _backfaceCulling ? BackfaceCullState : RasterizerStateDescription.CULL_NONE,
blendState,
renderMode);
@@ -164,7 +171,7 @@ public void SetBackfaceCulling(bool enabled)
if (_backfaceCulling == enabled) return;
_backfaceCulling = enabled;
- var state = enabled ? RasterizerStateDescription.DEFAULT : RasterizerStateDescription.CULL_NONE;
+ var state = enabled ? BackfaceCullState : RasterizerStateDescription.CULL_NONE;
foreach (var material in _materialCache.Values)
material.RasterizerState = state;
}
@@ -193,8 +200,9 @@ public Texture2D GetOrBuildTexture(ITexture texture)
return tex;
}
- // Geometry only carries positions/uvs/normals — tangents are derived here per-triangle
- // (standard UV-gradient method) since Moby/Tie meshes have no baked tangent data.
+ // Normals and tangents are decoded straight from the source vertex data (VertexFormat0/1's
+ // packed 11:11:10 words — see PackedNormal/GeometryMath) rather than derived here; GeometryData
+ // only falls back to UV-gradient derivation for formats that don't carry real data at all.
// useVertexAlpha: see Material.UsesVertexAlphaCandidate — when set, GetVertexAlphaCandidates()
// is written into each vertex's color alpha instead of the default fully-opaque white, and
// GetOrBuildMaterial picks a shader that actually reads it.
@@ -203,58 +211,24 @@ private static Vertex3D[] ConvertGeometryToVertices(IGeometry geometry, bool use
var positions = geometry.GetVertexPositions();
var uvs = geometry.GetTextureCoordinates();
var normals = geometry.GetNormals();
+ var tangents = geometry.GetTangents();
var vertexAlpha = useVertexAlpha ? geometry.GetVertexAlphaCandidates() : null;
- var indices = geometry.GetIndices();
int vertexCount = positions.Length / 3;
- var pos = new Vector3[vertexCount];
- var uv = new Vector2[vertexCount];
- var norm = new Vector3[vertexCount];
- for (int i = 0; i < vertexCount; i++)
- {
- pos[i] = new Vector3(positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2]);
- uv[i] = new Vector2(uvs[i * 2], uvs[i * 2 + 1]);
- norm[i] = normals != null && normals.Length >= i * 3 + 3
- ? new Vector3(normals[i * 3], normals[i * 3 + 1], normals[i * 3 + 2])
- : Vector3.Zero;
- }
-
- var tangentAccum = new Vector3[vertexCount];
- var bitangentAccum = new Vector3[vertexCount];
-
- for (int t = 0; t + 2 < indices.Length; t += 3)
- {
- uint i0 = indices[t], i1 = indices[t + 1], i2 = indices[t + 2];
- Vector3 edge1 = pos[i1] - pos[i0];
- Vector3 edge2 = pos[i2] - pos[i0];
- Vector2 duv1 = uv[i1] - uv[i0];
- Vector2 duv2 = uv[i2] - uv[i0];
-
- float det = duv1.X * duv2.Y - duv2.X * duv1.Y;
- if (MathF.Abs(det) < 1e-8f) continue;
-
- float r = 1.0f / det;
- Vector3 tangent = (edge1 * duv2.Y - edge2 * duv1.Y) * r;
- Vector3 bitangent = (edge2 * duv1.X - edge1 * duv2.X) * r;
-
- tangentAccum[i0] += tangent; tangentAccum[i1] += tangent; tangentAccum[i2] += tangent;
- bitangentAccum[i0] += bitangent; bitangentAccum[i1] += bitangent; bitangentAccum[i2] += bitangent;
- }
-
var vertices = new Vertex3D[vertexCount];
for (int i = 0; i < vertexCount; i++)
{
- Vector3 n = norm[i] != Vector3.Zero ? Vector3.Normalize(norm[i]) : Vector3.UnitY;
-
- Vector3 tan = tangentAccum[i] - n * Vector3.Dot(n, tangentAccum[i]);
- if (tan.LengthSquared() < 1e-12f)
- tan = MathF.Abs(n.Y) < 0.99f ? Vector3.Cross(Vector3.UnitY, n) : Vector3.Cross(Vector3.UnitX, n);
- tan = Vector3.Normalize(tan);
-
- float handedness = Vector3.Dot(Vector3.Cross(n, tan), bitangentAccum[i]) < 0f ? -1f : 1f;
+ var pos = new Vector3(positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2]);
+ var uv = new Vector2(uvs[i * 2], uvs[i * 2 + 1]);
+ var n = normals != null && normals.Length >= i * 3 + 3
+ ? new Vector3(normals[i * 3], normals[i * 3 + 1], normals[i * 3 + 2])
+ : Vector3.UnitY;
+ var tan = tangents != null && tangents.Length >= i * 4 + 4
+ ? new Vector4(tangents[i * 4], tangents[i * 4 + 1], tangents[i * 4 + 2], tangents[i * 4 + 3])
+ : new Vector4(1f, 0f, 0f, 1f);
float alpha = vertexAlpha != null && i < vertexAlpha.Length ? vertexAlpha[i] : 1f;
- vertices[i] = new Vertex3D(pos[i], uv[i], uv[i], n, new Vector4(tan, handedness), new Vector4(1f, 1f, 1f, alpha));
+ vertices[i] = new Vertex3D(pos, uv, uv, n, tan, new Vector4(1f, 1f, 1f, alpha));
}
return vertices;
diff --git a/ReLunacy.Engine/Rendering/Primitives.cs b/ReLunacy.Engine/Rendering/Primitives.cs
index d73eadf..0d15ea4 100644
--- a/ReLunacy.Engine/Rendering/Primitives.cs
+++ b/ReLunacy.Engine/Rendering/Primitives.cs
@@ -10,8 +10,14 @@ namespace ReLunacy.Engine.Rendering;
public static class Primitives
{
public static Mesh CreateCube(GraphicsDevice graphicsDevice, Material material, float size = 1.0f)
+ => CreateBox(graphicsDevice, material, new Vector3(size));
+
+ /// Same as but with independent X/Y/Z extents — every face's
+ /// normal/u/v is a single-axis unit vector, so scaling by a non-uniform half-extents vector
+ /// component-wise still lands each corner at the right axis-aligned offset.
+ public static Mesh CreateBox(GraphicsDevice graphicsDevice, Material material, Vector3 size)
{
- float h = size * 0.5f;
+ Vector3 h = size * 0.5f;
(Vector3 normal, Vector3 u, Vector3 v)[] faces =
[
@@ -49,4 +55,60 @@ public static Mesh CreateCube(GraphicsDevice graphicsDevice, Material
return new Mesh(graphicsDevice, material, new BasicMeshData(vertices, indices));
}
+
+ /// Builds a single box edge as actual triangle geometry instead of a GPU line
+ /// primitive — a pair of thin quads crossed in a "+" through the edge's centerline (one quad
+ /// thin along each of the two axes perpendicular to the edge), so it always presents real
+ /// screen-space area to a picking pass regardless of view angle, unlike a single flat quad
+ /// which can go edge-on and disappear. Same cross-section technique Replanetizer uses for
+ /// volume picking, but built as ONE shared unit-length edge (running -0.5 to +0.5 along local
+ /// X, constant thickness) meant to be GPU-instanced per real edge (12 per box) with a
+ /// per-instance Transform supplying that edge's actual length via non-uniform Scale.X — scale
+ /// is applied in local space before rotation (see Transform.GetMatrix()'s Scale*Rotation*
+ /// Translation order), so Scale.X always stretches along this mesh's own local length axis
+ /// regardless of how the instance is subsequently rotated to align with a real box edge. This
+ /// is what makes thickness constant across boxes of any size, since the thickness axes
+ /// (Y/Z) are never touched by that per-instance scale — see EntityVolume, which replaced its
+ /// old per-volume custom box mesh with instances of this one shared mesh.
+ public static Mesh CreateWireEdge(GraphicsDevice graphicsDevice, Material material, float thickness)
+ {
+ float t = MathF.Max(thickness, 0.001f) * 0.5f;
+
+ var vertices = new List(8);
+ var indices = new List(12);
+
+ void AddQuad(Vector3 axisThin, Vector3 normal)
+ {
+ Vector3 lengthExt = Vector3.UnitX * 0.5f;
+ Vector3 thinExt = axisThin * t;
+ Vector4 tangent = new(Vector3.UnitX, 1.0f);
+
+ uint baseIndex = (uint)vertices.Count;
+ vertices.Add(new Vertex3D(-lengthExt - thinExt, new(0, 1), new(0, 1), normal, tangent, Vector4.One));
+ vertices.Add(new Vertex3D(lengthExt - thinExt, new(1, 1), new(1, 1), normal, tangent, Vector4.One));
+ vertices.Add(new Vertex3D(lengthExt + thinExt, new(1, 0), new(1, 0), normal, tangent, Vector4.One));
+ vertices.Add(new Vertex3D(-lengthExt + thinExt, new(0, 0), new(0, 0), normal, tangent, Vector4.One));
+
+ // The two AddQuad calls below don't share a consistent (length, thin, normal)
+ // handedness — for one of them, UnitX x axisThin points opposite the declared
+ // `normal`. Flip the two triangles' winding in that case so the front face (by the
+ // standard CCW-from-outside convention) always actually faces `normal`, instead of
+ // silently depending on RasterizerState being CULL_NONE to hide the mismatch.
+ if (Vector3.Dot(Vector3.Cross(Vector3.UnitX, axisThin), normal) < 0f)
+ {
+ indices.Add(baseIndex + 0); indices.Add(baseIndex + 2); indices.Add(baseIndex + 1);
+ indices.Add(baseIndex + 0); indices.Add(baseIndex + 3); indices.Add(baseIndex + 2);
+ }
+ else
+ {
+ indices.Add(baseIndex + 0); indices.Add(baseIndex + 1); indices.Add(baseIndex + 2);
+ indices.Add(baseIndex + 0); indices.Add(baseIndex + 2); indices.Add(baseIndex + 3);
+ }
+ }
+
+ AddQuad(Vector3.UnitY, Vector3.UnitZ);
+ AddQuad(Vector3.UnitZ, Vector3.UnitY);
+
+ return new Mesh(graphicsDevice, material, new BasicMeshData([.. vertices], [.. indices]));
+ }
}
diff --git a/ReLunacy.Engine/Rendering/TextureUtils.cs b/ReLunacy.Engine/Rendering/TextureUtils.cs
index 6a9e671..db8ff21 100644
--- a/ReLunacy.Engine/Rendering/TextureUtils.cs
+++ b/ReLunacy.Engine/Rendering/TextureUtils.cs
@@ -46,6 +46,12 @@ public static class TextureUtils
private static readonly BlockDecoder Bc1Decoder = BlockDecoder.Create(BlockFormat.BC1);
private static readonly BlockDecoder Bc2Decoder = BlockDecoder.Create(BlockFormat.BC2);
private static readonly BlockDecoder Bc3Decoder = BlockDecoder.Create(BlockFormat.BC3);
+ // Unsigned variants: no confirmed case in this game's assets needs signed BC4/BC5 data, and
+ // ReconstructZ (which derives a normal map's Z from X/Y) isn't used here since this decode
+ // path is generic — it's shared by plain texture export too, where injecting a normal-map
+ // assumption into every BC5 texture would be wrong.
+ private static readonly BlockDecoder Bc4Decoder = BlockDecoder.Create(BlockFormat.BC4U);
+ private static readonly BlockDecoder Bc5Decoder = BlockDecoder.Create(BlockFormat.BC5U);
///
/// Decodes an ITexture's raw (possibly block-compressed) pixel data to a plain RGBA8888
@@ -65,10 +71,17 @@ public static class TextureUtils
return texture.Format switch
{
Assets.Interfaces.TextureFormat.R5G6B5 => RGB565ToRGBA8888(raw, width, height),
+ Assets.Interfaces.TextureFormat.A1R5G5B5 => A1RGB555ToRGBA8888(raw, width, height),
+ Assets.Interfaces.TextureFormat.RGBA4 => RGBA4444ToRGBA8888(raw, width, height),
Assets.Interfaces.TextureFormat.A8R8G8B8 => ARGB8888ToRGBA8888(raw, width, height),
+ Assets.Interfaces.TextureFormat.R8 => R8ToRGBA8888(raw, width, height),
+ Assets.Interfaces.TextureFormat.G8B8 => G8B8ToRGBA8888(raw, width, height),
+ Assets.Interfaces.TextureFormat.RGBA16F => RGBA16FToRGBA8888(raw, width, height),
Assets.Interfaces.TextureFormat.DXT1 => Bc1Decoder.Decode(width, height, raw),
Assets.Interfaces.TextureFormat.DXT3 => Bc2Decoder.Decode(width, height, raw),
Assets.Interfaces.TextureFormat.DXT5 => Bc3Decoder.Decode(width, height, raw),
+ Assets.Interfaces.TextureFormat.BC4 => Bc4Decoder.Decode(width, height, raw),
+ Assets.Interfaces.TextureFormat.BC5 => Bc5Decoder.Decode(width, height, raw),
_ => null,
};
}
@@ -144,6 +157,22 @@ public static Image ColourAsMain(this Image img, Colours colourFilter)
return img;
}
+ /// Reassembles a big-endian (disk-order) 16-bit pixel from a 2-byte source. All the
+ /// 16-bit format decoders below read from data already loaded as-is off disk (StreamHelper's
+ /// stream is big-endian, and Texture.Unswizzle/ReadTexture don't reorder bytes — see those for
+ /// why), so byte0 is always the high byte.
+ private static ushort ReadPixel16(byte[] rawData, int i) => (ushort)((rawData[i * 2] << 8) | rawData[i * 2 + 1]);
+
+ /// Expands an N-bit channel value to 8 bits by replicating its high bits into the low
+ /// bits (e.g. 5-bit 11111 -> 11111111, not 11111000) — the standard bit-replication expansion,
+ /// avoids the low end of the range never reaching full brightness/darkness.
+ private static byte Expand(int value, int bits) => (byte)((value << (8 - bits)) | (value >> (2 * bits - 8)));
+
+ // Previously computed R/B by right-shifting a 5-bit field into the top of an 8-bit channel
+ // with no expansion (max output ~0x1F, i.e. red/blue could never exceed ~12% brightness), and
+ // G by OR-ing an unshifted byte1 high-bits term against a shifted byte0 low-bits term — the
+ // two write to overlapping bit positions instead of adjacent ones, corrupting green on every
+ // pixel. Fixed by unpacking the full 16-bit word first, then expanding each channel properly.
public static byte[] RGB565ToRGBA8888(in byte[] rawData, int width, int height)
{
const int Rgb565Ps = 2;
@@ -156,12 +185,118 @@ public static byte[] RGB565ToRGBA8888(in byte[] rawData, int width, int height)
for (int i = 0; i < pixelCount; i++)
{
- result[i * Rgba8888Ps + 0] = (byte)((rawData[i * Rgb565Ps + 0] & 0b11111000) >> 3);
- result[i * Rgba8888Ps + 1] = (byte)((byte)((rawData[i * Rgb565Ps + 0] & 0b00000111) << 3) | (byte)(rawData[i * Rgb565Ps + 1] & 0b11100000));
- result[i * Rgba8888Ps + 2] = (byte)(rawData[i * Rgb565Ps + 1] & 0b00011111);
+ ushort px = ReadPixel16(rawData, i);
+ result[i * Rgba8888Ps + 0] = Expand((px >> 11) & 0x1F, 5);
+ result[i * Rgba8888Ps + 1] = Expand((px >> 5) & 0x3F, 6);
+ result[i * Rgba8888Ps + 2] = Expand(px & 0x1F, 5);
result[i * Rgba8888Ps + 3] = 0xFF;
}
return result;
}
+
+ /// Bit layout (MSB->LSB) A1 R5 G5 B5 — matches the format name and the equivalent
+ /// bare-Vulkan/D3D "A1R5G5B5" convention, not independently confirmed against real data.
+ public static byte[] A1RGB555ToRGBA8888(in byte[] rawData, int width, int height)
+ {
+ const int DstPs = 4;
+ int pixelCount = width * height;
+ byte[] result = new byte[pixelCount * DstPs];
+
+ for (int i = 0; i < pixelCount; i++)
+ {
+ ushort px = ReadPixel16(rawData, i);
+ result[i * DstPs + 0] = Expand((px >> 10) & 0x1F, 5);
+ result[i * DstPs + 1] = Expand((px >> 5) & 0x1F, 5);
+ result[i * DstPs + 2] = Expand(px & 0x1F, 5);
+ result[i * DstPs + 3] = (byte)(((px >> 15) & 0x1) * 0xFF);
+ }
+
+ return result;
+ }
+
+ /// Bit layout (MSB->LSB) R4 G4 B4 A4, following the format name's channel order —
+ /// unconfirmed against real data; if colors look swapped/tinted on a real RGBA4 texture, this
+ /// is the first thing to try reordering (e.g. to A4R4G4B4).
+ public static byte[] RGBA4444ToRGBA8888(in byte[] rawData, int width, int height)
+ {
+ const int DstPs = 4;
+ int pixelCount = width * height;
+ byte[] result = new byte[pixelCount * DstPs];
+
+ for (int i = 0; i < pixelCount; i++)
+ {
+ ushort px = ReadPixel16(rawData, i);
+ result[i * DstPs + 0] = Expand((px >> 12) & 0xF, 4);
+ result[i * DstPs + 1] = Expand((px >> 8) & 0xF, 4);
+ result[i * DstPs + 2] = Expand((px >> 4) & 0xF, 4);
+ result[i * DstPs + 3] = Expand(px & 0xF, 4);
+ }
+
+ return result;
+ }
+
+ /// Single 8-bit channel, replicated across R/G/B for a legible grayscale view (same
+ /// convention as ColourAsMain below) rather than left only in the red channel.
+ public static byte[] R8ToRGBA8888(in byte[] rawData, int width, int height)
+ {
+ const int DstPs = 4;
+ int pixelCount = width * height;
+ byte[] result = new byte[pixelCount * DstPs];
+
+ for (int i = 0; i < pixelCount; i++)
+ {
+ byte v = rawData[i];
+ result[i * DstPs + 0] = v;
+ result[i * DstPs + 1] = v;
+ result[i * DstPs + 2] = v;
+ result[i * DstPs + 3] = 0xFF;
+ }
+
+ return result;
+ }
+
+ /// byte0=G, byte1=B per the format name's order (commonly a 2-channel tangent-space
+ /// normal map XY pair in other engines, but that's not confirmed for this game) — unconfirmed
+ /// against real data, same caveat as RGBA4444ToRGBA8888.
+ public static byte[] G8B8ToRGBA8888(in byte[] rawData, int width, int height)
+ {
+ const int SrcPs = 2, DstPs = 4;
+ int pixelCount = width * height;
+ byte[] result = new byte[pixelCount * DstPs];
+
+ for (int i = 0; i < pixelCount; i++)
+ {
+ result[i * DstPs + 0] = 0;
+ result[i * DstPs + 1] = rawData[i * SrcPs + 0];
+ result[i * DstPs + 2] = rawData[i * SrcPs + 1];
+ result[i * DstPs + 3] = 0xFF;
+ }
+
+ return result;
+ }
+
+ /// 4x 16-bit half-float channels (RGBA), clamped to [0,1] and scaled to 8-bit since
+ /// the output target here is always an LDR buffer (GPU upload or PNG export) — HDR values
+ /// above 1.0 just clip rather than tone-map. Byte order matches ReadPixel16 (big-endian
+ /// disk-order halves).
+ public static byte[] RGBA16FToRGBA8888(in byte[] rawData, int width, int height)
+ {
+ const int DstPs = 4;
+ int pixelCount = width * height;
+ byte[] result = new byte[pixelCount * DstPs];
+
+ for (int i = 0; i < pixelCount; i++)
+ {
+ for (int c = 0; c < 4; c++)
+ {
+ int srcIdx = i * 8 + c * 2;
+ ushort halfBits = (ushort)((rawData[srcIdx] << 8) | rawData[srcIdx + 1]);
+ float value = (float)BitConverter.UInt16BitsToHalf(halfBits);
+ result[i * DstPs + c] = (byte)(Math.Clamp(value, 0f, 1f) * 0xFF);
+ }
+ }
+
+ return result;
+ }
}
diff --git a/ReLunacy.Engine/Scene/Entity.cs b/ReLunacy.Engine/Scene/Entity.cs
index c7552a9..5776268 100644
--- a/ReLunacy.Engine/Scene/Entity.cs
+++ b/ReLunacy.Engine/Scene/Entity.cs
@@ -49,8 +49,34 @@ protected Entity()
public abstract void Draw(IRenderer renderer, OutputDescription outputDescription, CommandList commandList, Cam3D camera, ImmediateRenderer immediateRenderer);
- /// Meshes to draw for GPU picking, all tagged with this entity's own ID — populated from the last Draw() call. A Moby's bangles/submeshes all resolve back to the one Moby entity.
- public IEnumerable GetPickableMeshes() => cachedRenderables.Select(r => r.Mesh);
+ /// Meshes to draw for GPU picking, each with its own already-fully-world-baked
+ /// transform, all tagged with this entity's own ID — populated from the last Draw() call. A
+ /// Moby's bangles/submeshes all resolve back to the one Moby entity. Reads each Renderable's
+ /// OWN Transform(s) rather than this entity's Transform directly: every entity (Moby/Tie/UFrag,
+ /// and EntityVolume's 12 separate per-edge Renderables) constructs each Renderable with the
+ /// exact Transform that Renderable should be drawn/picked at, so this is just trusting that
+ /// directly instead of recomputing/assuming it's always equal to Entity.Transform — which lets
+ /// an entity with more than one Renderable (like EntityVolume) report each one's real world
+ /// position instead of collapsing them all onto one shared matrix. GetTransforms() returns a
+ /// capacity-sized backing array (rounded up to a power of two, padded with default Transforms
+ /// past the real count) — InstanceCount is the actual number of live entries, hence the
+ /// explicit bound below rather than trusting the span's own length; this matters even for a
+ /// non-instanced single-transform Renderable in principle, and is essential the moment
+ /// anything in this codebase uses real GPU instancing (useInstancing: true) again. Materializes
+ /// into a List rather than using yield return because ReadOnlySpan<Transform> can't be
+ /// held live across a yield boundary.
+ public IEnumerable<(Bliss.CSharp.Geometry.Meshes.IMesh mesh, Matrix4x4 world)> GetPickableMeshes()
+ {
+ var results = new List<(Bliss.CSharp.Geometry.Meshes.IMesh, Matrix4x4)>();
+ foreach (var renderable in cachedRenderables)
+ {
+ var transforms = renderable.GetTransforms();
+ int count = (int)renderable.InstanceCount;
+ for (int i = 0; i < count; i++)
+ results.Add((renderable.Mesh, transforms[i].GetMatrix()));
+ }
+ return results;
+ }
public virtual void DrawBoundingSphere(OutputDescription outputDescription, CommandList commandList, ImmediateRenderer immediateRenderer)
{
diff --git a/ReLunacy.Engine/Scene/EntityCluster.cs b/ReLunacy.Engine/Scene/EntityCluster.cs
index fd007c3..fc8112b 100644
--- a/ReLunacy.Engine/Scene/EntityCluster.cs
+++ b/ReLunacy.Engine/Scene/EntityCluster.cs
@@ -50,10 +50,10 @@ public void Add(IUFrag ufrag, GraphicsDevice gd)
Entities.Add(new EntityUFrag(gd, ufrag, _assetManager));
}
- public void Add(Volume volume)
+ public void Add(Volume volume, GraphicsDevice gd)
{
TotalEntities++;
- Entities.Add(new EntityVolume(volume));
+ Entities.Add(new EntityVolume(volume, gd));
}
public bool TryGetEntity(int id, [NotNullWhen(true)] out Entity? entity)
diff --git a/ReLunacy.Engine/Scene/EntityManager.cs b/ReLunacy.Engine/Scene/EntityManager.cs
index 4f6f500..059c005 100644
--- a/ReLunacy.Engine/Scene/EntityManager.cs
+++ b/ReLunacy.Engine/Scene/EntityManager.cs
@@ -1,3 +1,4 @@
+using System.Numerics;
using Bliss.CSharp.Camera.Dim3;
using Bliss.CSharp.Graphics.Rendering.Renderers;
using Bliss.CSharp.Graphics.Rendering.Renderers.Forward;
@@ -20,6 +21,29 @@ public class EntityManager : IDisposable
public bool renderVolumes = true;
public bool renderBoundingSpheres = false;
public bool FrustumCullingEnabled = true;
+ /// Skip drawing Mobys past their in-game display distance (read from the level's own
+ /// gameplay data). Defaults OFF, unlike — the game only
+ /// relies on a short display distance because its camera stays near the player, but the
+ /// editor's free-fly camera has no such guarantee, so a real, fairly common in-game value
+ /// (many old-engine instances sit around 64 units) reads as "this Moby just isn't loading" the
+ /// moment the camera is anywhere else. Toggle on from the Render menu when specifically
+ /// checking what the game itself would render at the current camera position.
+ public bool MobyDistanceCullingEnabled = false;
+ /// Absolute world-unit thickness of the edge geometry EntityVolume builds (see
+ /// Primitives.CreateWireEdge) — the same for every volume regardless of its own size. This
+ /// same geometry is both the visible wireframe box and its own GPU pick target — a solid pick
+ /// hitbox would make clicking anywhere inside a (often large) volume select it instead of
+ /// whatever's actually behind the click, so picking is scoped to near the edges, same as what's
+ /// actually drawn. Kept on EntityManager rather than read directly from EditorSettings because
+ /// ReLunacy.Engine has no reference to the app project — View3D syncs this from
+ /// Program.Settings.VolumeWireThickness every frame, same pattern as Camera.FarPlane.
+ public float VolumeWireThickness = 0.1f;
+ /// RGBA (0-1 per channel, matching ImGui's ColorEdit4) tint for a Volume's wireframe
+ /// box, unselected/selected. Synced from Program.Settings by View3D every frame, same reason
+ /// and pattern as — EntityVolume converts these to Bliss's
+ /// byte-channel Color when (re)building its shared tint materials.
+ public Vector4 VolumeColor = new(1f, 1f, 0f, 1f);
+ public Vector4 VolumeSelectedColor = new(1f, 1f, 1f, 1f);
public int MobysCount => Regions.Sum(r => r.MobyInstances.Size);
public int VolumesCount => Regions.Sum(r => r.Volumes.Size);
diff --git a/ReLunacy.Engine/Scene/EntityMoby.cs b/ReLunacy.Engine/Scene/EntityMoby.cs
index e7ca03e..5b5e81f 100644
--- a/ReLunacy.Engine/Scene/EntityMoby.cs
+++ b/ReLunacy.Engine/Scene/EntityMoby.cs
@@ -18,6 +18,9 @@ public class EntityMoby : Entity
public override Vector4 BoundingSphere { get; set; }
+ /// In-game display distance for this instance (units), < 0 = unlimited. Read straight from the level's own gameplay data — see MobyInstanceOld/New.
+ public float DisplayDistance { get; }
+
public EntityMoby(IPlacedInstance mobyInstance, AssetManager assetManager)
{
BaseMoby = mobyInstance.Asset;
@@ -41,6 +44,8 @@ public EntityMoby(IPlacedInstance mobyInstance, AssetManager assetManager
var (center, radius) = BaseMoby.GetBoundingSphere();
BoundingSphere = new Vector4(center, radius);
+ DisplayDistance = mobyInstance.DisplayDistance;
+
Name = !string.IsNullOrEmpty(mobyInstance.Name) ? mobyInstance.Name.Split('/')[^1] : $"Moby_{BaseMoby.Id:X}_{mobyInstance.Group}";
assetManager.Mobys.TryGetValue(BaseMoby.Id, out var models);
@@ -55,6 +60,10 @@ public override void Draw(IRenderer renderer, OutputDescription outputDescriptio
var sphereCenter = new Vector3(sphere.X, sphere.Y, sphere.Z);
if (EntityManager.Singleton.FrustumCullingEnabled && !camera.GetFrustum().ContainsSphere(sphereCenter, sphere.W)) return;
+ // camera.Position is stored negated relative to world/entity positions (same convention
+ // used throughout the editor — see PropertyInspectorFrame's distance-to-entity readout).
+ if (EntityManager.Singleton.MobyDistanceCullingEnabled && DisplayDistance >= 0 && Vector3.Distance(sphereCenter, -camera.Position) > DisplayDistance) return;
+
if (Models is null) return;
if (EntityManager.Singleton.renderBoundingSpheres)
diff --git a/ReLunacy.Engine/Scene/EntityRegion.cs b/ReLunacy.Engine/Scene/EntityRegion.cs
index 0a4e7b3..d5c8e76 100644
--- a/ReLunacy.Engine/Scene/EntityRegion.cs
+++ b/ReLunacy.Engine/Scene/EntityRegion.cs
@@ -31,7 +31,7 @@ public EntityRegion(Region region, AssetManager assetManager, GraphicsDevice gd)
Volumes = new EntityCluster([], assetManager);
foreach (var volume in region.Volumes)
- Volumes.Add(volume);
+ Volumes.Add(volume, gd);
foreach (var zone in region.Zones)
Zones.Add(new EntityZone(zone, gd, assetManager));
diff --git a/ReLunacy.Engine/Scene/EntityVolume.cs b/ReLunacy.Engine/Scene/EntityVolume.cs
index fc61113..043860b 100644
--- a/ReLunacy.Engine/Scene/EntityVolume.cs
+++ b/ReLunacy.Engine/Scene/EntityVolume.cs
@@ -1,10 +1,17 @@
using System.Numerics;
+using Bliss.CSharp;
using Bliss.CSharp.Camera.Dim3;
using Bliss.CSharp.Colors;
+using Bliss.CSharp.Geometry.Meshes;
using Bliss.CSharp.Graphics.Rendering.Renderers;
using Bliss.CSharp.Graphics.Rendering.Renderers.Forward;
+using Bliss.CSharp.Graphics.VertexTypes;
+using Bliss.CSharp.Images;
+using Bliss.CSharp.Materials;
+using Bliss.CSharp.Textures;
using Bliss.CSharp.Transformations;
using ReLunacy.Engine.Assets.LevelElements;
+using ReLunacy.Engine.Rendering;
using Veldrith;
namespace ReLunacy.Engine.Scene;
@@ -14,24 +21,235 @@ public class EntityVolume : Entity
public readonly Volume BaseVolume;
public override Vector4 BoundingSphere { get; set; } = Vector4.Zero;
- public Vector3 scale;
+ /// The box's real (possibly non-uniform) half-extents source — kept separate from
+ /// Transform.Scale (always 1,1,1 for a Volume) because each edge instance takes this as an
+ /// explicit length rather than folding it into the placement transform. Only settable via
+ /// , which keeps BoundingSphere and the edge instances in sync with it —
+ /// never assign this field directly.
+ public Vector3 scale { get; private set; }
public override string Name { get; protected set; }
- public EntityVolume(Volume volume)
+ // Both entirely flat-color materials, tinted by the map's own Color — swapped on
+ // Renderable.Material directly when selection changes, no mesh rebuild needed. Static/shared
+ // since neither carries any per-instance state; built lazily since GlobalResource isn't
+ // guaranteed initialized before the first Volume loads otherwise. Colors are live-configurable
+ // (Editor Settings), so EnsureMaterialsCurrent() rebuilds these in place — and bumps
+ // _materialsVersion so every EntityVolume's Draw() knows to re-fetch its Renderable's Material
+ // reference, even one that isn't changing selection state this frame — whenever
+ // EntityManager's color fields drift from what's currently baked in.
+ private static Material? _unselectedMaterial;
+ private static Material? _selectedMaterial;
+ private static Vector4 _appliedVolumeColor = new(float.NaN);
+ private static Vector4 _appliedVolumeSelectedColor = new(float.NaN);
+ private static int _materialsVersion;
+
+ private static Material GetUnselectedMaterial(GraphicsDevice gd) { EnsureMaterialsCurrent(gd); return _unselectedMaterial!; }
+ private static Material GetSelectedMaterial(GraphicsDevice gd) { EnsureMaterialsCurrent(gd); return _selectedMaterial!; }
+
+ private static void EnsureMaterialsCurrent(GraphicsDevice gd)
+ {
+ var color = EntityManager.Singleton.VolumeColor;
+ var selectedColor = EntityManager.Singleton.VolumeSelectedColor;
+ if (_unselectedMaterial != null && color == _appliedVolumeColor && selectedColor == _appliedVolumeSelectedColor)
+ return;
+
+ _unselectedMaterial = BuildTintMaterial(gd, ToColor(color));
+ _selectedMaterial = BuildTintMaterial(gd, ToColor(selectedColor));
+ _appliedVolumeColor = color;
+ _appliedVolumeSelectedColor = selectedColor;
+ _materialsVersion++;
+ }
+
+ private static Color ToColor(Vector4 v) => new(
+ (byte)(Math.Clamp(v.X, 0f, 1f) * 255f),
+ (byte)(Math.Clamp(v.Y, 0f, 1f) * 255f),
+ (byte)(Math.Clamp(v.Z, 0f, 1f) * 255f),
+ (byte)(Math.Clamp(v.W, 0f, 1f) * 255f));
+
+ // GlobalResource.DefaultModelTexture — despite its name/every other comment in this file
+ // previously assuming it was white — is actually Bliss's own hardcoded 1x1 50% GRAY
+ // placeholder (confirmed via decompile: GlobalResource's static constructor builds it as
+ // `new Image(1, 1, Color.Gray)`). The fragment shader does texelColor * maps[0].color, so
+ // every volume tint was silently getting halved (pure Yellow (1,1,0) * Gray (0.5,0.5,0.5) =
+ // a dark olive yellow) — nothing to do with lighting or gamma, just multiplying against the
+ // wrong placeholder texture. Own dedicated solid-WHITE 1x1 texture instead, so the tint color
+ // is the only thing that reaches the framebuffer.
+ private static Texture2D? _whiteTexture;
+
+ private static Texture2D GetWhiteTexture(GraphicsDevice gd) => _whiteTexture ??= new Texture2D(gd, new Image(1, 1, Color.White));
+
+ private static Material BuildTintMaterial(GraphicsDevice gd, Color tint)
+ {
+ // Material's own default (RasterizerStateDescription.DEFAULT) back-face culls, and is
+ // never touched by AssetManager.SetBackfaceCulling (that only tracks materials it built
+ // itself) — these are edges, not opaque faces, so they should always draw from both sides
+ // regardless of the app-wide backface-culling setting.
+ var material = new Material(GlobalResource.DefaultModelEffect, RasterizerStateDescription.CULL_NONE);
+ material.AddMaterialMap(new MaterialMapKey(MaterialMapType.Albedo), 0, new MaterialMap(GetWhiteTexture(gd), color: tint));
+ return material;
+ }
+
+ // One shared unit-length edge mesh (see Primitives.CreateWireEdge), reused across 12 separate
+ // Renderables per volume — replaces the old approach of building a whole unique box mesh per
+ // volume. NOT GPU-instanced: GlobalResource.DefaultModelEffect (used by BuildTintMaterial
+ // below) is compiled by Bliss itself with no macros at all, so its shader's "#if
+ // USE_INSTANCING" branch never compiles in and always falls back to a single uTransformation
+ // uniform — which Renderable sets to Matrix4x4.Identity whenever UseInstancing is true,
+ // trusting the shader to use per-instance attributes instead. Through this effect, an
+ // instanced Renderable silently renders every vertex at local-space identity instead of world
+ // position (confirmed empirically: volumes stopped rendering entirely the one time this was
+ // tried). 12 non-instanced Renderables sharing one mesh is more draw calls but actually works.
+ // Only the edge's own thickness lives in this mesh's geometry; each edge's real
+ // length/position/orientation comes from its own per-instance Transform (see
+ // RecomputeEdgeTransforms), so rebuilding this mesh (thickness changed) never requires
+ // recomputing those.
+ private static Mesh? _sharedEdgeMesh;
+ private static float _appliedEdgeThickness = float.NaN;
+ private static int _edgeMeshVersion;
+
+ private static Mesh SharedEdgeMesh => _sharedEdgeMesh!;
+
+ private static void EnsureEdgeMeshCurrent(GraphicsDevice gd)
+ {
+ float thickness = EntityManager.Singleton.VolumeWireThickness;
+ if (_sharedEdgeMesh != null && thickness == _appliedEdgeThickness)
+ return;
+
+ _sharedEdgeMesh?.Dispose();
+ // Material passed here is never actually used for drawing — every Renderable built from
+ // this mesh uses the explicit-material constructor, which overrides it. Only matters for
+ // the vertex layout compatibility check Mesh does at construction.
+ _sharedEdgeMesh = Primitives.CreateWireEdge(gd, GetUnselectedMaterial(gd), thickness);
+ _appliedEdgeThickness = thickness;
+ _edgeMeshVersion++;
+ }
+
+ private readonly GraphicsDevice _gd;
+ private Transform[] _edgeTransforms = [];
+ private bool _drawnSelected;
+ private int _appliedMaterialsVersion = -1;
+ private int _appliedEdgeMeshVersion = -1;
+
+ public EntityVolume(Volume volume, GraphicsDevice gd)
{
BaseVolume = volume;
+ _gd = gd;
Name = !string.IsNullOrEmpty(volume.Name) ? volume.Name : $"Volume_{ID}";
- Matrix4x4.Decompose(volume.transform, out scale, out var rotation, out var position);
+ Matrix4x4.Decompose(volume.transform, out var initialScale, out var rotation, out var position);
Transform = new Transform { Translation = position, Rotation = rotation, Scale = Vector3.One };
+ SetScale(initialScale);
+ }
+
+ /// Sets and, in the same step, recomputes the local-space bounding
+ /// sphere and the 12 edge instances — the three always have to change together, so this is the
+ /// only way to change the volume's size (from the Property Inspector or otherwise). Rebuilds
+ /// synchronously rather than waiting for Draw()'s IsDirty check: View3D's picking reads
+ /// cachedRenderables directly, independent of Draw() (which may not even run this frame if the
+ /// volume is culled or Render > Volumes is off), so a stale entry could otherwise report the
+ /// pre-resize size for up to a frame.
+ public void SetScale(Vector3 newScale)
+ {
+ scale = newScale;
+
// BoundingSphere is LOCAL space per Entity's convention (offset from Transform.Translation)
// — center coincides with the volume's own position (zero local offset), and the radius is
// the distance from that center to the cube's furthest corner: the half-extents vector's
// length, since one corner sits at exactly (sx/2, sy/2, sz/2) from center.
BoundingSphere = new Vector4(Vector3.Zero, (scale / 2f).Length());
+
+ RebuildEdgeRenderable();
+ }
+
+ /// Recomputes the 12 per-edge local Transforms (position/orientation/length) from the
+ /// current — each edge is a unit-length instance of
+ /// stretched along its own local X (see
+ /// ) and placed at one of the box's 4 corners parallel to
+ /// that axis, same layout the old single-mesh CreateWireBox used.
+ private void RecomputeEdgeTransforms()
+ {
+ var edges = new Transform[12];
+ int i = 0;
+ AddAxisEdges(Vector3.UnitX, scale.X, Vector3.UnitY, scale.Y, Vector3.UnitZ, scale.Z, edges, ref i);
+ AddAxisEdges(Vector3.UnitY, scale.Y, Vector3.UnitX, scale.X, Vector3.UnitZ, scale.Z, edges, ref i);
+ AddAxisEdges(Vector3.UnitZ, scale.Z, Vector3.UnitX, scale.X, Vector3.UnitY, scale.Y, edges, ref i);
+ _edgeTransforms = edges;
+ }
+
+ private static readonly float[] Signs = [-1f, 1f];
+
+ private void AddAxisEdges(Vector3 axisLength, float lengthExtent, Vector3 axisB, float extentB, Vector3 axisC, float extentC, Transform[] edges, ref int i)
+ {
+ foreach (float sb in Signs)
+ {
+ foreach (float sc in Signs)
+ {
+ Vector3 localCenter = axisB * (sb * extentB * 0.5f) + axisC * (sc * extentC * 0.5f);
+ edges[i++] = ComposeEdgeTransform(axisLength, MathF.Max(lengthExtent, 0.0001f), localCenter);
+ }
+ }
+ }
+
+ /// Builds one edge's full WORLD Transform by composing its volume-local placement
+ /// (length/orientation/offset) with this volume's own Transform, matching the same
+ /// Matrix4x4.Decompose-based composition already used to derive the volume's own Transform
+ /// from its source data in the constructor. Matrix4x4.Decompose can theoretically fail on a
+ /// degenerate input (never expected here — this volume's own Transform.Scale is always
+ /// Vector3.One, so there's no shear/reflection to trip it up), in which case the edge falls
+ /// back to this volume's own placement with a zero local offset rather than leaving it at a
+ /// stale or default Transform.
+ private Transform ComposeEdgeTransform(Vector3 lengthAxis, float length, Vector3 localCenter)
+ {
+ var local = new Transform
+ {
+ // Scale is applied in local mesh space BEFORE rotation (see Transform.GetMatrix()'s
+ // Scale*Rotation*Translation order), so Scale.X always stretches SharedEdgeMesh's own
+ // local length axis regardless of the rotation below — this is what keeps the
+ // thickness axes (Y/Z, left at 1) constant no matter how long the edge is.
+ Scale = new Vector3(length, 1f, 1f),
+ Rotation = AlignUnitXTo(lengthAxis),
+ Translation = localCenter,
+ };
+
+ var combined = local.GetMatrix() * Transform.GetMatrix();
+ if (!Matrix4x4.Decompose(combined, out var decomposedScale, out var decomposedRotation, out var decomposedTranslation))
+ return new Transform { Translation = Transform.Translation, Rotation = Transform.Rotation };
+
+ return new Transform { Scale = decomposedScale, Rotation = decomposedRotation, Translation = decomposedTranslation };
+ }
+
+ /// Rotation aligning SharedEdgeMesh's local +X (its length axis) to point along
+ /// (always UnitX/UnitY/UnitZ). Only the axis LINE matters, not its
+ /// polarity — the edge mesh is symmetric about its own center and radially symmetric in
+ /// cross-section, so a +90°/-90° sign mismatch here would still produce an identical result.
+ private static Quaternion AlignUnitXTo(Vector3 axis)
+ {
+ if (axis == Vector3.UnitY) return Quaternion.CreateFromAxisAngle(Vector3.UnitZ, MathF.PI / 2f);
+ if (axis == Vector3.UnitZ) return Quaternion.CreateFromAxisAngle(Vector3.UnitY, -MathF.PI / 2f);
+ return Quaternion.Identity;
+ }
+
+ /// Rebuilds both the edge transforms and the 12 per-edge Renderables from scratch —
+ /// needed whenever the volume's own size changes. NOT GPU-instanced (see the class-level
+ /// comment on for why) — 12 small Renderables all pointing at
+ /// the one shared mesh, which is still cheap to reconstruct (no unique per-volume mesh data
+ /// involved, unlike the old approach).
+ private void RebuildEdgeRenderable()
+ {
+ EnsureEdgeMeshCurrent(_gd);
+ RecomputeEdgeTransforms();
+
+ cachedRenderables.Clear();
+ var material = selected ? GetSelectedMaterial(_gd) : GetUnselectedMaterial(_gd);
+ foreach (var edgeTransform in _edgeTransforms)
+ cachedRenderables.Add(new Renderable(SharedEdgeMesh, edgeTransform, material));
+ IsDirty = false;
+ _drawnSelected = selected;
+ _appliedMaterialsVersion = _materialsVersion;
+ _appliedEdgeMeshVersion = _edgeMeshVersion;
}
public override void Draw(IRenderer renderer, OutputDescription outputDescription, CommandList commandList, Cam3D camera, ImmediateRenderer immediateRenderer)
@@ -51,7 +269,60 @@ public override void Draw(IRenderer renderer, OutputDescription outputDescriptio
if (EntityManager.Singleton.renderBoundingSpheres)
DrawBoundingSphere(outputDescription, commandList, immediateRenderer);
- immediateRenderer.DrawCubeWires(Transform, scale, selected ? Color.White : Color.DarkYellow);
+ // Must run before reading the version fields below: these are what actually rebuild the
+ // shared static materials/edge mesh in place if their configured values changed, and bump
+ // the corresponding version counter. Called unconditionally (not just from the getters) so
+ // a volume whose selection state isn't changing this frame still notices a color/thickness
+ // change on the very frame it happens, rather than only whenever some other volume's
+ // Draw() happens to touch a getter first.
+ EnsureMaterialsCurrent(_gd);
+ EnsureEdgeMeshCurrent(_gd);
+ bool materialsChanged = _appliedMaterialsVersion != _materialsVersion;
+ bool edgeMeshChanged = _appliedEdgeMeshVersion != _edgeMeshVersion;
+
+ if (IsDirty)
+ {
+ // Volume moved/rotated (Transform's setter sets IsDirty) — the 12 edge transforms are
+ // derived from Transform, so they need recomputing, not just a material/mesh swap.
+ RebuildEdgeRenderable();
+ }
+ else if (edgeMeshChanged)
+ {
+ // Only the shared mesh reference changed (thickness setting) — Renderable.Mesh has no
+ // setter, so new Renderables are still needed, but the existing edge transforms are
+ // still correct (thickness never factors into them) and don't need recomputing.
+ cachedRenderables.Clear();
+ var material = selected ? GetSelectedMaterial(_gd) : GetUnselectedMaterial(_gd);
+ foreach (var edgeTransform in _edgeTransforms)
+ cachedRenderables.Add(new Renderable(SharedEdgeMesh, edgeTransform, material));
+ _drawnSelected = selected;
+ _appliedMaterialsVersion = _materialsVersion;
+ _appliedEdgeMeshVersion = _edgeMeshVersion;
+ }
+ else if (_drawnSelected != selected || materialsChanged)
+ {
+ // Selection (or the shared tint materials themselves) changed without a geometry
+ // rebuild — swap the material reference directly on all 12 (Renderable.Material has a
+ // public setter that flags its own GPU buffer dirty), no need to touch the mesh or
+ // cachedRenderables list itself. View3D skips its generic selection-outline pass for
+ // Volumes entirely (see View3D.Render) in favor of this — the outline technique
+ // inflates along vertex normals and expects one closed mesh, which the 12 disjoint
+ // edge instances are not.
+ var material = selected ? GetSelectedMaterial(_gd) : GetUnselectedMaterial(_gd);
+ foreach (var renderable in cachedRenderables)
+ renderable.Material = material;
+ _drawnSelected = selected;
+ _appliedMaterialsVersion = _materialsVersion;
+ }
+
+ foreach (var renderable in cachedRenderables)
+ renderer.DrawRenderable(renderable);
+
EntitiesRenderedThisFrame++;
}
+
+ public override void Dispose()
+ {
+ GC.SuppressFinalize(this);
+ }
}
diff --git a/ReLunacy.sln b/ReLunacy.sln
index b3bf360..703567a 100644
--- a/ReLunacy.sln
+++ b/ReLunacy.sln
@@ -7,9 +7,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
ProjectSection(SolutionItems) = preProject
.gitignore = .gitignore
CHANGELOG.md = CHANGELOG.md
- LICENSE-LUNALIB.txt = LICENSE-LUNALIB.txt
- LICENSE-RELUNACY.txt = LICENSE-RELUNACY.txt
README.md = README.md
+ LICENSE = LICENSE
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "submodules", "submodules", "{BE399D72-C406-4FDB-A18C-FFEBFB2324C2}"
diff --git a/ReLunacy/Core/Frames/DockedFrames/AssetViewer.cs b/ReLunacy/Core/Frames/DockedFrames/AssetViewer.cs
index a73ca41..46b12dd 100644
--- a/ReLunacy/Core/Frames/DockedFrames/AssetViewer.cs
+++ b/ReLunacy/Core/Frames/DockedFrames/AssetViewer.cs
@@ -76,6 +76,7 @@ public class AssetViewer : DockedFrame, ILevelListener
public Vector2 RenderFramePos { get; private set; }
public Vector2 MousePos { get; private set; }
public MouseGrabHandler rmbghandler = new() { mouseButton = Bliss.CSharp.Interact.Mice.MouseButton.Right };
+ public MouseGrabHandler mmbghandler = new() { mouseButton = Bliss.CSharp.Interact.Mice.MouseButton.Middle };
private readonly GraphicsDevice graphicsDevice;
private RenderTexture2D renderTexture;
private readonly IRenderer renderer;
@@ -655,7 +656,10 @@ protected override void Render(double deltaTime)
HorizontalSplitter("##split_preview", ref previewHeight, rightWidth);
- ImGui.Text($"{RenderFrameSize.Width}x{RenderFrameSize.Height} - Distance to origin: {Camera.Position.Length()}m");
+ // Distance to Target (the orbit pivot), not Camera.Position.Length() (distance to world
+ // zero) — those were the same thing before middle-click pan could move Target away from
+ // Vector3.Zero, but "distance to origin" now means "distance to wherever the pivot is."
+ ImGui.Text($"{RenderFrameSize.Width}x{RenderFrameSize.Height} - Distance to target: {Vector3.Distance(Camera.Position, Camera.Target)}m");
ImGui.Separator();
// Lower part split vertically: asset info/shaders/export on the left (unchanged content),
@@ -848,7 +852,7 @@ private void Tick(double deltaTime)
Point absMousePos = new((int)windowMousePos.X, (int)windowMousePos.Y);
bool isHoveringWnd = ImGui.IsWindowHovered();
bool isMouseInCntReg = RenderFrameSize.Contains(absMousePos);
- CheckRotationInput(isMouseInCntReg);
+ CheckCameraDragInput(isMouseInCntReg);
// Scroll-zoom is independent of the RMB rotate-drag and gated purely on hovering the
// render image, not "anywhere in the window" — otherwise scrolling while reading the
@@ -1118,27 +1122,75 @@ private static void HorizontalSplitter(string id, ref float height, float width)
height += ImGui.GetIO().MouseDelta.Y;
}
- private void CheckRotationInput(bool allowGrab)
+ // Tracks the previous frame's drag state so the shared NoMouse flag (below) is only touched
+ // on a rising/falling edge, not every frame.
+ private bool wasDragging;
+
+ /// RMB drags orbit (rotates Position around the fixed Target); MMB drags pan (moves
+ /// Position and Target together, so the orbit origin itself relocates instead of just
+ /// spinning around it). Both share one method rather than two independent ones because they
+ /// also share the ImGuiConfigFlags.NoMouse relative-mouse-mode flag: two separate methods each
+ /// unconditionally setting/clearing that flag would have the second one clobber whatever the
+ /// first just set whenever only one of the two buttons is actually held.
+ private void CheckCameraDragInput(bool allowGrab)
{
ImGuiIOPtr io = ImGui.GetIO();
- if (rmbghandler.TryGrabMouse(allowGrab))
- {
+ bool rotating = rmbghandler.TryGrabMouse(allowGrab);
+ bool panning = mmbghandler.TryGrabMouse(allowGrab);
+ bool isDragging = rotating || panning;
+
+ // Edge-triggered, not level-triggered: NoMouse is also written by View3D's own drag
+ // handling (same relative-mouse-mode pattern, different viewport). Unconditionally
+ // clearing it every frame this viewport has nothing grabbed — what this used to do — would
+ // cut off a drag in progress over there if both frames tick within the same pass.
+ if (isDragging && !wasDragging)
io.ConfigFlags |= ImGuiConfigFlags.NoMouse;
- }
- else
- {
+ else if (!isDragging && wasDragging)
io.ConfigFlags &= ~ImGuiConfigFlags.NoMouse;
- return;
- }
+ wasDragging = isDragging;
+
+ if (!isDragging) return;
- Vector2 rot = Input.GetMouseDelta();
- rot *= Program.Settings.CamSensivity;
+ Vector2 delta = Input.GetMouseDelta();
- // rotateAroundTarget: true swings Position around the fixed Target (real orbit).
- // false — what this used to pass — keeps Position fixed and swings Target instead,
- // which is FPS-style look, not an orbit; that's why this never actually orbited.
- Camera.SetPitch(Camera.GetPitch() - rot.Y, true);
- Camera.SetYaw(Camera.GetYaw() - rot.X, true);
+ if (rotating)
+ {
+ Vector2 rot = delta * Program.Settings.CamSensivity;
+
+ // rotateAroundTarget: true swings Position around the fixed Target (real orbit).
+ // false — what this used to pass — keeps Position fixed and swings Target instead,
+ // which is FPS-style look, not an orbit; that's why this never actually orbited.
+ Camera.SetPitch(Camera.GetPitch() - rot.Y, true);
+ Camera.SetYaw(Camera.GetYaw() - rot.X, true);
+ }
+
+ if (panning)
+ {
+ // Screen-pixel delta -> world-space delta at the orbit target's own depth (same
+ // perspective back-solve as WorldScaleForPixelRadius, without that method's
+ // billboard-specific 0.005 constant), so the point under the cursor at drag-start
+ // stays roughly under the cursor while dragging, matching typical middle-click-pan
+ // tools.
+ float distance = Vector3.Distance(Camera.Position, Camera.Target);
+ float fovYRad = Camera.Fov * (MathF.PI / 180f);
+ float worldUnitsPerPixel = 2f * distance * MathF.Tan(fovYRad * 0.5f) / Math.Max(1, RenderFrameSize.Height);
+
+ // Built by hand instead of Cam3D.MoveRight/MoveUp: those use GetRight() = Cross(Forward,
+ // Up) and the raw Up field directly, neither of which is normalized — Up drifts and
+ // isn't guaranteed orthogonal to Forward after SetPitch/SetRoll, so pan speed would
+ // vary with pitch (shrinking toward zero looking straight up/down) and drift over time.
+ // right/up here are a proper orthonormal basis for the current view.
+ Vector3 forward = Camera.GetForward();
+ Vector3 right = Vector3.Normalize(Vector3.Cross(forward, Camera.Up));
+ Vector3 up = Vector3.Normalize(Vector3.Cross(right, forward));
+
+ // Signs make the dragged point track the cursor (drag right -> content follows right,
+ // i.e. camera moves left; drag down -> content follows down, i.e. camera moves up) —
+ // not runtime-verified; if the pan feels inverted, flip both signs here.
+ Vector3 shift = right * (-delta.X * worldUnitsPerPixel) + up * (delta.Y * worldUnitsPerPixel);
+ Camera.Position += shift;
+ Camera.Target += shift;
+ }
}
private void UpdateWindowSize()
diff --git a/ReLunacy/Core/Frames/DockedFrames/EditorSettingsFrame.cs b/ReLunacy/Core/Frames/DockedFrames/EditorSettingsFrame.cs
index 92740f8..b0327f6 100644
--- a/ReLunacy/Core/Frames/DockedFrames/EditorSettingsFrame.cs
+++ b/ReLunacy/Core/Frames/DockedFrames/EditorSettingsFrame.cs
@@ -68,6 +68,17 @@ protected override void Render(double deltaTime)
string langCode = LM.Languages.Values.ElementAt(selectedLanguage).LangCode;
LM.TrySetLanguage(langCode);
}
+ if (ImGui.BeginCombo(LM.Get("GUI_Frame_EditorSettings_UpdateChannel"), Program.Settings.UpdateChannel.ToString()))
+ {
+ foreach (var channel in Enum.GetValues())
+ {
+ if (ImGui.Selectable($"\t {channel}", channel == Program.Settings.UpdateChannel))
+ Program.Settings.UpdateChannel = channel;
+ }
+ ImGui.EndCombo();
+ }
+ ImGui.SameLine();
+ ImGuiPlus.HelpMarker(LM.Get("GUI_Frame_EditorSettings_UpdateChannelHelp"));
if (ImGui.CollapsingHeader(LM.Get("GUI_Common_AdvancedCollapsed")))
{
ImGui.Text(LM.Get("GUI_Frame_EditorSettings_CustomShadersPlaceholder"));
@@ -84,6 +95,12 @@ protected override void Render(double deltaTime)
ImGui.InputFloat(LM.Get("GUI_Frame_EditorSettings_GizmoSnapTranslation"), ref Program.Settings.GizmoSnapTranslation, 0.1f, 1.0f, "%.3fm");
ImGui.InputFloat(LM.Get("GUI_Frame_EditorSettings_GizmoSnapRotation"), ref Program.Settings.GizmoSnapRotation, 1.0f, 15.0f, "%.3f°");
ImGui.InputFloat(LM.Get("GUI_Frame_EditorSettings_GizmoSnapScale"), ref Program.Settings.GizmoSnapScale, 0.05f, 0.25f, "%.3f");
+ ImGui.SliderFloat(LM.Get("GUI_Frame_EditorSettings_VolumeWireThickness"), ref Program.Settings.VolumeWireThickness, 0.01f, 5f, "%.2f", ImGuiSliderFlags.AlwaysClamp);
+ ImGui.SameLine();
+ ImGuiPlus.HelpMarker(LM.Get("GUI_Frame_EditorSettings_VolumeWireThicknessHelp"));
+ ImGui.ColorEdit4(LM.Get("GUI_Frame_EditorSettings_VolumeColor"), ref Program.Settings.VolumeColor);
+ ImGui.ColorEdit4(LM.Get("GUI_Frame_EditorSettings_VolumeSelectedColor"), ref Program.Settings.VolumeSelectedColor);
+ ImGui.ColorEdit4(LM.Get("GUI_Frame_EditorSettings_SelectionOutlineColor"), ref Program.Settings.SelectionOutlineColor);
ImGui.EndGroup();
ImGui.EndTabItem();
}
diff --git a/ReLunacy/Core/Frames/DockedFrames/PropertyInspectorFrame.cs b/ReLunacy/Core/Frames/DockedFrames/PropertyInspectorFrame.cs
index 9ad9dc0..b223ed6 100644
--- a/ReLunacy/Core/Frames/DockedFrames/PropertyInspectorFrame.cs
+++ b/ReLunacy/Core/Frames/DockedFrames/PropertyInspectorFrame.cs
@@ -67,9 +67,19 @@ protected override void Render(double deltaTime)
}
if (ImGui.InputFloat3(LM.Get("GUI_Frame_InstanceInspector_Scale"), ref selectedScale, "%.3f"))
{
- var t = SelectedEntity.Transform;
- t.Scale = selectedScale;
- SelectedEntity.Transform = t;
+ // EntityVolume keeps its real box size in its own `scale` field rather than
+ // Transform.Scale (which it always leaves at 1,1,1 — see EntityVolume's constructor
+ // comment) — writing to Transform.Scale here for a Volume would silently do nothing.
+ if (SelectedEntity is EntityVolume volume)
+ {
+ volume.SetScale(selectedScale);
+ }
+ else
+ {
+ var t = SelectedEntity.Transform;
+ t.Scale = selectedScale;
+ SelectedEntity.Transform = t;
+ }
}
ImGui.SeparatorText(LM.Get("GUI_Frame_InstanceInspector_RenderingCategory"));
@@ -101,6 +111,15 @@ protected override void Render(double deltaTime)
ImGui.Text(LM.Get("GUI_Frame_InstanceInspector_MaterialAlphaClip", mat.AlphaClipThreshold));
ImGui.Text(LM.Get("GUI_Frame_InstanceInspector_MaterialAlbedoFormat", mat.AlbedoTexture?.Format.ToString() ?? "None"));
}
+ else if (SelectedEntity is EntityVolume volumeEntity)
+ {
+ // Volumes carry nothing beyond a transform in the level format itself — old engine has
+ // no ID/group at all (BaseVolume.Id is just its load-order index there), new engine adds
+ // a TUID + zone group from gp_prius's instance metadata section. This is genuinely all
+ // there is to show; see RegionReader.ReadVolumesOld/New.
+ ImGui.Text(LM.Get("GUI_Frame_InstanceInspector_VolumeId", volumeEntity.BaseVolume.Id));
+ ImGui.Text(LM.Get("GUI_Frame_InstanceInspector_VolumeGroup", volumeEntity.BaseVolume.group));
+ }
ImGui.Separator();
@@ -176,7 +195,7 @@ private void UpdateEntity(Entity? oldSelection, Entity? newSelection)
selectedPosition = SelectedEntity.Transform.Translation;
selectedAngle = SelectedEntity.Transform.Rotation.ToEuler() * (180f / MathF.PI);
- selectedScale = SelectedEntity.Transform.Scale;
+ selectedScale = SelectedEntity is EntityVolume volume ? volume.scale : SelectedEntity.Transform.Scale;
selectedBSphere = SelectedEntity.BoundingSphere.GetXYZ();
selectedBSphereRadius = SelectedEntity.BoundingSphere.W;
}
diff --git a/ReLunacy/Core/Frames/DockedFrames/TexturesExplorer.cs b/ReLunacy/Core/Frames/DockedFrames/TexturesExplorer.cs
index abae7dc..084a950 100644
--- a/ReLunacy/Core/Frames/DockedFrames/TexturesExplorer.cs
+++ b/ReLunacy/Core/Frames/DockedFrames/TexturesExplorer.cs
@@ -159,6 +159,14 @@ public bool SelectTexture(ulong textureId)
return true;
}
+ // Texture names come straight from the game's own string tables, which for some formats
+ // (e.g. new-engine shader-referenced texture names) are full slash-delimited asset paths,
+ // not bare filenames — writing that as-is into Path.Combine either creates unwanted nested
+ // directories under Extracted/ or fails outright. Keep only the last path segment, and fall
+ // back to the texture's index (not e.g. "unnamed") when it has no name at all.
+ private static string GetExportFileName(string? textureName, int index) =>
+ string.IsNullOrEmpty(textureName) ? $"Tex_{index}" : textureName.Split('/')[^1];
+
private static bool MaterialUsesTexture(IMaterial mat, ulong textureId) =>
mat.AlbedoTexture?.Id == textureId ||
mat.NormalTexture?.Id == textureId ||
@@ -396,7 +404,7 @@ protected override void Render(double deltaTime)
if (!Directory.Exists(path))
Directory.CreateDirectory(path);
- File.WriteAllBytes(Path.Combine(path, selection.TextureName != null ? selection.TextureName + ".raw" : $"Tex_{selectedTexture}.raw"), selection.Texture.GetPixelData());
+ File.WriteAllBytes(Path.Combine(path, GetExportFileName(selection.TextureName, selectedTexture) + ".raw"), selection.Texture.GetPixelData());
}
ImGui.SameLine();
if(ImGui.Button(LM.Get("GUI_Frame_TextureExplorer_Preview_ExportPNG")))
@@ -406,7 +414,7 @@ protected override void Render(double deltaTime)
Directory.CreateDirectory(path);
var clone = (Image)selection.BlissTexture.Images[0].Clone();
- clone.SaveAsPng(Path.Combine(path, selection.TextureName != null ? selection.TextureName + ".png" : $"Tex_{selectedTexture}.png"));
+ clone.SaveAsPng(Path.Combine(path, GetExportFileName(selection.TextureName, selectedTexture) + ".png"));
}
ImGui.Separator();
if (ImGui.Button(LM.Get("GUI_Frame_TextureExplorer_Preview_FindUsages")))
diff --git a/ReLunacy/Core/Frames/DockedFrames/View3D.cs b/ReLunacy/Core/Frames/DockedFrames/View3D.cs
index fdccb0d..f9a4f2c 100644
--- a/ReLunacy/Core/Frames/DockedFrames/View3D.cs
+++ b/ReLunacy/Core/Frames/DockedFrames/View3D.cs
@@ -79,6 +79,11 @@ protected override void Render(double deltaTime)
// keeping them in sync here is enough — no separate recompute needed.
Camera.Fov = Program.Settings.CamFOV;
Camera.FarPlane = Program.Settings.RenderDistance;
+ // EntityManager (ReLunacy.Engine) has no reference to Program.Settings (app-layer) — see
+ // EntityManager.VolumeWireThickness's own comment.
+ EntityManager.Singleton.VolumeWireThickness = Program.Settings.VolumeWireThickness;
+ EntityManager.Singleton.VolumeColor = Program.Settings.VolumeColor;
+ EntityManager.Singleton.VolumeSelectedColor = Program.Settings.VolumeSelectedColor;
UpdateWindowSize();
Tick(deltaTime);
@@ -99,14 +104,18 @@ protected override void Render(double deltaTime)
// Drawn after the main opaque pass (not from inside Entity.Draw) since the inflated-hull
// outline technique needs real scene depth already written to correctly clip to the rim.
- if (SelectedEntity != null)
+ // Volumes opt out entirely: EntityVolume already recolors its own wireframe box on
+ // selection (see its Draw()), and the inflated-hull technique expects one closed mesh —
+ // a volume's pick/wire mesh is 12 disjoint GPU-instanced edges, not a closed surface, so
+ // inflating along vertex normals would produce a patchy, disconnected-looking rim instead
+ // of a clean outline.
+ if (SelectedEntity != null && SelectedEntity is not EntityVolume)
{
- var world = SelectedEntity.Transform.GetMatrix();
- var entries = SelectedEntity.GetPickableMeshes().Select(mesh => (mesh, world));
+ var entries = SelectedEntity.GetPickableMeshes();
selectionOutlineRenderer.DrawOutline(
commandList, renderTexture.Framebuffer.OutputDescription,
Camera.GetView() * Camera.GetProjection(), entries,
- new Vector4(1f, 0.65f, 0f, 1f));
+ Program.Settings.SelectionOutlineColor);
}
immediateRenderer.End();
@@ -201,9 +210,8 @@ private void PickEntityUnderCursor()
var entities = EntityManager.Singleton.AllEntities().ToList();
var entries = entities.SelectMany(e =>
{
- var world = e.Transform.GetMatrix();
uint id = (uint)e.ID;
- return e.GetPickableMeshes().Select(mesh => (mesh, world, id));
+ return e.GetPickableMeshes().Select(pm => (pm.mesh, pm.world, id));
});
uint hitId;
diff --git a/ReLunacy/Core/Frames/Modals/LevelExportModal.cs b/ReLunacy/Core/Frames/Modals/LevelExportModal.cs
index c43a7ad..56a147f 100644
--- a/ReLunacy/Core/Frames/Modals/LevelExportModal.cs
+++ b/ReLunacy/Core/Frames/Modals/LevelExportModal.cs
@@ -1,5 +1,6 @@
using System.Numerics;
using ReLunacy.Engine.Export;
+using ReLunacy.Engine.Games;
using ReLunacy.Engine.Scene;
using ReLunacy.Utility;
using ReLunacy.Utility.Localization;
@@ -49,7 +50,7 @@ protected override void Render(double deltaTime)
private void StartExport()
{
- string levelName = ExportPaths.SanitizeFileName(Path.GetFileName(Program.ProvidedPath.TrimEnd(Path.DirectorySeparatorChar)));
+ string levelName = ExportPaths.SanitizeFileName(GameLibraryScanner.GetLevelNameFromPath(Program.ProvidedPath));
string directory = Path.Combine(Program.EditorPath, "Exported", "Levels");
string path = Path.Combine(directory, $"{levelName}.glb");
var options = new LevelExportOptions(exportMobys, exportTies, exportUFrags);
diff --git a/ReLunacy/Core/Frames/Modals/UpdateInfoFrame.cs b/ReLunacy/Core/Frames/Modals/UpdateInfoFrame.cs
new file mode 100644
index 0000000..f43625f
--- /dev/null
+++ b/ReLunacy/Core/Frames/Modals/UpdateInfoFrame.cs
@@ -0,0 +1,62 @@
+using System.Diagnostics;
+using System.Numerics;
+using ReLunacy.Utility;
+using ReLunacy.Utility.Localization;
+
+namespace ReLunacy.Core.Frames.Modals;
+
+/// Shown when UpdateChecker finds a newer release than the one currently running, on
+/// either update channel (see EditorSettings.UpdateChannel). isNightly changes the wording since
+/// nightly builds don't carry a clean version number, just a commit-hash-and-date identity baked
+/// into the release asset's filename by .github/workflows/nightly.yml.
+public class UpdateInfoFrame : Modal
+{
+ protected override ImGuiWindowFlags WindowFlags { get; set; } = ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoDocking;
+
+ private readonly string link;
+ private readonly string newVersionLabel;
+ private readonly DateTime releaseDate;
+ private readonly bool isNightly;
+
+ public UpdateInfoFrame(string url, string newVersionLabel, DateTime releaseDate, bool isNightly = false)
+ {
+ FrameName = LM.Get("GUI_Frame_UpdateInfo_Title");
+ link = url;
+ this.newVersionLabel = newVersionLabel;
+ this.releaseDate = releaseDate;
+ this.isNightly = isNightly;
+ }
+
+ protected override void Render(double deltaTime)
+ {
+ ImGui.TextWrapped(LM.Get(isNightly ? "GUI_Frame_UpdateInfo_NightlyAvailable" : "GUI_Frame_UpdateInfo_StableAvailable"));
+
+ ImGui.Text(LM.Get("GUI_Frame_UpdateInfo_CurrentVersion"));
+ ImGui.SameLine();
+ ImGui.TextColored(new Vector4(0xA0 / 255f, 0xA0 / 255f, 0x24 / 255f, 1f), $"v{ProgramInfo.Version}");
+
+ ImGui.Text(LM.Get(isNightly ? "GUI_Frame_UpdateInfo_NewBuild" : "GUI_Frame_UpdateInfo_NewVersion"));
+ ImGui.SameLine();
+ ImGui.TextColored(new Vector4(0x24 / 255f, 1f, 0x24 / 255f, 1f), isNightly ? newVersionLabel : $"v{newVersionLabel}");
+
+ ImGui.Spacing();
+ var diff = DateTime.Now - releaseDate;
+ string ago = diff.TotalDays >= 1
+ ? LM.Get("GUI_Frame_UpdateInfo_DaysAgo", (int)diff.TotalDays)
+ : diff.TotalHours >= 1
+ ? LM.Get("GUI_Frame_UpdateInfo_HoursAgo", (int)diff.TotalHours)
+ : LM.Get("GUI_Frame_UpdateInfo_MinutesAgo", (int)diff.TotalMinutes);
+ ImGui.Text(LM.Get("GUI_Frame_UpdateInfo_Released", ago, releaseDate.ToString("dd/MM/yyyy HH:mm:ss")));
+
+ ImGui.Spacing();
+ ImGui.Separator();
+ ImGui.Spacing();
+
+ if (ImGuiPlus.CenteredButton(LM.Get("GUI_Frame_UpdateInfo_Download"), new Vector2(150, 40)))
+ Process.Start(new ProcessStartInfo(link) { UseShellExecute = true });
+
+ ImGui.SameLine();
+ if (ImGui.Button(LM.Get("GUI_Common_CloseWord")))
+ isOpen = false;
+ }
+}
diff --git a/ReLunacy/Core/Frames/UpdateInfoFrame.cs b/ReLunacy/Core/Frames/UpdateInfoFrame.cs
deleted file mode 100644
index 8a3cc79..0000000
--- a/ReLunacy/Core/Frames/UpdateInfoFrame.cs
+++ /dev/null
@@ -1,86 +0,0 @@
-using System.Diagnostics;
-using Vector4 = System.Numerics.Vector4;
-
-namespace ReLunacy.Frames;
-
-public class UpdateInfoFrame : Frame
-{
- protected override ImGuiWindowFlags WindowFlags { get; set; } = ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoDocking;
-
- private DateTime now = DateTime.Now;
- private DateTime UpdateReleaseDate;
- private string NewUpdateTag;
- private string Link;
- Task? versionsCompareTask;
- private UpdateChecker.VersionsCompare? versionsDiff;
-
- public UpdateInfoFrame() : base()
- {
- FrameName = "Update Available!";
- }
-
- public UpdateInfoFrame(string url, string newUpdateTag, DateTime updateDate) : this()
- {
- UpdateReleaseDate = updateDate;
- NewUpdateTag = newUpdateTag;
- Link = url;
-
- versionsCompareTask = UpdateChecker.GetVersionsCompare(NewUpdateTag);
- }
-
- protected override void Render(double deltaTime)
- {
- if (versionsDiff == null && versionsCompareTask != null && versionsCompareTask.IsCompletedSuccessfully)
- {
- versionsDiff = versionsCompareTask.Result;
- }
-
- ImGui.BeginGroup();
- ImGui.TextWrapped("A new update for ReLunacy is available! (auto-updater soon)");
- ImGui.Text("Current version:");
- ImGui.SameLine();
- ImGui.TextColored(new Vector4((float)0xA0 / 0xFF, (float)0xA0 / 0xFF, (float)0x24 / 0xFF, 1), $"v{ProgramInfo.Version}");
- ImGui.Text("New version:");
- ImGui.SameLine();
- ImGui.TextColored(new Vector4((float)0x24 / 0xFF, 1, (float)0x24 / 0xFF, 1), $"v{NewUpdateTag}");
- if (new Version(NewUpdateTag).Major > new Version(ProgramInfo.Version).Major)
- {
- ImGui.SameLine();
- ImGui.Text("[MAJOR UPDATE]");
- }
- else if (new Version(NewUpdateTag).Minor > new Version(ProgramInfo.Version).Minor)
- {
- ImGui.SameLine();
- ImGui.Text("[PATCH]");
- }
- else if (new Version(NewUpdateTag).Build > new Version(ProgramInfo.Version).Build)
- {
- ImGui.SameLine();
- ImGui.Text("[HOTFIX]");
- }
- ImGui.Spacing();
- ImGui.Spacing();
- ImGui.Spacing();
- var diff = now - UpdateReleaseDate;
- var days = diff.Days > 0 ? diff.Days.ToString() + " days, " : "";
- var hours = diff.Days > 0 || diff.Hours > 0 ? diff.Hours.ToString() + " hours and " : "";
- ImGui.Text($"Released {days}{hours}{diff.Minutes} minutes ago. ({UpdateReleaseDate:dd/MM/yyyy HH:mm:ss})");
- ImGui.Spacing();
- ImGui.Text($"Commits since {ProgramInfo.Version}: {(versionsDiff != null ? versionsDiff.total_commits : "fetching commits...")}");
- ImGui.Spacing();
- ImGui.Separator();
- ImGui.Spacing();
- if(ImGuiPlus.CenteredButton("Download update", new(150, 40)))
- {
- Process.Start(new ProcessStartInfo(Link) { UseShellExecute = true });
- }
- ImGui.EndGroup();
- }
-
- public override void RenderAsWindow(double deltaTime)
- {
- //ImGui.SetNextWindowSize(new(350, 175), ImGuiCond.Appearing);
- ImGui.SetNextWindowPos(ImGui.GetWorkCenter(ImGui.GetMainViewport()), ImGuiCond.Appearing, new(0.5f));
- base.RenderAsWindow(deltaTime);
- }
-}
\ No newline at end of file
diff --git a/ReLunacy/Core/MenuBar/AboutMenuDraw.cs b/ReLunacy/Core/MenuBar/AboutMenuDraw.cs
index feeb1dc..ff18757 100644
--- a/ReLunacy/Core/MenuBar/AboutMenuDraw.cs
+++ b/ReLunacy/Core/MenuBar/AboutMenuDraw.cs
@@ -1,4 +1,5 @@
using System.Diagnostics;
+using ReLunacy.Utility;
using ReLunacy.Utility.Localization;
namespace ReLunacy.MenuBar;
@@ -23,5 +24,6 @@ internal static void CheckForUpdate()
allowCheckforUpdate = false;
cooldownCallback = new Timer(_ => allowCheckforUpdate = true, null, 120_000, Timeout.Infinite);
+ UpdateChecker.CheckUpdates(Program.Settings.UpdateChannel);
}
}
diff --git a/ReLunacy/Core/MenuBar/RenderMenuDraw.cs b/ReLunacy/Core/MenuBar/RenderMenuDraw.cs
index 4bf9470..fed9630 100644
--- a/ReLunacy/Core/MenuBar/RenderMenuDraw.cs
+++ b/ReLunacy/Core/MenuBar/RenderMenuDraw.cs
@@ -35,4 +35,10 @@ internal static void ShowBoundingSpheres()
if (!ImGui.MenuItem(LM.Get("GUI_MenuItem_RenderBoundingSpheres"), "", EntityManager.Singleton.renderBoundingSpheres, !Program.Settings.LegacyRenderingMode)) return;
EntityManager.Singleton.renderBoundingSpheres = !EntityManager.Singleton.renderBoundingSpheres;
}
+
+ internal static void ShowMobyDistanceCulling()
+ {
+ if (!ImGui.MenuItem(LM.Get("GUI_MenuItem_MobyDistanceCulling"), "", EntityManager.Singleton.MobyDistanceCullingEnabled, !Program.Settings.LegacyRenderingMode)) return;
+ EntityManager.Singleton.MobyDistanceCullingEnabled = !EntityManager.Singleton.MobyDistanceCullingEnabled;
+ }
}
diff --git a/ReLunacy/Core/Overlay.cs b/ReLunacy/Core/Overlay.cs
index 1f614d8..dfb1f14 100644
--- a/ReLunacy/Core/Overlay.cs
+++ b/ReLunacy/Core/Overlay.cs
@@ -1,5 +1,6 @@
using System.Numerics;
using ReLunacy.Core.Frames.DockedFrames;
+using ReLunacy.Engine.Games;
using ReLunacy.Engine.Scene;
using ReLunacy.Utility;
using ReLunacy.Utility.Localization;
@@ -22,10 +23,7 @@ private static string levelName
get
{
if (string.IsNullOrEmpty(Program.ProvidedPath)) return "None";
-
- List chunks = [.. Program.ProvidedPath.Split(Path.DirectorySeparatorChar)];
- chunks.RemoveAll(s => s == "");
- return chunks[^1];
+ return GameLibraryScanner.GetLevelNameFromPath(Program.ProvidedPath);
}
}
diff --git a/ReLunacy/Core/Window.cs b/ReLunacy/Core/Window.cs
index 2ccd539..9a490a0 100644
--- a/ReLunacy/Core/Window.cs
+++ b/ReLunacy/Core/Window.cs
@@ -393,6 +393,8 @@ private void RenderMenuBar()
RenderMenuDraw.ShowUFrags();
RenderMenuDraw.ShowVolumes();
RenderMenuDraw.ShowBoundingSpheres();
+ ImGui.Separator();
+ RenderMenuDraw.ShowMobyDistanceCulling();
ImGui.EndMenu();
}
diff --git a/ReLunacy/Locales/en.json b/ReLunacy/Locales/en.json
index 51bf657..04c1116 100644
--- a/ReLunacy/Locales/en.json
+++ b/ReLunacy/Locales/en.json
@@ -57,6 +57,11 @@
"GUI_Frame_EditorSettings_GizmoSnapScale": "Scale Snap",
"GUI_Frame_EditorSettings_GizmoSnapTranslation": "Translation Snap",
"GUI_Frame_EditorSettings_GizmosSize": "Gizmos Size",
+ "GUI_Frame_EditorSettings_VolumeWireThickness": "Volume Wire Thickness",
+ "GUI_Frame_EditorSettings_VolumeWireThicknessHelp": "How thick a volume's wireframe box edges are, in absolute world units — the same for every volume regardless of its own size. This is also the clickable region in the 3D view — volumes have no solid pick area, only their edges, so clicking inside an empty volume doesn't select it.",
+ "GUI_Frame_EditorSettings_VolumeColor": "Volume Color",
+ "GUI_Frame_EditorSettings_VolumeSelectedColor": "Volume Selected Color",
+ "GUI_Frame_EditorSettings_SelectionOutlineColor": "Selection Outline Color",
"GUI_Frame_EditorSettings_GraphicsBackend": "Graphic Backend",
"GUI_Frame_EditorSettings_Language": "Language",
"GUI_Frame_EditorSettings_MSAALevel": "MSAA level",
@@ -75,6 +80,8 @@
"GUI_Frame_EditorSettings_SaveApply": "Save & Apply",
"GUI_Frame_EditorSettings_Sensitivity": "Sensitivity",
"GUI_Frame_EditorSettings_ToolsSettings": "Tools Settings",
+ "GUI_Frame_EditorSettings_UpdateChannel": "Update channel",
+ "GUI_Frame_EditorSettings_UpdateChannelHelp": "Stable checks the latest tagged GitHub release. Nightly checks the rolling nightly build published on every push to the nightly branch — unstable, dev-facing, updated far more often.",
"GUI_Frame_EditorSettings_UseFrustrumCulling": "Frustrum culling",
"GUI_Frame_EditorSettings_VSync": "V-Sync",
"GUI_Frame_EditorSettings_VisualSettings": "Visual Settings",
@@ -95,6 +102,8 @@
"GUI_Frame_InstanceInspector_MaterialAlbedoFormat": "Albedo texture format: {0}",
"GUI_Frame_InstanceInspector_NameChangeNotice": "The entity name cannot be changed.",
"GUI_Frame_InstanceInspector_ObjectPath": "Object path",
+ "GUI_Frame_InstanceInspector_VolumeId": "Volume ID: {0:X}",
+ "GUI_Frame_InstanceInspector_VolumeGroup": "Zone group: {0}",
"GUI_Frame_InstanceInspector_OpenInAssetViewer": "Open asset in Asset Viewer",
"GUI_Frame_InstanceInspector_Position": "Position",
"GUI_Frame_InstanceInspector_RenderingCategory": "Rendering",
@@ -158,6 +167,17 @@
"GUI_Frame_TextureExplorer_Preview_TextureSizeOnDisk": "Size on Disk",
"GUI_Frame_TextureExplorer_SearchHint": "Search among {0} textures...",
"GUI_Frame_TextureExplorer_SearchLabel": "Search",
+ "GUI_Frame_UpdateInfo_Title": "Update Available!",
+ "GUI_Frame_UpdateInfo_StableAvailable": "A new update for ReLunacy is available!",
+ "GUI_Frame_UpdateInfo_NightlyAvailable": "A newer nightly build of ReLunacy is available!",
+ "GUI_Frame_UpdateInfo_CurrentVersion": "Current version:",
+ "GUI_Frame_UpdateInfo_NewVersion": "New version:",
+ "GUI_Frame_UpdateInfo_NewBuild": "New build:",
+ "GUI_Frame_UpdateInfo_Released": "Released {0} ({1}).",
+ "GUI_Frame_UpdateInfo_DaysAgo": "{0} day(s) ago",
+ "GUI_Frame_UpdateInfo_HoursAgo": "{0} hour(s) ago",
+ "GUI_Frame_UpdateInfo_MinutesAgo": "{0} minute(s) ago",
+ "GUI_Frame_UpdateInfo_Download": "Download update",
"GUI_Frame_View3D": "3D View",
"GUI_LoadLevelModal_Title": "Loading level",
"GUI_MenuItem_CheckUpdates": "Check for updates",
@@ -167,6 +187,7 @@
"GUI_MenuItem_OfficialGithub": "Official GitHub",
"GUI_MenuItem_RenderBoundingSpheres": "Bounding Spheres",
"GUI_MenuItem_RenderMobys": "Mobys",
+ "GUI_MenuItem_MobyDistanceCulling": "Moby Distance Culling",
"GUI_MenuItem_RenderTies": "Ties",
"GUI_MenuItem_RenderUFrags": "UFrags",
"GUI_MenuItem_RenderVolumes": "Volumes",
diff --git a/ReLunacy/NightlyBuildInfo.cs b/ReLunacy/NightlyBuildInfo.cs
new file mode 100644
index 0000000..3b30275
--- /dev/null
+++ b/ReLunacy/NightlyBuildInfo.cs
@@ -0,0 +1,12 @@
+namespace ReLunacy;
+
+// Rewritten by .github/workflows/nightly.yml right before a nightly build compiles, stamping in
+// the short commit hash and build date baked into that build's release asset filename (see
+// UpdateChecker.CheckNightly, which extracts the same info back out of the filename to compare).
+// Left null here for every other build (local dev, stable releases) — nightly-update comparisons
+// only make sense when the running binary actually knows which nightly build it is.
+public static class NightlyBuildInfo
+{
+ public const string? CommitHash = null;
+ public const string? BuildDate = null;
+}
diff --git a/ReLunacy/ProgramInfo.cs b/ReLunacy/ProgramInfo.cs
index dfa58cb..cb70689 100644
--- a/ReLunacy/ProgramInfo.cs
+++ b/ReLunacy/ProgramInfo.cs
@@ -8,7 +8,7 @@ namespace ReLunacy;
public static class ProgramInfo
{
- public const string Name = "ReLunacy_Blissed";
+ public const string Name = "ReLunacy";
public const string DisplayName = "ReLunacy";
public const string Version = "0.04";
public const string GithubURL = "https://github.com/VELD-Dev/ReLunacy/";
diff --git a/ReLunacy/ReLunacy.csproj b/ReLunacy/ReLunacy.csproj
index 3747d18..291f235 100644
--- a/ReLunacy/ReLunacy.csproj
+++ b/ReLunacy/ReLunacy.csproj
@@ -57,7 +57,6 @@
-
diff --git a/ReLunacy/Utility/EditorSettings.cs b/ReLunacy/Utility/EditorSettings.cs
index 7db1ec3..deb6fee 100644
--- a/ReLunacy/Utility/EditorSettings.cs
+++ b/ReLunacy/Utility/EditorSettings.cs
@@ -3,6 +3,12 @@
using ReLunacy.Utility;
using Veldrith;
+public enum UpdateChannel
+{
+ Stable,
+ Nightly,
+}
+
[JsonObject]
public class EditorSettings
{
@@ -34,6 +40,10 @@ public class EditorSettings
public float GizmoSnapTranslation;
public float GizmoSnapRotation;
public float GizmoSnapScale;
+ public float VolumeWireThickness;
+ public Vector4 VolumeColor;
+ public Vector4 VolumeSelectedColor;
+ public Vector4 SelectionOutlineColor;
internal LunaLog.LogLevel LogLevel;
public Dictionary CustomShaders = [];
public bool LegacyRenderingMode;
@@ -44,6 +54,11 @@ public class EditorSettings
// culling can be flipped on live, per-session, to see how bad it actually is on real data rather
// than assuming — not a confirmed-safe rendering mode.
public bool BackfaceCulling;
+ // See UpdateChecker: Stable checks GitHub's normal "latest release"; Nightly checks the
+ // rolling "nightly" tag release .github/workflows/nightly.yml keeps updated on every push to
+ // the nightly branch. Independent of which build the user is actually running — someone on a
+ // stable build can still opt into nightly update notifications and vice versa.
+ public UpdateChannel UpdateChannel;
[JsonIgnore]
public float CamFOVRad => CamFOV * (MathF.PI / 180f);
@@ -82,8 +97,13 @@ public EditorSettings()
GizmoSnapTranslation = 1.0f;
GizmoSnapRotation = 15.0f;
GizmoSnapScale = 0.25f;
+ VolumeWireThickness = 0.1f;
+ VolumeColor = new Vector4(1f, 1f, 0f, 1f);
+ VolumeSelectedColor = new Vector4(1f, 1f, 1f, 1f);
+ SelectionOutlineColor = new Vector4(1f, 0.65f, 0f, 1f);
LegacyRenderingMode = false;
BackfaceCulling = false;
+ UpdateChannel = UpdateChannel.Stable;
#if DEBUG
LogLevel = LunaLog.LogLevel.Debug;
#else
diff --git a/ReLunacy/Utility/UpdateChecker.cs b/ReLunacy/Utility/UpdateChecker.cs
new file mode 100644
index 0000000..cdeafd4
--- /dev/null
+++ b/ReLunacy/Utility/UpdateChecker.cs
@@ -0,0 +1,140 @@
+using System.Globalization;
+using System.Net;
+using Newtonsoft.Json.Linq;
+using ReLunacy.Core;
+using ReLunacy.Core.Frames.Modals;
+
+namespace ReLunacy.Utility;
+
+// Recreated after the LibLunacy/Bliss merge deleted the old implementation (see git history for
+// the pre-rewrite version this is loosely based on) — now channel-aware per EditorSettings.
+//
+// Stable checks GitHub's normal "latest release" and compares its tag as a Version against
+// ProgramInfo.Version, same as before.
+//
+// Nightly is a different shape entirely: .github/workflows/nightly.yml keeps a single rolling
+// release under the "nightly" tag, and (per its allowUpdates/replacesArtifacts settings)
+// accumulates every nightly build's artifacts there rather than replacing them — so a nightly tag
+// has no single meaningful version number, just a growing list of dated, commit-stamped assets.
+// Comparison instead extracts the commit hash baked into the newest asset's filename for this
+// platform and compares it against NightlyBuildInfo.CommitHash (this build's own identity, which
+// is null unless this binary is itself a nightly build the workflow stamped).
+public static class UpdateChecker
+{
+ private const string RepoApiBase = "https://api.github.com/repos/VELD-Dev/ReLunacy";
+
+ private static HttpClient CreateClient()
+ {
+ var client = new HttpClient(new HttpClientHandler { UseDefaultCredentials = true });
+ client.DefaultRequestHeaders.Add("User-Agent", "ReLunacy-UpdateChecker");
+ client.DefaultRequestHeaders.Add("Accept", "application/vnd.github+json");
+ return client;
+ }
+
+ public static async void CheckUpdates(UpdateChannel channel)
+ {
+ try
+ {
+ if (channel == UpdateChannel.Nightly)
+ await CheckNightly();
+ else
+ await CheckStable();
+ }
+ catch (Exception e)
+ {
+ LunaLog.LogWarn($"Failed to check for updates: {e}");
+ }
+ }
+
+ private static async Task CheckStable()
+ {
+ using var client = CreateClient();
+ var response = await client.GetAsync($"{RepoApiBase}/releases/latest");
+ response.EnsureSuccessStatusCode();
+ var data = JObject.Parse(await response.Content.ReadAsStringAsync());
+
+ string? tag = (string?)data["tag_name"];
+ string? url = (string?)data["html_url"];
+ string? publishedAt = (string?)data["published_at"];
+ if (tag == null || url == null || publishedAt == null) return;
+
+ if (!TryParseVersion(tag, out var newVersion) || !TryParseVersion(ProgramInfo.Version, out var currentVersion))
+ {
+ LunaLog.LogWarn($"Could not compare release tag '{tag}' against current version '{ProgramInfo.Version}'.");
+ return;
+ }
+
+ if (newVersion > currentVersion)
+ {
+ LunaLog.LogInfo($"A stable update is available: v{tag}");
+ LunaWindow.Instance.AddFrame(new UpdateInfoFrame(url, tag, DateTime.Parse(publishedAt, CultureInfo.InvariantCulture)));
+ }
+ else
+ {
+ LunaLog.LogInfo("No stable update available.");
+ }
+ }
+
+ private static async Task CheckNightly()
+ {
+ using var client = CreateClient();
+ var response = await client.GetAsync($"{RepoApiBase}/releases/tags/nightly");
+ if (response.StatusCode == HttpStatusCode.NotFound)
+ {
+ LunaLog.LogInfo("No nightly release exists yet.");
+ return;
+ }
+ response.EnsureSuccessStatusCode();
+ var data = JObject.Parse(await response.Content.ReadAsStringAsync());
+
+ string? url = (string?)data["html_url"];
+ var assets = data["assets"] as JArray;
+ if (url == null || assets == null || assets.Count == 0) return;
+
+ // Filenames are "ReLunacy-nightly-{yyyy-MM-dd}.{shortCommit}.{platformRid}.{ext}" (see the
+ // nightly workflow) — pick this platform's newest by filename, which sorts lexicographically
+ // the same as chronologically thanks to the leading yyyy-MM-dd.
+ string platformRid = OperatingSystem.IsWindows() ? "win-x64" : "linux-x64";
+ var latestForPlatform = assets
+ .Where(a => ((string?)a["name"])?.Contains(platformRid) == true)
+ .OrderByDescending(a => (string?)a["name"], StringComparer.Ordinal)
+ .FirstOrDefault();
+ if (latestForPlatform == null)
+ {
+ LunaLog.LogInfo($"No nightly build published for this platform ({platformRid}) yet.");
+ return;
+ }
+
+ string assetName = (string)latestForPlatform["name"]!;
+ string? remoteCommit = ExtractCommitHash(assetName);
+ string? publishedAt = (string?)latestForPlatform["created_at"] ?? (string?)data["published_at"];
+ if (remoteCommit == null) return;
+
+ if (NightlyBuildInfo.CommitHash != null && remoteCommit == NightlyBuildInfo.CommitHash)
+ {
+ LunaLog.LogInfo("You're already on the latest nightly build.");
+ return;
+ }
+
+ LunaLog.LogInfo($"A nightly update is available: {assetName}");
+ LunaWindow.Instance.AddFrame(new UpdateInfoFrame(
+ url, assetName,
+ publishedAt != null ? DateTime.Parse(publishedAt, CultureInfo.InvariantCulture) : DateTime.Now,
+ isNightly: true));
+ }
+
+ private static string? ExtractCommitHash(string assetName)
+ {
+ // ReLunacy-nightly-2026-07-25.abcdef1.win-x64.zip -> "abcdef1"
+ var parts = assetName.Split('.');
+ return parts.Length >= 2 ? parts[1] : null;
+ }
+
+ private static bool TryParseVersion(string raw, out Version version)
+ {
+ string cleaned = raw.TrimStart('v', 'V');
+ bool ok = Version.TryParse(cleaned, out var parsed);
+ version = parsed ?? new Version(0, 0);
+ return ok;
+ }
+}
diff --git a/media/demo.gif b/media/demo.gif
deleted file mode 100644
index 001ac3b..0000000
Binary files a/media/demo.gif and /dev/null differ
diff --git a/media/demo.mp4 b/media/demo.mp4
new file mode 100644
index 0000000..a085130
Binary files /dev/null and b/media/demo.mp4 differ