diff --git a/.github/workflows/build-and-publish.yml b/.github/workflows/build-and-publish.yml new file mode 100644 index 0000000..568c863 --- /dev/null +++ b/.github/workflows/build-and-publish.yml @@ -0,0 +1,32 @@ +# This workflow will build a .NET project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net + +name: Build Project and Publish +on: + push: + tags: + - "[0-9]+.[0-9]+.[0-9]+" + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + submodules: 'recursive' + + - name: "Setup Library SDKs & Components" + uses: X-Hax/SA3D.ProjectConfigurations/.github/actions/setup-sdks-components@main + + - name: Build + run: dotnet build -c Release ./src + + - name: "Upload Packages" + uses: X-Hax/SA3D.ProjectConfigurations/.github/actions/upload-packages@main + with: + nuget-key: ${{ secrets.NUGET_KEY }} + is-release: ${{ startsWith(github.ref, 'refs/tags/') }} + release-tag: ${{ github.ref_name }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..1483ff0 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,27 @@ +# This workflow will build a .NET project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net + +name: Build Project + +on: + push: + branches: [ "dev", "main" ] + pull_request: + branches: [ "dev", "main" ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + submodules: 'recursive' + + - name: "Setup Library SDKs & Components" + uses: X-Hax/SA3D.ProjectConfigurations/.github/actions/setup-sdks-components@main + + - name: Build + run: dotnet build -c Release ./src diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8a30d25 --- /dev/null +++ b/.gitignore @@ -0,0 +1,398 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..6ed3e9c --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "src/SA3D.ProjectConfigurations"] + path = src/SA3D.ProjectConfigurations + url = https://github.com/X-Hax/SA3D.ProjectConfigurations.git diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6b0c013 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# SA3D.Modeling +A Sonic Adventure modeling library with support for all game related model formats. Also contains support for various other SEGA based games, although support is not guaranteed. + +## Contents +| Namespace (SA3D.Modeling.*) | Description | +|----------------------------- |-------------------------------------------------------------------------------------------------------------------------------------- | +| File | Model data storage file handlers for select native- and X-Hax custom file-formats. | +| Mesh | Library for handling, reading and writing mesh data. | +| Mesh.Basic | Basic mesh data library. Used in SA1 (everything) and SA2 (collision geometry only) | +| Mesh.Chunk | Chunk mesh data library. Used in SA2. | +| Mesh.Gamecube | Gamecube-like mesh data library. Used in SA2B and its ports. | +| Mesh.Buffer | SA3D internal mesh format. Used for conversion and rendering purposes. Is a simplified version of Chunk and mixes in Basic elements. | +| Mesh.Weighted | SA3D internal mesh format. Used only for conversion purposes, as it is more in line with most modern mesh formats. | +| ObjectData | Library for handling, reading and writing node and geometry container data. | +| Animation | Library for handling, reading and writing animation data. | +| Structs | Common structure code between all namespaces. | +| Strippify | Triangle strip generating and handling code. | + +## Releasing +!! Requires authorization via the X-Hax organisation + +1. Edit the version number in src/SA3D.Common/SA3D.Common.csproj; Example: `1.0.0` -> `2.0.0` +2. Commit the change but dont yet push. +3. Tag the commit: `git tag -a [version number] HEAD -m "Release version [version number]"` +4. Push with tags: `git push --follow-tags` + +This will automatically start the Github `Build and Publish` workflow \ No newline at end of file diff --git a/src/.editorconfig b/src/.editorconfig new file mode 120000 index 0000000..29c6ba7 --- /dev/null +++ b/src/.editorconfig @@ -0,0 +1 @@ +SA3D.ProjectConfigurations/.editorconfig \ No newline at end of file diff --git a/src/SA3D.Modeling.sln b/src/SA3D.Modeling.sln new file mode 100644 index 0000000..f70b8d7 --- /dev/null +++ b/src/SA3D.Modeling.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.7.34202.233 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SA3D.Modeling", "SA3D.Modeling\SA3D.Modeling.csproj", "{7AD0BC1C-070B-4CB3-B1D2-98EB451DBEC4}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7AD0BC1C-070B-4CB3-B1D2-98EB451DBEC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7AD0BC1C-070B-4CB3-B1D2-98EB451DBEC4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7AD0BC1C-070B-4CB3-B1D2-98EB451DBEC4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7AD0BC1C-070B-4CB3-B1D2-98EB451DBEC4}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {3D099321-2535-4D95-9B92-FDCD041E8841} + EndGlobalSection +EndGlobal diff --git a/src/SA3D.Modeling/Animation/Enums.cs b/src/SA3D.Modeling/Animation/Enums.cs new file mode 100644 index 0000000..d3c11a4 --- /dev/null +++ b/src/SA3D.Modeling/Animation/Enums.cs @@ -0,0 +1,130 @@ +using SA3D.Common; +using System; + +namespace SA3D.Modeling.Animation +{ + /// + /// Depicts keyframe contents. + /// + [Flags] + public enum KeyframeAttributes : ushort + { + /// + /// Animation includes position keyframes. + /// + Position = Flag16.B0, + + /// + /// Animation includes rotation (euler angles) keyframes. + /// + EulerRotation = Flag16.B1, + + /// + /// Animation includes scale keyframes. + /// + Scale = Flag16.B2, + + /// + /// Animation includes vector keyframes. + /// + Vector = Flag16.B3, + + /// + /// Animation includes vertex keyframes. + /// + Vertex = Flag16.B4, + + /// + /// Animation includes normal keyframes. + /// + Normal = Flag16.B5, + + /// + /// Animation includes target keyframes. + /// + Target = Flag16.B6, + + /// + /// Animation includes roll keyframes. + /// + Roll = Flag16.B7, + + /// + /// Animation includes angle keyframes. + /// + Angle = Flag16.B8, + + /// + /// Animation includes light color keyframes. + /// + LightColor = Flag16.B9, + + /// + /// Animation includes intensity keyframes. + /// + Intensity = Flag16.B10, + + /// + /// Animation includes spotlight keyframes. + /// + Spot = Flag16.B11, + + /// + /// Animation includes point keyframes. + /// + Point = Flag16.B12, + + /// + /// Animation includes rotation (quaternion) keyframes. + /// + QuaternionRotation = Flag16.B13 + } + + /// + /// Keyframe interpolation mode. + /// + public enum InterpolationMode + { + /// + /// Linear interpolation. + /// + Linear, + + /// + /// Spline interpolation (?). + /// + Spline, + + /// + /// User defined interpolation. + /// + User + } + + /// + /// Animation enum extension methods. + /// + public static class EnumExtensions + { + /// + /// Counts the number of channels defined in animation attributes. + /// + /// Animation attributes to count. + /// The number of channel defined in animation attributes. + public static int ChannelCount(this KeyframeAttributes attributes) + { + int channels = 0; + + ushort value = (ushort)attributes; + for(int i = 0; i < 14; i++, value >>= 1) + { + if((value & 1) != 0) + { + channels++; + } + } + + return channels; + } + } +} diff --git a/src/SA3D.Modeling/Animation/Frame.cs b/src/SA3D.Modeling/Animation/Frame.cs new file mode 100644 index 0000000..e672032 --- /dev/null +++ b/src/SA3D.Modeling/Animation/Frame.cs @@ -0,0 +1,87 @@ +using SA3D.Modeling.Structs; +using System.Numerics; + +namespace SA3D.Modeling.Animation +{ + /// + /// Frame on timeline with interpolated values from a keyframe storage. + /// + public struct Frame + { + /// + /// Position on the timeline. + /// + public float FrameTime { get; set; } + + /// + /// Position at the frame. + /// + public Vector3? Position { get; set; } + + /// + /// Rotation (euler angles) at the frame. + /// + public Vector3? EulerRotation { get; set; } + + /// + /// Scale at the frame. + /// + public Vector3? Scale { get; set; } + + /// + /// Vector at the frame. + /// + public Vector3? Vector { get; set; } + + /// + /// Vertex positions at the frame. + /// + public Vector3[]? Vertex { get; set; } + + /// + /// Vertex normals at the frame. + /// + public Vector3[]? Normal { get; set; } + + /// + /// Camera target position at the frame. + /// + public Vector3? Target { get; set; } + + /// + /// Camera roll at the frame. + /// + public float? Roll { get; set; } + + /// + /// Camera FOV at the frame. + /// + public float? Angle { get; set; } + + /// + /// Light color at the frame. + /// + public Color? Color { get; set; } + + /// + /// Light intensity at the frame. + /// + public float? Intensity { get; set; } + + /// + /// Spotlight at the frame. + /// + public Spotlight? Spotlight { get; set; } + + /// + /// Point light stuff at the frame. + /// + public Vector2? Point { get; set; } + + /// + /// Rotation (quaternion) at the frame. + /// + public Quaternion? QuaternionRotation { get; set; } + + } +} diff --git a/src/SA3D.Modeling/Animation/Keyframes.cs b/src/SA3D.Modeling/Animation/Keyframes.cs new file mode 100644 index 0000000..30d49de --- /dev/null +++ b/src/SA3D.Modeling/Animation/Keyframes.cs @@ -0,0 +1,475 @@ +using SA3D.Common.IO; +using SA3D.Common.Lookup; +using SA3D.Modeling.Animation.Utilities; +using SA3D.Modeling.ObjectData; +using SA3D.Modeling.Structs; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace SA3D.Modeling.Animation +{ + /// + /// Keyframe storage for an animation. + /// + public class Keyframes + { + /// + /// Transform position keyframes. + /// + public SortedDictionary Position { get; private set; } + + /// + /// Transform rotation (euler angles) keyframes. + /// + public SortedDictionary EulerRotation { get; private set; } + + /// + /// Transform scale keyframes. + /// + public SortedDictionary Scale { get; private set; } + + /// + /// General vector3 keyframes. + /// + public SortedDictionary Vector { get; private set; } + + /// + /// Mesh vertex positions. + /// + public SortedDictionary> Vertex { get; private set; } + + /// + /// Mesh vertex normals. + /// + public SortedDictionary> Normal { get; private set; } + + /// + /// Camera lookat target. + /// + public SortedDictionary Target { get; private set; } + + /// + /// Camera Roll (euler angle). + /// + public SortedDictionary Roll { get; private set; } + + /// + /// Camera field of view (radians). + /// + public SortedDictionary Angle { get; private set; } + + /// + /// Light Color. + /// + public SortedDictionary LightColor { get; private set; } + + /// + /// Light intensity. + /// + public SortedDictionary Intensity { get; private set; } + + /// + /// Spotlights. + /// + public SortedDictionary Spot { get; private set; } + + /// + /// Point light positions. + /// + public SortedDictionary Point { get; private set; } + + /// + /// Rotation (quaternion) keyframes. + /// + public SortedDictionary QuaternionRotation { get; private set; } + + /// + /// Whether any keyframes exist in this keyframe set + /// + public bool HasKeyframes + => GetKeyEnumerable().Any(x => x.Any()); + + /// + /// Returns the number of keyframes in the biggest keyframe dictionary. + /// + public uint KeyframeCount + { + get + { + bool hasKeys = false; + uint maxKey = 0; + foreach(IEnumerable keys in GetKeyEnumerable()) + { + if(!keys.Any()) + { + continue; + } + + hasKeys = true; + maxKey = uint.Max(maxKey, keys.Last()); + } + + if(hasKeys) + { + return maxKey + 1; + } + else + { + return 0; + } + } + } + + /// + /// Channels that contain keyframes. + /// + public KeyframeAttributes Type + { + get + { + KeyframeAttributes attribs = 0; + + foreach((KeyframeAttributes type, IEnumerable keys) in GetTypeKeyEnumerable()) + { + if(keys.Any()) + { + attribs |= type; + } + } + + return attribs; + } + } + + + /// + /// Creates an empty keyframe storage + /// + public Keyframes() + { + Position = new(); + EulerRotation = new(); + Scale = new(); + Vector = new(); + Vertex = new(); + Normal = new(); + Target = new(); + Roll = new(); + Angle = new(); + LightColor = new(); + Intensity = new(); + Spot = new(); + Point = new(); + QuaternionRotation = new(); + } + + + private IEnumerable> GetKeyEnumerable() + { + yield return Position.Keys; + yield return EulerRotation.Keys; + yield return Scale.Keys; + yield return Vector.Keys; + yield return Vertex.Keys; + yield return Normal.Keys; + yield return Target.Keys; + yield return Roll.Keys; + yield return Angle.Keys; + yield return LightColor.Keys; + yield return Intensity.Keys; + yield return Spot.Keys; + yield return Point.Keys; + yield return QuaternionRotation.Keys; + } + + private IEnumerable<(KeyframeAttributes type, IEnumerable keys)> GetTypeKeyEnumerable() + { + uint current = 1; + foreach(IEnumerable keys in GetKeyEnumerable()) + { + yield return ((KeyframeAttributes)current, keys); + current <<= 1; + } + } + + /// + /// Returns a all values at a specific frame + /// + /// Frame to get the values of + /// + public Frame GetFrameAt(float frame) + { + return new() + { + FrameTime = frame, + Position = Position.ValueAtFrame(frame), + EulerRotation = EulerRotation.ValueAtFrame(frame), + Scale = Scale.ValueAtFrame(frame), + Vector = Vector.ValueAtFrame(frame), + Vertex = Vertex.ValueAtFrame(frame), + Normal = Normal.ValueAtFrame(frame), + Target = Target.ValueAtFrame(frame), + Roll = Roll.ValueAtFrame(frame), + Angle = Angle.ValueAtFrame(frame), + Color = LightColor.ValueAtFrame(frame), + Intensity = Intensity.ValueAtFrame(frame), + Spotlight = Spot.ValueAtFrame(frame), + Point = Point.ValueAtFrame(frame), + QuaternionRotation = QuaternionRotation.ValueAtFrame(frame), + }; + } + + /// + /// Optimizes the keyframes. + /// + /// + /// Difference threshold to use between quaternion keyframes. + /// Difference threshold to use between colors. + /// Compare angle keyframes as degrees and not as radians. + /// Frame from which to start optimizing. uses default. + /// Frame at which to end optimizing. uses default. + public void Optimize( + float generalThreshold, + float quaternionThreshold, + float colorThreshold, + bool asDegrees, + uint? start = null, + uint? end = null) + { + Position.OptimizeVector3(generalThreshold, start, end); + + if(asDegrees) + { + EulerRotation.OptimizeVector3Degrees(generalThreshold, start, end); + Roll.OptimizeFloat(generalThreshold, start, end); + Angle.OptimizeFloat(generalThreshold, start, end); + } + else + { + EulerRotation.OptimizeVector3(generalThreshold, start, end); + Roll.OptimizeFloat(generalThreshold, start, end); + Angle.OptimizeFloat(generalThreshold, start, end); + } + + Scale.OptimizeVector3(generalThreshold, start, end); + Vector.OptimizeVector3(generalThreshold, start, end); + Target.OptimizeVector3(generalThreshold, start, end); + LightColor.OptimizeColor(colorThreshold, start, end); + Intensity.OptimizeFloat(generalThreshold, start, end); + Spot.OptimizeSpotlight(generalThreshold, start, end); + Point.OptimizeVector2(generalThreshold, start, end); + QuaternionRotation.OptimizeQuaternion(quaternionThreshold, start, end); + } + + /// + /// Ensures that specified node transform properties have start- and end-frames. + /// + /// The node for which to ensure frames. If no keyframes exist, then they will be added with the values from this node. + /// Keyframe types to target. + /// The frame until which keyframes need to exist. + public void EnsureNodeKeyframes(Node node, KeyframeAttributes targets, uint endFrame) + { + void Ensure(SortedDictionary keyframes, KeyframeAttributes type, T value) + { + if(!targets.HasFlag(type)) + { + return; + } + + if(!keyframes.ContainsKey(0)) + { + keyframes.Add(0, value); + } + + if(keyframes.Keys.Max() < endFrame) + { + keyframes.Add(endFrame, value); + } + } + + Ensure(Position, KeyframeAttributes.Position, node.Position); + Ensure(EulerRotation, KeyframeAttributes.EulerRotation, node.EulerRotation); + Ensure(QuaternionRotation, KeyframeAttributes.QuaternionRotation, node.QuaternionRotation); + Ensure(Scale, KeyframeAttributes.Scale, node.Scale); + } + + + /// + /// Writes the keyframe set to an endian stack writer. + /// + /// The writer to write to. + /// Which channels should be written + /// Pointer references to utilize. + /// Whether to write euler rotations 16-bit instead of 32-bit. + public (uint address, uint count)[] Write(EndianStackWriter writer, KeyframeAttributes writeAttributes, PointerLUT lut, bool shortRot = false) + { + int channels = writeAttributes.ChannelCount(); + (uint address, uint count)[] keyframeLocs = new (uint address, uint count)[channels]; + int channelIndex = -1; + + foreach((KeyframeAttributes type, IEnumerable keys) in GetTypeKeyEnumerable()) + { + if(!writeAttributes.HasFlag(type)) + { + continue; + } + + channelIndex++; + + int count = keys.Count(); + if(count == 0) + { + continue; + } + + uint[]? arrayData = null; + if(type == KeyframeAttributes.Vertex) + { + arrayData = writer.WriteVector3ArrayData(Vertex, lut); + } + else if(type == KeyframeAttributes.Normal) + { + arrayData = writer.WriteVector3ArrayData(Normal, lut); + } + + keyframeLocs[channelIndex] = (writer.PointerPosition, (uint)count); + + switch(type) + { + case KeyframeAttributes.Position: + writer.WriteVector3Set(Position, FloatIOType.Float); + break; + case KeyframeAttributes.EulerRotation: + writer.WriteVector3Set(EulerRotation, shortRot ? FloatIOType.BAMS16 : FloatIOType.BAMS32); + break; + case KeyframeAttributes.Scale: + writer.WriteVector3Set(Scale, FloatIOType.Float); + break; + case KeyframeAttributes.Vector: + writer.WriteVector3Set(Vector, FloatIOType.Float); + break; + case KeyframeAttributes.Vertex: + case KeyframeAttributes.Normal: + writer.WriteVector3ArraySet(arrayData!); + break; + case KeyframeAttributes.Target: + writer.WriteVector3Set(Target, FloatIOType.Float); + break; + case KeyframeAttributes.Roll: + writer.WriteFloatSet(Roll, true); + break; + case KeyframeAttributes.Angle: + writer.WriteFloatSet(Angle, true); + break; + case KeyframeAttributes.LightColor: + writer.WriteColorSet(LightColor, ColorIOType.ARGB8_32); + break; + case KeyframeAttributes.Intensity: + writer.WriteFloatSet(Intensity, false); + break; + case KeyframeAttributes.Spot: + writer.WriteSpotlightSet(Spot); + break; + case KeyframeAttributes.Point: + writer.WriteVector2Set(Point, FloatIOType.Float); + break; + case KeyframeAttributes.QuaternionRotation: + writer.WriteQuaternionSet(QuaternionRotation); + break; + default: + break; + } + } + + return keyframeLocs; + } + + /// + /// Reads a set of keyframes off an endian stack reader. + /// + /// The reader to read from. + /// Address at which to start reading. + /// Channels that the keyframes contain. + /// Pointer references to utilize. + /// Whether to write euler rotations 16-bit instead of 32-bit. + /// The keyframes that were read. + public static Keyframes Read(EndianStackReader reader, ref uint address, KeyframeAttributes type, PointerLUT lut, bool shortRot = false) + { + int channelCount = type.ChannelCount(); + uint keyframePointerArray = address; + uint keyframeCountArray = (uint)(address + (4 * channelCount)); + + Keyframes result = new(); + + foreach(KeyframeAttributes flag in Enum.GetValues()) + { + if(!type.HasFlag(flag)) + { + continue; + } + + if(reader.TryReadPointer(keyframePointerArray, out uint setAddress)) + { + uint frameCount = reader.ReadUInt(keyframeCountArray); + switch(flag) + { + case KeyframeAttributes.Position: + reader.ReadVector3Set(setAddress, frameCount, result.Position, FloatIOType.Float); + break; + case KeyframeAttributes.EulerRotation: + reader.ReadVector3Set(setAddress, frameCount, result.EulerRotation, shortRot ? FloatIOType.BAMS16 : FloatIOType.BAMS32); + break; + case KeyframeAttributes.Scale: + reader.ReadVector3Set(setAddress, frameCount, result.Scale, FloatIOType.Float); + break; + case KeyframeAttributes.Vector: + reader.ReadVector3Set(setAddress, frameCount, result.Vector, FloatIOType.Float); + break; + case KeyframeAttributes.Vertex: + reader.ReadVector3ArraySet(setAddress, frameCount, "vertex_", result.Vertex, lut); + break; + case KeyframeAttributes.Normal: + reader.ReadVector3ArraySet(setAddress, frameCount, "normal_", result.Normal, lut); + break; + case KeyframeAttributes.Target: + reader.ReadVector3Set(setAddress, frameCount, result.Target, FloatIOType.Float); + break; + case KeyframeAttributes.Roll: + reader.ReadFloatSet(setAddress, frameCount, result.Roll, true); + break; + case KeyframeAttributes.Angle: + reader.ReadFloatSet(setAddress, frameCount, result.Angle, true); + break; + case KeyframeAttributes.LightColor: + reader.ReadColorSet(setAddress, frameCount, result.LightColor, ColorIOType.ARGB8_32); + break; + case KeyframeAttributes.Intensity: + reader.ReadFloatSet(setAddress, frameCount, result.Intensity, false); + break; + case KeyframeAttributes.Spot: + reader.ReadSpotSet(setAddress, frameCount, result.Spot); + break; + case KeyframeAttributes.Point: + reader.ReadVector2Set(setAddress, frameCount, result.Point, FloatIOType.Float); + break; + case KeyframeAttributes.QuaternionRotation: + reader.ReadQuaternionSet(setAddress, frameCount, result.QuaternionRotation); + break; + default: + break; + } + } + + keyframePointerArray += 4; + keyframeCountArray += 4; + } + + address = keyframeCountArray; + + return result; + } + + } +} \ No newline at end of file diff --git a/src/SA3D.Modeling/Animation/LandentryMotion.cs b/src/SA3D.Modeling/Animation/LandentryMotion.cs new file mode 100644 index 0000000..095944d --- /dev/null +++ b/src/SA3D.Modeling/Animation/LandentryMotion.cs @@ -0,0 +1,160 @@ +using SA3D.Common.IO; +using SA3D.Modeling.ObjectData; +using SA3D.Modeling.ObjectData.Enums; +using SA3D.Modeling.Structs; +using System; + +namespace SA3D.Modeling.Animation +{ + /// + /// Level geometry animation (only used in sa1) + /// + public class LandEntryMotion + { + /// + /// Size of the structure in bytes. + /// + public static uint StructSize => 24; + + /// + /// First keyframe / Keyframe to start the animation at. + /// + public float Frame { get; set; } + + /// + /// Keyframes traversed per frame-update / Animation Speed. + /// + public float Step { get; set; } + + /// + /// Last keyframe / Length of the animation. + /// + public float MaxFrame { get; set; } + + /// + /// Model that is being animated. + /// + public Node Model { get; set; } + + /// + /// The corresponding node motion pair. + /// + public NodeMotion NodeMotion { get; set; } + + /// + /// Texture list address to use. + /// + public uint TextureListPointer { get; set; } + + + /// + /// Creates a new geometry animation + /// + /// First keyframe / Keyframe to start the animation at. + /// Keyframes traversed per frame-update / Animation Speed. + /// Last keyframe / Length of the animation. + /// Model that is being animated. + /// Animation to play. + /// Texture list address to use. + public LandEntryMotion(float frame, float step, float maxFrame, Node model, Motion motion, uint textureListPointer) + : this(frame, step, maxFrame, model, new NodeMotion(model, motion), textureListPointer) { } + + /// + /// Creates a new geometry animation + /// + /// First keyframe / Keyframe to start the animation at. + /// Keyframes traversed per frame-update / Animation Speed. + /// Last keyframe / Length of the animation. + /// Model and animation to use. + /// Texture list address to use. + public LandEntryMotion(float frame, float step, float maxFrame, NodeMotion nodeMotion, uint textureListPointer) + : this(frame, step, maxFrame, nodeMotion.Model, nodeMotion, textureListPointer) { } + + /// + /// Creates a new geometry animation + /// + /// First keyframe / Keyframe to start the animation at. + /// Keyframes traversed per frame-update / Animation Speed. + /// Last keyframe / Length of the animation. + /// Model that is being animated. + /// Model and animation to use. + /// Texture list address to use. + public LandEntryMotion(float frame, float step, float maxFrame, Node model, NodeMotion nodeMotion, uint textureListPointer) + { + Frame = frame; + Step = step; + MaxFrame = maxFrame; + Model = model; + NodeMotion = nodeMotion; + TextureListPointer = textureListPointer; + } + + + + /// + /// Reads a geometry animation from a byte array + /// + /// Byte source + /// Address at which the geometry animation is located + /// Attach format + /// + /// + public static LandEntryMotion Read(EndianStackReader data, uint address, ModelFormat format, PointerLUT lut) + { + float frame = data.ReadFloat(address); + float step = data.ReadFloat(address + 4); + float maxFrame = data.ReadFloat(address + 8); + + uint modelAddress = data.ReadPointer(address + 0xC); + Node model = Node.Read(data, modelAddress, format, lut); + + uint motionAddress = data.ReadPointer(address + 0x10); + NodeMotion action = NodeMotion.Read(data, motionAddress, format, lut); + + uint texListPtr = data.ReadUInt(address + 0x14); + + return new LandEntryMotion(frame, step, maxFrame, model, action, texListPtr); + } + + /// + /// Write the model and animation data to an endian stack writer. + /// + /// The writer to write to. + /// The format to write the model data in. + /// Pointer references to utilize. + public void WriteData(EndianStackWriter writer, ModelFormat format, PointerLUT lut) + { + Model.Write(writer, format, lut); + NodeMotion.Write(writer, format, lut); + } + + /// + /// Writes the landentry motion structure to an endian stack writer. + /// + /// + /// Requires the data to be written before via + /// + /// The writer to write to. + /// Pointer references to utilize. + /// + public void Write(EndianStackWriter writer, PointerLUT lut) + { + if(!lut.Nodes.TryGetAddress(Model, out uint mdlAddress)) + { + throw new NullReferenceException($"Model \"{Model.Label}\" has not been written yet / cannot be found in the pointer LUT!"); + } + + if(!lut.NodeMotions.TryGetAddress(NodeMotion, out uint actionAddress)) + { + throw new NullReferenceException($"Nodemotion has not been written yet / cannot be found in the pointer LUT!"); + } + + writer.WriteFloat(Frame); + writer.WriteFloat(Step); + writer.WriteFloat(MaxFrame); + writer.WriteUInt(mdlAddress); + writer.WriteUInt(actionAddress); + writer.WriteUInt(TextureListPointer); + } + } +} diff --git a/src/SA3D.Modeling/Animation/Motion.cs b/src/SA3D.Modeling/Animation/Motion.cs new file mode 100644 index 0000000..5101a89 --- /dev/null +++ b/src/SA3D.Modeling/Animation/Motion.cs @@ -0,0 +1,292 @@ +using SA3D.Common.IO; +using SA3D.Common.Lookup; +using SA3D.Modeling.ObjectData; +using SA3D.Modeling.Structs; +using System.Collections.Generic; +using System.Linq; +using static SA3D.Common.StringExtensions; + +namespace SA3D.Modeling.Animation +{ + /// + /// Animation data for various targets. + /// + public class Motion : ILabel + { + /// + /// Size of the motion struct in bytes. + /// + public const uint StructSize = 16; + + /// + public string Label { get; set; } + + /// + /// Node in the models node tree that this animation targets. + /// + public uint ModelCount { get; set; } + + /// + /// Intepolation mode between keyframes. + /// + public InterpolationMode InterpolationMode { get; set; } + + /// + /// Whether to use 16-bit for euler rotation BAMS values. + /// + public bool ShortRot { get; set; } + + /// + /// Keyframes based on their model id + /// + public Dictionary Keyframes { get; } + + /// + /// Types of keyframe stored in this animation. + /// + public KeyframeAttributes KeyframeTypes + { + get + { + KeyframeAttributes type = 0; + foreach(Keyframes kf in Keyframes.Values) + { + type |= kf.Type; + } + + return type | ManualKeyframeTypes; + } + } + + /// + /// Manually enforced keyframe types. + /// + public KeyframeAttributes ManualKeyframeTypes { get; set; } + + + /// + /// Whether the motion transforms nodes. + /// + public bool IsNodeMotion + => !IsShapeMotion && !IsCameraMotion && !IsSpotLightMotion && !IsLightMotion; + + /// + /// Whether the motion alters vertex positions and/or normals of meshes. + /// + public bool IsShapeMotion + => HasAnyAttributes(KeyframeAttributes.Vertex | KeyframeAttributes.Normal); + + /// + /// Whether the motion transforms a camera. + /// + public bool IsCameraMotion + => HasAnyAttributes(KeyframeAttributes.Angle | KeyframeAttributes.Roll | KeyframeAttributes.Target); + + /// + /// Whether the motion targets a spotlight + /// + public bool IsSpotLightMotion + => HasAnyAttributes(KeyframeAttributes.Spot); + + /// + /// Whether the motion targets lights + /// + public bool IsLightMotion + => HasAnyAttributes(KeyframeAttributes.Intensity | KeyframeAttributes.LightColor | KeyframeAttributes.Vector); + + + /// + /// Creates a new empty motion. + /// + public Motion() + { + Label = "animation_" + GenerateIdentifier(); + Keyframes = new(); + } + + + private bool HasAnyAttributes(KeyframeAttributes attributes) + { + return (KeyframeTypes & attributes) != 0; + } + + /// + /// Returns the number of frames in this motion. + /// + /// + public uint GetFrameCount() + { + uint result = 0; + foreach(Keyframes k in Keyframes.Values) + { + result = uint.Max(result, k.KeyframeCount); + } + + return result; + } + + /// + /// Optimizes all keyframes across the motion. + /// + /// + /// Difference threshold to use between quaternion keyframes. + /// Difference threshold to use between colors. + /// Compare angle keyframes as degrees and not as radians. + /// Frame from which to start optimizing. uses default. + /// Frame at which to end optimizing. uses default. + public void Optimize( + float generalThreshold, + float quaternionThreshold, + float colorThreshold, + bool asDegrees, + uint? start = null, + uint? end = null) + { + foreach(Keyframes keyframes in Keyframes.Values) + { + keyframes.Optimize(generalThreshold, quaternionThreshold, colorThreshold, asDegrees, start, end); + } + } + + /// + /// Ensures that the transform propertie of all nodes in a model tree have start- and end-frames. + /// + /// Any node from a tree for which keyframes should be ensured.. + /// Keyframe types to target. + /// If enabled, new keyframe sets will be created for any node that does not have any yet. Otherwise, only preexisting keyframe sets will be ensured to have start and end. + public void EnsureNodeKeyframes(Node model, KeyframeAttributes targetTypes, bool createKeyframes) + { + if(default == (targetTypes & ( + KeyframeAttributes.Position + | KeyframeAttributes.EulerRotation + | KeyframeAttributes.QuaternionRotation + | KeyframeAttributes.Scale))) + { + return; + } + + uint maxFrame = GetFrameCount() - 1; + + int i = 0; + foreach(Node node in model.GetAnimTreeNodes()) + { + if(!Keyframes.TryGetValue(i, out Keyframes? keyframes)) + { + if(!createKeyframes) + { + continue; + } + + keyframes = new(); + Keyframes.Add(i, keyframes); + } + + keyframes.EnsureNodeKeyframes(node, targetTypes, maxFrame); + + i++; + } + } + + + /// + /// Writes the motion to an endian stack writer. + /// + /// The writer to write to. + /// Pointer references to utilize. + public uint Write(EndianStackWriter writer, PointerLUT lut) + { + uint onWrite() + { + KeyframeAttributes type = KeyframeTypes; + int channels = type.ChannelCount(); + + uint keyframeCount = uint.Max(ModelCount, (uint)Keyframes.Keys.Max() + 1u); + + (uint address, uint count)[][] keyFrameLocations = new (uint addr, uint count)[keyframeCount][]; + + for(int i = 0; i < keyframeCount; i++) + { + keyFrameLocations[i] = !Keyframes.ContainsKey(i) + ? new (uint, uint)[channels] + : Keyframes[i].Write(writer, type, lut, ShortRot); + } + + uint keyframesAddr = writer.PointerPosition; + + foreach((uint addr, uint count)[] kf in keyFrameLocations) + { + for(int i = 0; i < kf.Length; i++) + { + writer.WriteUInt(kf[i].addr); + } + + for(int i = 0; i < kf.Length; i++) + { + writer.WriteUInt(kf[i].count); + } + } + + uint result = writer.PointerPosition; + + writer.WriteUInt(keyframesAddr); + writer.WriteUInt(GetFrameCount()); + writer.WriteUShort((ushort)type); + writer.WriteUShort((ushort)((channels & 0xF) | ((int)InterpolationMode << 6))); + + return result; + + } + + return lut.GetAddAddress(this, onWrite); + } + + /// + /// Reads a motion off an endian stack reader. + /// + /// Byte source + /// Address at which to start reading. + /// Number of nodes in the tree of the targeted model. + /// Pointer references to utilize. + /// Whether euler rotations are stored in 16-bit instead of 32-bit. + /// The motion that was read + public static Motion Read(EndianStackReader reader, uint address, uint modelCount, PointerLUT lut, bool shortRot = false) + { + Motion onRead() + { + uint keyframeAddr = reader.ReadPointer(address); + // offset 4 is frame count. We dont need to read that. + KeyframeAttributes keyframeType = (KeyframeAttributes)reader.ReadUShort(address + 8); + + ushort tmp = reader.ReadUShort(address + 10); + InterpolationMode mode = (InterpolationMode)((tmp >> 6) & 0x3); + int channels = tmp & 0xF; + + Motion result = new() + { + InterpolationMode = mode, + ModelCount = modelCount, + ShortRot = shortRot, + ManualKeyframeTypes = keyframeType + }; + + for(int i = 0; i < modelCount; i++) + { + Keyframes kf = Animation.Keyframes.Read(reader, ref keyframeAddr, keyframeType, lut, shortRot); + result.Keyframes.Add(i, kf); + } + + return result; + + } + + return lut.GetAddLabeledValue(address, "animation_", onRead); + } + + + /// + public override string ToString() + { + return $"{Label} : {ModelCount} - {Keyframes.Count}"; + } + } +} diff --git a/src/SA3D.Modeling/Animation/NodeMotion.cs b/src/SA3D.Modeling/Animation/NodeMotion.cs new file mode 100644 index 0000000..60e7101 --- /dev/null +++ b/src/SA3D.Modeling/Animation/NodeMotion.cs @@ -0,0 +1,89 @@ +using SA3D.Common; +using SA3D.Common.IO; +using SA3D.Common.Lookup; +using SA3D.Modeling.ObjectData; +using SA3D.Modeling.ObjectData.Enums; +using SA3D.Modeling.Structs; + +namespace SA3D.Modeling.Animation +{ + /// + /// Pairs a node and motion together. + /// + public class NodeMotion : ILabel + { + /// + public string Label { get; set; } + + /// + /// Assigned node. + /// + public Node Model { get; set; } + + /// + /// Assigned motion. + /// + public Motion Animation { get; set; } + + + /// + /// Creates a new node motion. + /// + /// The model of the pair. + /// The animation of the pair. + public NodeMotion(Node model, Motion animation) + { + Label = "action_" + StringExtensions.GenerateIdentifier(); + Model = model; + Animation = animation; + } + + + /// + /// Writes the node motion and its contents to an endian stack writer. + /// + /// The writer to write to. + /// The format in which the model should be written. + /// Pointer references to utilize. + /// Address at which the node motion was written. + public uint Write(EndianStackWriter writer, ModelFormat format, PointerLUT lut) + { + uint OnWrite(NodeMotion nodeMotion) + { + uint nodeAddress = Model.Write(writer, format, lut); + uint motionAddress = Animation.Write(writer, lut); + + uint result = writer.PointerPosition; + + writer.WriteUInt(nodeAddress); + writer.WriteUInt(motionAddress); + + return result; + } + + return lut.GetAddAddress(this, OnWrite); + } + + /// + /// Reads a NodeMotion off an endian stack reader. + /// + /// The reader to read from. + /// Address at which to start reading. + /// The format that the node should be read in. + /// Pointer references to utilize. + /// The node motion pair that was read + public static NodeMotion Read(EndianStackReader reader, uint address, ModelFormat format, PointerLUT lut) + { + NodeMotion onRead() + { + Node mdl = Node.Read(reader, reader.ReadPointer(address), format, lut); + Motion mtn = Motion.Read(reader, reader.ReadPointer(address + 4), (uint)mdl.GetTreeNodeCount(), lut); + + return new NodeMotion(mdl, mtn); + } + + return lut.GetAddLabeledValue(address, "action_", onRead); + } + + } +} diff --git a/src/SA3D.Modeling/Animation/Spotlight.cs b/src/SA3D.Modeling/Animation/Spotlight.cs new file mode 100644 index 0000000..9506e72 --- /dev/null +++ b/src/SA3D.Modeling/Animation/Spotlight.cs @@ -0,0 +1,101 @@ +using SA3D.Common.IO; +using System; +using static SA3D.Common.MathHelper; + +namespace SA3D.Modeling.Animation +{ + /// + /// Spotlight for cutscenes. + /// + public struct Spotlight + { + /// + /// Size of the spotlight struct. + /// + public static uint StructSize => 16; + + /// + /// Closest light distance. + /// + public float near; + + /// + /// Furthest light distance. + /// + public float far; + + /// + /// Inner cone angle. + /// + public float insideAngle; + + /// + /// Outer cone angle. + /// + public float outsideAngle; + + /// + /// Linearly interpolate between two spotlights. + /// + /// Spotlight from which to start interpolating. + /// Spotlight to which to interpolate. + /// Value by which to interpolate + /// The interpolated spotlight. + public static Spotlight Lerp(Spotlight from, Spotlight to, float time) + { + float inverse = 1 - time; + return new Spotlight() + { + near = (to.near * time) + (from.near * inverse), + far = (to.far * time) + (from.far * inverse), + insideAngle = (to.insideAngle * time) + (from.insideAngle * inverse), + outsideAngle = (to.outsideAngle * time) + (from.outsideAngle * inverse), + }; + } + + /// + /// Calculates the distance between two spotlight values (handled like a Vector4). + /// + /// First spotlight. + /// Second spotlight. + /// The distance + public static float Distance(Spotlight from, Spotlight to) + { + return MathF.Sqrt( + MathF.Pow(from.near - to.near, 2) + + MathF.Pow(from.far - to.far, 2) + + MathF.Pow(from.insideAngle - to.insideAngle, 2) + + MathF.Pow(from.outsideAngle - to.outsideAngle, 2) + ); + } + + /// + /// Reads a spotlight off an endian stack reader. + /// + /// The reader to read from. + /// Address at which to start reading. + /// The spotlight that was read. + public static Spotlight Read(EndianStackReader reader, uint address) + { + return new Spotlight() + { + near = reader.ReadFloat(address), + far = reader.ReadFloat(address + 4), + insideAngle = BAMSToRad(reader.ReadInt(address + 8)), + outsideAngle = BAMSToRad(reader.ReadInt(address + 12)) + }; + } + + /// + /// Writes the spotlight to an endian stack writer. + /// + /// The writer to write to. + public readonly void Write(EndianStackWriter writer) + { + writer.WriteFloat(near); + writer.WriteFloat(far); + writer.WriteInt(RadToBAMS(insideAngle)); + writer.WriteInt(RadToBAMS(outsideAngle)); + } + } +} diff --git a/src/SA3D.Modeling/Animation/Utilities/KeyframeInterpolate.cs b/src/SA3D.Modeling/Animation/Utilities/KeyframeInterpolate.cs new file mode 100644 index 0000000..7a0f1ff --- /dev/null +++ b/src/SA3D.Modeling/Animation/Utilities/KeyframeInterpolate.cs @@ -0,0 +1,225 @@ +using SA3D.Common.Lookup; +using SA3D.Modeling.Structs; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Numerics; + +namespace SA3D.Modeling.Animation.Utilities +{ + /// + /// Keyframe interpolation methods + /// + internal static class KeyframeInterpolate + { + /// + /// Searches through a keyframe dictionary and returns the interpolation between the values last and next.
+ /// If the returned float is 0, then next will be default (as its not used) + ///
+ /// Type of the Keyframe values + /// KEyframes to iterate through + /// Current frame to get + /// + /// Last Keyframe before given frame + /// Next Keyframe after given frame + /// + private static bool GetNearestFrames(SortedDictionary keyframes, float timestamp, out float interpolation, out T before, [MaybeNullWhen(false)] out T next) + { + if(timestamp < 0) + { + timestamp = 0; + } + + // if there is only one frame, we can take that one + next = default; + interpolation = 0; + + if(keyframes.Count == 1) + { + foreach(T val in keyframes.Values) // faster than converting to an array and accessing the first index + { + before = val; + return false; + } + } + + // if the given frame is spot on and exists, then we can use it + uint baseFrame = (uint)Math.Floor(timestamp); + if(timestamp == baseFrame && keyframes.ContainsKey(baseFrame)) + { + before = keyframes[baseFrame]; + return false; + } + + // we gotta find the frames that the given frame is between + // this is pretty easy thanks to the fact that the dictionary is always sorted + + // getting the first frame index + SortedDictionary.KeyCollection keys = keyframes.Keys; + uint nextSmallestFrame = keys.First(); + + // if the smallest frame is greater than the frame we are at right now, then we can just return the frame + if(nextSmallestFrame > baseFrame) + { + before = keyframes[nextSmallestFrame]; + return false; + } + + // getting the actual next smallest and biggest frames + uint nextBiggestFrame = baseFrame; + foreach(uint key in keyframes.Keys) + { + if(key > nextSmallestFrame && key <= baseFrame) + { + nextSmallestFrame = key; + } + else if(key > baseFrame) + { + // the first bigger value must be the next biggest frame + nextBiggestFrame = key; + break; + } + } + + // if the next biggest frame hasnt changed, then that means we are past the last frame + before = keyframes[nextSmallestFrame]; + if(nextBiggestFrame == baseFrame) + { + return false; + } + + // the regular result + next = keyframes[nextBiggestFrame]; + + // getting the interpolation between the two frames + float duration = nextBiggestFrame - nextSmallestFrame; + interpolation = (timestamp - nextSmallestFrame) / duration; + return true; + } + + public static Vector3? ValueAtFrame(this SortedDictionary keyframes, float frame) + { + if(keyframes.Count == 0) + { + return null; + } + + if(!GetNearestFrames(keyframes, frame, out float interpolation, out Vector3 before, out Vector3 next)) + { + return before; + } + else + { + return Vector3.Lerp(before, next, interpolation); + } + } + + public static Vector3[]? ValueAtFrame(this SortedDictionary> keyframes, float frame) + { + if(keyframes.Count == 0) + { + return null; + } + + if(!GetNearestFrames(keyframes, frame, out float interpolation, out ILabeledArray before, out ILabeledArray? next)) + { + return before.ToArray(); + } + + Vector3[] result = new Vector3[before.Length]; + for(int i = 0; i < result.Length; i++) + { + result[i] = Vector3.Lerp(before[i], next[i], interpolation); + } + + return result; + } + + public static Vector2? ValueAtFrame(this SortedDictionary keyframes, float frame) + { + if(keyframes.Count == 0) + { + return null; + } + + if(!GetNearestFrames(keyframes, frame, out float interpolation, out Vector2 before, out Vector2 next)) + { + return before; + } + else + { + return Vector2.Lerp(before, next, interpolation); + } + } + + public static Color? ValueAtFrame(this SortedDictionary keyframes, float frame) + { + if(keyframes.Count == 0) + { + return null; + } + + if(!GetNearestFrames(keyframes, frame, out float interpolation, out Color before, out Color next)) + { + return before; + } + else + { + return Color.Lerp(before, next, interpolation); + } + } + + public static float? ValueAtFrame(this SortedDictionary keyframes, float frame) + { + if(keyframes.Count == 0) + { + return null; + } + + if(!GetNearestFrames(keyframes, frame, out float interpolation, out float before, out float next)) + { + return before; + } + else + { + return (next * interpolation) + (before * (1 - interpolation)); + } + } + + public static Spotlight? ValueAtFrame(this SortedDictionary keyframes, float frame) + { + if(keyframes.Count == 0) + { + return null; + } + + if(!GetNearestFrames(keyframes, frame, out float interpolation, out Spotlight before, out Spotlight next)) + { + return before; + } + else + { + return Spotlight.Lerp(before, next, interpolation); + } + } + + public static Quaternion? ValueAtFrame(this SortedDictionary keyframes, float frame) + { + if(keyframes.Count == 0) + { + return null; + } + + if(!GetNearestFrames(keyframes, frame, out float interpolation, out Quaternion before, out Quaternion next)) + { + return before; + } + else + { + return Quaternion.Lerp(before, next, interpolation); + } + } + + } +} diff --git a/src/SA3D.Modeling/Animation/Utilities/KeyframeOptimizationUtils.cs b/src/SA3D.Modeling/Animation/Utilities/KeyframeOptimizationUtils.cs new file mode 100644 index 0000000..39072f0 --- /dev/null +++ b/src/SA3D.Modeling/Animation/Utilities/KeyframeOptimizationUtils.cs @@ -0,0 +1,131 @@ +using SA3D.Common; +using SA3D.Modeling.Structs; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace SA3D.Modeling.Animation.Utilities +{ + internal static class KeyframeOptimizationUtils + { + private static void RemoveDeviations( + this SortedDictionary keyframes, + uint? start, + uint? end, + float deviationThreshold, + Func lerp, + Func calculateDeviation) + { + start ??= keyframes.Keys.FirstOrDefault(); + end ??= keyframes.Keys.LastOrDefault(); + + if(end - start < 2 || deviationThreshold <= 0.0) + { + return; + } + + List frames = new(); + for(uint frame = start.Value; frame <= end; frame++) + { + if(keyframes.ContainsKey(frame)) + { + frames.Add(frame); + } + } + + // whenever a frame is removed, we skip the next one. + // repeat that until we reach an iteration where no frame was removed + + bool done; + do + { + done = true; + for(int i = 1; i < frames.Count - 1; i++) + { + uint previous = frames[i - 1]; + uint current = frames[i]; + uint next = frames[i + 1]; + + float linearFac = (current - previous) / (float)(next - previous); + + T linear = lerp(keyframes[previous], keyframes[next], linearFac); + T actual = keyframes[current]; + + float deviation = calculateDeviation(linear, actual); + if(deviation < deviationThreshold) + { + keyframes.Remove(current); + frames.RemoveAt(i); + done = false; + } + } + } + while(!done); + } + + public static void OptimizeFloat(this SortedDictionary keyframes, float deviationThreshold, uint? start, uint? end) + { + keyframes.RemoveDeviations( + start, end, deviationThreshold, + (a, b, t) => (a * (1 - t)) + (b * t), + (a, b) => Math.Abs(a - b)); + } + + public static void OptimizeFloatDegrees(this SortedDictionary keyframes, float deviationThreshold, uint? start, uint? end) + { + keyframes.RemoveDeviations( + start, end, deviationThreshold, + (a, b, t) => (a * (1 - t)) + (b * t), + (a, b) => MathHelper.RadToDeg(Math.Abs(a - b))); + } + + public static void OptimizeVector2(this SortedDictionary keyframes, float deviationThreshold, uint? start, uint? end) + { + keyframes.RemoveDeviations( + start, end, deviationThreshold, + Vector2.Lerp, + (a, b) => (a - b).Length()); + } + + public static void OptimizeVector3(this SortedDictionary keyframes, float deviationThreshold, uint? start, uint? end) + { + keyframes.RemoveDeviations( + start, end, deviationThreshold, + Vector3.Lerp, + (a, b) => (a - b).Length()); + } + + public static void OptimizeVector3Degrees(this SortedDictionary keyframes, float deviationThreshold, uint? start, uint? end) + { + keyframes.RemoveDeviations( + start, end, deviationThreshold, + Vector3.Lerp, + (a, b) => MathHelper.RadToDeg((a - b).Length())); + } + + public static void OptimizeColor(this SortedDictionary keyframes, float deviationThreshold, uint? start, uint? end) + { + keyframes.RemoveDeviations( + start, end, deviationThreshold, + Color.Lerp, + Color.Distance); + } + + public static void OptimizeQuaternion(this SortedDictionary keyframes, float deviationThreshold, uint? start, uint? end) + { + keyframes.RemoveDeviations( + start, end, deviationThreshold, + QuaternionUtilities.RealLerp, + (a, b) => (a - b).Length()); + } + + public static void OptimizeSpotlight(this SortedDictionary keyframes, float deviationThreshold, uint? start, uint? end) + { + keyframes.RemoveDeviations( + start, end, deviationThreshold, + Spotlight.Lerp, + Spotlight.Distance); + } + } +} diff --git a/src/SA3D.Modeling/Animation/Utilities/KeyframeRead.cs b/src/SA3D.Modeling/Animation/Utilities/KeyframeRead.cs new file mode 100644 index 0000000..8587994 --- /dev/null +++ b/src/SA3D.Modeling/Animation/Utilities/KeyframeRead.cs @@ -0,0 +1,142 @@ +using SA3D.Common.IO; +using SA3D.Common.Lookup; +using SA3D.Modeling.Structs; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using static SA3D.Common.MathHelper; + +namespace SA3D.Modeling.Animation.Utilities +{ + internal static class KeyframeRead + { + public static void ReadVector3Set(this EndianStackReader reader, uint address, uint count, SortedDictionary dictionary, FloatIOType type) + { + if(type == FloatIOType.BAMS16) + { + for(int i = 0; i < count; i++) + { + uint frame = reader.ReadUShort(address); + address += 2; + dictionary.Add(frame, reader.ReadVector3(ref address, type)); + } + } + else + { + for(int i = 0; i < count; i++) + { + uint frame = reader.ReadUInt(address); + address += 4; + dictionary.Add(frame, reader.ReadVector3(ref address, type)); + } + } + + } + + public static void ReadVector3ArraySet(this EndianStackReader reader, uint address, uint count, string labelPrefix, SortedDictionary> dictionary, PointerLUT lut) + { + if(count == 0) + { + return; + } + + uint startAddr = address; + + // + SortedDictionary frameAddresses = new(); + for(int i = 0; i < count; i++) + { + uint frame = reader.ReadUInt(address); + uint ptr = reader.ReadPointer(address += 4); + address += 4; + + frameAddresses.Add(frame, ptr); + } + + uint[] addresses = frameAddresses.Values.Distinct().Order().ToArray(); + // get the smallest array size + uint size = (startAddr - addresses[^1]) / 12; + for(int i = 1; i < addresses.Length; i++) + { + for(int j = 0; j < i; j++) + { + uint newSize = (addresses[i] - addresses[j]) / 12; + if(newSize < size) + { + size = newSize; + } + } + } + + foreach(KeyValuePair item in frameAddresses) + { + ILabeledArray vectors = lut.GetAddLabeledValue(item.Value, labelPrefix, () => + { + LabeledArray result = new(size); + + uint ptr = item.Value; + for(int j = 0; j < size; j++) + { + result[j] = reader.ReadVector3(ref ptr); + } + + return result; + }); + + dictionary.Add(item.Key, vectors); + } + } + + public static void ReadVector2Set(this EndianStackReader reader, uint address, uint count, SortedDictionary dictionary, FloatIOType type) + { + for(int i = 0; i < count; i++) + { + uint frame = reader.ReadUInt(address); + address += 4; + dictionary.Add(frame, reader.ReadVector2(ref address, type)); + } + } + + public static void ReadColorSet(this EndianStackReader reader, uint address, uint count, SortedDictionary dictionary, ColorIOType type) + { + for(int i = 0; i < count; i++) + { + uint frame = reader.ReadUInt(address); + address += 4; + dictionary.Add(frame, reader.ReadColor(ref address, type)); + } + } + + public static void ReadFloatSet(this EndianStackReader reader, uint address, uint count, SortedDictionary dictionary, bool BAMS) + { + for(int i = 0; i < count; i++) + { + uint frame = reader.ReadUInt(address); + float value = BAMS ? BAMSToRad(reader.ReadInt(address + 4)) : reader.ReadFloat(address + 4); + address += 8; + dictionary.Add(frame, value); + } + } + + public static void ReadSpotSet(this EndianStackReader reader, uint address, uint count, SortedDictionary dictionary) + { + for(int i = 0; i < count; i++) + { + uint frame = reader.ReadUInt(address); + Spotlight value = Spotlight.Read(reader, address + 4); + address += 8 + Spotlight.StructSize; + dictionary.Add(frame, value); + } + } + + public static void ReadQuaternionSet(this EndianStackReader reader, uint address, uint count, SortedDictionary dictionary) + { + for(int i = 0; i < count; i++) + { + uint frame = reader.ReadUInt(address); + address += 4; + dictionary.Add(frame, reader.ReadQuaternion(ref address)); + } + } + } +} diff --git a/src/SA3D.Modeling/Animation/Utilities/KeyframeRotationUtils.cs b/src/SA3D.Modeling/Animation/Utilities/KeyframeRotationUtils.cs new file mode 100644 index 0000000..a9cfb72 --- /dev/null +++ b/src/SA3D.Modeling/Animation/Utilities/KeyframeRotationUtils.cs @@ -0,0 +1,433 @@ +using SA3D.Modeling.Structs; +using System; +using System.Collections.Generic; +using System.Numerics; +using Matrix4x4KF = System.Collections.Generic.SortedDictionary; +using QuaternionKF = System.Collections.Generic.SortedDictionary; +using EulerKF = System.Collections.Generic.SortedDictionary; + +namespace SA3D.Modeling.Animation.Utilities +{ + /// + /// Utility methods for converting keyframe rotations from and to matrices and each other. + /// + public static class KeyframeRotationUtils + { + #region Quaternion -> Euler + + /// + /// Converts quaternion rotation keyframes to euler rotation keyframes. + /// + /// The quaternion keyframes to convert. + /// The deviation threshold below which converted values should be ignored. + /// Whether euler angles are applied in ZYX order. + /// Keyframes in which the result should be stored. + public static void QuaternionToEuler(QuaternionKF source, float deviationThreshold, bool rotateZYX, EulerKF result) + { + if(source.Count == 0) + { + return; + } + + deviationThreshold = Math.Max(deviationThreshold, 0); + + uint previousFrame = uint.MaxValue; + foreach(KeyValuePair item in source) + { + if(previousFrame != uint.MaxValue) + { + float frameCount = item.Key - previousFrame; + + Vector3 previousEuler = result[previousFrame]; + for(uint i = 1; i <= frameCount; i++) + { + float fac = i / frameCount; + Quaternion lerp = Quaternion.Lerp(source[previousFrame], item.Value, fac); + + previousEuler = lerp.QuaternionToCompatibleEuler(previousEuler, rotateZYX); + + result.Add(previousFrame + i, previousEuler); + } + + if(deviationThreshold > 0) + { + result.OptimizeVector3(deviationThreshold, previousFrame, item.Key); + } + } + else + { + Vector3 rotation = item.Value.QuaternionToEuler(rotateZYX); + result.Add(item.Key, rotation); + } + + previousFrame = item.Key; + } + } + + /// + /// Converts quaternion rotation keyframes to euler rotation keyframes. + /// + /// The quaternion keyframes to convert. + /// The deviation threshold below which converted values should be ignored. + /// Whether euler angles are applied in ZYX order. + /// The converted euler rotation keyframes. + public static EulerKF QuaternionToEuler(QuaternionKF source, float deviationThreshold, bool rotateZYX) + { + EulerKF result = new(); + QuaternionToEuler(source, deviationThreshold, rotateZYX, result); + return result; + } + + /// + /// Converts quaternion rotation keyframes to euler rotation keyframes. + /// + /// The keyframes to convert and output to. + /// The deviation threshold below which converted values should be ignored. + /// Whether euler angles are applied in ZYX order. + /// Whether quaternion keyframes should be cleared after converting. + public static void QuaternionToEuler(this Keyframes keyframes, float deviationThreshold, bool rotateZYX, bool clearQuaternion) + { + keyframes.EulerRotation.Clear(); + QuaternionToEuler(keyframes.QuaternionRotation, deviationThreshold, rotateZYX, keyframes.EulerRotation); + + if(clearQuaternion) + { + keyframes.QuaternionRotation.Clear(); + } + } + + #endregion + + + #region Euler -> Quaternion + + /// + /// Converts euler rotation keyframes to quaternion rotation keyframes. + /// + /// The euler keyframes to convert. + /// The deviation threshold below which converted values should be ignored. + /// Whether euler angles are applied in ZYX order. + /// Keyframes in which the result should be stored. + public static void EulerToQuaternion(EulerKF source, float deviationThreshold, bool rotateZYX, QuaternionKF result) + { + if(source.Count == 0) + { + return; + } + + deviationThreshold = Math.Max(deviationThreshold, 0); + + uint previousFrame = uint.MaxValue; + foreach(KeyValuePair item in source) + { + if(previousFrame != uint.MaxValue) + { + float frameCount = item.Key - previousFrame; + + for(uint i = 1; i <= frameCount; i++) + { + float fac = i / frameCount; + Vector3 lerp = Vector3.Lerp(source[previousFrame], item.Value, fac); + result.Add(previousFrame + i, lerp.EulerToQuaternion(rotateZYX)); + } + + if(deviationThreshold > 0) + { + result.OptimizeQuaternion(deviationThreshold, previousFrame, item.Key); + } + } + else + { + Quaternion quaternion = item.Value.EulerToQuaternion(rotateZYX); + result.Add(item.Key, quaternion); + } + + previousFrame = item.Key; + } + } + + /// + /// Converts euler rotation keyframes to quaternion rotation keyframes. + /// + /// The euler keyframes to convert. + /// The deviation threshold below which converted values should be ignored. + /// Whether euler angles are applied in ZYX order. + /// The converted quaternion rotation keyframes. + public static QuaternionKF EulerToQuaternion(EulerKF source, float deviationThreshold, bool rotateZYX) + { + QuaternionKF result = new(); + EulerToQuaternion(source, deviationThreshold, rotateZYX, result); + return result; + } + + /// + /// Converts euler rotation keyframes to quaternion rotation keyframes. + /// + /// The keyframes to convert and output to. + /// The deviation threshold below which converted values should be ignored. + /// Whether euler angles are applied in ZYX order. + /// Whether euler keyframes should be cleared after converting. + public static void EulerToQuaternion(this Keyframes keyframes, float deviationThreshold, bool rotateZYX, bool clearEuler) + { + keyframes.QuaternionRotation.Clear(); + EulerToQuaternion(keyframes.EulerRotation, deviationThreshold, rotateZYX, keyframes.QuaternionRotation); + + if(clearEuler) + { + keyframes.EulerRotation.Clear(); + } + } + + #endregion + + + #region Euler / Quaternion -> Matrix + + private static Matrix4x4[]? GetComplementaryMatrices(Vector3 previous, Vector3 current, bool rotateZYX) + { + Vector3 dif = current - previous; + float maxDif = Vector3.Abs(dif).GreatestValue(); + + int complementary_len = (int)MathF.Floor(maxDif / MathF.PI); + if(complementary_len == 0) + { + return null; + } + + complementary_len++; + float dif_fac = 1.0f / (complementary_len + 1); + + Matrix4x4[] result = new Matrix4x4[complementary_len]; + + for(int i = 0; i < complementary_len; i++) + { + Vector3 compl_euler = previous + (dif * (dif_fac * (i + 1))); + result[i] = MatrixUtilities.CreateRotationMatrix(compl_euler, rotateZYX); + } + + return result; + } + + + /// + /// Converts rotation keyframes to rotation matrices. Will use Euler or Quaternion rotations. If both are present, euler is used regardless. + /// + /// The keyframes to convert. + /// Whether the result is intended to be handled like quaternion rotations. + /// The deviation threshold below which converted values should be ignored. + /// Whether euler angles are applied in ZYX order. + /// Whether the output was converted between euler and quaternion + /// Complementary matrices for quaternion target. + /// The converted rotation matrix keyframes. + public static Matrix4x4KF GetRotationMatrices(this Keyframes keyframes, bool targetQuaternion, float deviationThreshold, bool rotateZYX, out bool converted, out Dictionary? complementary) + { + Matrix4x4KF result = new(); + complementary = null; + converted = false; + + if(keyframes.EulerRotation.Count == 0 && keyframes.QuaternionRotation.Count == 0) + { + return result; + } + + if(targetQuaternion) + { + QuaternionKF output = keyframes.QuaternionRotation; + + if(keyframes.EulerRotation.Count > 0) // If eulers exist, convert to quaternion regardless of whether quaternions had values before + { + converted = true; + output = EulerToQuaternion(keyframes.EulerRotation, deviationThreshold, rotateZYX); + } + + foreach(KeyValuePair quaternion in output) + { + result.Add(quaternion.Key, Matrix4x4.CreateFromQuaternion(quaternion.Value)); + } + } + else + { + EulerKF output = keyframes.EulerRotation; + + if(output.Count == 0) + { + converted = true; + output = QuaternionToEuler(keyframes.QuaternionRotation, deviationThreshold, rotateZYX); + } + + Vector3? previous = null; + uint previousFrame = 0; + complementary = new(); + + foreach(KeyValuePair rotation in output) + { + result.Add(rotation.Key, MatrixUtilities.CreateRotationMatrix(rotation.Value, rotateZYX)); + + if(previous != null) + { + Matrix4x4[]? compl_matrices = GetComplementaryMatrices(previous.Value, rotation.Value, rotateZYX); + if(compl_matrices != null) + { + complementary.Add(previousFrame, compl_matrices); + } + } + + previous = rotation.Value; + previousFrame = rotation.Key; + } + + if(complementary.Count == 0) + { + complementary = null; + } + } + + return result; + } + + private static void ConvertMatrixToQuaternion(Matrix4x4KF source, QuaternionKF result) + { + foreach(KeyValuePair item in source) + { + Matrix4x4.Decompose(item.Value, out _, out Quaternion value, out _); + result.Add(item.Key, value); + } + } + + private static void ConvertMatrixToRotation(Matrix4x4KF source, bool rotateZYX, Dictionary? complementary, EulerKF result) + { + Vector3 previousEuler = default; + + foreach(KeyValuePair item in source) + { + previousEuler = MatrixUtilities.ToCompatibleEuler(item.Value, previousEuler, rotateZYX); + result.Add(item.Key, previousEuler); + + if(complementary?.TryGetValue(item.Key, out Matrix4x4[]? matrices) == true) + { + for(int i = 0; i < matrices.Length; i++) + { + previousEuler = MatrixUtilities.ToCompatibleEuler(matrices[i], previousEuler, rotateZYX); + } + } + } + } + + #endregion + + + #region Matrix -> Quaternion + + /// + /// Converts rotation matrix keyframes to quaternion rotation keyframes. + /// + /// Rotation matrix keyframes to convert. + /// Whether the matrices should be handled as quaternion rotations. + /// The deviation threshold below which converted values should be ignored. + /// Whether the euler angles should be applied in ZYX order. + /// The keyframes to output to. + public static void MatrixToQuaternion(Matrix4x4KF source, bool wasQuaternion, float deviationThreshold, bool rotateZYX, QuaternionKF result) + { + if(wasQuaternion) + { + ConvertMatrixToQuaternion(source, result); + } + else + { + EulerKF rotations = new(); + ConvertMatrixToRotation(source, rotateZYX, null, rotations); + EulerToQuaternion(rotations, deviationThreshold, rotateZYX, result); + } + } + + /// + /// Converts rotation matrix keyframes to quaternion rotation keyframes. + /// + /// Rotation matrix keyframes to convert. + /// Whether the matrices should be handled as quaternion rotations. + /// The deviation threshold below which converted values should be ignored. + /// Whether the euler angles should be applied in ZYX order. + /// The converted quaternion keyframes. + public static QuaternionKF MatrixToQuaternion(Matrix4x4KF source, bool wasQuaternion, float deviationThreshold, bool rotateZYX) + { + QuaternionKF result = new(); + MatrixToQuaternion(source, wasQuaternion, deviationThreshold, rotateZYX, result); + return result; + } + + /// + /// Converts rotation matrix keyframes to quaternion rotation keyframes. + /// + /// The keyframes to store the converted rotation into. + /// Rotation matrix keyframes to convert. + /// Whether the matrices should be handled as quaternion rotations. + /// The deviation threshold below which converted values should be ignored. + /// Whether the euler angles should be applied in ZYX order. + public static void MatrixToQuaternion(this Keyframes keyframes, Matrix4x4KF source, bool wasQuaternion, float deviationThreshold, bool rotateZYX) + { + keyframes.QuaternionRotation.Clear(); + MatrixToQuaternion(source, wasQuaternion, deviationThreshold, rotateZYX, keyframes.QuaternionRotation); + } + + #endregion + + + #region Matrix -> Euler + + /// + /// Converts rotation matrix keyframes to euler rotation keyframes. + /// + /// Rotation matrix keyframes to convert. + /// Whether the matrices should be handled as quaternion rotations. + /// The deviation threshold below which converted values should be ignored. + /// Whether the euler angles should be applied in ZYX order. + /// Rotation matrices to be applied in between keyframes. Used for achieving angle differences greater than 180 degrees. + /// The keyframes to output to. + public static void MatrixToEuler(Matrix4x4KF source, bool wasQuaternion, float deviationThreshold, bool rotateZYX, Dictionary? complementary, EulerKF result) + { + if(wasQuaternion) + { + QuaternionKF quaternions = new(); + ConvertMatrixToQuaternion(source, quaternions); + QuaternionToEuler(quaternions, deviationThreshold, rotateZYX, result); + } + else + { + ConvertMatrixToRotation(source, rotateZYX, complementary, result); + } + } + + /// + /// Converts rotation matrix keyframes to euler rotation keyframes. + /// + /// Rotation matrix keyframes to convert. + /// Whether the matrices should be handled as quaternion rotations. + /// The deviation threshold below which converted values should be ignored. + /// Whether the euler angles should be applied in ZYX order. + /// Rotation matrices to be applied in between keyframes. Used for achieving angle differences greater than 180 degrees. + /// The converted euler keyframes. + public static EulerKF MatrixToEuler(Matrix4x4KF source, bool wasQuaternion, float deviationThreshold, bool rotateZYX, Dictionary? complementary) + { + EulerKF result = new(); + MatrixToEuler(source, wasQuaternion, deviationThreshold, rotateZYX, complementary, result); + return result; + } + + /// + /// Converts rotation matrix keyframes to euler rotation keyframes. + /// + /// The keyframes to store the converted rotation into. + /// Rotation matrix keyframes to convert. + /// Whether the matrices should be handled as quaternion rotations. + /// The deviation threshold below which converted values should be ignored. + /// Whether the euler angles should be applied in ZYX order. + /// Rotation matrices to be applied in between keyframes. Used for achieving angle differences greater than 180 degrees. + public static void MatrixToEuler(this Keyframes keyframes, Matrix4x4KF source, bool wasQuaternion, float deviationThreshold, bool rotateZYX, Dictionary? complementary) + { + keyframes.EulerRotation.Clear(); + MatrixToEuler(source, wasQuaternion, deviationThreshold, rotateZYX, complementary, keyframes.EulerRotation); + } + + #endregion + } +} diff --git a/src/SA3D.Modeling/Animation/Utilities/KeyframeWrite.cs b/src/SA3D.Modeling/Animation/Utilities/KeyframeWrite.cs new file mode 100644 index 0000000..e632386 --- /dev/null +++ b/src/SA3D.Modeling/Animation/Utilities/KeyframeWrite.cs @@ -0,0 +1,107 @@ +using SA3D.Common; +using SA3D.Common.IO; +using SA3D.Common.Lookup; +using SA3D.Modeling.Structs; +using System.Collections.Generic; +using System.Numerics; + +namespace SA3D.Modeling.Animation.Utilities +{ + internal static class KeyframeWrite + { + public static void WriteVector3Set(this EndianStackWriter writer, SortedDictionary dict, FloatIOType ioType) + { + foreach(KeyValuePair pair in dict) + { + writer.WriteUInt(pair.Key); + writer.WriteVector3(pair.Value, ioType); + } + } + + public static void WriteVector2Set(this EndianStackWriter writer, SortedDictionary dict, FloatIOType ioType) + { + foreach(KeyValuePair pair in dict) + { + writer.WriteUInt(pair.Key); + writer.WriteVector2(pair.Value, ioType); + } + } + + public static void WriteColorSet(this EndianStackWriter writer, SortedDictionary dict, ColorIOType ioType) + { + foreach(KeyValuePair pair in dict) + { + writer.WriteUInt(pair.Key); + writer.WriteColor(pair.Value, ioType); + } + } + + public static uint[] WriteVector3ArrayData(this EndianStackWriter writer, SortedDictionary> dict, PointerLUT lut) + { + uint[] result = new uint[dict.Count]; + int i = 0; + + foreach(KeyValuePair> pair in dict) + { + result[i] = pair.Key; + result[i++] = lut.GetAddAddress(pair.Value, (array) => + { + uint result = writer.PointerPosition; + + foreach(Vector3 v in pair.Value) + { + writer.WriteVector3(v); + } + + return result; + }); + i++; + } + + return result; + } + + public static void WriteVector3ArraySet(this EndianStackWriter writer, uint[] arrayData) + { + foreach(uint value in arrayData) + { + writer.WriteUInt(value); + } + } + + public static void WriteFloatSet(this EndianStackWriter writer, SortedDictionary dict, bool BAMS) + { + foreach(KeyValuePair pair in dict) + { + writer.WriteUInt(pair.Key); + if(BAMS) + { + writer.WriteInt(MathHelper.RadToBAMS(pair.Value)); + } + else + { + writer.WriteFloat(pair.Value); + } + } + } + + public static void WriteSpotlightSet(this EndianStackWriter writer, SortedDictionary dict) + { + foreach(KeyValuePair pair in dict) + { + writer.WriteUInt(pair.Key); + pair.Value.Write(writer); + } + } + + public static void WriteQuaternionSet(this EndianStackWriter writer, SortedDictionary dict) + { + foreach(KeyValuePair pair in dict) + { + writer.WriteUInt(pair.Key); + writer.WriteQuaternion(pair.Value); + } + } + + } +} diff --git a/src/SA3D.Modeling/File/AnimationFile.cs b/src/SA3D.Modeling/File/AnimationFile.cs new file mode 100644 index 0000000..1c7e56d --- /dev/null +++ b/src/SA3D.Modeling/File/AnimationFile.cs @@ -0,0 +1,357 @@ +using SA3D.Common.IO; +using SA3D.Modeling.Animation; +using SA3D.Modeling.Structs; +using System; +using System.IO; +using static SA3D.Modeling.File.FileHeaders; +using SA3D.Common; + +namespace SA3D.Modeling.File +{ + /// + /// Animation file contents. + /// + public class AnimationFile + { + /// + /// Animation of the file. + /// + public Motion Animation { get; } + + /// + /// Metadata in the file. + /// + public MetaData MetaData { get; } + + + private AnimationFile(Motion animation, MetaData metaData) + { + Animation = animation; + MetaData = metaData; + } + + + /// + /// Checks whether data is formatted as a animation file. + /// + /// The data to check. + public static bool CheckIsAnimationFile(byte[] data) + { + return CheckIsAnimationFile(data, 0); + } + + /// + /// Checks whether data is formatted as a animation file. + /// + /// The data to check. + /// Address at which to check. + public static bool CheckIsAnimationFile(byte[] data, uint address) + { + return CheckIsAnimationFile(new EndianStackReader(data), address); + } + + /// + /// Checks whether data is formatted as a animation file. + /// + /// The reader to read from. + public static bool CheckIsAnimationFile(EndianStackReader reader) + { + return CheckIsAnimationFile(reader, 0); + } + + /// + /// Checks whether data is formatted as a animation file. + /// + /// The reader to read from. + /// Address at which to check. + public static bool CheckIsAnimationFile(EndianStackReader reader, uint address) + { + return reader.ReadUInt(address) == NMDM || ( + (reader.ReadULong(address) & HeaderMask) == SAANIM + && reader[address + 7] <= CurrentAnimVersion); + } + + + /// + /// Reads a animation file. + /// + /// Path to the file to read. + /// The animation file that was read. + public static AnimationFile ReadFromFile(string filepath) + { + return ReadFromData(System.IO.File.ReadAllBytes(filepath), 0); + } + + /// + /// Reads a animation file. + /// + /// Path to the file to read. + /// Number of nodes in the targeted model node tree.
Only acts as fallback, in case the file does not contain the value. + /// Whether euler rotations are stored in 16-bit instead of 32-bit.
Only acts as fallback, in case the file does not contain the value. + /// The animation file that was read. + public static AnimationFile ReadFromFile(string filepath, uint? nodeCount, bool shortRot) + { + return ReadFromData(System.IO.File.ReadAllBytes(filepath), 0, nodeCount, shortRot); + } + + /// + /// Reads a animation file off byte data. + /// + /// The data to read. + /// The animation file that was read. + public static AnimationFile ReadFromData(byte[] data) + { + return ReadFromData(data, 0, null, false); + } + + /// + /// Reads a animation file off byte data. + /// + /// The data to read. + /// Address at which to start reading. + /// The animation file that was read. + public static AnimationFile ReadFromData(byte[] data, uint address) + { + return ReadFromData(data, address, null, false); + } + + /// + /// Reads a animation file off byte data. + /// + /// The data to read. + /// Address at which to start reading. + /// Number of nodes in the targeted model node tree.
Only acts as fallback, in case the file does not contain the value. + /// Whether euler rotations are stored in 16-bit instead of 32-bit.
Only acts as fallback, in case the file does not contain the value. + /// The animation file that was read. + public static AnimationFile ReadFromData(byte[] data, uint address, uint? nodeCount, bool shortRot) + { + using(EndianStackReader reader = new(data)) + { + return Read(reader, address, nodeCount, shortRot); + } + } + + /// + /// Reads a animation file off an endian stack reader. + /// + /// The reader to read from. + /// The animation file that was read. + public static AnimationFile Read(EndianStackReader reader) + { + return Read(reader, 0, null, false); + } + + /// + /// Reads a animation file off an endian stack reader. + /// + /// The reader to read from. + /// Address at which to start reading. + /// The animation file that was read. + public static AnimationFile Read(EndianStackReader reader, uint address) + { + return Read(reader, address, null, false); + } + + /// + /// Reads a animation file off an endian stack reader. + /// + /// The reader to read from. + /// Address at which to start reading. + /// Number of nodes in the targeted model node tree.
Only acts as fallback, in case the file does not contain the value. + /// Whether euler rotations are stored in 16-bit instead of 32-bit.
Only acts as fallback, in case the file does not contain the value. + /// The animation file that was read. + public static AnimationFile Read(EndianStackReader reader, uint address, uint? nodeCount, bool shortRot) + { + reader.PushBigEndian(false); + + try + { + if(reader.ReadUInt(address) == NMDM) + { + return ReadNM(reader, address, nodeCount); + } + else if((reader.ReadULong(address) & HeaderMask) == SAANIM) + { + return ReadSA(reader, address, nodeCount, shortRot); + } + else + { + throw new FormatException("Animation file invalid!"); + } + } + finally + { + reader.PopEndian(); + } + } + + private static AnimationFile ReadNM(EndianStackReader reader, uint address, uint? nodeCount) + { + if(nodeCount == null) + { + throw new ArgumentException("Cannot read NMDM animations without providing node count!"); + } + + // Determines big endian via the framecount. + // As long as that one is not bigger than 65,535 or 18 minutes of animation at 60fps, we good + reader.PushBigEndian(reader.CheckBigEndian32(address + 0xC)); + uint prevImageBase = reader.ImageBase; + + try + { + uint dataAddress = address + 8; + reader.ImageBase = unchecked((uint)-dataAddress); + Motion motion = Motion.Read(reader, dataAddress, nodeCount.Value, new(), true); + return new(motion, new()); + } + finally + { + reader.ImageBase = prevImageBase; + reader.PopEndian(); + } + } + + private static AnimationFile ReadSA(EndianStackReader reader, uint address, uint? nodeCount, bool shortRot) + { + byte version = reader[7]; + if(version > CurrentAnimVersion) + { + throw new FormatException("Not a valid SAANIM file."); + } + + uint motionAddress = reader.ReadUInt(address + 8); + + MetaData metaData = new(); + if(version >= 2) + { + // motion v2 uses metadata v3 + metaData = MetaData.Read(reader, address + 0xC, 3, false); + } + else if(reader.TryReadPointer(address + 0xC, out uint labelAddr)) + { + metaData.Labels.Add(motionAddress, reader.ReadNullterminatedString(labelAddr)); + } + + if(version > 0) + { + const uint shortRotMask = (uint)Flag32.B31; + uint fileNodeCount = reader.ReadUInt(0x10); + shortRot = (fileNodeCount & shortRotMask) != 0; + nodeCount = fileNodeCount & ~shortRotMask; + } + else if(nodeCount == null) + { + throw new ArgumentException("Cannot open version 0 animations without providing node count!"); + } + + PointerLUT lut = new(metaData.Labels); + Motion motion = Motion.Read(reader, motionAddress, nodeCount.Value, lut, shortRot); + + return new(motion, metaData); + } + + + /// + /// Write the animation file to a file. Previous labels may get lost. + /// + /// Path to the file to write to. + /// + public void WriteToFile(string filepath) + { + WriteToFile(filepath, Animation, MetaData); + } + + /// + /// Writes the animation file to a byte array. Previous labels may get lost. + /// + /// + /// + public byte[] WriteToData() + { + return WriteToData(Animation, MetaData); + } + + /// + /// Writes the animation file to an endian stack writer. Previous labels may get lost. + /// + /// The writer to write to. + /// + public void Write(EndianStackWriter writer) + { + Write(writer, Animation, MetaData); + } + + + /// + /// Write a animation file to a file. + /// + /// Path to the file to write to. + /// The animation to write. + /// The metadata to include. + /// + public static void WriteToFile(string filepath, Motion animation, MetaData? metaData = null) + { + using(FileStream stream = System.IO.File.Create(filepath)) + { + EndianStackWriter writer = new(stream); + Write(writer, animation, metaData); + } + } + + /// + /// Writes a animation file to a byte array. + /// + /// The animation to write. + /// The metadata to include. + /// The written byte data. + /// + public static byte[] WriteToData(Motion animation, MetaData? metaData = null) + { + using(MemoryStream stream = new()) + { + EndianStackWriter writer = new(stream); + Write(writer, animation, metaData); + return stream.ToArray(); + } + } + + /// + /// Writes a animation file to an endian stack writer. + /// + /// The writer to write to. + /// The animation to write. + /// The metadata to include. + /// + public static void Write(EndianStackWriter writer, Motion animation, MetaData? metaData = null) + { + writer.WriteULong(SAANIMVer); + + uint placeholderAddr = writer.Position; + // 4 bytes: motion address placeholder + // 4 bytes: metadata placeholder + writer.WriteEmpty(8); + + uint animFileInfo = animation.ModelCount; + if(animation.ShortRot) + { + animFileInfo |= (uint)Flag32.B31; + } + + writer.WriteUInt(animFileInfo); + + PointerLUT lut = new(); + + uint motionAddress = animation.Write(writer, lut); + + metaData ??= new(); + metaData.Labels = lut.Labels.GetDictFrom(); + uint metaDataAddress = metaData.Write(writer); + + uint end = writer.Position; + writer.Seek(placeholderAddr, SeekOrigin.Begin); + writer.WriteUInt(motionAddress); + writer.WriteUInt(metaDataAddress); + writer.Seek(end, SeekOrigin.Begin); + } + } +} diff --git a/src/SA3D.Modeling/File/FileHeaders.cs b/src/SA3D.Modeling/File/FileHeaders.cs new file mode 100644 index 0000000..03e29ec --- /dev/null +++ b/src/SA3D.Modeling/File/FileHeaders.cs @@ -0,0 +1,94 @@ +namespace SA3D.Modeling.File +{ + internal class FileHeaders + { + public const ulong HeaderMask = ~((ulong)0xFF << 56); + + public const ulong LVL = 0x4C564Cu; + public const ulong MDL = 0x4C444Du; + + public const ulong SA1 = 0x314153u; + public const ulong SADX = 0x58444153u; + public const ulong SA2 = 0x324153u; + public const ulong SA2B = 0x42324153u; + public const ulong BUF = 0x465542u; + + #region Landtable + + public const ulong SA1LVL = (LVL << 24) | SA1; + public const ulong SADXLVL = (LVL << 32) | SADX; + public const ulong SA2LVL = (LVL << 24) | SA2; + public const ulong SA2BLVL = (LVL << 32) | SA2B; + public const ulong BUFLVL = (LVL << 24) | BUF; + + public const ulong CurrentLandtableVersion = 3; + public const ulong CurrentLandtableVersionShifted = CurrentLandtableVersion << 56; + + public const ulong SA1LVLVer = SA1LVL | CurrentLandtableVersionShifted; + public const ulong SADXLVLVer = SADXLVL | CurrentLandtableVersionShifted; + public const ulong SA2LVLVer = SA2LVL | CurrentLandtableVersionShifted; + public const ulong SA2BLVLVer = SA2BLVL | CurrentLandtableVersionShifted; + public const ulong BUFLVLVer = BUFLVL | CurrentLandtableVersionShifted; + + #endregion + + #region Model + + public const ulong SA1MDL = (MDL << 24) | SA1; + public const ulong SADXMDL = (MDL << 32) | SADX; + public const ulong SA2MDL = (MDL << 24) | SA2; + public const ulong SA2BMDL = (MDL << 32) | SA2B; + public const ulong BUFMDL = (MDL << 24) | BUF; + + public const ulong CurrentModelVersion = 3; + public const ulong CurrentModelVersionShifted = CurrentModelVersion << 56; + + public const ulong SA1MDLVer = SA1MDL | CurrentModelVersionShifted; + public const ulong SADXMDLVer = SADXMDL | CurrentModelVersionShifted; + public const ulong SA2MDLVer = SA2MDL | CurrentModelVersionShifted; + public const ulong SA2BMDLVer = SA2BMDL | CurrentModelVersionShifted; + public const ulong BUFMDLVer = BUFMDL | CurrentModelVersionShifted; + + #endregion + + #region Animation + + public const ulong SAANIM = 0x4D494E414153u; + + public const ulong CurrentAnimVersion = 2; + public const ulong CurrentAnimVersionShifted = CurrentAnimVersion << 56; + + public const ulong SAANIMVer = SAANIM | CurrentAnimVersionShifted; + + + #endregion + + #region Other + + /// + /// NJ header. + /// + public const ushort NJ = (ushort)0x4A4Eu; + + /// + /// GJ Header. + /// + public const ushort GJ = (ushort)0x4A47u; + + /// + /// Chunk model block header. + /// + public const ushort CM = (ushort)0x4D43u; + + /// + /// Basic model block header. + /// + public const ushort BM = (ushort)0x4D42u; + + /// + /// NM (motion) file header. + /// + public const uint NMDM = 0x4D444D4Eu; + #endregion + } +} diff --git a/src/SA3D.Modeling/File/LevelFile.cs b/src/SA3D.Modeling/File/LevelFile.cs new file mode 100644 index 0000000..9ae6a17 --- /dev/null +++ b/src/SA3D.Modeling/File/LevelFile.cs @@ -0,0 +1,290 @@ +using SA3D.Common.IO; +using SA3D.Modeling.ObjectData; +using SA3D.Modeling.ObjectData.Enums; +using SA3D.Modeling.Structs; +using System; +using System.IO; +using static SA3D.Modeling.File.FileHeaders; + +namespace SA3D.Modeling.File +{ + /// + /// Level geometry file contents. + /// + public class LevelFile + { + /// + /// Landtable of the file. + /// + public LandTable Level { get; } + + /// + /// MetaData of/for a LVL file + /// + public MetaData MetaData { get; } + + + private LevelFile(LandTable level, MetaData metaData) + { + Level = level; + MetaData = metaData; + } + + + /// + /// Checks whether data is formatted as a level file. + /// + /// The data to check. + public static bool CheckIsLevelFile(byte[] data) + { + return CheckIsLevelFile(data, 0); + } + + /// + /// Checks whether data is formatted as a level file. + /// + /// The data to check. + /// Address at which to check. + public static bool CheckIsLevelFile(byte[] data, uint address) + { + return CheckIsLevelFile(new EndianStackReader(data), address); + } + + /// + /// Checks whether data is formatted as a level file. + /// + /// The reader to read from. + public static bool CheckIsLevelFile(EndianStackReader reader) + { + return CheckIsLevelFile(reader, 0); + } + + /// + /// Checks whether data is formatted as a level file. + /// + /// The reader to read from. + /// Address at which to check. + public static bool CheckIsLevelFile(EndianStackReader reader, uint address) + { + switch(reader.ReadULong(address) & HeaderMask) + { + case SA1LVL: + case SADXLVL: + case SA2LVL: + case SA2BLVL: + case BUFLVL: + break; + default: + return false; + } + + return reader[address + 7] <= CurrentLandtableVersion; + } + + + /// + /// Reads a level file. + /// + /// Path to the file to read. + /// The level file that was read. + public static LevelFile ReadFromFile(string filepath) + { + return ReadFromData(System.IO.File.ReadAllBytes(filepath)); + } + + /// + /// Reads a level file off byte data. + /// + /// The data to read. + /// The level file that was read. + public static LevelFile ReadFromData(byte[] data) + { + return ReadFromData(data, 0); + } + + /// + /// Reads a level file off byte data. + /// + /// The data to read. + /// Address at which to start reading. + /// The level file that was read. + public static LevelFile ReadFromData(byte[] data, uint address) + { + using(EndianStackReader reader = new(data)) + { + return Read(reader, address); + } + } + + /// + /// Reads a level file off an endian stack reader. + /// + /// The reader to read from. + /// The level file that was read. + public static LevelFile Read(EndianStackReader reader) + { + return Read(reader, 0); + } + + /// + /// Reads a level file off an endian stack reader. + /// + /// The reader to read from. + /// Address at which to start reading. + /// The level file that was read. + public static LevelFile Read(EndianStackReader reader, uint address) + { + reader.PushBigEndian(false); + + try + { + ulong header = reader.ReadULong(0) & HeaderMask; + byte version = reader[7]; + + ModelFormat format = header switch + { + SA1LVL => ModelFormat.SA1, + SADXLVL => ModelFormat.SADX, + SA2LVL => ModelFormat.SA2, + SA2BLVL => ModelFormat.SA2B, + BUFLVL => ModelFormat.Buffer, + _ => throw new FormatException("File invalid; Header malformed"), + }; + + if(version > CurrentLandtableVersion) + { + throw new FormatException("File invalid; Version not supported"); + } + + MetaData metaData = MetaData.Read(reader, address + 0xC, version, false); + PointerLUT lut = new(metaData.Labels); + + uint ltblAddress = reader.ReadUInt(address + 8); + LandTable table = LandTable.Read(reader, ltblAddress, format, lut); + + return new(table, metaData); + } + finally + { + reader.PopEndian(); + } + } + + + /// + /// Write the level file to a file. Previous labels may get lost. + /// + /// Path to the file to write to. + /// + public void WriteToFile(string filepath) + { + WriteToFile(filepath, Level, MetaData); + } + + /// + /// Writes the level file to a byte array. Previous labels may get lost. + /// + /// + /// + public byte[] WriteToData() + { + return WriteToData(Level, MetaData); + } + + /// + /// Writes the level file to an endian stack writer. Previous labels may get lost. + /// + /// The writer to write to. + /// + public void Write(EndianStackWriter writer) + { + Write(writer, Level, MetaData); + } + + + /// + /// Write a level file to a file. + /// + /// Path to the file to write to. + /// The level to write. + /// The metadata to include. + /// + public static void WriteToFile(string filepath, LandTable level, MetaData? metaData = null) + { + using(FileStream stream = System.IO.File.Create(filepath)) + { + EndianStackWriter writer = new(stream); + Write(writer, level, metaData); + } + } + + /// + /// Writes a level file to a byte array. + /// + /// The level to write. + /// The metadata to include. + /// The written byte data. + /// + public static byte[] WriteToData(LandTable level, MetaData? metaData = null) + { + using(MemoryStream stream = new()) + { + EndianStackWriter writer = new(stream); + Write(writer, level, metaData); + return stream.ToArray(); + } + } + + /// + /// Writes a level file to an endian stack writer. + /// + /// The writer to write to. + /// The level to write. + /// The metadata to include. + /// + public static void Write(EndianStackWriter writer, LandTable level, MetaData? metaData = null) + { + // writing indicator + switch(level.Format) + { + case ModelFormat.SA1: + writer.WriteULong(SA1LVLVer); + break; + case ModelFormat.SADX: + writer.WriteULong(SADXLVLVer); + break; + case ModelFormat.SA2: + writer.WriteULong(SA2LVLVer); + break; + case ModelFormat.SA2B: + writer.WriteULong(SA2BLVLVer); + break; + case ModelFormat.Buffer: + writer.WriteULong(BUFLVLVer); + break; + default: + break; + } + + uint placeholderAddr = writer.Position; + // 4 bytes: landtable address placeholder + // 4 bytes: metadata placeholder + writer.WriteEmpty(8); + + PointerLUT lut = new(); + + uint ltblAddress = level.Write(writer, lut); + + metaData ??= new(); + metaData.Labels = lut.Labels.GetDictFrom(); + uint metaDataAddress = metaData.Write(writer); + + uint end = writer.Position; + writer.Seek(placeholderAddr, SeekOrigin.Begin); + writer.WriteUInt(ltblAddress); + writer.WriteUInt(metaDataAddress); + writer.Seek(end, SeekOrigin.Begin); + } + + } +} diff --git a/src/SA3D.Modeling/File/MetaBlockType.cs b/src/SA3D.Modeling/File/MetaBlockType.cs new file mode 100644 index 0000000..56d6278 --- /dev/null +++ b/src/SA3D.Modeling/File/MetaBlockType.cs @@ -0,0 +1,58 @@ +namespace SA3D.Modeling.File +{ + /// + /// Meta data type + /// + public enum MetaBlockType : uint + { + /// + /// Data labels. + /// + Label = 0x4C42414C, + + /// + /// List of animation files paths. + /// + Animation = 0x4D494E41, + + /// + /// List of morph animation file paths. + /// + Morph = 0x46524F4D, + + /// + /// Author of the file. + /// + Author = 0x48545541, + + /// + /// Tool used to create the file. + /// + Tool = 0x4C4F4F54, + + /// + /// Description given to the file. + /// + Description = 0x43534544, + + /// + /// Texture info. + /// + Texture = 0x584554, + + /// + /// Name of the action that the data belongs to. + /// + ActionName = 0x4143544E, + + /// + /// Name of the object that the data belongs to. + /// + ObjectName = 0x4F424A4E, + + /// + /// End marker. + /// + End = 0x444E45 + } +} diff --git a/src/SA3D.Modeling/File/MetaData.cs b/src/SA3D.Modeling/File/MetaData.cs new file mode 100644 index 0000000..a34277f --- /dev/null +++ b/src/SA3D.Modeling/File/MetaData.cs @@ -0,0 +1,397 @@ +using SA3D.Common.IO; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + + +namespace SA3D.Modeling.File +{ + /// + /// Meta data storage for files. + /// + public class MetaData + { + /// + /// Author of the file. + /// + public string? Author { get; set; } + + /// + /// Description of the files contents. + /// + public string? Description { get; set; } + + /// + /// Action name. + /// + public string? ActionName { get; set; } + + /// + /// Object name. + /// + public string? ObjectName { get; set; } + + /// + /// C struct labels (only for reading). + /// + public Dictionary Labels { get; set; } + + /// + /// Animation file paths. + /// + public List AnimFiles { get; } + + /// + /// Morph animation file paths. + /// + public List MorphFiles { get; } + + /// + /// Other chunk blocks that have no implementation. + /// + public Dictionary Other { get; set; } + + + /// + /// Creates a new empty set of meta data. + /// + public MetaData() + { + AnimFiles = new(); + MorphFiles = new(); + Other = new(); + Labels = new(); + } + + + private void ReadVersion0(EndianStackReader reader, uint address) + { + // reading animation locations + if(reader.TryReadPointer(address, out uint dataAddr)) + { + uint pathAddr = reader.ReadPointer(dataAddr); + while(pathAddr != uint.MaxValue) + { + AnimFiles.Add(reader.ReadNullterminatedString(pathAddr)); + pathAddr = reader.ReadPointer(dataAddr += 4); + } + } + + // reading morph locations + if(reader.TryReadPointer(address + 4, out dataAddr)) + { + uint pathAddr = reader.ReadPointer(dataAddr); + while(pathAddr != uint.MaxValue) + { + MorphFiles.Add(reader.ReadNullterminatedString(pathAddr)); + pathAddr = reader.ReadPointer(dataAddr += 4); + } + } + } + + private void ReadVersion1(EndianStackReader reader, uint address, bool hasAnimMorphFiles) + { + if(hasAnimMorphFiles) + { + ReadVersion0(reader, address); + address += 8; + } + + if(!reader.TryReadPointer(address, out uint labelsAddress)) + { + return; + } + + uint labelPointer = reader.ReadUInt(labelsAddress); + while(labelPointer != uint.MaxValue) + { + uint labelTextPointer = reader.ReadUInt(labelsAddress + 4); + string labelText = reader.ReadNullterminatedString(labelTextPointer); + Labels.Add(labelPointer, labelText); + + labelsAddress += 8; + labelPointer = reader.ReadUInt(labelsAddress); + } + } + + private bool ReadMetaBlockType(EndianStackReader reader, ref uint address, MetaBlockType type) + { + switch(type) + { + case MetaBlockType.Label: + while(reader.ReadULong(address) != ulong.MaxValue) + { + uint labelAddress = reader.ReadUInt(address); + string label = reader.ReadNullterminatedString(reader.ReadPointer(address += 4)); + if(!Labels.TryAdd(labelAddress, label)) + { + Labels[labelAddress] = label; + } + + address += 4; + } + + break; + case MetaBlockType.Animation: + while(reader.ReadUInt(address) != uint.MaxValue) + { + AnimFiles.Add( + reader.ReadNullterminatedString( + reader.ReadPointer(address))); + address += 4; + } + + break; + case MetaBlockType.Morph: + while(reader.ReadUInt(address) != uint.MaxValue) + { + MorphFiles.Add( + reader.ReadNullterminatedString( + reader.ReadPointer(address))); + address += 4; + } + + break; + case MetaBlockType.Author: + Author = reader.ReadNullterminatedString(address); + break; + case MetaBlockType.Description: + Description = reader.ReadNullterminatedString(address); + break; + case MetaBlockType.ActionName: + ActionName = reader.ReadNullterminatedString(address); + break; + case MetaBlockType.ObjectName: + ObjectName = reader.ReadNullterminatedString(address); + break; + case MetaBlockType.Tool: + break; + case MetaBlockType.Texture: + break; + case MetaBlockType.End: + break; + default: + return false; + } + + return true; + } + + private void ReadVersion2(EndianStackReader reader, uint address) + { + if(!reader.TryReadPointer(address, out uint tmpAddr)) + { + return; + } + + MetaBlockType type = (MetaBlockType)reader.ReadUInt(tmpAddr); + + while(type != MetaBlockType.End) + { + uint blockSize = reader.ReadUInt(tmpAddr + 4); + uint blockStart = tmpAddr + 8; + tmpAddr = blockStart + blockSize; + + ReadMetaBlockType(reader, ref blockStart, type); + + type = (MetaBlockType)reader.ReadUInt(tmpAddr); + } + } + + private void ReadVersion3(EndianStackReader reader, uint address) + { + if(!reader.TryReadPointer(address, out uint metaAddr)) + { + return; + } + + MetaBlockType type = (MetaBlockType)reader.ReadUInt(metaAddr); + + while(type != MetaBlockType.End) + { + uint blockSize = reader.ReadUInt(metaAddr += 4); + metaAddr += 4; + + reader.ImageBase = ~metaAddr + 1; + uint blockAddr = metaAddr; + + if(!ReadMetaBlockType(reader, ref blockAddr, type)) + { + byte[] block = reader.Slice((int)blockAddr, (int)blockSize).ToArray(); + Other.Add((uint)type, block); + } + + metaAddr += blockSize; + type = (MetaBlockType)reader.ReadUInt(metaAddr); + } + } + + /// + /// Reads meta data off an endian stack reader. + /// + /// The reader to read from + /// Address at which to start reading. + /// File version. + /// Whether the file contains animation and morph animation file paths (only applicable to Version 1) + /// + public static MetaData Read(EndianStackReader reader, uint address, int version, bool hasAnimMorphFiles) + { + MetaData result = new(); + uint prevImageBase = reader.ImageBase; + reader.ImageBase = 0; + + switch(version) + { + case 0: + result.ReadVersion0(reader, address); + break; + case 1: + result.ReadVersion1(reader, address, hasAnimMorphFiles); + break; + case 2: + result.ReadVersion2(reader, address); + break; + case 3: + result.ReadVersion3(reader, address); + break; + default: + break; + } + + reader.ImageBase = prevImageBase; + return result; + } + + + /// + /// Writes the meta data to a stream + /// + /// Output stream + public uint Write(EndianStackWriter writer) + { + uint result = writer.PointerPosition; + + if(Labels.Count > 0) + { + WriteBlock(writer, MetaBlockType.Label, () => + { + uint straddr = (uint)((Labels.Count * 8) + 8); + + foreach(KeyValuePair label in Labels) + { + writer.WriteUInt(label.Key); + writer.WriteUInt(straddr); + straddr += CalcStringLength(label.Value); + } + + writer.WriteLong(-1L); + + foreach(KeyValuePair label in Labels) + { + WriteString(writer, label.Value); + } + }); + } + + if(AnimFiles?.Count > 0) + { + WriteStringList(writer, MetaBlockType.Animation, AnimFiles); + } + + if(MorphFiles?.Count > 0) + { + WriteStringList(writer, MetaBlockType.Morph, MorphFiles); + } + + WriteStringBlock(writer, MetaBlockType.Author, Author); + WriteStringBlock(writer, MetaBlockType.Description, Description); + WriteStringBlock(writer, MetaBlockType.ActionName, ActionName); + WriteStringBlock(writer, MetaBlockType.ObjectName, ObjectName); + + foreach(KeyValuePair item in Other) + { + writer.WriteUInt(item.Key); + writer.WriteUInt((uint)item.Value.Length); + writer.Write(item.Value); + } + + writer.WriteUInt((uint)MetaBlockType.End); + writer.WriteUInt(0); + + return result; + } + + #region Utility methods + + private static void WriteBlock(EndianStackWriter writer, MetaBlockType type, Action write) + { + uint start = writer.Position; + + writer.WriteUInt((uint)type); + writer.WriteEmpty(4); + + write(); + + uint bytesWritten = writer.Position - start - 8; + + uint prevPos = writer.Position; + writer.Seek(start + 4, SeekOrigin.Begin); + writer.WriteUInt(bytesWritten); + writer.Seek(prevPos, SeekOrigin.Begin); + } + + private static void WriteString(EndianStackWriter writer, string value) + { + byte[] bytes = Encoding.UTF8.GetBytes(value + "\0"); + uint start = writer.Position; + writer.Write(bytes); + writer.AlignFrom(4, start); + } + + private static uint CalcStringLength(string value) + { + uint length = (uint)Encoding.UTF8.GetBytes(value).Length + 1; + uint padding = length % 4; + if(padding > 0) + { + length += 4 - padding; + } + + return length; + } + + private static void WriteStringBlock(EndianStackWriter writer, MetaBlockType type, string? value) + { + if(string.IsNullOrEmpty(value)) + { + return; + } + + WriteBlock(writer, type, () => WriteString(writer, value)); + } + + private static void WriteStringList(EndianStackWriter writer, MetaBlockType type, List values) + { + WriteBlock(writer, type, () => + { + uint straddr = (uint)((values.Count + 1) * 4); + + foreach(string value in values) + { + writer.WriteUInt(straddr); + straddr += CalcStringLength(value); + } + + writer.WriteUInt(uint.MaxValue); + + foreach(string value in values) + { + WriteString(writer, value); + } + }); + } + + #endregion + } +} diff --git a/src/SA3D.Modeling/File/ModelFile.cs b/src/SA3D.Modeling/File/ModelFile.cs new file mode 100644 index 0000000..cf06926 --- /dev/null +++ b/src/SA3D.Modeling/File/ModelFile.cs @@ -0,0 +1,437 @@ +using SA3D.Modeling.Mesh; +using SA3D.Modeling.ObjectData.Enums; +using SA3D.Modeling.ObjectData; +using SA3D.Modeling.Structs; +using SA3D.Common.IO; +using System.IO; +using System; +using static SA3D.Modeling.File.FileHeaders; + +namespace SA3D.Modeling.File +{ + /// + /// Node model with attach data file contents. + /// + public class ModelFile + { + /// + /// Whether the file is an NJ binary + /// + public bool NJFile { get; } + + /// + /// Attach format of the file + /// + public ModelFormat Format { get; } + + /// + /// Hierarchy tip of the file + /// + public Node Model { get; } + + /// + /// Meta data of the file + /// + public MetaData MetaData { get; } + + + private ModelFile(ModelFormat format, Node model, MetaData metaData, bool nj) + { + Format = format; + Model = model; + MetaData = metaData; + NJFile = nj; + } + + + /// + /// Checks whether data is formatted as a model file. + /// + /// The data to check. + public static bool CheckIsModelFile(byte[] data) + { + return CheckIsModelFile(data, 0); + } + + /// + /// Checks whether data is formatted as a model file. + /// + /// The data to check. + /// Address at which to check. + public static bool CheckIsModelFile(byte[] data, uint address) + { + return CheckIsModelFile(new EndianStackReader(data), address); + } + + /// + /// Checks whether data is formatted as a model file. + /// + /// The reader to read from. + public static bool CheckIsModelFile(EndianStackReader reader) + { + return CheckIsModelFile(reader, 0); + } + + /// + /// Checks whether data is formatted as a model file. + /// + /// The reader to read from. + /// Address at which to check. + public static bool CheckIsModelFile(EndianStackReader reader, uint address) + { + try + { + _ = GetNJModelBlockAddress(reader, address); + return true; + } + catch(FormatException) { } + + switch(reader.ReadULong(address) & HeaderMask) + { + case SA1MDL: + case SADXMDL: + case SA2MDL: + case SA2BMDL: + case BUFMDL: + break; + default: + return false; + } + + return reader[address + 7] <= CurrentModelVersion; + } + + + /// + /// Reads a model file. + /// + /// The path to the file that should be read. + /// The model file that was read. + public static ModelFile ReadFromFile(string filepath) + { + return ReadFromData(System.IO.File.ReadAllBytes(filepath)); + } + + /// + /// Reads a model file off byte data. + /// + /// Data to read. + /// The model file that was read. + public static ModelFile ReadFromData(byte[] data) + { + return ReadFromData(data, 0); + } + + /// + /// Reads a model file off byte data. + /// + /// The data to read from. + /// The address at which to start reading. + /// The model file that was read. + public static ModelFile ReadFromData(byte[] data, uint address) + { + using(EndianStackReader reader = new(data)) + { + return Read(reader, address); + } + } + + /// + /// Reads a model file off an endian stack reader. + /// + /// The reader to read from. + /// The model file that was read. + public static ModelFile Read(EndianStackReader reader) + { + return Read(reader, 0); + } + + /// + /// Reads a model file off an endian stack reader. + /// + /// The reader to read from. + /// Address at which to start reading. + /// The model file that was read. + public static ModelFile Read(EndianStackReader reader, uint address) + { + reader.PushBigEndian(false); + + try + { + return reader.ReadUShort(address) is NJ or GJ + ? ReadNJ(reader, address) + : ReadSA(reader, address); + } + finally + { + reader.PopEndian(); + } + } + + + private static uint GetNJModelBlockAddress(EndianStackReader reader, uint address) + { + uint blockAddress = address; + while(address < reader.Length + 8) + { + ushort njHeader = reader.ReadUShort(blockAddress); + + if(njHeader is not NJ or GJ) + { + throw new FormatException("Malformatted NJ data."); + } + + ushort blockHeader = reader.ReadUShort(blockAddress + 2); + + if(blockHeader is BM or CM) + { + return blockAddress; + } + + uint blockSize = reader.ReadUInt(blockAddress + 4); + blockAddress += 8 + blockSize; + } + + throw new FormatException("No model block found"); + } + + private static ModelFile ReadNJ(EndianStackReader reader, uint address) + { + uint blockAddress = GetNJModelBlockAddress(reader, address); + ushort blockHeader = reader.ReadUShort(blockAddress + 2); + + uint modelAddress = blockAddress + 8; + bool fileEndian = reader.CheckBigEndian32(modelAddress); + reader.PushBigEndian(fileEndian); + + reader.ImageBase = unchecked((uint)-modelAddress); + + ModelFormat format = blockHeader switch + { + BM => ModelFormat.SA1, + CM => ModelFormat.SA2, + _ => throw new FormatException() + }; + + Node model = Node.Read(reader, modelAddress, format, new()); + + return new(format, model, new(), true); + } + + private static ModelFile ReadSA(EndianStackReader reader, uint address) + { + ulong header8 = reader.ReadULong(address) & HeaderMask; + ModelFormat format = header8 switch + { + SA1MDL => ModelFormat.SA1, + SADXMDL => ModelFormat.SADX, + SA2MDL => ModelFormat.SA2, + SA2BMDL => ModelFormat.SA2B, + BUFMDL => ModelFormat.Buffer, + _ => throw new FormatException("File invalid; Header malformed"), + }; + + // checking the version + byte version = reader[address + 7]; + if(version > CurrentModelVersion) + { + throw new FormatException("File invalid; Unsupported version"); + } + + MetaData metaData = MetaData.Read(reader, address + 0xC, version, true); + PointerLUT lut = new(metaData.Labels); + + uint prevImageBase = reader.ImageBase; + if(address != 0) + { + reader.ImageBase = unchecked((uint)-address); + } + + uint modelAddr = reader.ReadPointer(address + 8); + Node model = Node.Read(reader, modelAddr, format, lut); + + reader.ImageBase = prevImageBase; + + return new(format, model, metaData, false); + } + + + /// + /// Write the model file to a file. Previous labels may get lost. + /// + /// Path to the file to write to. + /// + public void WriteToFile(string filepath) + { + WriteToFile(filepath, Model, NJFile, MetaData, Format); + } + + /// + /// Writes the model file to a byte array. Previous labels may get lost. + /// + /// + /// The written byte data. + public byte[] WriteToData() + { + return WriteToData(Model, NJFile, MetaData, Format); + } + + /// + /// Writes the model file to an endian stack writer. Previous labels may get lost. + /// + /// The writer to write to. + /// + public void Write(EndianStackWriter writer) + { + Write(writer, Model, NJFile, MetaData, Format); + } + + + /// + /// Write a model file to a file. + /// + /// Path to the file to write to. + /// The model to write. + /// Whether to format as an NJ file. + /// The metadata to include. + /// The format to write in. + /// + public static void WriteToFile(string filepath, Node model, bool nj = false, MetaData? metaData = null, ModelFormat? format = null) + { + using(FileStream stream = System.IO.File.Create(filepath)) + { + EndianStackWriter writer = new(stream); + Write(writer, model, nj, metaData, format); + } + } + + /// + /// Writes a model file to a byte array. + /// + /// The model to write. + /// Whether to format as an NJ file. + /// The metadata to include. + /// The format to write in. + /// + /// The written byte data. + public static byte[] WriteToData(Node model, bool nj = false, MetaData? metaData = null, ModelFormat? format = null) + { + using(MemoryStream stream = new()) + { + EndianStackWriter writer = new(stream); + Write(writer, model, nj, metaData, format); + return stream.ToArray(); + } + } + + /// + /// Writes a model file to an endian stack writer. + /// + /// The writer to write to. + /// The model to write. + /// Whether to format as an NJ file. + /// The metadata to include. + /// The format to write in. + /// + public static void Write(EndianStackWriter writer, Node model, bool nj = false, MetaData? metaData = null, ModelFormat? format = null) + { + format ??= model.GetAttachFormat() switch + { + AttachFormat.Buffer => ModelFormat.Buffer, + AttachFormat.BASIC => ModelFormat.SA1, + AttachFormat.CHUNK => ModelFormat.SA2, + AttachFormat.GC => ModelFormat.SA2B, + _ => throw new InvalidOperationException(), + }; + + if(nj) + { + WriteNJ(writer, model, format.Value); + } + else + { + WriteSA(writer, model, format.Value, metaData); + } + } + + private static void WriteNJ(EndianStackWriter writer, Node model, ModelFormat format) + { + writer.WriteUShort(NJ); + switch(format) + { + case ModelFormat.SA1 or ModelFormat.SADX: + writer.WriteUShort(BM); + break; + case ModelFormat.SA2: + writer.WriteUShort(CM); + break; + default: + throw new ArgumentException($"Attach format {format} not supported for NJ binaries"); + } + + uint placeholderAddress = writer.Position; + writer.WriteEmpty(4); // file length placeholder + + uint nodeStart = writer.Position; + writer.ImageBase = unchecked((uint)-nodeStart); + writer.WriteEmpty(Node.StructSize); + + PointerLUT lut = new(); + + model.Child?.Write(writer, format, lut); + model.Next?.Write(writer, format, lut); + model.Attach?.Write(writer, format, lut); + + uint byteSize = writer.Position - nodeStart; + + // write the root node to the start + uint end = writer.Position; + writer.Seek(placeholderAddress, SeekOrigin.Begin); + writer.WriteUInt(byteSize); + model.Write(writer, format, lut); + + // replace size + writer.Seek(end, SeekOrigin.Begin); + } + + private static void WriteSA(EndianStackWriter writer, Node model, ModelFormat format, MetaData? metadata) + { + ulong header = format switch + { + ModelFormat.SA1 => SA1MDLVer, + ModelFormat.SADX => SADXMDLVer, + ModelFormat.SA2 => SA2MDLVer, + ModelFormat.SA2B => SA2BMDLVer, + ModelFormat.Buffer => BUFMDLVer, + _ => throw new ArgumentException($"Model format {format} not supported for SAMDL files"), + }; + + writer.WriteULong(header); + + uint placeholderAddr = writer.Position; + // 4 bytes: node address placeholder + // 4 bytes: metadata placeholder + writer.WriteEmpty(8); + + PointerLUT lut = new(); + uint modelAddress = model.Write(writer, format, lut); + + metadata ??= new(); + metadata.Labels = lut.Labels.GetDictFrom(); + uint metadataAddress = metadata.Write(writer); + + uint end = writer.Position; + writer.Seek(placeholderAddr, SeekOrigin.Begin); + writer.WriteUInt(modelAddress); + writer.WriteUInt(metadataAddress); + writer.Seek(end, SeekOrigin.Begin); + } + + + /// + public override string ToString() + { + return $"{(NJFile ? "" : "NJ")} Modelfile - {Format}"; + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Attach.cs b/src/SA3D.Modeling/Mesh/Attach.cs new file mode 100644 index 0000000..753e7c2 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Attach.cs @@ -0,0 +1,265 @@ +using SA3D.Common; +using SA3D.Common.IO; +using SA3D.Common.Lookup; +using SA3D.Modeling.Mesh.Buffer; +using SA3D.Modeling.Mesh.Basic; +using SA3D.Modeling.Mesh.Chunk; +using SA3D.Modeling.Mesh.Gamecube; +using SA3D.Modeling.ObjectData.Enums; +using SA3D.Modeling.Structs; +using System; +using System.Collections.Generic; +using System.Linq; +using static SA3D.Common.StringExtensions; + +namespace SA3D.Modeling.Mesh +{ + /// + /// 3D mesh data attach. Its possible for multiple attaches to make up one full mesh. + /// + public class Attach : ICloneable, ILabel + { + /// + public string Label { get; set; } + + /// + /// Format of the attach. + /// + public virtual AttachFormat Format => AttachFormat.Buffer; + + /// + /// Bounding sphere of the attach. + /// + public Bounds MeshBounds { get; set; } + + /// + /// Mesh data ready to draw and used for converting to other attach formats. + /// + public BufferMesh[] MeshData { get; set; } + + + /// + /// Base constructor for derived attach types. + /// + protected Attach() + { + MeshData = Array.Empty(); + Label = "attach_" + GenerateIdentifier(); + } + + /// + /// Create a new attach using existing meshdata. + /// + /// The meshdata to use. + public Attach(BufferMesh[] meshdata) + { + MeshData = meshdata; + Label = "attach_" + GenerateIdentifier(); + } + + + /// + /// Checks whether the attaches mesh data has/relies on weights. + /// + /// Whether the attaches mesh data has/relies on weights + public virtual bool CheckHasWeights() + { + return MeshData.Any(x => x.ContinueWeight || x.IndexList == null || x.IndexList.Length == 0); + } + + /// + /// Returns opaque and transparent buffer meshes from . Meshes without polygons will be ignored. + /// + /// + public (BufferMesh[] opaque, BufferMesh[] transparent) GetDisplayMeshes() + { + List opaque = new(); + List transparent = new(); + + foreach(BufferMesh mesh in MeshData) + { + if(mesh.Corners == null) + { + continue; + } + + if(mesh.Material.UseAlpha) + { + transparent.Add(mesh); + } + else + { + opaque.Add(mesh); + } + } + + return (opaque.ToArray(), transparent.ToArray()); + } + + /// + /// Recalculates from the attach data. + /// + public virtual void RecalculateBounds() + { + if(MeshData.Length == 0) + { + MeshBounds = default; + return; + } + + MeshBounds = Bounds.FromPoints(MeshData.SelectManyIgnoringNull(x => x.Vertices).Select(x => x.Position)); + } + + + /// + /// Reads an attach from an endian stack reader. + /// + /// The reader to read from. + /// Address at which to start reading. + /// Type of attach to read. + /// Pointer references to use. + /// The buffer attach that was read. + public static Attach Read(EndianStackReader reader, uint address, ModelFormat format, PointerLUT lut) + { + return format switch + { + ModelFormat.SA1 or ModelFormat.SADX => BasicAttach.Read(reader, address, format == ModelFormat.SADX, lut), + ModelFormat.SA2 => ChunkAttach.Read(reader, address, lut), + ModelFormat.SA2B => GCAttach.Read(reader, address, lut), + ModelFormat.Buffer => ReadBuffer(reader, address, lut), + _ => throw new ArgumentException("Invalid format.", nameof(format)), + }; + } + + /// + /// Reads a buffer attach from an endian stack reader. + /// + /// The reader to read from. + /// Address at which to start reading. + /// Pointer references to use. + /// The buffer attach that was read. + public static Attach ReadBuffer(EndianStackReader reader, uint address, PointerLUT lut) + { + Attach onRead() + { + uint meshCount = reader.ReadUInt(address); + uint meshAddr = reader.ReadPointer(address + 4); + + uint[] meshAddresses = new uint[meshCount]; + for(int i = 0; i < meshCount; i++) + { + meshAddresses[i] = reader.ReadPointer(meshAddr); + meshAddr += 4; + } + + BufferMesh[] meshes = new BufferMesh[meshCount]; + + for(int i = 0; i < meshCount; i++) + { + meshes[i] = BufferMesh.Read(reader, meshAddresses[i]); + } + + return new Attach(meshes); + } + + return lut.GetAddLabeledValue(address, "attach_", onRead); + } + + + /// + /// Checks whether the attach can be written in the given model format. + /// + /// The format to check. + /// Whether the model can be written. + public virtual bool CanWrite(ModelFormat format) + { + return format is ModelFormat.Buffer; + } + + /// + /// Writes the attach and returns the address to the mesh + /// + /// + /// + /// + /// address pointing to the attach + public uint Write(EndianStackWriter writer, ModelFormat format, PointerLUT lut) + { + if(!CanWrite(format)) + { + throw new ArgumentException($"Attach type \"{Format}\" does not support writing in model format \"{format}\"."); + } + + uint onWrite() + { + if(format == ModelFormat.Buffer) + { + return WriteBuffer(writer); + } + else + { + return WriteInternal(writer, format, lut); + } + } + + return lut.GetAddAddress(this, onWrite); + } + + /// + /// The internal method for writing attach data. + /// + /// The writer to write to. + /// The model format to write as. + /// Pointer references to use. + /// The address at which the attach was written. + protected virtual uint WriteInternal(EndianStackWriter writer, ModelFormat format, PointerLUT lut) + { + return WriteBuffer(writer); + } + + private uint WriteBuffer(EndianStackWriter writer) + { + // write the meshes first + uint[] meshAddresses = new uint[MeshData.Length]; + for(int i = 0; i < MeshData.Length; i++) + { + meshAddresses[i] = MeshData[i].Write(writer); + } + + // write the pointer array + uint arrayAddr = writer.Position + writer.ImageBase; + for(int i = 0; i < MeshData.Length; i++) + { + writer.WriteUInt(meshAddresses[i]); + } + + uint address = writer.PointerPosition; + + writer.WriteUInt((uint)meshAddresses.Length); + writer.WriteUInt(arrayAddr); + + return address; + } + + + object ICloneable.Clone() + { + return Clone(); + } + + /// + /// Creates a deep clone of the attach. + /// + /// The cloned attach. + public virtual Attach Clone() + { + return new(MeshData.ContentClone()) { Label = Label }; + } + + /// + public override string ToString() + { + return $"{Label} - Buffer"; + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Basic/BasicAttach.cs b/src/SA3D.Modeling/Mesh/Basic/BasicAttach.cs new file mode 100644 index 0000000..997b5f5 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Basic/BasicAttach.cs @@ -0,0 +1,222 @@ +using SA3D.Common; +using SA3D.Common.IO; +using SA3D.Common.Lookup; +using SA3D.Modeling.Mesh.Buffer; +using SA3D.Modeling.Mesh.Converters; +using SA3D.Modeling.ObjectData.Enums; +using SA3D.Modeling.Structs; +using System; +using System.Numerics; +using static SA3D.Common.StringExtensions; + +namespace SA3D.Modeling.Mesh.Basic +{ + /// + /// Mesh data format used by SA1 and SA2 + /// + public sealed class BasicAttach : Attach + { + /// + /// Vertex positions. + /// + public ILabeledArray Positions { get; } + + /// + /// Vertex normals. + /// + public ILabeledArray Normals { get; } + + /// + /// Polygon structures. + /// + public ILabeledArray Meshes { get; } + + /// + /// Materials for the meshes. + /// + public ILabeledArray Materials { get; } + + /// + public override AttachFormat Format + => AttachFormat.BASIC; + + + private BasicAttach(ILabeledArray positions, ILabeledArray normals, ILabeledArray meshes, ILabeledArray materials, Bounds meshBounds) : base() + { + Label = "attach_" + GenerateIdentifier(); + + Positions = positions; + Normals = normals; + Meshes = meshes; + Materials = materials; + MeshBounds = meshBounds; + } + + /// + /// Creates a new BASIC attach using existing data. + ///
Array labels are automatically generated. + ///
+ /// Vertex positions. + /// Vertex normals. + /// Polygons structures. + /// Materials the meshes. + public BasicAttach(Vector3[] positions, Vector3[] normals, BasicMesh[] meshes, BasicMaterial[] materials) : base() + { + string identifier = GenerateIdentifier(); + Label = "attach_" + identifier; + + Positions = new LabeledArray("vertex_" + identifier, positions); + Normals = new LabeledArray("normal_" + identifier, normals); + Meshes = new LabeledArray("meshlist_" + identifier, meshes); + Materials = new LabeledArray("matlist_" + identifier, materials); + } + + /// + /// Creates a new BASIC attach using existing data. + /// + /// Vertex positions. + /// Vertex normals. + /// Polygons structures. + /// Materials the meshes. + public BasicAttach(ILabeledArray positions, ILabeledArray normals, ILabeledArray meshes, ILabeledArray materials) : base() + { + Label = "attach_" + GenerateIdentifier(); + + Positions = positions; + Normals = normals; + Meshes = meshes; + Materials = materials; + + if(normals != null && positions.Length != normals.Length) + { + throw new ArgumentException("Position and Normal count doesnt match!"); + } + } + + /// + public override bool CheckHasWeights() + { + return false; + } + + /// + public override void RecalculateBounds() + { + MeshBounds = Bounds.FromPoints(Positions); + } + + /// + public override bool CanWrite(ModelFormat format) + { + return base.CanWrite(format) || format is ModelFormat.SA1 or ModelFormat.SADX; + } + + /// + /// Converts the baisc attach to a set of buffer meshes. + /// + /// Whether to optimize the buffer mesh data. + /// The converted buffer meshes. + public BufferMesh[] ConvertToBufferMeshData(bool optimize) + { + return BasicConverter.ConvertBasicToBuffer(this, optimize); + } + + + /// + protected override uint WriteInternal(EndianStackWriter writer, ModelFormat format, PointerLUT lut) + { + uint posAddress = writer.WriteCollection(Positions, (w, v) => w.WriteVector3(v)); + uint nrmAddress = writer.WriteCollection(Normals, (w, v) => w.WriteVector3(v)); + + uint meshAddress = writer.WriteCollection(Meshes, + (w, m) => m.WriteMeshset(w, format == ModelFormat.SADX, lut), + (w, m) => m.WriteData(w, lut)); + + uint materialAddress = writer.WriteCollection(Materials, (w, m) => m.Write(w)); + + uint outAddress = writer.PointerPosition; + + writer.WriteUInt(posAddress); + writer.WriteUInt(nrmAddress); + writer.WriteUInt((uint)Positions.Length); + writer.WriteUInt(meshAddress); + writer.WriteUInt(materialAddress); + writer.WriteUShort((ushort)Meshes.Length); + writer.WriteUShort((ushort)Materials.Length); + MeshBounds.Write(writer); + + if(format == ModelFormat.SADX) + { + writer.WriteUInt(0); + } + + return outAddress; + } + + /// + /// Reads a BASIC attach from an endian stack reader. + /// + /// The reader to read from. + /// Address at which the attach is located. + /// Whether the attach is from SADX. + /// Pointer references to use. + /// The BASIC attach that was read. + public static BasicAttach Read(EndianStackReader reader, uint address, bool DX, PointerLUT lut) + { + BasicAttach onRead() + { + ILabeledArray readArray(uint arrayOffset, uint countOffset, string genPrefix, bool shortCount, uint elementSize, EndianIOExtensions.ReadValueDelegate read) + { + uint itemCount = shortCount + ? reader.ReadUShort(address + countOffset) + : reader.ReadUInt(address + countOffset); + + /* === Note regarding Empty arrays here === + * Some modded models in the past appear to have used empty arrays. + * In an effort to support them, we just create a new array for them. + * Seeing how this is only for old mod models, its not tragic if + * we have to remove it in case they actually break something else. + */ + + if(itemCount == 0) + { + return new LabeledArray(0); + } + + uint itemAddr = reader.ReadPointer(address + arrayOffset); + return reader.ReadLabeledArray(itemAddr, itemCount, elementSize, read, genPrefix, lut); + } + + uint meshSize = DX ? BasicMesh.StructSizeDX : BasicMesh.StructSize; + + ILabeledArray positions /***/ = readArray(0x00, 0x08, "vertex_", false, 12, /*******************/ (r, p) => r.ReadVector3(p)); + ILabeledArray normals /*****/ = readArray(0x04, 0x08, "normal_", false, 12, /*******************/ (r, p) => r.ReadVector3(p)); + ILabeledArray meshes /****/ = readArray(0x0C, 0x14, "meshlist_", true, meshSize, /************/ (r, p) => BasicMesh.Read(r, p, lut)); + ILabeledArray materials = readArray(0x10, 0x16, "matlist_", true, BasicMaterial.StructSize, (r, p) => BasicMaterial.Read(r, p)); + + Bounds bounds = Bounds.Read(reader, address + 24); + + return new BasicAttach(positions, normals, meshes, materials, bounds); + } + + return lut.GetAddLabeledValue(address, "attach_", onRead); + } + + + /// + public override Attach Clone() + { + return new BasicAttach(Positions.Clone(), Normals.Clone(), Meshes.ContentClone(), Materials.Clone(), MeshBounds) + { + Label = Label + }; + } + + /// + public override string ToString() + { + return $"{Label} - BASIC"; + } + } +} + diff --git a/src/SA3D.Modeling/Mesh/Basic/BasicMaterial.cs b/src/SA3D.Modeling/Mesh/Basic/BasicMaterial.cs new file mode 100644 index 0000000..c4bb320 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Basic/BasicMaterial.cs @@ -0,0 +1,362 @@ +using SA3D.Common.IO; +using SA3D.Modeling.Structs; +using System; + +namespace SA3D.Modeling.Mesh.Basic +{ + /// + /// BASIC format material + /// + public struct BasicMaterial + { + /// + /// Number of bytes the structure occupies. + /// + public const uint StructSize = 20; + + /// + /// Material with default values. + /// + public static readonly BasicMaterial DefaultValues = new() + { + DiffuseColor = Color.ColorWhite, + SpecularColor = new Color(0xFF, 0xFF, 0xFF, 0), + UseAlpha = true, + UseTexture = true, + DoubleSided = true, + FlatShading = false, + IgnoreLighting = false, + ClampU = false, + ClampV = false, + MirrorU = false, + MirrorV = false, + EnvironmentMap = false, + DestinationAlpha = BlendMode.SrcAlphaInverted, + SourceAlpha = BlendMode.SrcAlpha, + }; + + /// + /// Diffuse color. + /// + public Color DiffuseColor { get; set; } + + /// + /// Specular color. + /// + public Color SpecularColor { get; set; } + + /// + /// Specular exponent. + /// + public float SpecularExponent { get; set; } + + /// + /// Texture ID. + /// + public uint TextureID { get; set; } + + /// + /// Attributes containing various information. + /// + public uint Attributes { get; set; } + + #region Attribute Properties + + /// + /// User defined attributes. + ///
| 0x0000007F + ///
+ public byte UserAttributes + { + readonly get => (byte)(Attributes & 0x7Fu); + set => Attributes = (Attributes & ~0x7Fu) | (value & 0x7Fu); + } + + /// + /// Editor property (?). + ///
| 0x00000080 + ///
+ public bool PickStatus + { + readonly get => (Attributes & 0x80u) != 0; + set => SetAttributeBit(0x80u, value); + } + + /// + /// Mipmad distance multiplier. + ///
| 0x00000F00 + ///
+ public float MipmapDistanceMultiplier + { + readonly get => ((Attributes & 0xF00u) >> 8) * 0.25f; + set => Attributes = (Attributes & ~0xF00u) | ((uint)Math.Max(0, Math.Min(0xF, Math.Round(value / 0.25, MidpointRounding.AwayFromZero))) << 8); + } + + /// + /// Super sampling (Anisotropic filtering?). + ///
| 0x00001000 + ///
+ public bool SuperSample + { + readonly get => GetAttributeBit(0x1000u); + set => SetAttributeBit(0x1000u, value); + } + + /// + /// Texture filter mode. + ///
| 0x00006000 + ///
+ public FilterMode FilterMode + { + readonly get => (FilterMode)((Attributes >> 13) & 3); + set => Attributes = (Attributes & ~0x6000u) | ((uint)value << 13); + } + + /// + /// Texture clamp along the V axis. + ///
| 0x00008000 + ///
+ public bool ClampV + { + readonly get => GetAttributeBit(0x8000u); + set => SetAttributeBit(0x8000u, value); + } + + /// + /// Texture clamp along the U axis. + ///
| 0x00010000 + ///
+ public bool ClampU + { + readonly get => GetAttributeBit(0x10000u); + set => SetAttributeBit(0x10000u, value); + } + + /// + /// Texture mirror along the V axis. + ///
| 0x00020000 + ///
+ public bool MirrorV + { + readonly get => GetAttributeBit(0x20000u); + set => SetAttributeBit(0x20000u, value); + } + + /// + /// Texture mirror along the U axis. + ///
| 0x00040000 + ///
+ public bool MirrorU + { + readonly get => GetAttributeBit(0x40000u); + set => SetAttributeBit(0x40000u, value); + } + + /// + /// Disables specular shading. + ///
| 0x00080000 + ///
+ public bool IgnoreSpecular + { + readonly get => GetAttributeBit(0x80000u); + set => SetAttributeBit(0x80000u, value); + } + + /// + /// Enables alpha blending. + ///
| 0x00100000 + ///
+ public bool UseAlpha + { + readonly get => GetAttributeBit(0x100000u); + set => SetAttributeBit(0x100000u, value); + } + + /// + /// Enables texture rendering. + ///
| 0x00200000 + ///
+ public bool UseTexture + { + readonly get => GetAttributeBit(0x200000u); + set => SetAttributeBit(0x200000u, value); + } + + /// + /// Applies the texture based on angle between camera and mesh normals (matcap method). + ///
| 0x00400000 + ///
+ public bool EnvironmentMap + { + readonly get => GetAttributeBit(0x400000); + set => SetAttributeBit(0x400000u, value); + } + + /// + /// Disables backface culling. + ///
| 0x00800000 + ///
+ public bool DoubleSided + { + readonly get => GetAttributeBit(0x800000); + set => SetAttributeBit(0x800000u, value); + } + + /// + /// Ignores interpolated normals and instead uses polygon-wide normals. + ///
| 0x01000000 + ///
+ public bool FlatShading + { + readonly get => GetAttributeBit(0x1000000); + set => SetAttributeBit(0x1000000u, value); + } + + /// + /// Disables shading altogether. + ///
| 0x02000000 + ///
+ public bool IgnoreLighting + { + readonly get => GetAttributeBit(0x2000000); + set => SetAttributeBit(0x2000000u, value); + } + + /// + /// Destination blend mode. + ///
| 0x1C000000 + ///
+ public BlendMode DestinationAlpha + { + readonly get => (BlendMode)((Attributes >> 26) & 7); + set => Attributes = (uint)((Attributes & ~0x1C000000) | ((uint)value << 26)); + } + + /// + /// Source blend mode. + ///
| 0xE0000000 + ///
+ public BlendMode SourceAlpha + { + readonly get => (BlendMode)((Attributes >> 29) & 7); + set => Attributes = (Attributes & ~0xE0000000) | ((uint)value << 29); + } + + private readonly bool GetAttributeBit(uint mask) + { + return (Attributes & mask) != 0; + } + + private void SetAttributeBit(uint mask, bool value) + { + if(value) + { + Attributes |= mask; + } + else + { + Attributes &= ~mask; + } + } + + #endregion + + + /// + /// Creates a new basic material from a template. + /// + /// The template. + public BasicMaterial(BasicMaterial template) + { + DiffuseColor = template.DiffuseColor; + SpecularColor = template.SpecularColor; + SpecularExponent = template.SpecularExponent; + TextureID = template.TextureID; + Attributes = template.Attributes; + } + + + /// + /// Reads a material from an endian stack reader. + /// + /// The reader to read from. + /// Address at which the material is located. + /// The read material. + public static BasicMaterial Read(EndianStackReader reader, uint address) + { + Color dif = reader.ReadColor(ref address, ColorIOType.ARGB8_32); + Color spec = reader.ReadColor(ref address, ColorIOType.ARGB8_32); + float exp = reader.ReadFloat(address); + uint texID = reader.ReadUInt(address + 4); + uint attribs = reader.ReadUInt(address + 8); + + return new BasicMaterial() + { + DiffuseColor = dif, + SpecularColor = spec, + SpecularExponent = exp, + TextureID = texID, + Attributes = attribs + }; + } + + /// + /// Writes the materials structure to an endian stack writer. + /// + /// The writer to write to. + public readonly void Write(EndianStackWriter writer) + { + writer.WriteColor(DiffuseColor, ColorIOType.ARGB8_32); + writer.WriteColor(SpecularColor, ColorIOType.ARGB8_32); + writer.WriteFloat(SpecularExponent); + writer.WriteUInt(TextureID); + writer.WriteUInt(Attributes); + } + + + /// + public override readonly bool Equals(object? obj) + { + return obj is BasicMaterial material && + DiffuseColor == material.DiffuseColor && + SpecularColor == material.SpecularColor && + SpecularExponent == material.SpecularExponent && + TextureID == material.TextureID && + Attributes == material.Attributes; + } + + /// + public override readonly int GetHashCode() + { + return HashCode.Combine(DiffuseColor, SpecularColor, SpecularExponent, TextureID, Attributes); + } + + /// + public override readonly string ToString() + { + return $"Texture: {TextureID} / Use Alpha: {UseAlpha}"; + } + + /// + /// Compares two materials for equality. + /// + /// Lefthand material + /// Righthand material + /// Whether the materials are equal. + public static bool operator ==(BasicMaterial left, BasicMaterial right) + { + return left.Equals(right); + } + + /// + /// Compares two materials for inequality. + /// + /// Lefthand material + /// Righthand material + /// Whether the materials are inequal. + public static bool operator !=(BasicMaterial left, BasicMaterial right) + { + return !(left == right); + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Basic/BasicMesh.cs b/src/SA3D.Modeling/Mesh/Basic/BasicMesh.cs new file mode 100644 index 0000000..a10b753 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Basic/BasicMesh.cs @@ -0,0 +1,364 @@ +using SA3D.Common.IO; +using SA3D.Common.Lookup; +using SA3D.Modeling.Mesh.Basic.Polygon; +using SA3D.Modeling.Structs; +using System; +using System.Linq; +using System.Numerics; +using static SA3D.Common.StringExtensions; + +namespace SA3D.Modeling.Mesh.Basic +{ + /// + /// BASIC format mesh structure for holding polygon information. + /// + public class BasicMesh : ICloneable + { + /// + /// Number of bytes the structure occupies. + /// + public const uint StructSize = 24; + + /// + /// Number of bytes the structure occupies. (SADX) + /// + public const uint StructSizeDX = 28; + + private ILabeledArray? _normals; + private ILabeledArray? _colors; + private ILabeledArray? _texcoords; + + /// + /// Index indicating which material to use from . + /// + public ushort MaterialIndex { get; set; } + + /// + /// Indicating how polygons are stored. + /// + public BasicPolygonType PolygonType { get; } + + /// + /// Polygons of the mesh. + /// + public LabeledReadOnlyArray Polygons { get; } + + /// + /// The amount of corners/loops in the polygons. Determines the lengths of the other arrays. + /// + public int PolygonCornerCount { get; } + + /// + /// Polygon attributes. (Unused) + /// + public uint PolyAttributes { get; set; } + + /// + /// Per corner custom polygon normals. + /// + public ILabeledArray? Normals + { + get => _normals; + set + { + if(value != null && value.Length != PolygonCornerCount) + { + throw new ArgumentException($"New array has a length of {value.Length}, while {PolygonCornerCount} is expected"); + } + + _normals = value; + } + } + + /// + /// Per corner polygon colors. + /// + public ILabeledArray? Colors + { + get => _colors; + set + { + if(value != null && value.Length != PolygonCornerCount) + { + throw new ArgumentException($"New array has a length of {value.Length}, while {PolygonCornerCount} is expected"); + } + + _colors = value; + } + } + + /// + /// Per corner polygon texture coordinates. + /// + public ILabeledArray? Texcoords + { + get => _texcoords; + set + { + if(value != null && value.Length != PolygonCornerCount) + { + throw new ArgumentException($"New array has a length of {value.Length}, while {PolygonCornerCount} is expected"); + } + + _texcoords = value; + } + } + + private BasicMesh( + ILabeledArray? normals, + ILabeledArray? colors, + ILabeledArray? texcoords, + ushort materialIndex, + BasicPolygonType polygonType, + LabeledReadOnlyArray polygons, + int polygonCornerCount, + uint polyAttributes) + { + _normals = normals; + _colors = colors; + _texcoords = texcoords; + MaterialIndex = materialIndex; + PolygonType = polygonType; + Polygons = polygons; + PolygonCornerCount = polygonCornerCount; + PolyAttributes = polyAttributes; + } + + /// + /// Creates a new basic mesh from preexisting data. + /// + /// Index indicating which material to use. + /// Indicating how polygons are stored. + /// Polygons of the mesh. + /// Per corner custom polygon normals. + /// Per corner polygon colors. + /// Per corner polygon texture coordinates. + public BasicMesh( + ushort materialID, + BasicPolygonType polyType, + LabeledReadOnlyArray polys, + ILabeledArray? normals, + ILabeledArray? colors, + ILabeledArray? texcoords) + { + MaterialIndex = materialID; + PolygonType = polyType; + Polygons = polys; + + foreach(IBasicPolygon p in Polygons) + { + PolygonCornerCount += p.NumIndices; + } + + if(normals != null && normals.Length != PolygonCornerCount) + { + throw new ArgumentException($"Polygon corner count ({PolygonCornerCount}) and normal count ({normals.Length}) dont match up!", nameof(normals)); + } + + if(colors != null && colors.Length != PolygonCornerCount) + { + throw new ArgumentException($"Polygon corner count ({PolygonCornerCount}) and colors count ({colors.Length}) dont match up!", nameof(colors)); + } + + if(texcoords != null && texcoords.Length != PolygonCornerCount) + { + throw new ArgumentException($"Polygon corner count ({PolygonCornerCount}) and texcoord count ({texcoords.Length}) dont match up!", nameof(texcoords)); + } + + Normals = normals; + Colors = colors; + Texcoords = texcoords; + } + + /// + /// Creates a new (empty) mesh based on polygon data. + /// + /// Indicating how polygons are stored. + /// Polygons to use. + /// Index indicating which material to use. + /// Whether the mesh contains custom normals + /// Whether the model contains colors. + /// Whether the model contains texture coordinate. + public BasicMesh( + BasicPolygonType polygonType, + IBasicPolygon[] polygons, + ushort materialIndex, + bool hasNormal, + bool hasColor, + bool hasTexcoords) + { + PolygonType = polygonType; + MaterialIndex = materialIndex; + + string identifier = GenerateIdentifier(); + Polygons = new LabeledReadOnlyArray("poly_" + identifier, polygons); + + foreach(IBasicPolygon p in polygons) + { + PolygonCornerCount += p.NumIndices; + } + + if(hasNormal) + { + Normals = new LabeledArray("polynormal_" + identifier, PolygonCornerCount); + } + + if(hasColor) + { + Colors = new LabeledArray("vcolor_" + identifier, PolygonCornerCount); + } + + if(hasTexcoords) + { + Texcoords = new LabeledArray("uv_" + identifier, PolygonCornerCount); + } + } + + + /// + /// Reads a basic mesh off an endian strack reader. + /// + /// Reader to read from. + /// Address at which the mesh is located. + /// Pointer references to use. + /// The read mesh. + public static BasicMesh Read(EndianStackReader reader, uint address, PointerLUT lut) + { + ushort header = reader.ReadUShort(address); + ushort materialID = (ushort)(header & 0x3FFFu); + BasicPolygonType polyType = (BasicPolygonType)(header >> 14); + uint polyAttributes = reader.ReadUInt(address + 8); + + //================================================================== + + ushort polyCount = reader.ReadUShort(address + 2); + uint polyAddr = reader.ReadPointer(address + 4); + + IBasicPolygon onReadPolys(EndianStackReader reader, ref uint address) + { + return IBasicPolygon.Read(reader, ref address, polyType); + } + + LabeledReadOnlyArray polys = reader.ReadLabeledReadOnlyArray(polyAddr, polyCount, onReadPolys, "poly_", lut); + + int cornerCount = polys.Sum(x => x.NumIndices); + + //================================================================== + + LabeledArray? ReadArray(uint offset, string prefix, uint valueSize, EndianIOExtensions.ReadValueDelegate readValue) + { + LabeledArray? result = null; + + if(reader.TryReadPointer(address + offset, out uint pointer)) + { + result = reader.ReadLabeledArray(pointer, (uint)cornerCount, valueSize, readValue, prefix, lut); + } + + return result; + } + + LabeledArray? normals /****/ = ReadArray(0x0C, "polynormal_", 12, (r, p) => r.ReadVector3(p)); + LabeledArray? colors /*******/ = ReadArray(0x10, "vcolor_", /**/ 4, (r, p) => r.ReadColor(p, ColorIOType.ARGB8_32)); + LabeledArray? texcoords /**/ = ReadArray(0x14, "polynormal_", 08, (r, p) => r.ReadVector2(p, FloatIOType.Short) / 255f); + + //================================================================== + + return new BasicMesh( + normals, + colors, + texcoords, + materialID, + polyType, + polys, + cornerCount, + polyAttributes); + } + + /// + /// Writes the different data arrays to a stream + /// + /// Output stream + /// + public void WriteData(EndianStackWriter writer, PointerLUT lut) + { + _ = lut.GetAddAddress(Polygons, (array) => + { + uint result = writer.PointerPosition; + + foreach(IBasicPolygon p in array) + { + p.Write(writer); + } + + writer.Align(4); + + return result; + }); + + _ = writer.WriteCollectionWithLUT(Normals, (w, v) => w.WriteVector3(v), lut); + _ = writer.WriteCollectionWithLUT(Colors, (w, c) => w.WriteColor(c, ColorIOType.ARGB8_32), lut); + _ = writer.WriteCollectionWithLUT(Texcoords, (w, v) => w.WriteVector2(v * 255f, FloatIOType.Short), lut); + } + + /// + /// Writes the meshset to a stream + /// + /// Ouput stream + /// Whether the mesh should be written for SADX + /// + public void WriteMeshset(EndianStackWriter writer, bool DX, PointerLUT lut) + { + uint normalsAddress = 0; + uint colorsAddress = 0; + uint texcoordAddress = 0; + + if(!lut.All.TryGetAddress(Polygons, out uint polyAddress) + || (Normals != null && !lut.All.TryGetAddress(Normals, out normalsAddress)) + || (Colors != null && !lut.All.TryGetAddress(Colors, out colorsAddress)) + || (Texcoords != null && !lut.All.TryGetAddress(Texcoords, out texcoordAddress))) + { + throw new NullReferenceException("Data has not been written yet"); + } + + ushort header = MaterialIndex; + header |= (ushort)((uint)PolygonType << 14); + + writer.WriteUShort(header); + writer.WriteUShort((ushort)Polygons.Length); + writer.WriteUInt(polyAddress); + writer.WriteUInt(PolyAttributes); + writer.WriteUInt(normalsAddress); + writer.WriteUInt(colorsAddress); + writer.WriteUInt(texcoordAddress); + + if(DX) + { + writer.WriteEmpty(4); + } + } + + + object ICloneable.Clone() + { + return Clone(); + } + + /// + /// Creates a deep clone of the mesh. + /// + /// The clone. + public BasicMesh Clone() + { + return new BasicMesh( + Normals?.Clone(), + Colors?.Clone(), + Texcoords?.Clone(), + MaterialIndex, + PolygonType, + Polygons, + PolygonCornerCount, + PolyAttributes); + } + + } +} diff --git a/src/SA3D.Modeling/Mesh/Basic/Polygon/BasicMultiPolygon.cs b/src/SA3D.Modeling/Mesh/Basic/Polygon/BasicMultiPolygon.cs new file mode 100644 index 0000000..6ab00c9 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Basic/Polygon/BasicMultiPolygon.cs @@ -0,0 +1,116 @@ +using SA3D.Common.IO; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace SA3D.Modeling.Mesh.Basic.Polygon +{ + /// + /// A BASIC polygon containing a variable number of corners. + /// + public struct BasicMultiPolygon : IBasicPolygon + { + /// + /// Indices of the polygon. + /// + public ushort[] Indices { get; set; } + + /// + /// Whether the backface culling direction is flipped. + /// + public bool Reversed { get; set; } + + /// + public readonly uint Size => (uint)(2 + (Indices.Length * 2)); + + /// + public readonly int NumIndices => Indices.Length; + + + /// + public readonly ushort this[int index] + { + get => Indices[index]; + set => Indices[index] = value; + } + + /// + /// Creates a new multi polygon. + /// + /// Indices of the polygon. + /// Whether the polygons backface culling direction is flipped. + public BasicMultiPolygon(ushort[] indices, bool reversed) + { + Indices = indices; + Reversed = reversed; + } + + /// + /// Creates a new empty multi polygon. + /// + /// Number of indices the polygon holds. + /// Whether the polygons backface culling direction is flipped. + public BasicMultiPolygon(uint size, bool reversed) + : this(new ushort[size], reversed) { } + + + /// + /// Reads a basic multi polygon off of an endian stack reader. Advances the address by the number of bytes read. + /// + /// The reader to read from. + /// Address at which the polygon is located. + /// The polygon that was read. + public static BasicMultiPolygon Read(EndianStackReader data, ref uint address) + { + ushort header = data.ReadUShort(address); + ushort[] indices = new ushort[header & 0x7FFF]; + bool reversed = (header & 0x8000) != 0; + address += 2; + for(int i = 0; i < indices.Length; i++) + { + indices[i] = data.ReadUShort(address); + address += 2; + } + + return new BasicMultiPolygon(indices, reversed); + } + + /// + public readonly void Write(EndianStackWriter writer) + { + writer.WriteUShort((ushort)((Indices.Length & 0x7FFF) | (Reversed ? 0x8000 : 0))); + for(int i = 0; i < Indices.Length; i++) + { + writer.WriteUShort(Indices[i]); + } + } + + + /// + public readonly IEnumerator GetEnumerator() + { + return ((IEnumerable)Indices).GetEnumerator(); + } + + /// + readonly IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + + /// + public readonly object Clone() + { + return new BasicMultiPolygon(Indices.ToArray(), Reversed); + } + + /// + public override readonly string ToString() + { + return $"Multi: {Reversed} - {Indices.Length}"; + } + + + } +} diff --git a/src/SA3D.Modeling/Mesh/Basic/Polygon/BasicPolygonType.cs b/src/SA3D.Modeling/Mesh/Basic/Polygon/BasicPolygonType.cs new file mode 100644 index 0000000..509ceb9 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Basic/Polygon/BasicPolygonType.cs @@ -0,0 +1,29 @@ +namespace SA3D.Modeling.Mesh.Basic.Polygon +{ + /// + /// The different primitive types for BASIC meshes + /// + public enum BasicPolygonType + { + /// + /// Arranges polygons in a triangle list. + /// + Triangles, + + /// + /// Arranges polygons in a quad list. + /// + Quads, + + /// + /// Arranges polygons with an arbitrary number of corners in a list. + /// + NPoly, + + /// + /// Arranges triangles in strips. + /// + TriangleStrips + } + +} diff --git a/src/SA3D.Modeling/Mesh/Basic/Polygon/BasicQuad.cs b/src/SA3D.Modeling/Mesh/Basic/Polygon/BasicQuad.cs new file mode 100644 index 0000000..9349f3b --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Basic/Polygon/BasicQuad.cs @@ -0,0 +1,146 @@ +using SA3D.Common.IO; +using System; +using System.Collections; +using System.Collections.Generic; + +namespace SA3D.Modeling.Mesh.Basic.Polygon +{ + /// + /// A polygon with four corners. + /// + public struct BasicQuad : IBasicPolygon + { + /// + public readonly uint Size => 8; + + /// + public readonly int NumIndices => 4; + + + /// + /// First vertex index. + /// + public ushort Index1 { get; set; } + + /// + /// Second vertex index. + /// + public ushort Index2 { get; set; } + + /// + /// Third vertex index. + /// + public ushort Index3 { get; set; } + + /// + /// Fourth vertex index. + /// + public ushort Index4 { get; set; } + + + /// + public ushort this[int index] + { + readonly get => index switch + { + 0 => Index1, + 1 => Index2, + 2 => Index3, + 3 => Index4, + _ => throw new IndexOutOfRangeException(), + }; + set + { + switch(index) + { + case 0: + Index1 = value; + break; + case 1: + Index2 = value; + break; + case 2: + Index3 = value; + break; + case 3: + Index4 = value; + break; + default: + throw new IndexOutOfRangeException(); + } + } + } + + + /// + /// Creates a new populated basic quad. + /// + /// First vertex index. + /// Second vertex index. + /// Third vertex index. + /// Fourth vertex index. + public BasicQuad(ushort index1, ushort index2, ushort index3, ushort index4) + { + Index1 = index1; + Index2 = index2; + Index3 = index3; + Index4 = index4; + } + + + /// + public readonly void Write(EndianStackWriter writer) + { + writer.WriteUShort(Index1); + writer.WriteUShort(Index2); + writer.WriteUShort(Index3); + writer.WriteUShort(Index4); + } + + /// + /// Reads a quad off an endian stack reader. Advances the address by the number of bytes read. + /// + /// The Reader to read from. + /// Address at which the quad is located. + /// The quad that was read. + public static BasicQuad Read(EndianStackReader reader, ref uint address) + { + BasicQuad t = new( + reader.ReadUShort(address), + reader.ReadUShort(address + 2), + reader.ReadUShort(address + 4), + reader.ReadUShort(address + 6)); + + address += 8; + return t; + } + + + /// + public readonly IEnumerator GetEnumerator() + { + yield return Index1; + yield return Index2; + yield return Index3; + yield return Index4; + } + + readonly IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + + /// + public readonly object Clone() + { + return this; + } + + /// + public override readonly string ToString() + { + return $"Quad: [{Index1}, {Index2}, {Index3}, {Index4}]"; + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Basic/Polygon/BasicTriangle.cs b/src/SA3D.Modeling/Mesh/Basic/Polygon/BasicTriangle.cs new file mode 100644 index 0000000..214d177 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Basic/Polygon/BasicTriangle.cs @@ -0,0 +1,131 @@ +using SA3D.Common.IO; +using System; +using System.Collections; +using System.Collections.Generic; + +namespace SA3D.Modeling.Mesh.Basic.Polygon +{ + /// + /// A polygon with three corners. + /// + public struct BasicTriangle : IBasicPolygon + { + /// + public readonly uint Size => 6; + + /// + public readonly int NumIndices => 3; + + + /// + /// First vertex index. + /// + public ushort Index1 { get; set; } + + /// + /// Second vertex index. + /// + public ushort Index2 { get; set; } + + /// + /// Third vertex index. + /// + public ushort Index3 { get; set; } + + + /// + public ushort this[int index] + { + readonly get => index switch + { + 0 => Index1, + 1 => Index2, + 2 => Index3, + _ => throw new IndexOutOfRangeException(), + }; + set + { + switch(index) + { + case 0: + Index1 = value; + break; + case 1: + Index2 = value; + break; + case 2: + Index3 = value; + break; + default: + throw new IndexOutOfRangeException(); + } + } + } + + /// + /// Creates a new populated basic quad. + /// + /// First vertex index. + /// Second vertex index. + /// Third vertex index. + public BasicTriangle(ushort index1, ushort index2, ushort index3) + { + Index1 = index1; + Index2 = index2; + Index3 = index3; + } + + + /// + public readonly void Write(EndianStackWriter writer) + { + writer.WriteUShort(Index1); + writer.WriteUShort(Index2); + writer.WriteUShort(Index3); + } + + /// + /// Reads a quad off an endian stack reader. Advances the address by the number of bytes read. + /// + /// The Reader to read from. + /// Address at which the quad is located. + /// The quad that was read. + public static BasicTriangle Read(EndianStackReader reader, ref uint address) + { + BasicTriangle t = new( + reader.ReadUShort(address), + reader.ReadUShort(address + 2), + reader.ReadUShort(address + 4)); + + address += 6; + return t; + } + + + /// + public readonly IEnumerator GetEnumerator() + { + yield return Index1; + yield return Index2; + yield return Index3; + } + + readonly IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + public readonly object Clone() + { + return this; + } + + /// + public override readonly string ToString() + { + return $"Triangle: [{Index1}, {Index2}, {Index3}]"; + } + + } +} diff --git a/src/SA3D.Modeling/Mesh/Basic/Polygon/IBasicPolygon.cs b/src/SA3D.Modeling/Mesh/Basic/Polygon/IBasicPolygon.cs new file mode 100644 index 0000000..d8c858f --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Basic/Polygon/IBasicPolygon.cs @@ -0,0 +1,55 @@ +using SA3D.Common.IO; +using System; +using System.Collections.Generic; + +namespace SA3D.Modeling.Mesh.Basic.Polygon +{ + /// + /// BASIC polygon interface. + /// + public interface IBasicPolygon : ICloneable, IEnumerable + { + /// + /// Size of the primitive in bytes. + /// + public uint Size { get; } + + /// + /// Number of indices in the polygon. + /// + public int NumIndices { get; } + + /// + /// Access and set vertex indices of the polygon. + /// + /// The index of the corner. + /// The vertex index. + public ushort this[int index] { get; set; } + + + /// + /// Reads a primitive off an endian stack reader. + /// + /// The reader to read from. + /// Address at which the primitive is located. + /// Type of primitive to read. + /// The read primitive + /// + public static IBasicPolygon Read(EndianStackReader reader, ref uint address, BasicPolygonType type) + { + return type switch + { + BasicPolygonType.Triangles => BasicTriangle.Read(reader, ref address), + BasicPolygonType.Quads => BasicQuad.Read(reader, ref address), + BasicPolygonType.NPoly or BasicPolygonType.TriangleStrips => BasicMultiPolygon.Read(reader, ref address), + _ => throw new ArgumentException("Unknown poly type!", nameof(type)), + }; + } + + /// + /// Writes the polygon to an endian stack writer. + /// + /// The writer to write to. + public void Write(EndianStackWriter writer); + } +} diff --git a/src/SA3D.Modeling/Mesh/Buffer/BufferCorner.cs b/src/SA3D.Modeling/Mesh/Buffer/BufferCorner.cs new file mode 100644 index 0000000..6e9b153 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Buffer/BufferCorner.cs @@ -0,0 +1,147 @@ +using SA3D.Common.IO; +using SA3D.Modeling.Structs; +using System; +using System.Numerics; + +namespace SA3D.Modeling.Mesh.Buffer +{ + /// + /// A single corner in a triangle + /// + public struct BufferCorner : IEquatable + { + /// + /// Size of a buffer corner in bytes. + /// + public const uint StructSize = 14; + + /// + /// Size of a buffer corner without color in bytes. + /// + public const uint StructSizeNoColor = 10; + + /// + /// Vertex cache index to use. + /// + public ushort VertexIndex { get; set; } + + /// + /// Color. + /// + public Color Color { get; set; } + + /// + /// Coordinates for texture rendering. + /// + public Vector2 Texcoord { get; set; } + + + /// + /// Creates a new buffer corner. + /// + /// Buffer array index of the vertex + /// Color. + /// Coordinates for texture rendering. + public BufferCorner(ushort vertexIndex, Color color, Vector2 texcoord) + { + VertexIndex = vertexIndex; + Color = color; + Texcoord = texcoord; + } + + /// + /// Creates a new white buffer corner with no texture coordinates. + /// + /// Buffer array index of the vertex + public BufferCorner(ushort vertexIndex) + { + VertexIndex = vertexIndex; + Color = BufferMesh.DefaultColor; + Texcoord = Vector2.Zero; + } + + + /// + /// Writes the buffer corner to an endian stack writer. + /// + /// The writer to write to. + /// Whether to write the corners color too. + public readonly void Write(EndianStackWriter writer, bool writeColor) + { + writer.WriteUShort(VertexIndex); + writer.WriteVector2(Texcoord); + if(writeColor) + { + writer.WriteColor(Color, ColorIOType.RGBA8); + } + } + + /// + /// Reads a buffer corner off an endian stack reader. Advances the address by the number of bytes read. + /// + /// The reader to read from. + /// Address at which to start reading. + /// Whether the corner contains a color. + /// The corner that was read. + public static BufferCorner Read(EndianStackReader reader, ref uint address, bool hasColor) + { + ushort index = reader.ReadUShort(address); + address += 2; + Vector2 texcoord = reader.ReadVector2(ref address); + Color col = hasColor ? reader.ReadColor(ref address, ColorIOType.RGBA8) : BufferMesh.DefaultColor; + + return new BufferCorner(index, col, texcoord); + } + + + /// + public override readonly bool Equals(object? obj) + { + return obj is BufferCorner corner && + VertexIndex == corner.VertexIndex && + Color == corner.Color && + Texcoord == corner.Texcoord; + } + + /// + public override readonly int GetHashCode() + { + return System.HashCode.Combine(VertexIndex, Color, Texcoord); + } + + /// + readonly bool IEquatable.Equals(BufferCorner other) + { + return Equals(other); + } + + /// + /// Compares two corners for equality. + /// + /// Lefthand corners. + /// Righthand corners. + /// Whether the two corners are equal. + public static bool operator ==(BufferCorner l, BufferCorner r) + { + return l.Equals(r); + } + + /// + /// Compares two corners for inequality. + /// + /// Lefthand corners. + /// Righthand corners. + /// Whether the two corners are inequal. + public static bool operator !=(BufferCorner l, BufferCorner r) + { + return !l.Equals(r); + } + + + /// + public override readonly string ToString() + { + return $"{VertexIndex}: \t{Color}; \t{Texcoord.DebugString()}"; + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Buffer/BufferMaterial.cs b/src/SA3D.Modeling/Mesh/Buffer/BufferMaterial.cs new file mode 100644 index 0000000..4facc46 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Buffer/BufferMaterial.cs @@ -0,0 +1,394 @@ +using SA3D.Common.IO; +using SA3D.Modeling.Mesh.Gamecube.Enums; +using SA3D.Modeling.Structs; + +namespace SA3D.Modeling.Mesh.Buffer +{ + /// + /// Rendering properties for a buffer mesh. + /// + public struct BufferMaterial + { + /// + /// Size of the structure in bytes. + /// + public const uint StructSize = 0x20; + + /// + /// Default material values. + /// + public static readonly BufferMaterial DefaultValues = new() + { + Diffuse = Color.ColorWhite, + Specular = Color.ColorWhite, + SpecularExponent = 11, + Ambient = Color.ColorBlack, + SourceBlendMode = BlendMode.SrcAlpha, + DestinationBlendmode = BlendMode.SrcAlphaInverted, + TextureFiltering = FilterMode.Bilinear, + GCShadowStencil = 1, + GCTexCoordID = GCTexCoordID.TexCoord0, + GCTexCoordType = GCTexCoordType.Matrix2x4, + GCTexCoordSource = GCTexCoordSource.TexCoord0, + GCMatrixID = GCTexcoordMatrix.Identity, + }; + + #region Storage properties + + /// + /// The diffuse color. + /// + public Color Diffuse { readonly get; set; } + + /// + /// The specular color. + /// + public Color Specular { readonly get; set; } + + /// + /// The specular exponent. + /// + public float SpecularExponent { readonly get; set; } + + /// + /// The Ambient color. + /// + public Color Ambient { readonly get; set; } + + /// + /// Texture Index. + /// + public uint TextureIndex { readonly get; set; } + + /// + /// Texture filtering mode. + /// + public FilterMode TextureFiltering { readonly get; set; } + + /// + /// Mipmap distance multiplier. + /// + public float MipmapDistanceMultiplier { readonly get; set; } + + /// + /// Source blend mode. + /// + public BlendMode SourceBlendMode { readonly get; set; } + + /// + /// Destination blend mode. + /// + public BlendMode DestinationBlendmode { readonly get; set; } + + /// + /// Additional Material attributes. + /// + public MaterialAttributes Attributes { readonly get; set; } + + /// + /// Data container for all gamecube related info. + /// + public uint GamecubeData { readonly get; set; } + + #endregion + + #region Attribute Properties + + /// + /// Whether textures should be rendered. + ///
Wrapper around flag in . + ///
+ public bool UseTexture + { + readonly get => HasAttributes(MaterialAttributes.UseTexture); + set => SetAttributes(MaterialAttributes.UseTexture, value); + } + + /// + /// Enables anisotropic filtering. + ///
Wrapper around flag in . + ///
+ public bool AnisotropicFiltering + { + readonly get => HasAttributes(MaterialAttributes.AnisotropicFiltering); + set => SetAttributes(MaterialAttributes.AnisotropicFiltering, value); + } + + /// + /// Clamps texture corrdinates along the horizontal axis between -1 and 1. + ///
Wrapper around flag in . + ///
+ public bool ClampU + { + readonly get => HasAttributes(MaterialAttributes.ClampU); + set => SetAttributes(MaterialAttributes.ClampU, value); + } + + /// + /// Clamps texture corrdinates along the vertical axis between -1 and 1. + ///
Wrapper around flag in . + ///
+ public bool ClampV + { + readonly get => HasAttributes(MaterialAttributes.ClampV); + set => SetAttributes(MaterialAttributes.ClampV, value); + } + + /// + /// Mirrors texture coordinates along the horizontal axis every other full unit. + ///
Wrapper around flag in . + ///
+ public bool MirrorU + { + readonly get => HasAttributes(MaterialAttributes.MirrorU); + set => SetAttributes(MaterialAttributes.MirrorU, value); + } + + /// + /// Mirrors texture coordinates along the vertical axis every other full unit. + ///
Wrapper around flag in . + ///
+ public bool MirrorV + { + readonly get => HasAttributes(MaterialAttributes.MirrorV); + set => SetAttributes(MaterialAttributes.MirrorV, value); + } + + /// + /// Whether to use normal mapping for textures. + ///
Wrapper around flag in . + ///
+ public bool NormalMapping + { + readonly get => HasAttributes(MaterialAttributes.NormalMapping); + set => SetAttributes(MaterialAttributes.NormalMapping, value); + } + + /// + /// Ignores lighting as a whole. + ///
Wrapper around flag in . + ///
+ public bool NoLighting + { + readonly get => HasAttributes(MaterialAttributes.NoLighting); + set => SetAttributes(MaterialAttributes.NoLighting, value); + } + + /// + /// Ignores ambient lighting. + ///
Wrapper around flag in . + ///
+ public bool NoAmbient + { + readonly get => HasAttributes(MaterialAttributes.NoAmbient); + set => SetAttributes(MaterialAttributes.NoAmbient, value); + } + + /// + /// Ignores specular lighting. + ///
Wrapper around flag in . + ///
+ public bool NoSpecular + { + readonly get => HasAttributes(MaterialAttributes.NoSpecular); + set => SetAttributes(MaterialAttributes.NoSpecular, value); + } + + /// + /// Ignores interpolated normals and instead renders every polygon flat. + ///
Wrapper around flag in . + ///
+ public bool Flat + { + readonly get => HasAttributes(MaterialAttributes.Flat); + set => SetAttributes(MaterialAttributes.Flat, value); + } + + /// + /// Enables transparent rendering. + ///
Wrapper around flag in . + ///
+ public bool UseAlpha + { + readonly get => HasAttributes(MaterialAttributes.UseAlpha); + set => SetAttributes(MaterialAttributes.UseAlpha, value); + } + + /// + /// Enables backface culling. + ///
Wrapper around flag in . + ///
+ public bool BackfaceCulling + { + readonly get => HasAttributes(MaterialAttributes.BackfaceCulling); + set => SetAttributes(MaterialAttributes.BackfaceCulling, value); + } + + #endregion + + #region Gamecube Properties + + /// + /// GC Specific: Shadow stencil. + /// + public byte GCShadowStencil + { + readonly get => (byte)((GamecubeData >> 24) & 0xFF); + set + { + GamecubeData &= 0xFFFFFF; + GamecubeData |= (uint)value << 24; + } + } + + /// + /// GC Specific: Output location to use for generated texture coordinates. + /// + public GCTexCoordID GCTexCoordID + { + readonly get => (GCTexCoordID)((GamecubeData >> 16) & 0xFF); + set + { + GamecubeData &= 0xFF00FFFF; + GamecubeData |= (uint)value << 16; + } + } + + /// + /// GC Specific: The function to use for generating the texture coordinates + /// + public GCTexCoordType GCTexCoordType + { + readonly get => (GCTexCoordType)((GamecubeData >> 12) & 0xF); + set + { + GamecubeData &= 0xFFFF0FFF; + GamecubeData |= (uint)value << 12; + } + } + + /// + /// GC Specific: The source which should be used to generate the texture coordinates + /// + public GCTexCoordSource GCTexCoordSource + { + readonly get => (GCTexCoordSource)((GamecubeData >> 4) & 0xFF); + set + { + GamecubeData &= 0xFFFFF00F; + GamecubeData |= (uint)value << 4; + } + } + + /// + /// GC Specific: The ID of the matrix to use for generating the texture coordinates + /// + public GCTexcoordMatrix GCMatrixID + { + readonly get => (GCTexcoordMatrix)(GamecubeData & 0xF); + set + { + GamecubeData &= 0xFFFFFFF0; + GamecubeData |= (uint)value; + } + } + + #endregion + + /// + /// Creates a new buffer material from a template. + /// + /// The template to use. + public BufferMaterial(BufferMaterial template) + { + Diffuse = template.Diffuse; + Specular = template.Specular; + SpecularExponent = template.SpecularExponent; + Ambient = template.Ambient; + TextureIndex = template.TextureIndex; + TextureFiltering = template.TextureFiltering; + MipmapDistanceMultiplier = template.MipmapDistanceMultiplier; + SourceBlendMode = template.SourceBlendMode; + DestinationBlendmode = template.DestinationBlendmode; + Attributes = template.Attributes; + GamecubeData = template.GamecubeData; + } + + + /// + /// Set material attributes. + /// + /// The attributes to set. + /// New state for the attributes. + public void SetAttributes(MaterialAttributes attrib, bool state) + { + if(state) + { + Attributes |= attrib; + } + else + { + Attributes &= ~attrib; + } + } + + /// + /// Checks if materials attributes are set. + /// + /// The attributes to check. + /// Whether all specified attributes are set. + public readonly bool HasAttributes(MaterialAttributes attrib) + { + return Attributes.HasFlag(attrib); + } + + + /// + /// Writes the material to an endian stack writer. + /// + /// The writer to write to. + public readonly void Write(EndianStackWriter writer) + { + uint attributes = (uint)Attributes; + attributes |= (uint)SourceBlendMode << 16; + attributes |= (uint)DestinationBlendmode << 19; + attributes |= (uint)TextureFiltering << 22; + + writer.WriteColor(Diffuse, ColorIOType.RGBA8); + writer.WriteColor(Specular, ColorIOType.RGBA8); + writer.WriteFloat(SpecularExponent); + writer.WriteColor(Ambient, ColorIOType.RGBA8); + writer.WriteUInt(TextureIndex); + writer.WriteFloat(MipmapDistanceMultiplier); + writer.WriteUInt(attributes); + writer.WriteUInt(GamecubeData); + } + + /// + /// Reads a buffer material off an endian stack reader. + /// + /// The reader to read from. + /// Address at which to start reading. + /// The buffer material that was read. + public static BufferMaterial Read(EndianStackReader reader, uint address) + { + BufferMaterial result = default; + + result.Diffuse = reader.ReadColor(address, ColorIOType.RGBA8); + result.Specular = reader.ReadColor(address + 4, ColorIOType.RGBA8); + result.SpecularExponent = reader.ReadFloat(address + 8); + result.Ambient = reader.ReadColor(address + 0xC, ColorIOType.RGBA8); + result.TextureIndex = reader.ReadUInt(address + 0x10); + result.MipmapDistanceMultiplier = reader.ReadFloat(address + 0x14); + uint attributes = reader.ReadUInt(address + 0x18); + result.GamecubeData = reader.ReadUInt(address + 0x1C); + + result.Attributes = (MaterialAttributes)(attributes & 0xFFFF); + result.SourceBlendMode = (BlendMode)((attributes >> 16) & 7); + result.DestinationBlendmode = (BlendMode)((attributes >> 19) & 7); + result.TextureFiltering = (FilterMode)(attributes >> 22); + + return result; + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Buffer/BufferMesh.cs b/src/SA3D.Modeling/Mesh/Buffer/BufferMesh.cs new file mode 100644 index 0000000..da19b6f --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Buffer/BufferMesh.cs @@ -0,0 +1,702 @@ +using SA3D.Common; +using SA3D.Common.IO; +using SA3D.Modeling.Strippify; +using SA3D.Modeling.Structs; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace SA3D.Modeling.Mesh.Buffer +{ + /// + /// Data set for a renderable mesh
+ /// Can also consist of only vertices. + ///
+ public class BufferMesh : ICloneable + { + /// + /// Default normal direction for buffer mesh vertices. + /// + public static readonly Vector3 DefaultNormal = Vector3.UnitY; + + /// + /// Default color for buffer mesh corners. + /// + public static readonly Color DefaultColor = Color.ColorWhite; + + + /// + /// Mesh vertices. + /// + public BufferVertex[]? Vertices { get; } + + /// + /// Polygon rendering information. + /// + public BufferMaterial Material { get; private set; } + + /// + /// Polygon corners. + /// + public BufferCorner[]? Corners { get; private set; } + + /// + /// Index list combining polygon corners into triangles. + ///
If null, use the corners in order. + ///
+ public uint[]? IndexList { get; private set; } + + /// + /// When set, / is made up of one big triangle strip, instead of individual triangles. + /// + public bool Strippified { get; private set; } + + /// + /// If true, the vertices will be added onto the existing buffered vertices. + /// + public bool ContinueWeight { get; } + + /// + /// Whether the model uses vertex normals. + /// + public bool HasNormals { get; } + + /// + /// Whether the model uses polygon colors. + /// + public bool HasColors { get; private set; } + + /// + /// Index offset for when writing vertices into the buffer array. + /// + public ushort VertexWriteOffset { get; internal set; } + + /// + /// Index offset for when reading vertices from the buffer array for rendering. + /// + public ushort VertexReadOffset { get; internal set; } + + + /// + /// Creates a new buffer mesh. + /// + /// Buffer vertices. + /// Polygon rendering information. + /// Polygon corners. + /// Index list combining polygon corners into triangles + /// When set, is made up of one big triangle strip, instead of individual triangles. + /// If true, the vertices will be added onto the existing buffered vertices. + /// Whether the model uses vertex normals. + /// Whether the model uses polygon colors. + /// Index offset for when writing vertices into the buffer array. + /// Index offset for when reading vertices from the buffer array for rendering. + /// + public BufferMesh( + BufferVertex[]? vertices, + BufferMaterial material, + BufferCorner[]? corners, + uint[]? indexList, + bool strippified, + bool continueWeight, + bool hasNormals, + bool hasColors, + ushort vertexWriteOffset, + ushort vertexReadOffset) + { + Vertices = vertices; + Material = material; + Corners = corners; + IndexList = indexList; + Strippified = strippified; + ContinueWeight = continueWeight; + HasNormals = hasNormals; + HasColors = hasColors; + VertexWriteOffset = vertexWriteOffset; + VertexReadOffset = vertexReadOffset; + + VerifyVertexData(); + VerifyPolygonData(); + } + + /// + /// Creates a new buffer mesh from only vertex data. + /// + /// Buffer vertices. + /// If true, the vertices will be added onto the existing buffered vertices. + /// Whether the model uses vertex normals. + /// Index offset for when writing vertices into the buffer array. + /// + public BufferMesh(BufferVertex[] vertices, bool continueWeight, bool hasNormals, ushort vertexWriteOffset) + { + Vertices = vertices; + ContinueWeight = continueWeight; + VertexWriteOffset = vertexWriteOffset; + HasNormals = hasNormals; + + VerifyVertexData(); + } + + /// + /// Creates a new buffer mesh with only polygon data. + /// + /// Polygon rendering information. + /// Polygon corners. + /// Index list combining polygon corners into triangles + /// When set, is made up of one big triangle strip, instead of individual triangles. + /// Whether the model uses polygon colors. + /// Index offset for when reading vertices from the buffer array for rendering. + /// + public BufferMesh(BufferMaterial material, BufferCorner[] corners, uint[]? indexList, bool strippified, bool hasColors, ushort vertexReadOffset) + { + Material = material; + Corners = corners; + IndexList = indexList; + Strippified = strippified; + HasColors = hasColors; + VertexReadOffset = vertexReadOffset; + + VerifyPolygonData(); + } + + + private void VerifyVertexData() + { + if(Vertices == null || Vertices.Length == 0) + { + throw new ArgumentException("Vertices can't be empty", "vertices"); + } + } + + private void VerifyPolygonData() + { + if(Corners == null || Corners.Length == 0) + { + throw new ArgumentException("Corners can't be empty", "corners"); + } + + if(IndexList != null && IndexList.Length == 0) + { + throw new ArgumentException("Triangle list cant be empty", "triangleList"); + } + } + + + /// + /// Compiles the triangle list of indices to . + /// + /// The index triangle list. + /// + public uint[] GetIndexTriangleList() + { + if(Corners == null) + { + throw new InvalidOperationException("The mesh contains no polygon information."); + } + + uint[] result; + + if(IndexList == null) + { + if(Strippified) + { + List triangles = new(); + + bool rev = false; + + for(uint i = 2; i < Corners.Length; i++, rev = !rev) + { + uint i1 = i - 2; + uint i2 = i - 1; + uint i3 = i; + + BufferCorner c1 = Corners[i1]; + BufferCorner c2 = Corners[i2]; + BufferCorner c3 = Corners[i3]; + + if(c1.VertexIndex == c2.VertexIndex + || c2.VertexIndex == c3.VertexIndex + || c3.VertexIndex == c1.VertexIndex) + { + continue; + } + + if(rev) + { + triangles.Add(i2); + triangles.Add(i1); + triangles.Add(i3); + } + else + { + triangles.Add(i1); + triangles.Add(i2); + triangles.Add(i3); + } + } + + result = triangles.ToArray(); + } + else + { + result = new uint[Corners.Length]; + for(uint i = 0; i < result.Length; i++) + { + result[i] = i; + } + } + } + else + { + if(Strippified) + { + List triangles = new(); + bool rev = false; + + for(int i = 2; i < IndexList.Length; i++, rev = !rev) + { + uint i1 = IndexList[i - 2]; + uint i2 = IndexList[i - 1]; + uint i3 = IndexList[i]; + + if(i1 == i2 || i2 == i3 || i3 == i1) + { + continue; + } + + if(rev) + { + triangles.Add(i2); + triangles.Add(i1); + triangles.Add(i3); + } + else + { + triangles.Add(i1); + triangles.Add(i2); + triangles.Add(i3); + } + } + + result = triangles.ToArray(); + + } + else + { + // fast copy + result = IndexList.ToArray(); + } + + } + + return result; + } + + /// + /// Compiles the triangle list of . + /// + /// + /// + public BufferCorner[] GetCornerTriangleList() + { + if(Corners == null) + { + throw new InvalidOperationException("The mesh contains no polygon information."); + } + + BufferCorner[] result; + + if(IndexList == null) + { + if(Strippified) + { + List triangles = new(); + bool rev = false; + + for(uint i = 2; i < Corners.Length; i++, rev = !rev) + { + BufferCorner c1 = Corners[i - 2]; + BufferCorner c2 = Corners[i - 1]; + BufferCorner c3 = Corners[i]; + + if(c1.VertexIndex == c2.VertexIndex + || c2.VertexIndex == c3.VertexIndex + || c3.VertexIndex == c1.VertexIndex) + { + continue; + } + + if(rev) + { + triangles.Add(c2); + triangles.Add(c1); + triangles.Add(c3); + } + else + { + triangles.Add(c1); + triangles.Add(c2); + triangles.Add(c3); + } + } + + result = triangles.ToArray(); + } + else + { + // fast copy + result = Corners.ToArray(); + } + } + else + { + if(Strippified) + { + List triangles = new(); + bool rev = false; + + for(uint i = 2; i < IndexList.Length; i++, rev = !rev) + { + uint i1 = IndexList[i - 2]; + uint i2 = IndexList[i - 1]; + uint i3 = IndexList[i]; + + if(i1 == i2 || i2 == i3 || i3 == i1) + { + continue; + } + + if(rev) + { + triangles.Add(Corners[i2]); + triangles.Add(Corners[i1]); + triangles.Add(Corners[i3]); + } + else + { + triangles.Add(Corners[i1]); + triangles.Add(Corners[i2]); + triangles.Add(Corners[i3]); + } + } + + result = triangles.ToArray(); + } + else + { + result = new BufferCorner[IndexList.Length]; + for(int i = 0; i < result.Length; i++) + { + result[i] = Corners[IndexList[i]]; + } + } + } + + return result; + } + + /// + /// Attempts to optimize the polygons by strippifying and/or generating index lists. + /// + public void OptimizePolygons() + { + if(Corners == null) + { + return; + } + + BufferCorner[] corners = GetCornerTriangleList(); + + // filter degenerate triangles + int newArraySize = corners.Length; + for(int i = 0; i < newArraySize; i += 3) + { + ushort index1 = corners[i].VertexIndex; + ushort index2 = corners[i + 1].VertexIndex; + ushort index3 = corners[i + 2].VertexIndex; + + if(index1 == index2 || index2 == index3 || index3 == index1) + { + corners[i] = corners[newArraySize - 3]; + corners[i + 1] = corners[newArraySize - 2]; + corners[i + 2] = corners[newArraySize - 1]; + i -= 3; + newArraySize -= 3; + } + } + + if(newArraySize == 0) + { + Corners = null; + IndexList = null; + Material = default; + HasColors = false; + VertexReadOffset = 0; + return; + } + + Array.Resize(ref corners, newArraySize); + + if(!corners.TryCreateDistinctMap(out DistinctMap distinctMap)) + { + Corners = corners; + return; + } + + int[][] strips = TriangleStrippifier.Global.Strippify(distinctMap.Map!); + + uint stripLength = (uint)TriangleStrippifier.JoinedStripEnumerator(strips, null).Count(); + uint structSize = HasColors ? BufferCorner.StructSize : BufferCorner.StructSizeNoColor; + + uint sizeTriList = structSize * (uint)corners.Length; + uint sizeTriListIndexed = (uint)((structSize * distinctMap.Values.Count) + (corners.Length * 2)); + uint sizeStrips = structSize * stripLength; + uint sizeStripsIndexed = (uint)((structSize * distinctMap.Values.Count) + (stripLength * 2)); + + uint smallestSize = uint.Min( + uint.Min(sizeTriList, sizeTriListIndexed), + uint.Min(sizeStrips, sizeStripsIndexed)); + + if(sizeStripsIndexed == smallestSize) + { + Corners = distinctMap.ValueArray; + IndexList = (uint[])(object)TriangleStrippifier.JoinStrips(strips, null); + Strippified = true; + } + else if(sizeStrips == smallestSize) + { + Corners = TriangleStrippifier.JoinedStripEnumerator(strips, null) + .Select(x => distinctMap.Values[x]).ToArray(); + IndexList = null; + Strippified = true; + } + else if(sizeTriListIndexed == smallestSize) + { + Corners = distinctMap.ValueArray; + IndexList = (uint[])(object)distinctMap.Map!; + Strippified = false; + } + else + { + Corners = corners; + IndexList = null; + Strippified = false; + } + + + } + + /// + /// Optimizes a collection of buffer meshes by optimizing the polygons and combining vertex and poly data between meshes. + ///
Reuses arrays and buffermeshes. + ///
+ /// The collection to optimize + /// The optimized buffermeshes. + public static BufferMesh[] Optimize(IList input) + { + List result = new(); + + foreach(BufferMesh mesh in input) + { + mesh.OptimizePolygons(); + + if(mesh.Vertices != null) + { + result.Add(mesh); + continue; + } + + if(result.Count > 0 && result[^1].Corners == null) + { + BufferMesh prev = result[^1]; + prev.Corners = mesh.Corners!; + prev.IndexList = mesh.IndexList; + prev.Material = mesh.Material; + prev.HasColors = mesh.HasColors; + prev.VertexReadOffset = mesh.VertexReadOffset; + } + else + { + result.Add(mesh); + } + } + + return result.ToArray(); + } + + + /// + /// Writes the buffer mesh to an endian stack writer. + /// + /// The writer to write to. + public uint Write(EndianStackWriter writer) + { + uint vtxAddr = 0; + if(Vertices != null) + { + vtxAddr = writer.PointerPosition; + foreach(BufferVertex vtx in Vertices) + { + vtx.Write(writer, HasNormals); + } + } + + uint cornerAddr = 0; + if(Corners != null) + { + cornerAddr = writer.PointerPosition; + foreach(BufferCorner c in Corners) + { + c.Write(writer, HasColors); + } + } + + uint triangleAddr = 0; + if(IndexList != null) + { + triangleAddr = writer.PointerPosition; + foreach(uint t in IndexList) + { + writer.WriteUInt(t); + } + } + + uint address = writer.PointerPosition; + + ushort flags = 0; + if(ContinueWeight) + { + flags |= 1; + } + + if(Strippified) + { + flags |= 2; + } + + if(HasNormals) + { + flags |= 4; + } + + if(HasColors) + { + flags |= 8; + } + + + writer.WriteUShort(VertexReadOffset); + writer.WriteUShort(VertexWriteOffset); + writer.WriteUShort(flags); + + writer.WriteUShort((ushort)(Vertices?.Length ?? 0)); + writer.WriteUInt(vtxAddr); + + writer.WriteUInt((uint)(Corners?.Length ?? 0)); + writer.WriteUInt(cornerAddr); + + writer.WriteUInt((uint)(IndexList?.Length ?? 0)); + writer.WriteUInt(triangleAddr); + + Material.Write(writer); + + return address; + } + + /// + /// Reads a buffer mesh from a byte array + /// + /// Byte source + /// Address at which the buffermesh is located + public static BufferMesh Read(EndianStackReader reader, uint address) + { + ushort vertexReadOffset = reader.ReadUShort(address + 0); + ushort vertexWriteOffset = reader.ReadUShort(address + 2); + + ushort flags = reader.ReadUShort(address + 4); + bool continueWeight = (flags & 1) != 0; + bool strippified = (flags & 2) != 0; + bool hasNormals = (flags & 4) != 0; + bool hasColors = (flags & 8) != 0; + + + BufferVertex[]? vertices = null; + if(reader.TryReadPointer(address + 8, out uint vtxAddr)) + { + vertices = new BufferVertex[reader.ReadUShort(address + 6)]; + for(int i = 0; i < vertices.Length; i++) + { + vertices[i] = BufferVertex.Read(reader, ref vtxAddr, hasNormals); + } + } + + BufferCorner[]? corners = null; + if(reader.TryReadPointer(address + 0x10, out uint cornerAddr)) + { + corners = new BufferCorner[reader.ReadUInt(address + 0xC)]; + for(int i = 0; i < corners.Length; i++) + { + corners[i] = BufferCorner.Read(reader, ref cornerAddr, hasColors); + } + } + + uint[]? triangles = null; + if(reader.TryReadPointer(address + 0x18, out uint triangleAddr)) + { + triangles = new uint[reader.ReadUInt(address + 0x14)]; + for(int i = 0; i < triangles.Length; i++) + { + triangles[i] = reader.ReadUInt(triangleAddr); + triangleAddr += 4; + } + } + + BufferMaterial material = BufferMaterial.Read(reader, address + 0x1C); + + return new( + vertices, + material, + corners, + triangles, + strippified, + continueWeight, + hasNormals, + hasColors, + vertexWriteOffset, + vertexReadOffset + ); + } + + + object ICloneable.Clone() + { + return Clone(); + } + + /// + /// Creates a deep clone of the mesh. + /// + /// The clone of the mesh. + public BufferMesh Clone() + { + return new( + Vertices?.ToArray(), + Material, + Corners?.ToArray(), + IndexList?.ToArray(), + Strippified, + ContinueWeight, + HasNormals, + HasColors, + VertexWriteOffset, + VertexReadOffset + ); + } + + /// + public override string ToString() + { + return $"{Vertices?.Length} - {Corners?.Length} - {IndexList?.Length}"; + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Buffer/BufferVertex.cs b/src/SA3D.Modeling/Mesh/Buffer/BufferVertex.cs new file mode 100644 index 0000000..f04ba9c --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Buffer/BufferVertex.cs @@ -0,0 +1,218 @@ +using SA3D.Common.IO; +using SA3D.Modeling.Structs; +using System; +using System.Numerics; + +namespace SA3D.Modeling.Mesh.Buffer +{ + /// + /// A single point in space with a direction and weight. + /// + public struct BufferVertex : IEquatable + { + /// + /// Position in 3D space. + /// + public Vector3 Position { get; set; } + + /// + /// Normalized direction that the vertex if facing in. + /// + public Vector3 Normal { get; set; } + + /// + /// Index in the vertex cache that this vertex occupies. + /// + public ushort Index { get; set; } + + /// + /// Influence of the assigned node on the vertices position and direction. + /// + public float Weight { get; set; } + + + /// + /// Creates a new buffer vertex with default normal and full weight. + /// + /// Position in 3D space. + /// Index in the buffer array that this vertex occupies. + public BufferVertex(Vector3 position, ushort index) + { + Position = position; + Normal = BufferMesh.DefaultNormal; + Index = index; + Weight = 1; + } + + /// + /// Creates a new buffer vertex with full weight. + /// + /// Position in 3D space. + /// Normalized direction that the vertex if facing in. + /// Index in the buffer array that this vertex occupies. + public BufferVertex(Vector3 position, Vector3 normal, ushort index) + { + Position = position; + Normal = normal; + Index = index; + Weight = 1; + } + + /// + /// Creates a new buffer vertex. + /// + /// Position in 3D space. + /// Normalized direction that the vertex if facing in. + /// Index in the buffer array that this vertex occupies. + /// Influence of the assigned node on the vertices position and direction. + public BufferVertex(Vector3 position, Vector3 normal, ushort index, float weight) + { + Position = position; + Normal = normal; + Index = index; + Weight = weight; + } + + + + /// + /// Writes the vertex to an endian stack writer. + /// + /// The writer to write to. + /// Whether the normal should be written too. + public readonly void Write(EndianStackWriter writer, bool writeNormal) + { + writer.WriteUShort(Index); + writer.WriteUShort((ushort)(Weight * ushort.MaxValue)); + + writer.WriteVector3(Position); + if(writeNormal) + { + writer.WriteVector3(Normal); + } + } + + /// + /// Reads a buffer vertex off an endian stack reader. Advances the address by the number of bytes read. + /// + /// Byte source + /// Address at which to start reading. + /// Whether the vertex contains a normal. + /// The vertex that was read + public static BufferVertex Read(EndianStackReader reader, ref uint address, bool hasNormal) + { + const float WeightFactor = 1f / ushort.MaxValue; + + BufferVertex result = default; + + result.Index = reader.ReadUShort(address); + result.Weight = reader.ReadUShort(address + 2) * WeightFactor; + address += 4; + result.Position = reader.ReadVector3(ref address); + result.Normal = hasNormal ? reader.ReadVector3(ref address) : BufferMesh.DefaultNormal; + + return result; + } + + + /// + public override readonly bool Equals(object? obj) + { + return obj is BufferVertex vertex && + Position.Equals(vertex.Position) && + Normal.Equals(vertex.Normal) && + Index == vertex.Index && + Weight == vertex.Weight; + } + + /// + public override readonly int GetHashCode() + { + return HashCode.Combine(Position, Normal, Index, Weight); + } + + /// + public readonly bool Equals(BufferVertex other) + { + return Equals(other); + } + + /// + /// Compares two vertices for equality. + /// + /// Lefthand vertex. + /// Righthand vertex. + /// Whether the two vertices are equal. + public static bool operator ==(BufferVertex left, BufferVertex right) + { + return left.Equals(right); + } + + /// + /// Compares two vertices for inequality. + /// + /// Lefthand vertex. + /// Righthand vertex. + /// Whether the two vertices are inequal. + public static bool operator !=(BufferVertex left, BufferVertex right) + { + return !(left == right); + } + + + /// + /// Adds the position and normal of two vertices together. + ///
Index is taken from the lefthand vertex, + ///
Weight is always 1. + ///
+ /// Lefthand vertex. + /// Righthand vertex. + /// + public static BufferVertex operator +(BufferVertex l, BufferVertex r) + { + return new BufferVertex() + { + Position = l.Position + r.Position, + Normal = l.Normal + r.Normal, + Index = l.Index, + Weight = 1 + }; + } + + /// + /// Multiplies position and normal of a vertex by a value. + /// + /// Vertex to multiply. + /// Value to multiply by. + /// + public static BufferVertex operator *(BufferVertex l, float r) + { + return new BufferVertex() + { + Position = l.Position * r, + Normal = l.Normal * r, + Index = l.Index, + Weight = l.Weight + }; + } + + /// + /// Multiplies position and normal of a vertex by a value. + /// + /// Value to multiply by. + /// Vertex to multiply. + public static BufferVertex operator *(float l, BufferVertex r) + { + return r * l; + } + + + /// + public override readonly string ToString() + { + return Weight == 1.0f + ? $"{Index}: {Position.DebugString()}; {Normal.DebugString()}" + : $"{Index}: {Position.DebugString()}; {Normal.DebugString()}; {Weight}"; + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Buffer/MaterialAttributes.cs b/src/SA3D.Modeling/Mesh/Buffer/MaterialAttributes.cs new file mode 100644 index 0000000..4c7c9d9 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Buffer/MaterialAttributes.cs @@ -0,0 +1,77 @@ +using SA3D.Common; +using System; + +namespace SA3D.Modeling.Mesh.Buffer +{ + /// + /// Rendering attributes of materials. + /// + [Flags] + public enum MaterialAttributes : ushort + { + /// + /// Whether textures should be rendered. + /// + UseTexture = Flag16.B0, + + /// + /// Enables anisotropic filtering. + /// + AnisotropicFiltering = Flag16.B1, + + /// + /// Clamps texture corrdinates along the horizontal axis between -1 and 1. + /// + ClampU = Flag16.B2, + + /// + /// Clamps texture corrdinates along the vertical axis between -1 and 1. + /// + ClampV = Flag16.B3, + + /// + /// Mirrors texture coordinates along the horizontal axis every other full unit. + /// + MirrorU = Flag16.B4, + + /// + /// Mirrors texture coordinates along the vertical axis every other full unit. + /// + MirrorV = Flag16.B5, + + /// + /// Whether to use normal mapping for textures. + /// + NormalMapping = Flag16.B6, + + /// + /// Ignores lighting as a whole. + /// + NoLighting = Flag16.B7, + + /// + /// Ignores ambient lighting. + /// + NoAmbient = Flag16.B8, + + /// + /// Ignores specular lighting. + /// + NoSpecular = Flag16.B9, + + /// + /// Ignores interpolated normals and instead renders every polygon flat. + /// + Flat = Flag16.B10, + + /// + /// Enables transparent rendering. + /// + UseAlpha = Flag16.B11, + + /// + /// Enables backface culling. + /// + BackfaceCulling = Flag16.B12, + } +} diff --git a/src/SA3D.Modeling/Mesh/Chunk/ChunkAttach.cs b/src/SA3D.Modeling/Mesh/Chunk/ChunkAttach.cs new file mode 100644 index 0000000..c818663 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Chunk/ChunkAttach.cs @@ -0,0 +1,235 @@ +using SA3D.Common.IO; +using SA3D.Common.Lookup; +using SA3D.Modeling.Mesh.Chunk.PolyChunks; +using SA3D.Modeling.Mesh.Chunk.Structs; +using SA3D.Modeling.Mesh.Converters; +using SA3D.Modeling.ObjectData; +using SA3D.Modeling.ObjectData.Enums; +using SA3D.Modeling.Structs; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using static SA3D.Common.StringExtensions; + +namespace SA3D.Modeling.Mesh.Chunk +{ + /// + /// Chunk format mesh data + /// + public sealed class ChunkAttach : Attach + { + /// + /// Vertex data blocks. + /// + public ILabeledArray? VertexChunks { get; set; } + + /// + /// Polygon data blocks. + /// + public ILabeledArray? PolyChunks { get; set; } + + /// + public override AttachFormat Format + => AttachFormat.CHUNK; + + + /// + /// Creates a new chunk attach. + /// + /// Vertex data blocks. + /// Polygon data blocks + public ChunkAttach(VertexChunk?[]? vertexChunks, PolyChunk?[]? polyChunks) : base() + { + string identifier = GenerateIdentifier(); + Label = "attach_" + identifier; + VertexChunks = vertexChunks == null ? null : new LabeledArray("vertex_" + identifier, vertexChunks); + PolyChunks = polyChunks == null ? null : new LabeledArray("poly_" + identifier, polyChunks); + } + + /// + /// Creates a new chunk attach. + /// + /// Vertex data blocks. + /// Polygon data blocks + public ChunkAttach(ILabeledArray? vertexChunks, ILabeledArray? polyChunks) : base() + { + Label = "attach_" + GenerateIdentifier(); + VertexChunks = vertexChunks; + PolyChunks = polyChunks; + } + + private ChunkAttach(string label, ILabeledArray? vertexChunks, ILabeledArray? polyChunks, Bounds meshBounds) : base() + { + Label = label; + VertexChunks = vertexChunks; + PolyChunks = polyChunks; + MeshBounds = meshBounds; + } + + + /// + public override bool CheckHasWeights() + { + if(PolyChunks == null || !PolyChunks.Any(a => a is StripChunk)) + { + return VertexChunks != null && VertexChunks.Any(a => a?.HasWeight == true); + } + + HashSet ids = new(); + if(VertexChunks != null) + { + foreach(VertexChunk? vc in VertexChunks) + { + if(vc == null) + { + continue; + } + + if(vc.HasWeight) + { + return true; + } + + ids.UnionWith(Enumerable.Range(vc.IndexOffset, vc.Vertices.Length)); + } + } + + return PolyChunks + .OfType() + .SelectMany(a => a.Strips) + .SelectMany(a => a.Corners) + .Any(a => !ids.Contains(a.Index)); + } + + /// + public override void RecalculateBounds() + { + if(PolyChunks == null || VertexChunks == null) + { + MeshBounds = default; + return; + } + + IEnumerable vertexEnumerator() + { + foreach(VertexChunk? cnk in VertexChunks!) + { + if(cnk == null) + { + continue; + } + + foreach(ChunkVertex vtx in cnk.Vertices) + { + yield return vtx.Position; + } + } + } + + MeshBounds = Bounds.FromPoints(vertexEnumerator()); + + if(CheckHasWeights()) + { + MeshBounds = new(MeshBounds.Position, 0); + } + } + + /// + public override bool CanWrite(ModelFormat format) + { + return base.CanWrite(format) || format is ModelFormat.SA2; + } + + /// + /// Calculates the active polygon chunks per chunk attaches in a model tree. + /// + /// The model to get the active polygon chunks for. + /// The active polygon chunks. + public static Dictionary GetActivePolyChunks(Node model) + { + if(model.GetAttachFormat() != AttachFormat.CHUNK) + { + throw new FormatException($"Model {model.Label} is not a chunk model."); + } + + return ChunkConverter.GetActivePolyChunks(model); + } + + /// + /// Reads a chunk attach off an endian byte reader. + /// + /// Reader to read from. + /// Address at which to start reading. + /// Pointer references to utilize. + /// The chunk attach that was read. + public static ChunkAttach Read(EndianStackReader reader, uint address, PointerLUT lut) + { + ChunkAttach onRead() + { + ILabeledArray? vertexChunks = null; + if(reader.TryReadPointer(address, out uint vertexAddress)) + { + vertexChunks = lut.GetAddLabeledValue>(vertexAddress, "vertex_", + () => new(VertexChunk.ReadArray(reader, vertexAddress))); + } + + ILabeledArray? polyChunks = null; + if(reader.TryReadPointer(address + 4, out uint polyAddress)) + { + polyChunks = lut.GetAddLabeledValue>(polyAddress, "poly_", + () => new(PolyChunk.ReadArray(reader, polyAddress, lut))); + } + + return new ChunkAttach(vertexChunks, polyChunks) + { + MeshBounds = Bounds.Read(reader, address + 8) + }; + } + + return lut.GetAddLabeledValue(address, "attach_", onRead); + } + + /// + protected override uint WriteInternal(EndianStackWriter writer, ModelFormat format, PointerLUT lut) + { + uint vertexAddress = lut.GetAddAddress(VertexChunks, () => VertexChunk.WriteArray(writer, VertexChunks!)); + uint polyAddress = lut.GetAddAddress(PolyChunks, () => PolyChunk.WriteArray(writer, PolyChunks!, lut)); + uint address = writer.PointerPosition; + + writer.WriteUInt(vertexAddress); + writer.WriteUInt(polyAddress); + MeshBounds.Write(writer); + + return address; + } + + + /// + public override ChunkAttach Clone() + { + LabeledArray? vertexChunks = null; + LabeledArray? polyChunks = null; + + if(VertexChunks != null) + { + VertexChunk?[] chunks = VertexChunks.Select(x => x?.Clone()).ToArray(); + vertexChunks = new(VertexChunks.Label, chunks); + } + + if(PolyChunks != null) + { + PolyChunk?[] chunks = PolyChunks.Select(x => x?.Clone()).ToArray(); + polyChunks = new(PolyChunks.Label, chunks); + } + + return new ChunkAttach(Label, vertexChunks, polyChunks, MeshBounds); + } + + /// + public override string ToString() + { + return $"CHUNK {Label} - V[{VertexChunks?.Length}], P[{PolyChunks?.Length}]"; + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Chunk/ChunkTypeExtensions.cs b/src/SA3D.Modeling/Mesh/Chunk/ChunkTypeExtensions.cs new file mode 100644 index 0000000..2427a1f --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Chunk/ChunkTypeExtensions.cs @@ -0,0 +1,171 @@ +using System; + +namespace SA3D.Modeling.Mesh.Chunk +{ + /// + /// Extension methods + /// + public static class ChunkTypeExtensions + { + internal const byte _bits = 1; + internal const byte _tiny = 8; + internal const byte _material = 16; + internal const byte _vertex = 32; + internal const byte _volume = 56; + internal const byte _strip = 64; + + /// + /// Checks whether a vertex chunktype uses 4 component vectors for positions/normals. + /// + /// The type to check. + public static bool CheckIsVec4(this VertexChunkType type) + { + return type is VertexChunkType.BlankVec4 or VertexChunkType.NormalVec4; + } + + /// + /// Checks whether a vertex chunktype uses 32 bit compressed vectors for normals. + /// + /// The type to check. + public static bool CheckIsNormal32(this VertexChunkType type) + { + return type is VertexChunkType.Normal32 + or VertexChunkType.Normal32Diffuse + or VertexChunkType.Normal32UserAttributes; + } + + /// + /// Checks whether a vertex chunktype has normals. + /// + /// The type to check. + public static bool CheckHasNormal(this VertexChunkType type) + { + return type + is VertexChunkType.NormalVec4 + or VertexChunkType.Normal + or VertexChunkType.NormalDiffuse + or VertexChunkType.NormalUserAttributes + or VertexChunkType.NormalAttributes + or VertexChunkType.NormalDiffuseSpecular5 + or VertexChunkType.NormalDiffuseSpecular4 + or VertexChunkType.NormalIntensity + or VertexChunkType.Normal32 + or VertexChunkType.Normal32Diffuse + or VertexChunkType.Normal32UserAttributes; + } + + /// + /// Checks whether a vertex chunktype has diffuse colors. + /// + /// The type to check. + /// + public static bool CheckHasDiffuseColor(this VertexChunkType type) + { + return type + is VertexChunkType.Diffuse + or VertexChunkType.DiffuseSpecular5 + or VertexChunkType.DiffuseSpecular4 + or VertexChunkType.Intensity + or VertexChunkType.NormalDiffuse + or VertexChunkType.NormalDiffuseSpecular5 + or VertexChunkType.NormalDiffuseSpecular4 + or VertexChunkType.NormalIntensity + or VertexChunkType.Normal32Diffuse; + + } + + /// + /// Checks whether a vertex chunktype has specular colors. + /// + /// The type to check. + /// + public static bool CheckHasSpecularColor(this VertexChunkType type) + { + return type + is VertexChunkType.DiffuseSpecular5 + or VertexChunkType.DiffuseSpecular4 + or VertexChunkType.Intensity + or VertexChunkType.NormalDiffuseSpecular5 + or VertexChunkType.NormalDiffuseSpecular4 + or VertexChunkType.NormalIntensity; + } + + /// + /// Checks whether a vertex chunktype has attributes (user attributes included too). + /// + /// The type to check. + /// + public static bool CheckHasAttributes(this VertexChunkType type) + { + return type is VertexChunkType.Attributes + or VertexChunkType.UserAttributes + or VertexChunkType.NormalAttributes + or VertexChunkType.NormalUserAttributes + or VertexChunkType.Normal32UserAttributes; + } + + /// + /// Checks whether a vertex chunktype has weights. + /// + /// The type to check. + /// + public static bool CheckHasWeights(this VertexChunkType type) + { + return type is VertexChunkType.Attributes + or VertexChunkType.NormalAttributes; + } + + /// + /// Checks whether a vertex chunktype has diffuse colors + /// + /// The type to check. + /// + public static bool CheckStripHasColor(this PolyChunkType type) + { + return type is PolyChunkType.Strip_Color + or PolyChunkType.Strip_TexColor + or PolyChunkType.Strip_HDTexColor; + } + + /// + /// Returns the number of 4-byte values a chunk vertex has. + /// + /// Type to get the size of. + /// + public static ushort GetIntegerSize(this VertexChunkType type) + { + switch(type) + { + case VertexChunkType.Blank: + return 3; + case VertexChunkType.BlankVec4: + case VertexChunkType.Diffuse: + case VertexChunkType.UserAttributes: + case VertexChunkType.Attributes: + case VertexChunkType.DiffuseSpecular5: + case VertexChunkType.DiffuseSpecular4: + case VertexChunkType.Intensity: + case VertexChunkType.Normal32: + return 4; + case VertexChunkType.Normal32Diffuse: + case VertexChunkType.Normal32UserAttributes: + return 5; + case VertexChunkType.Normal: + return 6; + case VertexChunkType.NormalDiffuse: + case VertexChunkType.NormalUserAttributes: + case VertexChunkType.NormalAttributes: + case VertexChunkType.NormalDiffuseSpecular5: + case VertexChunkType.NormalDiffuseSpecular4: + case VertexChunkType.NormalIntensity: + return 7; + case VertexChunkType.NormalVec4: + return 8; + case VertexChunkType.Null: + case VertexChunkType.End: + default: + throw new ArgumentException($"Invalid vertex chunk type: {type}", nameof(type)); + } + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Chunk/PolyChunk.cs b/src/SA3D.Modeling/Mesh/Chunk/PolyChunk.cs new file mode 100644 index 0000000..6b80eda --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Chunk/PolyChunk.cs @@ -0,0 +1,234 @@ +using SA3D.Common.IO; +using SA3D.Modeling.Mesh.Chunk.PolyChunks; +using SA3D.Modeling.Structs; +using System; +using System.Collections.Generic; + +namespace SA3D.Modeling.Mesh.Chunk +{ + /// + /// Polychunk base class. + /// + public abstract class PolyChunk : ICloneable + { + /// + /// Type. + /// + public PolyChunkType Type { get; protected set; } + + /// + /// Additonal attributes. + /// + public byte Attributes { get; set; } + + /// + /// Size of the chunk in bytes. + /// + public abstract uint ByteSize { get; } + + /// + /// Base constructor for every poly chunk. + /// + /// + protected PolyChunk(PolyChunkType type) + { + Type = type; + } + + + /// + /// Writes the poly chunk to an endian stack writer. + /// + /// The writer to write to. + /// Pointer references to utilize. + public void Write(EndianStackWriter writer, PointerLUT lut) + { + lut.PolyChunks.Add(writer.PointerPosition, this); + writer.WriteUShort((ushort)((byte)Type | (Attributes << 8))); + InternalWrite(writer); + } + + /// + /// Writes an array of poly chunks to an endian stack writer. Includes NULL and END chunks. + /// + /// The writer to write to. + /// Chunks to writ. + /// Pointer references to utilize. + /// + public static uint WriteArray(EndianStackWriter writer, IEnumerable chunks, PointerLUT lut) + { + uint result = writer.PointerPosition; + + foreach(PolyChunk? chunk in chunks) + { + if(chunk == null) + { + writer.WriteEmpty(2); + continue; + } + + chunk.Write(writer, lut); + } + + // end chunk + writer.WriteUShort(0xFF); + + return result; + } + + /// + /// Writes the poly chunks body to an endian stack writer. + /// + /// The writer to write to. + protected abstract void InternalWrite(EndianStackWriter writer); + + /// + /// Reads a poly chunk off an endian stack reader. Advances the address by the number of bytes read. + /// + /// Reader to read from. + /// Address at which to start reading. + /// Pointer references to utilize. + /// The poly chunk that was read. + public static PolyChunk Read(EndianStackReader reader, ref uint address, PointerLUT lut) + { + ushort header = reader.ReadUShort(address); + PolyChunkType type = (PolyChunkType)(header & 0xFF); + byte attribs = (byte)(header >> 8); + + if(!Enum.IsDefined(type) || type is PolyChunkType.End or PolyChunkType.Null) + { + throw new FormatException($"Poly chunk type is invalid: {type}"); + } + + PolyChunk chunk; + switch(type) + { + case PolyChunkType.BlendAlpha: + chunk = new BlendAlphaChunk(); + address += 2; + break; + case PolyChunkType.MipmapDistanceMultiplier: + chunk = new MipmapDistanceMultiplierChunk(); + address += 2; + break; + case PolyChunkType.SpecularExponent: + chunk = new SpecularExponentChunk(); + address += 2; + break; + case PolyChunkType.CacheList: + chunk = new CacheListChunk(); + address += 2; + break; + case PolyChunkType.DrawList: + chunk = new DrawListChunk(); + address += 2; + break; + case PolyChunkType.TextureID: + case PolyChunkType.TextureID2: + chunk = TextureChunk.Read(reader, address); + address += chunk.ByteSize; + break; + case PolyChunkType.Material_Diffuse: + case PolyChunkType.Material_Ambient: + case PolyChunkType.Material_DiffuseAmbient: + case PolyChunkType.Material_Specular: + case PolyChunkType.Material_DiffuseSpecular: + case PolyChunkType.Material_AmbientSpecular: + case PolyChunkType.Material_DiffuseAmbientSpecular: + case PolyChunkType.Material_Diffuse2: + case PolyChunkType.Material_Ambient2: + case PolyChunkType.Material_DiffuseAmbient2: + case PolyChunkType.Material_Specular2: + case PolyChunkType.Material_DiffuseSpecular2: + case PolyChunkType.Material_AmbientSpecular2: + case PolyChunkType.Material_DiffuseAmbientSpecular2: + chunk = MaterialChunk.Read(reader, ref address); + break; + case PolyChunkType.Material_Bump: + chunk = MaterialBumpChunk.Read(reader, address); + address += 16; + break; + case PolyChunkType.Volume_Polygon3: + case PolyChunkType.Volume_Polygon4: + case PolyChunkType.Volume_Strip: + chunk = VolumeChunk.Read(reader, ref address); + break; + case PolyChunkType.Strip_Blank: + case PolyChunkType.Strip_Tex: + case PolyChunkType.Strip_HDTex: + case PolyChunkType.Strip_Normal: + case PolyChunkType.Strip_TexNormal: + case PolyChunkType.Strip_HDTexNormal: + case PolyChunkType.Strip_Color: + case PolyChunkType.Strip_TexColor: + case PolyChunkType.Strip_HDTexColor: + case PolyChunkType.Strip_BlankDouble: + case PolyChunkType.Strip_TexDouble: + case PolyChunkType.Strip_HDTexDouble: + chunk = StripChunk.Read(reader, ref address); + break; + case PolyChunkType.Null: + case PolyChunkType.End: + default: + throw new InvalidOperationException(); // cant be reached + } + + chunk.Attributes = attribs; + lut.PolyChunks.Add(address, chunk); + return chunk; + } + + /// + /// Reads an array of poly chunks off an endian stack reader. Respects NULL and END chunks. + /// + /// The reader to read from. + /// Address at which to start reading. + /// Pointer references to utilize. + /// The poly chunks that were read. + public static PolyChunk?[] ReadArray(EndianStackReader reader, uint address, PointerLUT lut) + { + List result = new(); + + PolyChunkType readType() + { + return (PolyChunkType)(reader.ReadUShort(address) & 0xFF); + } + + for(PolyChunkType type = readType(); type != PolyChunkType.End; type = readType()) + { + if(type == PolyChunkType.Null) + { + result.Add(null); + address += 2; + continue; + } + + result.Add(Read(reader, ref address, lut)); + } + + return result.ToArray(); + } + + + + object ICloneable.Clone() + { + return Clone(); + } + + /// + /// Creates a deep clone of the poly chunk. + /// + /// The cloned poly chunk + public virtual PolyChunk Clone() + { + return (PolyChunk)MemberwiseClone(); + } + + /// + public override string ToString() + { + return Type.ToString(); + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Chunk/PolyChunkType.cs b/src/SA3D.Modeling/Mesh/Chunk/PolyChunkType.cs new file mode 100644 index 0000000..9f15fc5 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Chunk/PolyChunkType.cs @@ -0,0 +1,210 @@ +namespace SA3D.Modeling.Mesh.Chunk +{ + /// + /// Chunk type + /// + public enum PolyChunkType : byte + { + /// + /// Null chunk. + /// + Null = 0, + + /// + /// Contains transparency blendmodes. + /// + BlendAlpha = ChunkTypeExtensions._bits + 0, + + /// + /// Contains mipmap distance multiplier. + /// + MipmapDistanceMultiplier = ChunkTypeExtensions._bits + 1, + + /// + /// Contains specularity exponent. + /// + SpecularExponent = ChunkTypeExtensions._bits + 2, + + /// + /// Contains index for caching poly chunks. + /// + CacheList = ChunkTypeExtensions._bits + 3, + + /// + /// Contains index for drawing poly chunks + /// + DrawList = ChunkTypeExtensions._bits + 4, + + /// + /// Contains texture information. + /// + TextureID = ChunkTypeExtensions._tiny + 0, + + /// + /// Contains texture information. Same as . + /// + TextureID2 = ChunkTypeExtensions._tiny + 1, + + /// + /// Material; Contains diffuse color (ARGB32). + /// + Material_Diffuse = ChunkTypeExtensions._material + 1, + + /// + /// Material; Contains ambient color (RGB24). + /// + Material_Ambient = ChunkTypeExtensions._material + 2, + + /// + /// Material; Contains diffuse color (ARGB32) and ambient color (RGB24). + /// + Material_DiffuseAmbient = ChunkTypeExtensions._material + 3, + + /// + /// Material; Contains specular exponent and color (RGB24). + /// + Material_Specular = ChunkTypeExtensions._material + 4, + + /// + /// Material; Contains diffuse color (ARGB32), specular exponent and color (RGB24). + /// + Material_DiffuseSpecular = ChunkTypeExtensions._material + 5, + + /// + /// Material; Contains ambient color (RGB24), specular exponent and color (RGB24). + /// + Material_AmbientSpecular = ChunkTypeExtensions._material + 6, + + /// + /// Material; Contains diffuse color (ARGB32), ambient color (RGB24), specular exponent and color (RGB24). + /// + Material_DiffuseAmbientSpecular = ChunkTypeExtensions._material + 7, + + /// + /// Material; Contains "bump" information (?). + /// + Material_Bump = ChunkTypeExtensions._material + 8, + + /// + /// Material; Contains diffuse color (ARGB32). + ///
Same as . + ///
+ Material_Diffuse2 = ChunkTypeExtensions._material + 9, + + /// + /// Material; Contains ambient color (RGB24). + ///
Same as . + ///
+ Material_Ambient2 = ChunkTypeExtensions._material + 10, + + /// + /// Material; Contains diffuse color (ARGB32) and ambient color (RGB24). + ///
Same as . + ///
+ Material_DiffuseAmbient2 = ChunkTypeExtensions._material + 11, + + /// + /// Material; Contains specular exponent and color (RGB24). + ///
Same as . + ///
+ Material_Specular2 = ChunkTypeExtensions._material + 12, + + /// + /// Material; Contains diffuse color (ARGB32), specular exponent and color (RGB24). + ///
Same as . + ///
+ Material_DiffuseSpecular2 = ChunkTypeExtensions._material + 13, + + /// + /// Material; Contains ambient color (RGB24), specular exponent and color (RGB24). + ///
Same as . + ///
+ Material_AmbientSpecular2 = ChunkTypeExtensions._material + 14, + + /// + /// Material; Contains diffuse color (ARGB32), ambient color (RGB24), specular exponent and color (RGB24). + ///
Same as . + ///
+ Material_DiffuseAmbientSpecular2 = ChunkTypeExtensions._material + 15, + + /// + /// Volume defined from triangles. + /// + Volume_Polygon3 = ChunkTypeExtensions._volume + 0, + + /// + /// Volume defined from quads. + /// + Volume_Polygon4 = ChunkTypeExtensions._volume + 1, + + /// + /// Volume defined from triangle strips. + /// + Volume_Strip = ChunkTypeExtensions._volume + 2, + + /// + /// Triangle strips for rendering; No additional info. + /// + Strip_Blank = ChunkTypeExtensions._strip + 0, + + /// + /// Triangle strips for rendering; Contains texture coordinates (0-255 range). + /// + Strip_Tex = ChunkTypeExtensions._strip + 1, + + /// + /// Triangle strips for rendering; Contains texture coordinates (0-1023 range). + /// + Strip_HDTex = ChunkTypeExtensions._strip + 2, + + /// + /// Triangle strips for rendering; Contains normals. + /// + Strip_Normal = ChunkTypeExtensions._strip + 3, + + /// + /// Triangle strips for rendering; Contains normals, texture coordinates (0-255 range). + /// + Strip_TexNormal = ChunkTypeExtensions._strip + 4, + + /// + /// Triangle strips for rendering; Contains normals, texture coordinates (0-1023 range). + /// + Strip_HDTexNormal = ChunkTypeExtensions._strip + 5, + + /// + /// Triangle strips for rendering; Contains colors (ARGB32). + /// + Strip_Color = ChunkTypeExtensions._strip + 6, + + /// + /// Triangle strips for rendering; Contains colors (ARGB32), texture coordinates (0-255 range). + /// + Strip_TexColor = ChunkTypeExtensions._strip + 7, + + /// + /// Triangle strips for rendering; Contains colors (ARGB32), texture coordinates (0-1023 range). + /// + Strip_HDTexColor = ChunkTypeExtensions._strip + 8, + + /// + /// Triangle strips for rendering; The same as . + /// + Strip_BlankDouble = ChunkTypeExtensions._strip + 9, + + /// + /// Triangle strips for rendering; Contains 2 sets of texture coordinates (0-255 range). + /// + Strip_TexDouble = ChunkTypeExtensions._strip + 10, + + /// + /// Triangle strips for rendering; Contains 2 sets of texture coordinates (0-1023 range). + /// + Strip_HDTexDouble = ChunkTypeExtensions._strip + 11, + + /// + /// End marker chunk. + /// + End = 255 + } +} diff --git a/src/SA3D.Modeling/Mesh/Chunk/PolyChunks/BitsChunk.cs b/src/SA3D.Modeling/Mesh/Chunk/PolyChunks/BitsChunk.cs new file mode 100644 index 0000000..d369dbd --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Chunk/PolyChunks/BitsChunk.cs @@ -0,0 +1,22 @@ +using SA3D.Common.IO; + +namespace SA3D.Modeling.Mesh.Chunk.PolyChunks +{ + /// + /// Base class for poly chunks with no body. + /// + public abstract class BitsChunk : PolyChunk + { + /// + public override uint ByteSize => 2; + + /// + /// Base constructor for bits chunks. + /// + /// + protected BitsChunk(PolyChunkType type) : base(type) { } + + /// + protected override void InternalWrite(EndianStackWriter writer) { } + } +} diff --git a/src/SA3D.Modeling/Mesh/Chunk/PolyChunks/BlendAlphaChunk.cs b/src/SA3D.Modeling/Mesh/Chunk/PolyChunks/BlendAlphaChunk.cs new file mode 100644 index 0000000..99c570a --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Chunk/PolyChunks/BlendAlphaChunk.cs @@ -0,0 +1,37 @@ +namespace SA3D.Modeling.Mesh.Chunk.PolyChunks +{ + /// + /// Sets the blendmode of the following strip chunks. + /// + public class BlendAlphaChunk : BitsChunk + { + /// + /// Source blendmode. + /// + public BlendMode SourceAlpha + { + get => (BlendMode)((Attributes >> 3) & 7); + set => Attributes = (byte)((Attributes & ~0x38) | ((byte)value << 3)); + } + + /// + /// Destination blendmode. + /// + public BlendMode DestinationAlpha + { + get => (BlendMode)(Attributes & 7); + set => Attributes = (byte)((Attributes & ~7) | (byte)value); + } + + /// + /// Creates a new blendalpha chunk. + /// + public BlendAlphaChunk() : base(PolyChunkType.BlendAlpha) { } + + /// + public override string ToString() + { + return $"BlendAlpha - {SourceAlpha} -> {DestinationAlpha}"; + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Chunk/PolyChunks/CacheListChunk.cs b/src/SA3D.Modeling/Mesh/Chunk/PolyChunks/CacheListChunk.cs new file mode 100644 index 0000000..1b5110e --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Chunk/PolyChunks/CacheListChunk.cs @@ -0,0 +1,28 @@ +namespace SA3D.Modeling.Mesh.Chunk.PolyChunks +{ + /// + /// Caches the succeeding polygon chunks of the same attach into specified index. + /// + public class CacheListChunk : BitsChunk + { + /// + /// Cache ID. + /// + public byte List + { + get => Attributes; + set => Attributes = value; + } + + /// + /// Creates a new cache list chunk. + /// + public CacheListChunk() : base(PolyChunkType.CacheList) { } + + /// + public override string ToString() + { + return $"Cache list - {List}"; + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Chunk/PolyChunks/DrawListChunk.cs b/src/SA3D.Modeling/Mesh/Chunk/PolyChunks/DrawListChunk.cs new file mode 100644 index 0000000..b303b5b --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Chunk/PolyChunks/DrawListChunk.cs @@ -0,0 +1,28 @@ +namespace SA3D.Modeling.Mesh.Chunk.PolyChunks +{ + /// + /// Draws the polygon chunks cached by a specific index. + /// + public class DrawListChunk : BitsChunk + { + /// + /// Cache ID + /// + public byte List + { + get => Attributes; + set => Attributes = value; + } + + /// + /// Creates a new draw list chunk. + /// + public DrawListChunk() : base(PolyChunkType.DrawList) { } + + /// + public override string ToString() + { + return $"Draw List - {List}"; + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Chunk/PolyChunks/MaterialBumpChunk.cs b/src/SA3D.Modeling/Mesh/Chunk/PolyChunks/MaterialBumpChunk.cs new file mode 100644 index 0000000..7885863 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Chunk/PolyChunks/MaterialBumpChunk.cs @@ -0,0 +1,79 @@ +using SA3D.Common.IO; + +namespace SA3D.Modeling.Mesh.Chunk.PolyChunks +{ + /// + /// Polychunk with unknown usage. + /// + public class MaterialBumpChunk : SizedChunk + { + /// + public override ushort Size => 6; + + /// + /// DX. + /// + public ushort DX { get; set; } + + /// + /// DY. + /// + public ushort DY { get; set; } + + /// + /// DZ. + /// + public ushort DZ { get; set; } + + /// + /// UX. + /// + public ushort UX { get; set; } + + /// + /// UY. + /// + public ushort UY { get; set; } + + /// + /// UZ. + /// + public ushort UZ { get; set; } + + /// + /// Creates a new material bump chunk. + /// + public MaterialBumpChunk() : base(PolyChunkType.Material_Bump) { } + + internal static MaterialBumpChunk Read(EndianStackReader reader, uint address) + { + ushort header = reader.ReadUShort(address); + byte attrib = (byte)(header >> 8); + // skipping size + address += 4; + + return new MaterialBumpChunk() + { + Attributes = attrib, + DX = reader.ReadUShort(address), + DY = reader.ReadUShort(address += 2), + DZ = reader.ReadUShort(address += 2), + UX = reader.ReadUShort(address += 2), + UY = reader.ReadUShort(address += 2), + UZ = reader.ReadUShort(address += 2), + }; + } + + /// + protected override void InternalWrite(EndianStackWriter writer) + { + base.InternalWrite(writer); + writer.WriteUShort(DX); + writer.WriteUShort(DY); + writer.WriteUShort(DZ); + writer.WriteUShort(UX); + writer.WriteUShort(UY); + writer.WriteUShort(UZ); + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Chunk/PolyChunks/MaterialChunk.cs b/src/SA3D.Modeling/Mesh/Chunk/PolyChunks/MaterialChunk.cs new file mode 100644 index 0000000..46b083c --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Chunk/PolyChunks/MaterialChunk.cs @@ -0,0 +1,181 @@ +using SA3D.Common.IO; +using SA3D.Modeling.Structs; +using System; + +namespace SA3D.Modeling.Mesh.Chunk.PolyChunks +{ + /// + /// Material information for the following strip chunks + /// + public class MaterialChunk : SizedChunk + { + private Color? _diffuse; + private Color? _ambient; + private Color? _specular; + + /// + /// Whether the material type is a second type + /// + public bool Second + { + get => ((byte)Type & 0x08) != 0; + set => TypeAttribute(0x08, value); + } + + /// + public override ushort Size + { + get + { + byte type = (byte)Type; + + return (ushort)(2 * + ((type & 1) + + ((type >> 1) & 1) + + ((type >> 2) & 1))); + } + } + + /// + /// Source blendmode + /// + public BlendMode SourceAlpha + { + get => (BlendMode)((Attributes >> 3) & 7); + set => Attributes = (byte)((Attributes & ~0x38) | ((byte)value << 3)); + } + + /// + /// Destination blendmode + /// + public BlendMode DestinationAlpha + { + get => (BlendMode)(Attributes & 7); + set => Attributes = (byte)((Attributes & ~7) | (byte)value); + } + + /// + /// Diffuse color + /// + public Color? Diffuse + { + get => _diffuse; + set + { + TypeAttribute(0x01, value.HasValue); + _diffuse = value; + } + } + + /// + /// Ambient color + /// + public Color? Ambient + { + get => _ambient; + set + { + TypeAttribute(0x02, value.HasValue); + _ambient = value; + } + } + + /// + /// Specular color + /// + public Color? Specular + { + get => _specular; + set + { + TypeAttribute(0x04, value.HasValue); + _specular = value; + } + } + + /// + /// Specular exponent
+ /// Requires to be set + ///
+ public byte SpecularExponent { get; set; } + + /// + /// Creates a new material chunk. Defaults to with a white diffuse color. + /// + public MaterialChunk() : base(PolyChunkType.Material_Diffuse) + { + _diffuse = Color.ColorWhite; + } + + private void TypeAttribute(byte val, bool state) + { + byte type = (byte)Type; + Type = (PolyChunkType)(byte)(state + ? type | val + : type & ~val); + } + + internal static MaterialChunk Read(EndianStackReader reader, ref uint address) + { + ushort header = reader.ReadUShort(address); + PolyChunkType type = (PolyChunkType)(header & 0xFF); + // skipping size + address += 4; + + MaterialChunk mat = new() + { + Attributes = (byte)(header >> 8) + }; + + if(((byte)type & 0x01) != 0) + { + mat.Diffuse = reader.ReadColor(ref address, ColorIOType.ARGB8_16); + } + + if(((byte)type & 0x02) != 0) + { + mat.Ambient = reader.ReadColor(ref address, ColorIOType.ARGB8_16); + } + + if(((byte)type & 0x04) != 0) + { + Color spec = reader.ReadColor(ref address, ColorIOType.ARGB8_16); + mat.SpecularExponent = spec.Alpha; + spec.Alpha = 255; + mat.Specular = spec; + } + + mat.Second = ((byte)type & 0x08) != 0; + + return mat; + } + + /// + protected override void InternalWrite(EndianStackWriter writer) + { + if(_diffuse == null && _specular == null && _ambient == null) + { + throw new InvalidOperationException("Material has no colors and thus no valid type!"); + } + + base.InternalWrite(writer); + + if(_diffuse.HasValue) + { + writer.WriteColor(_diffuse.Value, ColorIOType.ARGB8_16); + } + + if(_ambient.HasValue) + { + writer.WriteColor(_ambient.Value, ColorIOType.ARGB8_16); + } + + if(_specular.HasValue) + { + Color wSpecular = _specular.Value; + wSpecular.Alpha = SpecularExponent; + writer.WriteColor(wSpecular, ColorIOType.ARGB8_16); + } + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Chunk/PolyChunks/MipmapDistanceMultiplierChunk.cs b/src/SA3D.Modeling/Mesh/Chunk/PolyChunks/MipmapDistanceMultiplierChunk.cs new file mode 100644 index 0000000..7ffcdf1 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Chunk/PolyChunks/MipmapDistanceMultiplierChunk.cs @@ -0,0 +1,31 @@ +using System; + +namespace SA3D.Modeling.Mesh.Chunk.PolyChunks +{ + /// + /// Adjusts the mipmap distance of the following strip chunks + /// + public class MipmapDistanceMultiplierChunk : BitsChunk + { + /// + /// The mipmap distance multiplier
+ /// Ranges from 0 to 3.75f in increments of 0.25 + ///
+ public float MipmapDistanceMultiplier + { + get => (Attributes & 0xF) * 0.25f; + set => Attributes = (byte)((Attributes & 0xF0) | (byte)Math.Max(0, Math.Min(0xF, Math.Round(value / 0.25, MidpointRounding.AwayFromZero)))); + } + + /// + /// Creates a new mipmap distance multiplier chunk. + /// + public MipmapDistanceMultiplierChunk() : base(PolyChunkType.MipmapDistanceMultiplier) { } + + /// + public override string ToString() + { + return $"MMDM - {MipmapDistanceMultiplier}"; + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Chunk/PolyChunks/SizedChunk.cs b/src/SA3D.Modeling/Mesh/Chunk/PolyChunks/SizedChunk.cs new file mode 100644 index 0000000..d2706cc --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Chunk/PolyChunks/SizedChunk.cs @@ -0,0 +1,30 @@ +using SA3D.Common.IO; + +namespace SA3D.Modeling.Mesh.Chunk.PolyChunks +{ + /// + /// Base class for polygon chunks with a size header. + /// + public abstract class SizedChunk : PolyChunk + { + /// + /// Amount of shorts in the chunk + /// + public abstract ushort Size { get; } + + /// + public sealed override uint ByteSize => (Size * 2u) + 4u; + + /// + /// Base constructor for sized chunks. + /// + /// + public SizedChunk(PolyChunkType type) : base(type) { } + + /// + protected override void InternalWrite(EndianStackWriter writer) + { + writer.WriteUShort(Size); + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Chunk/PolyChunks/SpecularExponentChunk.cs b/src/SA3D.Modeling/Mesh/Chunk/PolyChunks/SpecularExponentChunk.cs new file mode 100644 index 0000000..9491457 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Chunk/PolyChunks/SpecularExponentChunk.cs @@ -0,0 +1,31 @@ +using System; + +namespace SA3D.Modeling.Mesh.Chunk.PolyChunks +{ + /// + /// Sets the specular exponent of the following strip chunks + /// + public class SpecularExponentChunk : BitsChunk + { + /// + /// Specular exponent
+ /// Ranges from 0 to 16 + ///
+ public byte SpecularExponent + { + get => (byte)(Attributes & 0x1F); + set => Attributes = (byte)((Attributes & ~0x1F) | Math.Min(value, (byte)16)); + } + + /// + /// Creates a new Specular exponent chunk. + /// + public SpecularExponentChunk() : base(PolyChunkType.SpecularExponent) { } + + /// + public override string ToString() + { + return $"Specular Exponent - {SpecularExponent}"; + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Chunk/PolyChunks/StripChunk.cs b/src/SA3D.Modeling/Mesh/Chunk/PolyChunks/StripChunk.cs new file mode 100644 index 0000000..3502a00 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Chunk/PolyChunks/StripChunk.cs @@ -0,0 +1,346 @@ +using SA3D.Common; +using SA3D.Common.IO; +using SA3D.Modeling.Mesh.Chunk.Structs; +using System; + +namespace SA3D.Modeling.Mesh.Chunk.PolyChunks +{ + /// + /// Chunk holding polygon data for rendering. + /// + public class StripChunk : SizedChunk + { + private int _triangleAttributeCount; + + #region Type dependent properties + + /// + /// The number of texture coordinate sets the polygons utilize. + /// + public int TexcoordCount + { + get + { + if(Type is PolyChunkType.Strip_Tex + or PolyChunkType.Strip_HDTex + or PolyChunkType.Strip_TexNormal + or PolyChunkType.Strip_HDTexNormal + or PolyChunkType.Strip_TexColor + or PolyChunkType.Strip_HDTexColor) + { + return 1; + } + else if(Type is PolyChunkType.Strip_TexDouble + or PolyChunkType.Strip_HDTexDouble) + { + return 2; + } + else + { + return 0; + } + } + } + + /// + /// Whether texture coordinates are in the 0-1023 range, instead of 0-255. + /// + public bool HasHDTexcoords => + Type is PolyChunkType.Strip_HDTex + or PolyChunkType.Strip_HDTexColor + or PolyChunkType.Strip_HDTexNormal + or PolyChunkType.Strip_HDTexDouble; + + /// + /// Whether polygons utilize normals. + /// + public bool HasNormals => + Type is PolyChunkType.Strip_Normal + or PolyChunkType.Strip_TexNormal + or PolyChunkType.Strip_HDTexNormal; + + /// + /// Whether polygons utilize colors. + /// + public bool HasColors => + Type is PolyChunkType.Strip_Color + or PolyChunkType.Strip_TexColor + or PolyChunkType.Strip_HDTexColor; + + #endregion + + #region Attribute Properties + + /// + /// Ignores lighting as a whole. + ///
0x01 in . + ///
+ public bool IgnoreLight + { + get => GetAttributeBit(1); + set => SetAttributeBit(0x01, value); + } + + /// + /// Ignores specular lighting. + ///
0x02 in . + ///
+ public bool IgnoreSpecular + { + get => GetAttributeBit(2); + set => SetAttributeBit(0x02, value); + } + + /// + /// Ignores ambient lighting. + ///
0x04 in . + ///
+ public bool IgnoreAmbient + { + get => GetAttributeBit(4); + set => SetAttributeBit(0x04, value); + } + + /// + /// Renders polygons with transparency enabled. + ///
0x08 in . + ///
+ public bool UseAlpha + { + get => GetAttributeBit(8); + set => SetAttributeBit(0x08, value); + } + + /// + /// Disables backface culling. + ///
0x10 in . + ///
+ public bool DoubleSide + { + get => GetAttributeBit(0x10); + set => SetAttributeBit(0x10, value); + } + + /// + /// Ignore normals and render every polygon flat. + ///
0x20 in . + ///
+ public bool FlatShading + { + get => GetAttributeBit(0x20); + set => SetAttributeBit(0x20, value); + } + + /// + /// Ignore texture coordinates and use normals for environment (matcap/normal) mapping. + ///
0x40 in . + ///
+ public bool EnvironmentMapping + { + get => GetAttributeBit(0x40); + set => SetAttributeBit(0x40, value); + } + + /// + /// Unknown effect. + ///
0x80 in . + ///
+ public bool UnknownAttribute + { + get => GetAttributeBit(0x80); + set => SetAttributeBit(0x80, value); + } + + private bool GetAttributeBit(byte bits) + { + return (Attributes & bits) != 0; + } + + private void SetAttributeBit(byte bits, bool value) + { + if(value) + { + Attributes |= bits; + } + else + { + Attributes &= (byte)~bits; + } + } + + #endregion + + + /// + /// Triangle strips making up the polygons. + /// + public ChunkStrip[] Strips { get; private set; } + + /// + /// Number of custom attributes for each triangle. + /// + public int TriangleAttributeCount + { + get => _triangleAttributeCount; + set + { + if(value is < 0 or > 3) + { + throw new ArgumentOutOfRangeException(nameof(value), "Value out of range. Must be between 0 and 3."); + } + + _triangleAttributeCount = value; + } + } + + /// + /// Raw size not constrained to 16 bits. + /// + public uint RawSize + { + get + { + uint result = 2; + + int texcoordCount = TexcoordCount; + bool hasNormals = HasNormals; + bool hasColors = HasColors; + + foreach(ChunkStrip str in Strips) + { + result += str.Size(texcoordCount, hasNormals, hasColors, _triangleAttributeCount); + } + + return result / 2; + } + } + + /// + public override ushort Size + { + get + { + uint result = RawSize; + + if(result > ushort.MaxValue) + { + throw new InvalidOperationException($"Strip chunk size ({result}) exceeds maximum size ({ushort.MaxValue})."); + } + + return (ushort)result; + } + } + + /// + /// Creates a new strip chunk. + /// + /// Type of strip chunk. + /// Triangle strips. + /// Number of custom attributes for each triangle. + /// + public StripChunk(PolyChunkType type, ChunkStrip[] strips, int triangleAttributeCount) : base(type) + { + if(type is < PolyChunkType.Strip_Blank or > PolyChunkType.Strip_HDTexDouble) + { + throw new ArgumentException($"Type \"{type}\" is not a valid strip chunk type!"); + } + + Strips = strips; + TriangleAttributeCount = triangleAttributeCount; + } + + /// + /// Creates a new strip chunk. + /// + /// Type of strip chunk. + /// Number of strips to create the stripchunk with. + /// Number of custom attributes for each triangle. + /// + public StripChunk(PolyChunkType type, ushort stripCount, int triangleAttributeCount) + : this(type, new ChunkStrip[stripCount], triangleAttributeCount) { } + + + /// + /// Changes the type of the strip chunk. + /// + public void ChangeType(PolyChunkType type) + { + if(type is < PolyChunkType.Strip_Blank or > PolyChunkType.Strip_HDTexDouble) + { + throw new ArgumentException($"Type \"{type}\" is not a valid strip chunk type!"); + } + + Type = type; + } + + + internal static StripChunk Read(EndianStackReader reader, ref uint address) + { + ushort header = reader.ReadUShort(address); + ushort header2 = reader.ReadUShort(address + 4); + + PolyChunkType type = (PolyChunkType)(header & 0xFF); + byte attribs = (byte)(header >> 8); + ushort polyCount = (ushort)(header2 & 0x3FFFu); + byte triangleAttributeCount = (byte)(header2 >> 14); + + StripChunk result = new(type, polyCount, triangleAttributeCount) + { + Attributes = attribs + }; + + address += 6; + + int texcoordCount = result.TexcoordCount; + bool hasNormals = result.HasNormals; + bool hasColors = result.HasColors; + bool hdTexcoord = result.HasHDTexcoords; + + for(int i = 0; i < polyCount; i++) + { + result.Strips[i] = ChunkStrip.Read(reader, ref address, texcoordCount, hdTexcoord, hasNormals, hasColors, triangleAttributeCount); + } + + return result; + } + + /// + protected override void InternalWrite(EndianStackWriter writer) + { + if(Strips.Length > 0x3FFF) + { + throw new InvalidOperationException($"Strip count ({Strips.Length}) exceeds maximum ({0x3FFF})"); + } + + base.InternalWrite(writer); + + writer.WriteUShort((ushort)(Strips.Length | (TriangleAttributeCount << 14))); + + int texcoordCount = TexcoordCount; + bool hasNormals = HasNormals; + bool hasColors = HasColors; + bool hdTexcoord = HasHDTexcoords; + + foreach(ChunkStrip s in Strips) + { + s.Write(writer, texcoordCount, hdTexcoord, hasNormals, hasColors, _triangleAttributeCount); + } + } + + + /// + public override StripChunk Clone() + { + return new( + Type, + Strips.ContentClone(), + TriangleAttributeCount); + } + + /// + public override string ToString() + { + return $"{Type} - 0x{Attributes:X2}, {TriangleAttributeCount} : {Strips.Length}"; + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Chunk/PolyChunks/TextureChunk.cs b/src/SA3D.Modeling/Mesh/Chunk/PolyChunks/TextureChunk.cs new file mode 100644 index 0000000..dded1ea --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Chunk/PolyChunks/TextureChunk.cs @@ -0,0 +1,136 @@ +using SA3D.Common.IO; +using System; + +namespace SA3D.Modeling.Mesh.Chunk.PolyChunks +{ + /// + /// Contains texture information. + /// + public class TextureChunk : PolyChunk + { + /// + public override uint ByteSize => 4; + + /// + /// Whether the chunktype is . + /// + public bool Second + { + get => Type == PolyChunkType.TextureID2; + set => Type = value ? PolyChunkType.TextureID2 : PolyChunkType.TextureID; + } + + /// + /// The mipmap distance multiplier. + ///
Ranges from 0 to 3.75f in increments of 0.25. + ///
+ public float MipmapDistanceMultiplier + { + get => (Attributes & 0xF) * 0.25f; + set => Attributes = (byte)((Attributes & 0xF0) | (byte)Math.Max(0, Math.Min(0xF, Math.Round(value / 0.25, MidpointRounding.AwayFromZero)))); + } + + /// + /// Clamps texture corrdinates on the vertical axis between -1 and 1. + /// + public bool ClampV + { + get => (Attributes & 0x10) != 0; + set => _ = value ? Attributes |= 0x10 : Attributes &= 0xEF; + } + + /// + /// Clamps texture corrdinates on the horizontal axis between -1 and 1. + /// + public bool ClampU + { + get => (Attributes & 0x20) != 0; + set => _ = value ? Attributes |= 0x20 : Attributes &= 0xDF; + } + + /// + /// Mirrors the texture every second time the texture is repeated along the vertical axis. + /// + public bool MirrorV + { + get => (Attributes & 0x40) != 0; + set => _ = value ? Attributes |= 0x40 : Attributes &= 0xBF; + } + + /// + /// Mirrors the texture every second time the texture is repeated along the horizontal axis. + /// + public bool MirrorU + { + get => (Attributes & 0x80) != 0; + set => _ = value ? Attributes |= 0x80 : Attributes &= 0x7F; + } + + + /// + /// Second set of data bytes. + /// + public ushort Data { get; private set; } + + /// + /// Texture ID to use. + /// + public ushort TextureID + { + get => (ushort)(Data & 0x1FFFu); + set => Data = (ushort)((Data & ~0x1FFF) | Math.Min(value, (ushort)0x1FFF)); + } + + /// + /// Whether to use super sampling (anisotropic filtering). + /// + public bool SuperSample + { + get => (Data & 0x2000) != 0; + set => _ = value ? Data |= 0x2000 : Data &= 0xDFFF; + } + + /// + /// Texture pixel filtering mode. + /// + public FilterMode FilterMode + { + get => (FilterMode)(Data >> 14); + set => Data = (ushort)((Data & ~0xC000) | ((ushort)value << 14)); + } + + + /// + /// Creates a new texture chunk. + /// + /// Whether it is + public TextureChunk(bool second = false) : base(second ? PolyChunkType.TextureID2 : PolyChunkType.TextureID) { } + + + internal static TextureChunk Read(EndianStackReader data, uint address) + { + ushort header = data.ReadUShort(address); + PolyChunkType type = (PolyChunkType)(header & 0xFF); + byte attribs = (byte)(header >> 8); + ushort cnkData = data.ReadUShort(address + 2); + + return new TextureChunk(type == PolyChunkType.TextureID2) + { + Attributes = attribs, + Data = cnkData + }; + } + + /// + protected override void InternalWrite(EndianStackWriter writer) + { + writer.WriteUShort(Data); + } + + /// + public override string ToString() + { + return $"{Type} - {TextureID}"; + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Chunk/PolyChunks/VolumeChunk.cs b/src/SA3D.Modeling/Mesh/Chunk/PolyChunks/VolumeChunk.cs new file mode 100644 index 0000000..5e08ac0 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Chunk/PolyChunks/VolumeChunk.cs @@ -0,0 +1,171 @@ +using SA3D.Common; +using SA3D.Common.IO; +using SA3D.Modeling.Mesh.Chunk.Structs; +using System; + +namespace SA3D.Modeling.Mesh.Chunk.PolyChunks +{ + /// + /// Chunk containing a volume build from polygons. + /// + public class VolumeChunk : SizedChunk + { + private int _polygonAttributeCount; + + /// + /// Polygons of the volume + /// + public IChunkVolumePolygon[] Polygons { get; } + + /// + /// User attribute count (ranges from 0 to 3) + /// + public int PolygonAttributeCount + { + get => _polygonAttributeCount; + set + { + if(value is < 0 or > 3) + { + throw new ArgumentOutOfRangeException(nameof(value), "Value out of range. Must be between 0 and 3."); + } + + _polygonAttributeCount = value; + } + } + + /// + /// Raw size not constrained to 16 bits. + /// + public uint RawSize + { + get + { + uint size = 2; + foreach(IChunkVolumePolygon p in Polygons) + { + size += p.Size(PolygonAttributeCount); + } + + return size / 2; + } + } + + /// + public override ushort Size + { + get + { + uint result = RawSize; + + if(result > ushort.MaxValue) + { + throw new InvalidOperationException($"Strip chunk size ({result}) exceeds maximum size ({ushort.MaxValue})."); + } + + return (ushort)result; + } + } + + + /// + /// Creates a new volume chunk. + /// + /// Type of volume chunk. + /// Polygons to use. + /// Number of attributes for each polygon. + public VolumeChunk(PolyChunkType type, IChunkVolumePolygon[] polygons, int polygonAttributeCount) : base(type) + { + if(type is < PolyChunkType.Volume_Polygon3 or > PolyChunkType.Volume_Strip) + { + throw new ArgumentException($"Type \"{type}\" is not a valid volume chunk type!"); + } + + Polygons = polygons; + PolygonAttributeCount = polygonAttributeCount; + } + + /// + /// Creates a new, empty volume chunk. + /// + /// Type of volume chunk. + /// Number of polygons in the chunk. + /// Number of attributes for each polygon. + public VolumeChunk(PolyChunkType type, ushort polygonCount, int polygonAttributeCount) + : this(type, new IChunkVolumePolygon[polygonCount], polygonAttributeCount) { } + + + /// + protected override void InternalWrite(EndianStackWriter writer) + { + if(Polygons.Length > 0x3FFF) + { + throw new InvalidOperationException($"Poly count ({Polygons.Length}) exceeds maximum ({0x3FFF})"); + } + + base.InternalWrite(writer); + + writer.WriteUShort((ushort)(Polygons.Length | (PolygonAttributeCount << 14))); + + foreach(IChunkVolumePolygon p in Polygons) + { + p.Write(writer, PolygonAttributeCount); + } + } + + internal static VolumeChunk Read(EndianStackReader reader, ref uint address) + { + ushort header = reader.ReadUShort(address); + ushort Header2 = reader.ReadUShort(address + 4); + + PolyChunkType type = (PolyChunkType)(header & 0xFF); + byte attrib = (byte)(header >> 8); + ushort polyCount = (ushort)(Header2 & 0x3FFFu); + byte userAttribs = (byte)(Header2 >> 14); + + VolumeChunk result = new(type, polyCount, userAttribs) + { + Attributes = attrib, + }; + + address += 6; + + if(type == PolyChunkType.Volume_Polygon3) + { + for(int i = 0; i < polyCount; i++) + { + result.Polygons[i] = ChunkVolumeTriangle.Read(reader, ref address, userAttribs); + } + } + else if(type == PolyChunkType.Volume_Polygon4) + { + for(int i = 0; i < polyCount; i++) + { + result.Polygons[i] = ChunkVolumeQuad.Read(reader, ref address, userAttribs); + } + } + else // Volume_Strip + { + for(int i = 0; i < polyCount; i++) + { + result.Polygons[i] = ChunkVolumeStrip.Read(reader, ref address, userAttribs); + } + } + + return result; + } + + + /// + public override VolumeChunk Clone() + { + return new(Type, Polygons.ContentClone(), PolygonAttributeCount); + } + + /// + public override string ToString() + { + return $"{Type} - {PolygonAttributeCount} : {Polygons.Length}"; + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Chunk/Structs/ChunkCorner.cs b/src/SA3D.Modeling/Mesh/Chunk/Structs/ChunkCorner.cs new file mode 100644 index 0000000..c0d27da --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Chunk/Structs/ChunkCorner.cs @@ -0,0 +1,105 @@ +using SA3D.Modeling.Structs; +using System; +using System.Numerics; + +namespace SA3D.Modeling.Mesh.Chunk.Structs +{ + /// + /// A single polygon corner for chunk models. + /// + public struct ChunkCorner : IEquatable + { + /// + /// Vertex Cache index. + /// + public ushort Index { get; set; } + + /// + /// Texture coordinates. + /// + public Vector2 Texcoord { get; set; } + + /// + /// Second set of texture coordinates. + /// + public Vector2 Texcoord2 { get; set; } + + /// + /// Normalized direction. + /// + public Vector3 Normal { get; set; } + + /// + /// Color. + /// + public Color Color { get; set; } + + /// + /// First set of attributes for the triangle that this corner closes. + /// + public ushort Attributes1 { get; set; } + + /// + /// Second set of attributes for the triangle that this corner closes. + /// + public ushort Attributes2 { get; set; } + + /// + /// Third set of attributes for the triangle that this corner closes. + /// + public ushort Attributes3 { get; set; } + + + /// + public override readonly bool Equals(object? obj) + { + return obj is ChunkCorner corner && + Index == corner.Index && + Texcoord.Equals(corner.Texcoord) && + Normal.Equals(corner.Normal) && + Color.Equals(corner.Color) && + Attributes1 == corner.Attributes1 && + Attributes2 == corner.Attributes2 && + Attributes3 == corner.Attributes3; + } + + /// + public override readonly int GetHashCode() + { + return HashCode.Combine(Index, Texcoord, Normal, Color, Attributes1, Attributes2, Attributes3); + } + + readonly bool IEquatable.Equals(ChunkCorner other) + { + return Equals(other); + } + + /// + /// Compares two chunk corners for equality. + /// + /// Lefthand corner. + /// Righthand corner. + /// Wether the corners are equal. + public static bool operator ==(ChunkCorner left, ChunkCorner right) + { + return left.Equals(right); + } + + /// + /// Compares two chunk corners for inequality. + /// + /// Lefthand corner. + /// Righthand corner. + /// Wether the corners are inequal. + public static bool operator !=(ChunkCorner left, ChunkCorner right) + { + return !(left == right); + } + + /// + public override readonly string ToString() + { + return $"{Index} : {Texcoord.DebugString()}, {Color}"; + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Chunk/Structs/ChunkStrip.cs b/src/SA3D.Modeling/Mesh/Chunk/Structs/ChunkStrip.cs new file mode 100644 index 0000000..9534a9a --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Chunk/Structs/ChunkStrip.cs @@ -0,0 +1,223 @@ +using SA3D.Common.IO; +using SA3D.Modeling.Structs; +using System; + +namespace SA3D.Modeling.Mesh.Chunk.Structs +{ + /// + /// Triangle string structure for strip chunks. + /// + public struct ChunkStrip : ICloneable + { + /// + /// Triangle corners. + ///
The first two corners are only used for their index. + ///
+ public ChunkCorner[] Corners { get; private set; } + + /// + /// Whether to inverse the culling direction of the triangles. + /// + public bool Reversed { get; private set; } + + + /// + /// Creates a new strip. + /// + /// Triangle corners. + /// Whether to inverse the culling direction of the triangles + public ChunkStrip(ChunkCorner[] corners, bool reverse) + { + Reversed = reverse; + Corners = corners; + } + + + /// + /// Calculates the size of the strip in bytes. + /// + /// Number of texture coordinate sets in the strip. + /// Whether the strip has normals. + /// Whether the strip has colors. + /// Number of attribute sets for every triangle. + /// The size of the strip in bytes. + public readonly uint Size(int texcoordCount, bool hasNormal, bool hasColor, int triangleAttributeCount) + { + uint structSize = (uint)(2u + + (texcoordCount * 4u) + + (hasNormal ? 12u : 0u) + + (hasColor ? 4u : 0u)); + + return (uint)( + 2u // strip header + + (Corners.Length * structSize) // individual corners + + ((Corners.Length - 2) * triangleAttributeCount * 2)); // triangle attributes + } + + /// + /// Reads a strip off an endian stack reader. Advances the address by the number of bytes read. + /// + /// The reader to read from. + /// Address at which to start reading. + /// Number of texture coordinate sets in the strip. + /// Whether the texture coordinate data ranges from 0-1023, instead of 0-255 + /// Whether the strip has normals. + /// Whether the strip has colors. + /// Number of attribute sets for every triangle. + /// The strip that was read. + public static ChunkStrip Read(EndianStackReader reader, ref uint address, int texcoordCount, bool hdTexcoord, bool hasNormal, bool hasColor, int triangleAttributeCount) + { + const float NormalFactor = 1f / short.MaxValue; + + short header = reader.ReadShort(address); + bool reverse = header < 0; + ChunkCorner[] corners = new ChunkCorner[Math.Abs(header)]; + + bool hasUV = texcoordCount > 0; + bool hasUV2 = texcoordCount > 1; + float uvMultiplier = hdTexcoord ? 1f / 1023f : 1f / 255f; + + bool flag1 = triangleAttributeCount > 0; + bool flag2 = triangleAttributeCount > 1; + bool flag3 = triangleAttributeCount > 2; + + address += 2; + + for(int i = 0; i < corners.Length; i++) + { + ChunkCorner c = new() + { + Index = reader.ReadUShort(address), + Color = Color.ColorWhite + }; + address += 2; + + if(hasUV) + { + c.Texcoord = reader.ReadVector2(ref address, FloatIOType.Short) * uvMultiplier; + + if(hasUV2) + { + c.Texcoord2 = reader.ReadVector2(ref address, FloatIOType.Short) * uvMultiplier; + } + } + + if(hasNormal) + { + c.Normal = reader.ReadVector3(ref address, FloatIOType.Short) * NormalFactor; + } + else if(hasColor) + { + c.Color = reader.ReadColor(ref address, ColorIOType.ARGB8_16); + } + + if(flag1 && i > 1) + { + c.Attributes1 = reader.ReadUShort(address); + address += 2; + if(flag2) + { + c.Attributes2 = reader.ReadUShort(address); + address += 2; + if(flag3) + { + c.Attributes3 = reader.ReadUShort(address); + address += 2; + } + } + } + + corners[i] = c; + } + + return new ChunkStrip(corners, reverse); + } + + /// + /// Writes the strip to an endian stack writer. + /// + /// The writer to write to. + /// Number of texture coordinate sets in the strip. + /// Whether the texture coordinate data ranges from 0-1023, instead of 0-255 + /// Whether the strip has normals. + /// Whether the strip has colors. + /// Number of attribute sets for every triangle. + public readonly void Write(EndianStackWriter writer, int texcoordCount, bool hdTexcoord, bool hasNormal, bool hasColor, int triangleAttributeCount) + { + if(Corners.Length > short.MaxValue) + { + throw new InvalidOperationException("Strip has too many corners!"); + } + + writer.WriteShort(Reversed + ? (short)-Corners.Length + : (short)Corners.Length); + + bool hasUV = texcoordCount > 0; + bool hasUV2 = texcoordCount > 1; + float uvMultiplier = hdTexcoord ? 1023f : 255f; + + bool flag1 = triangleAttributeCount > 0; + bool flag2 = triangleAttributeCount > 1; + bool flag3 = triangleAttributeCount > 2; + + for(int i = 0; i < Corners.Length; i++) + { + ChunkCorner c = Corners[i]; + writer.WriteUShort(c.Index); + if(hasUV) + { + writer.WriteVector2(c.Texcoord * uvMultiplier, FloatIOType.Short); + + if(hasUV2) + { + writer.WriteVector2(c.Texcoord2 * uvMultiplier, FloatIOType.Short); + } + } + + if(hasNormal) + { + writer.WriteVector3(c.Normal * short.MaxValue, FloatIOType.Short); + } + else if(hasColor) + { + writer.WriteColor(c.Color, ColorIOType.ARGB8_16); + } + + if(flag1 && i > 1) + { + writer.WriteUShort(c.Attributes1); + if(flag2) + { + writer.WriteUShort(c.Attributes2); + if(flag3) + { + writer.WriteUShort(c.Attributes3); + } + } + } + } + } + + + readonly object ICloneable.Clone() + { + return Clone(); + } + + /// + /// Creates a deep clone of the strip. + /// + /// The cloned strip. + public readonly ChunkStrip Clone() + { + return new((ChunkCorner[])Corners.Clone(), Reversed); + } + + /// + public override readonly string ToString() + { + return $"{Reversed} : {Corners.Length}"; + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Chunk/Structs/ChunkVertex.cs b/src/SA3D.Modeling/Mesh/Chunk/Structs/ChunkVertex.cs new file mode 100644 index 0000000..ca23742 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Chunk/Structs/ChunkVertex.cs @@ -0,0 +1,164 @@ +using SA3D.Modeling.Structs; +using System; +using System.Numerics; + +namespace SA3D.Modeling.Mesh.Chunk.Structs +{ + /// + /// Single vertex of a vertex chunk + /// + public struct ChunkVertex : IEquatable + { + /// + /// Position in 3D space. + /// + public Vector3 Position { get; set; } + + /// + /// Normalized direction. + /// + public Vector3 Normal { get; set; } + + /// + /// Diffuse Color. + /// + public Color Diffuse { get; set; } + + /// + /// Specular color. + /// + public Color Specular { get; set; } + + /// + /// Additonal Attributes. + /// + public uint Attributes { get; set; } + + /// + /// Vertex cache index. + /// + public ushort Index + { + readonly get => (ushort)(Attributes & 0xFFFF); + set => Attributes = (Attributes & ~0xFFFFu) | value; + } + + /// + /// Node influence. + /// + public float Weight + { + readonly get => (Attributes >> 16) / 255f; + set => Attributes = (Attributes & 0xFFFFF) | ((uint)Math.Round(value * 255f) << 16); + } + + + /// + /// Creates a chunk vertex with a normal. + /// + /// Position in 3D space. + /// Normalized direction. + public ChunkVertex(Vector3 position, Vector3 normal) : this() + { + Position = position; + Normal = normal; + Attributes = 0; + Weight = 1; + } + + /// + /// Creates a chunk vertex with a normal and attributes. + /// + /// Position in 3D space. + /// Normalized direction. + /// Additional attributes. + public ChunkVertex(Vector3 position, Vector3 normal, uint attribs) : this() + { + Position = position; + Normal = normal; + Attributes = attribs; + Weight = 1; + } + + /// + /// Creates a chunk vertex with a normal and weight info. + /// + /// Position in 3D space. + /// Normalized direction. + /// Vertex cache index. + /// Node influence. + public ChunkVertex(Vector3 position, Vector3 normal, ushort index, float weight) : this() + { + Position = position; + Normal = normal; + Index = index; + Weight = weight; + } + + /// + /// Creates a chunk with colors. + /// + /// Position in 3D space. + /// Diffuse color. + /// Specular color. + public ChunkVertex(Vector3 position, Color diffuse, Color specular) : this() + { + Position = position; + Diffuse = diffuse; + Specular = specular; + } + + + /// + public override readonly bool Equals(object? obj) + { + return obj is ChunkVertex vertex && + Position.Equals(vertex.Position) && + Normal.Equals(vertex.Normal) && + Diffuse.Equals(vertex.Diffuse) && + Specular.Equals(vertex.Specular) && + Attributes == vertex.Attributes; + } + + /// + public override readonly int GetHashCode() + { + return HashCode.Combine(Position, Normal, Diffuse, Specular, Attributes); + } + + /// + readonly bool IEquatable.Equals(ChunkVertex other) + { + return Equals(other); + } + + /// + /// Compares two chunk vertices for equality. + /// + /// Lefthand vertex. + /// Rigthand vertex. + /// Whether the vertices are equal. + public static bool operator ==(ChunkVertex left, ChunkVertex right) + { + return left.Equals(right); + } + + /// + /// Compares two chunk vertices for inequality. + /// + /// Lefthand vertex. + /// Rigthand vertex. + /// Whether the vertices are inequal. + public static bool operator !=(ChunkVertex left, ChunkVertex right) + { + return !(left == right); + } + + /// + public override readonly string ToString() + { + return $"{Position.DebugString()}, {Normal.DebugString()} : {Index}, {Weight:F3}"; + } + + } +} diff --git a/src/SA3D.Modeling/Mesh/Chunk/Structs/ChunkVolumeQuad.cs b/src/SA3D.Modeling/Mesh/Chunk/Structs/ChunkVolumeQuad.cs new file mode 100644 index 0000000..3a17ad5 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Chunk/Structs/ChunkVolumeQuad.cs @@ -0,0 +1,210 @@ +using SA3D.Common.IO; +using System; + +namespace SA3D.Modeling.Mesh.Chunk.Structs +{ + /// + /// Quad polygon for volume chunks. + /// + public struct ChunkVolumeQuad : IChunkVolumePolygon + { + /// + public readonly int NumIndices => 4; + + + /// + /// First vertex index. + /// + public ushort Index1 { get; set; } + + /// + /// Second vertex index. + /// + public ushort Index2 { get; set; } + + /// + /// Third vertex index. + /// + public ushort Index3 { get; set; } + + /// + /// Fourth vertex index. + /// + public ushort Index4 { get; set; } + + + /// + /// First polygon attribute. + /// + public ushort Attribute1 { get; set; } + + /// + /// Second polygon attribute. + /// + public ushort Attribute2 { get; set; } + + /// + /// Third polygon attribute. + /// + public ushort Attribute3 { get; set; } + + + /// + public ushort this[int index] + { + readonly get => index switch + { + 0 => Index1, + 1 => Index2, + 2 => Index3, + 4 => Index4, + _ => throw new IndexOutOfRangeException(), + }; + set + { + switch(index) + { + case 0: + Index1 = value; + break; + case 1: + Index2 = value; + break; + case 2: + Index3 = value; + break; + case 3: + Index4 = value; + break; + default: + throw new IndexOutOfRangeException(); + } + } + } + + + /// + /// Creates a new chunk volume quad. + /// + /// First vertex index. + /// Second vertex index. + /// Third vertex index. + /// Third vertex index. + /// First polygon attribute. + /// Second polygon attribute. + /// Third polygon attribute. + public ChunkVolumeQuad(ushort index1, ushort index2, ushort index3, ushort index4, ushort attribute1, ushort attribute2, ushort attribute3) + { + Index1 = index1; + Index2 = index2; + Index3 = index3; + Index4 = index4; + Attribute1 = attribute1; + Attribute2 = attribute2; + Attribute3 = attribute3; + } + + /// + /// Creates a new chunk volume quad. + /// + /// First vertex index. + /// Second vertex index. + /// Third vertex index. + /// Third vertex index. + public ChunkVolumeQuad(ushort index1, ushort index2, ushort index3, ushort index4) : this() + { + Index1 = index1; + Index2 = index2; + Index3 = index3; + Index4 = index4; + } + + + /// + public readonly ushort Size(int polygonAttributeCount) + { + return (ushort)(8u + (polygonAttributeCount * 2u)); + } + + /// + public readonly void Write(EndianStackWriter writer, int polygonAttributeCount) + { + writer.WriteUShort(Index1); + writer.WriteUShort(Index2); + writer.WriteUShort(Index3); + writer.WriteUShort(Index4); + + if(polygonAttributeCount > 0) + { + writer.WriteUShort(Attribute1); + if(polygonAttributeCount > 1) + { + writer.WriteUShort(Attribute2); + if(polygonAttributeCount > 0) + { + writer.WriteUShort(Attribute3); + } + } + } + } + + /// + /// Reads a chunk volume quad off an endian stack reader. Advances the address by the number of bytes read. + /// + /// Reader to read from. + /// Address at which to start reading. + /// Number of attributes to read for the quad. + /// The quad that was read. + public static ChunkVolumeQuad Read(EndianStackReader reader, ref uint address, int polygonAttributeCount) + { + ChunkVolumeQuad result = new( + reader.ReadUShort(address), + reader.ReadUShort(address + 2), + reader.ReadUShort(address + 4), + reader.ReadUShort(address + 6)); + + address += 8; + + if(polygonAttributeCount > 0) + { + result.Attribute1 = reader.ReadUShort(address); + address += 2; + + if(polygonAttributeCount > 1) + { + result.Attribute2 = reader.ReadUShort(address); + address += 2; + + if(polygonAttributeCount > 2) + { + result.Attribute3 = reader.ReadUShort(address); + address += 2; + } + } + } + + return result; + } + + + readonly object ICloneable.Clone() + { + return Clone(); + } + + /// + /// Creates a clone of the quad. + /// + /// The clonsed quad. + public readonly ChunkVolumeQuad Clone() + { + return this; + } + + /// + public override readonly string ToString() + { + return $"Quad - {{ {Index1}, {Index2}, {Index3}, {Index4} }}"; + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Chunk/Structs/ChunkVolumeStrip.cs b/src/SA3D.Modeling/Mesh/Chunk/Structs/ChunkVolumeStrip.cs new file mode 100644 index 0000000..0132067 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Chunk/Structs/ChunkVolumeStrip.cs @@ -0,0 +1,138 @@ +using SA3D.Common.IO; +using System; + +namespace SA3D.Modeling.Mesh.Chunk.Structs +{ + /// + /// Triangle strip polygon for volume chunks. + /// + public struct ChunkVolumeStrip : IChunkVolumePolygon + { + /// + /// Vertex indices. + /// + public ushort[] Indices { get; } + + /// + public readonly int NumIndices => Indices.Length; + + /// + /// Triangle attributes for each triangle. [triangle index, attribute index] + /// + public ushort[,] TriangleAttributes { get; } + + /// + /// Whether the triangles use reversed culling direction. + /// + public bool Reversed { get; set; } + + /// + public readonly ushort this[int index] + { + get => Indices[index]; + set => Indices[index] = value; + } + + private ChunkVolumeStrip(ushort[] indices, ushort[,] triangleAttributes, bool reversed) + { + Indices = indices; + TriangleAttributes = triangleAttributes; + Reversed = reversed; + } + + /// + /// Creates a new empty chunk volume strip. + /// + /// Number of vertex indices. + /// Whether the triangles use reversed culling direction. + public ChunkVolumeStrip(int size, bool reversed) + { + Indices = new ushort[size]; + TriangleAttributes = new ushort[size - 2, 3]; + Reversed = reversed; + } + + /// + /// Creates a new empty chunk volume strip. + /// + /// Vertex indices to use. + /// Whether the triangles use reversed culling direction. + public ChunkVolumeStrip(ushort[] indices, bool reversed) + { + Indices = indices; + TriangleAttributes = new ushort[Indices.Length - 2, 3]; + Reversed = reversed; + } + + /// + public readonly ushort Size(int polygonAttributeCount) + { + return (ushort)(2u + (2 * (Indices.Length + (TriangleAttributes.Length * polygonAttributeCount)))); + } + + /// + public readonly void Write(EndianStackWriter writer, int polygonAttributeCount) + { + short count = (short)Math.Min(Indices.Length, short.MaxValue); + writer.WriteShort(Reversed ? (short)-count : count); + + writer.WriteUShort(Indices[0]); + writer.WriteUShort(Indices[1]); + for(int i = 2; i < count; i++) + { + writer.WriteUShort(Indices[i]); + + for(int j = 0; j < polygonAttributeCount; j++) + { + writer.WriteUShort(TriangleAttributes[i - 2, j]); + } + } + } + + /// + /// Reads a chunk volume strip off an endian stack reader. Advances the address by the number of bytes read. + /// + /// Reader to read from. + /// Address at which to start reading. + /// Number of attributes to read for each triangle in the strip. + /// The strip that was read. + public static ChunkVolumeStrip Read(EndianStackReader reader, ref uint address, int polygonAttributeCount) + { + short header = reader.ReadShort(address); + ChunkVolumeStrip result = new(Math.Abs(header), header < 0); + address += 2; + + result.Indices[0] = reader.ReadUShort(address); + result.Indices[1] = reader.ReadUShort(address += 2); + + for(int i = 2; i < result.Indices.Length; i++) + { + result.Indices[i] = reader.ReadUShort(address += 2); + + for(int j = 0; j < polygonAttributeCount; j++) + { + result.TriangleAttributes[i - 2, j] = reader.ReadUShort(address += 2); + } + } + + return result; + } + + readonly object ICloneable.Clone() + { + return Clone(); + } + + /// + /// Returns a deep clone of the chunk volume strip. + /// + /// The cloned strip. + public readonly ChunkVolumeStrip Clone() + { + return new( + (ushort[])Indices.Clone(), + (ushort[,])TriangleAttributes.Clone(), + Reversed); + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Chunk/Structs/ChunkVolumeTriangle.cs b/src/SA3D.Modeling/Mesh/Chunk/Structs/ChunkVolumeTriangle.cs new file mode 100644 index 0000000..d357318 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Chunk/Structs/ChunkVolumeTriangle.cs @@ -0,0 +1,195 @@ +using SA3D.Common.IO; +using System; + +namespace SA3D.Modeling.Mesh.Chunk.Structs +{ + /// + /// Triangle polygon for volume chunks. + /// + public struct ChunkVolumeTriangle : IChunkVolumePolygon + { + /// + public readonly int NumIndices => 3; + + + /// + /// First vertex index. + /// + public ushort Index1 { get; set; } + + /// + /// Second vertex index. + /// + public ushort Index2 { get; set; } + + /// + /// Third vertex index. + /// + public ushort Index3 { get; set; } + + + /// + /// First polygon attribute. + /// + public ushort Attribute1 { get; set; } + + /// + /// Second polygon attribute. + /// + public ushort Attribute2 { get; set; } + + /// + /// Third polygon attribute. + /// + public ushort Attribute3 { get; set; } + + + /// + public ushort this[int index] + { + readonly get => index switch + { + 0 => Index1, + 1 => Index2, + 2 => Index3, + _ => throw new IndexOutOfRangeException(), + }; + set + { + switch(index) + { + case 0: + Index1 = value; + break; + case 1: + Index2 = value; + break; + case 2: + Index3 = value; + break; + default: + throw new IndexOutOfRangeException(); + } + } + } + + + /// + /// Creates a new chunk volume triangle. + /// + /// First vertex index. + /// Second vertex index. + /// Third vertex index. + /// First polygon attribute. + /// Second polygon attribute. + /// Third polygon attribute. + public ChunkVolumeTriangle(ushort index1, ushort index2, ushort index3, ushort attribute1, ushort attribute2, ushort attribute3) + { + Index1 = index1; + Index2 = index2; + Index3 = index3; + Attribute1 = attribute1; + Attribute2 = attribute2; + Attribute3 = attribute3; + } + + /// + /// Creates a new chunk volume triangle. + /// + /// First vertex index. + /// Second vertex index. + /// Third vertex index. + public ChunkVolumeTriangle(ushort index1, ushort index2, ushort index3) : this() + { + Index1 = index1; + Index2 = index2; + Index3 = index3; + } + + + /// + public readonly ushort Size(int polygonAttributeCount) + { + return (ushort)(6u + (polygonAttributeCount * 2u)); + } + + /// + public readonly void Write(EndianStackWriter writer, int polygonAttributeCount) + { + writer.WriteUShort(Index1); + writer.WriteUShort(Index2); + writer.WriteUShort(Index3); + + if(polygonAttributeCount > 0) + { + writer.WriteUShort(Attribute1); + if(polygonAttributeCount > 1) + { + writer.WriteUShort(Attribute2); + if(polygonAttributeCount > 0) + { + writer.WriteUShort(Attribute3); + } + } + } + } + + /// + /// Reads a chunk volume triangle off an endian stack reader. Advances the address by the number of bytes read. + /// + /// Reader to read from. + /// Address at which to start reading. + /// Number of attributes to read for the triangle. + /// The triangle that was read. + public static ChunkVolumeTriangle Read(EndianStackReader reader, ref uint address, int polygonAttributeCount) + { + ChunkVolumeTriangle result = new( + reader.ReadUShort(address), + reader.ReadUShort(address + 2), + reader.ReadUShort(address + 4)); + + address += 6; + + if(polygonAttributeCount > 0) + { + result.Attribute1 = reader.ReadUShort(address); + address += 2; + + if(polygonAttributeCount > 1) + { + result.Attribute2 = reader.ReadUShort(address); + address += 2; + + if(polygonAttributeCount > 2) + { + result.Attribute3 = reader.ReadUShort(address); + address += 2; + } + } + } + + return result; + } + + + readonly object ICloneable.Clone() + { + return Clone(); + } + + /// + /// Creates a clone of the triangle. + /// + /// The clonsed triangle. + public readonly ChunkVolumeTriangle Clone() + { + return this; + } + + /// + public override readonly string ToString() + { + return $"Triangle - {{ {Index1}, {Index2}, {Index3} }}"; + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Chunk/Structs/IChunkVolumePolygon.cs b/src/SA3D.Modeling/Mesh/Chunk/Structs/IChunkVolumePolygon.cs new file mode 100644 index 0000000..2d3d08b --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Chunk/Structs/IChunkVolumePolygon.cs @@ -0,0 +1,37 @@ +using SA3D.Common.IO; +using System; + +namespace SA3D.Modeling.Mesh.Chunk.Structs +{ + /// + /// Chunk volume polygon interface. + /// + public interface IChunkVolumePolygon : ICloneable + { + /// + /// Number of indices in the polygon. + /// + public int NumIndices { get; } + + /// + /// Access and set vertex indices of the polygon. + /// + /// The index of the corner. + /// The vertex index. + public ushort this[int index] { get; set; } + + /// + /// Calculates the size of the polygon in bytes. + /// + /// Number of attributes for every polygon. + /// The size of the polygon in bytes + public ushort Size(int polygonAttributeCount); + + /// + /// Write the polygon to an endian stack writer. + /// + /// The writer to write to. + /// Number of attributes for every polygon to write. + public abstract void Write(EndianStackWriter writer, int polygonAttributeCount); + } +} diff --git a/src/SA3D.Modeling/Mesh/Chunk/VertexChunk.cs b/src/SA3D.Modeling/Mesh/Chunk/VertexChunk.cs new file mode 100644 index 0000000..482f291 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Chunk/VertexChunk.cs @@ -0,0 +1,393 @@ +using SA3D.Common.IO; +using SA3D.Modeling.Mesh.Chunk.Structs; +using SA3D.Modeling.Structs; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace SA3D.Modeling.Mesh.Chunk +{ + /// + /// Set of vertex data of a chunk model + /// + public class VertexChunk : ICloneable + { + /// + /// Type of vertex chunk. + /// + public VertexChunkType Type { get; } + + /// + /// Various attributes. + /// + public byte Attributes { get; } + + /// + /// Determines how vertices are applied to the vertex cache. + /// + public WeightStatus WeightStatus => (WeightStatus)(Attributes & 3); + + /// + /// Offset that gets added to every index in the vertices. + /// + public ushort IndexOffset { get; set; } + + /// + /// Whether the chunk has weighted vertex data. + /// + public bool HasWeight => Type.CheckHasWeights(); + + /// + /// Whether the vertices contain normals. + /// + public bool HasNormals => Type.CheckHasNormal(); + + /// + /// Whether the vertices contain diffuse colors. + /// + public bool HasDiffuseColors => Type.CheckHasDiffuseColor(); + + /// + /// Whether the vertices contain specular colors. + /// + public bool HasSpecularColors => Type.CheckHasSpecularColor(); + + /// + /// Vertices of the chunk + /// + public ChunkVertex[] Vertices { get; } + + + /// + /// Creates a new Vertex chunk. + /// + /// Vertex chunk type. + /// Attributes of the chunk. + /// Index offset for all vertices. + /// Vertex data. + public VertexChunk(VertexChunkType type, byte attributes, ushort indexOffset, ChunkVertex[] vertices) + { + if(!Enum.IsDefined(type) || type is VertexChunkType.End or VertexChunkType.Null) + { + throw new ArgumentException($"Vertex chunk type is invalid: {type}", nameof(type)); + } + + Type = type; + Attributes = attributes; + IndexOffset = indexOffset; + Vertices = vertices; + } + + /// + /// Creates a new Vertex chunk with all relevant data + /// + /// Vertex chunk type. + /// Determines how vertices are applied to the vertex cache. + /// Index offset for all vertices. + /// Vertex data. + public VertexChunk(VertexChunkType type, WeightStatus weightstatus, ushort indexOffset, ChunkVertex[] vertices) + : this(type, (byte)weightstatus, indexOffset, vertices) { } + + + /// + /// Writes a vertex chunk to an endian stack writer. Splits it up into multiple chunks if necessary. + /// + /// The writer to write to. + public void Write(EndianStackWriter writer) + { + if(Vertices.Length > short.MaxValue) + { + throw new InvalidOperationException($"Vertex count ({Vertices.Length}) exceeds maximum vertex count (32767)"); + } + + ushort vertSize = Type.GetIntegerSize(); + ushort vertexLimitPerChunk = (ushort)((ushort.MaxValue - 1) / vertSize); // -1 because header2 also counts as part of the size, which is always there + ChunkVertex[] remainingVerts = (ChunkVertex[])Vertices.Clone(); + uint header1Base = (uint)Type | (uint)(Attributes << 8); + ushort offset = IndexOffset; + + bool hasNormal = Type.CheckHasNormal(); + bool vec4 = Type.CheckIsVec4(); + bool normal32 = Type.CheckIsNormal32(); + + while(remainingVerts.Length > 0) + { + ushort vertCount = ushort.Min((ushort)remainingVerts.Length, vertexLimitPerChunk); + ushort size = (ushort)((vertCount * vertSize) + 1); + + writer.WriteUInt(header1Base | (uint)(size << 16)); + writer.WriteUInt(offset | (uint)(vertCount << 16)); + + for(int i = 0; i < vertCount; i++) + { + ChunkVertex vtx = remainingVerts[i]; + writer.WriteVector3(vtx.Position); + if(vec4) + { + writer.WriteFloat(1.0f); + } + + if(hasNormal) + { + if(normal32) + { + ushort x = (ushort)Math.Round((vtx.Normal.X + 1) * 0x3FF); + ushort y = (ushort)Math.Round((vtx.Normal.Y + 1) * 0x3FF); + ushort z = (ushort)Math.Round((vtx.Normal.Z + 1) * 0x3FF); + + uint composed = (uint)((x << 20) | (y << 10) | z); + writer.WriteUInt(composed); + } + else + { + writer.WriteVector3(vtx.Normal); + if(vec4) + { + writer.WriteFloat(0.0f); + } + } + } + + switch(Type) + { + case VertexChunkType.Diffuse: + case VertexChunkType.NormalDiffuse: + case VertexChunkType.Normal32Diffuse: + writer.WriteColor(vtx.Diffuse, ColorIOType.ARGB8_32); + break; + case VertexChunkType.DiffuseSpecular5: + case VertexChunkType.NormalDiffuseSpecular5: + writer.WriteColor(vtx.Diffuse, ColorIOType.RGB565); + writer.WriteColor(vtx.Specular, ColorIOType.RGB565); + break; + case VertexChunkType.DiffuseSpecular4: + case VertexChunkType.NormalDiffuseSpecular4: + writer.WriteColor(vtx.Diffuse, ColorIOType.ARGB4); + writer.WriteColor(vtx.Specular, ColorIOType.RGB565); + break; + case VertexChunkType.Intensity: + case VertexChunkType.NormalIntensity: + writer.WriteUShort((ushort)Math.Round(vtx.Diffuse.GetLuminance() * ushort.MaxValue)); + writer.WriteUShort((ushort)Math.Round(vtx.Specular.GetLuminance() * ushort.MaxValue)); + break; + case VertexChunkType.Attributes: + case VertexChunkType.UserAttributes: + case VertexChunkType.NormalAttributes: + case VertexChunkType.NormalUserAttributes: + case VertexChunkType.Normal32UserAttributes: + writer.WriteUInt(vtx.Attributes); + break; + case VertexChunkType.Blank: + case VertexChunkType.BlankVec4: + case VertexChunkType.Normal: + case VertexChunkType.NormalVec4: + case VertexChunkType.Normal32: + break; + case VertexChunkType.Null: + case VertexChunkType.End: + default: + throw new InvalidOperationException(); // cant be reached + } + } + + remainingVerts = remainingVerts.Skip(vertCount).ToArray(); + if(!Type.CheckHasWeights()) + { + offset += vertCount; + } + } + } + + /// + /// Writes an array of vertex chunks to an endian stack writer. Includes NULL and END chunks. + /// + /// The writer to write to. + /// Chunks to write. + /// The address at wich the chunks were written + public static uint WriteArray(EndianStackWriter writer, IEnumerable chunks) + { + uint result = writer.PointerPosition; + + foreach(VertexChunk? cnk in chunks) + { + if(cnk == null) + { + writer.WriteEmpty(8); + } + else + { + cnk.Write(writer); + } + } + + // end chunk + writer.WriteUInt(0xFF); + writer.WriteEmpty(4); + + return result; + } + + /// + /// Reads a vertex chunk off an endian stack reader. + /// + /// The reader to read from. + /// Address at which to start reading. + /// The vertex chunk that was read. + public static VertexChunk Read(EndianStackReader reader, ref uint address) + { + uint header1 = reader.ReadUInt(address); + byte attribs = (byte)((header1 >> 8) & 0xFF); + VertexChunkType type = (VertexChunkType)(header1 & 0xFF); + + if(!Enum.IsDefined(type) || type is VertexChunkType.End or VertexChunkType.Null) + { + throw new FormatException($"Vertex chunk type is invalid: {type}"); + } + + uint header2 = reader.ReadUInt(address + 4); + ushort indexOffset = (ushort)(header2 & 0xFFFF); + ChunkVertex[] vertices = new ChunkVertex[(ushort)(header2 >> 16)]; + + address += 8; + + uint vec4 = type.CheckIsVec4() ? 4u : 0u; + bool hasNormal = type.CheckHasNormal(); + bool normal32 = type.CheckIsNormal32(); + + for(int i = 0; i < vertices.Length; i++) + { + ChunkVertex vtx = new(reader.ReadVector3(ref address), Color.ColorWhite, Color.ColorWhite); + address += vec4; + + if(!hasNormal) + { + vtx.Normal = Vector3.UnitY; + } + else if(normal32) + { + const float componentFactor = 1f / ushort.MaxValue; + + uint composed = reader.ReadUInt(address); + ushort x = (ushort)((composed >> 20) & 0x3FF); + ushort y = (ushort)((composed >> 10) & 0x3FF); + ushort z = (ushort)(composed & 0x3FF); + + vtx.Normal = new Vector3( + (x * componentFactor) - 1f, + (y * componentFactor) - 1f, + (z * componentFactor) - 1f); + + address += 4; + } + else + { + vtx.Normal = reader.ReadVector3(ref address); + address += vec4; + } + + switch(type) + { + case VertexChunkType.Diffuse: + case VertexChunkType.NormalDiffuse: + case VertexChunkType.Normal32Diffuse: + vtx.Diffuse = reader.ReadColor(ref address, ColorIOType.ARGB8_32); + break; + case VertexChunkType.DiffuseSpecular5: + case VertexChunkType.NormalDiffuseSpecular5: + vtx.Diffuse = reader.ReadColor(ref address, ColorIOType.RGB565); + vtx.Specular = reader.ReadColor(ref address, ColorIOType.RGB565); + break; + case VertexChunkType.DiffuseSpecular4: + case VertexChunkType.NormalDiffuseSpecular4: + vtx.Diffuse = reader.ReadColor(ref address, ColorIOType.ARGB4); + vtx.Specular = reader.ReadColor(ref address, ColorIOType.RGB565); + break; + case VertexChunkType.Intensity: + case VertexChunkType.NormalIntensity: + byte diffuseIntensity = (byte)(reader.ReadUShort(address) >> 16); + byte specularIntensity = (byte)(reader.ReadUShort(address + 2) >> 16); + + vtx.Diffuse = new(diffuseIntensity, diffuseIntensity, diffuseIntensity); + vtx.Specular = new(specularIntensity, specularIntensity, specularIntensity); + address += 4; + break; + case VertexChunkType.Attributes: + case VertexChunkType.UserAttributes: + case VertexChunkType.NormalAttributes: + case VertexChunkType.NormalUserAttributes: + case VertexChunkType.Normal32UserAttributes: + vtx.Attributes = reader.ReadUInt(address); + address += 4; + break; + case VertexChunkType.Blank: + case VertexChunkType.BlankVec4: + case VertexChunkType.Normal: + case VertexChunkType.NormalVec4: + case VertexChunkType.Normal32: + break; + case VertexChunkType.Null: + case VertexChunkType.End: + default: + throw new InvalidOperationException(); // cant be reached + } + + vertices[i] = vtx; + } + + return new VertexChunk(type, attribs, indexOffset, vertices); + } + + /// + /// Reads an array of chunk (respects NULL and END chunks). + /// + /// The reader to read form. + /// Addres at which to start reading. + /// The read vertex chunks. + public static VertexChunk?[] ReadArray(EndianStackReader reader, uint address) + { + List result = new(); + + VertexChunkType readType() + { + return (VertexChunkType)(reader.ReadUInt(address) & 0xFF); + } + + for(VertexChunkType type = readType(); type != VertexChunkType.End; type = readType()) + { + if(type == VertexChunkType.Null) + { + result.Add(null); + address += 8; + continue; + } + + result.Add(Read(reader, ref address)); + } + + return result.ToArray(); + } + + + object ICloneable.Clone() + { + return Clone(); + } + + /// + /// Creates a deep clone of the vertex chunk. + /// + /// + public VertexChunk Clone() + { + return new VertexChunk(Type, Attributes, IndexOffset, (ChunkVertex[])Vertices.Clone()); + } + + /// + public override string ToString() + { + return $"{Type}, {WeightStatus}, {IndexOffset} : [{Vertices.Length}]"; + } + } +} + diff --git a/src/SA3D.Modeling/Mesh/Chunk/VertexChunkType.cs b/src/SA3D.Modeling/Mesh/Chunk/VertexChunkType.cs new file mode 100644 index 0000000..e1a9f61 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Chunk/VertexChunkType.cs @@ -0,0 +1,113 @@ +namespace SA3D.Modeling.Mesh.Chunk +{ + /// + /// Types of vertex chunks. + /// + public enum VertexChunkType : byte + { + /// + /// Null chunk. + /// + Null = 0, + + /// + /// Position only; Uses GPU ready 4 component vector. + /// + BlankVec4 = ChunkTypeExtensions._vertex + 0, + + /// + /// Contains: Normals; Uses GPU ready 4 component vector. + /// + NormalVec4 = ChunkTypeExtensions._vertex + 1, + + /// + /// Position only. + /// + Blank = ChunkTypeExtensions._vertex + 2, + + /// + /// Contains: Diffuse colors (BGRA8). + /// + Diffuse = ChunkTypeExtensions._vertex + 3, + + /// + /// Contains: User defined attributes. + /// + UserAttributes = ChunkTypeExtensions._vertex + 4, + + /// + /// Contains: System defined attributes. + /// + Attributes = ChunkTypeExtensions._vertex + 5, + + /// + /// Contains: Diffuse colors (RGB565), Specular colors (RGB565). + /// + DiffuseSpecular5 = ChunkTypeExtensions._vertex + 6, + + /// + /// Contains: Diffuse colors (RGB4444), Specular colors (RGB565). + /// + DiffuseSpecular4 = ChunkTypeExtensions._vertex + 7, + + /// + /// Contains: Diffuse intensity (16-bit), Specular intensity (16-bit). + /// + Intensity = ChunkTypeExtensions._vertex + 8, + + /// + /// Contains: Normals. + /// + Normal = ChunkTypeExtensions._vertex + 9, + + /// + /// Contains: Normals, Normal, Diffuse colors (BGRA32). + /// + NormalDiffuse = ChunkTypeExtensions._vertex + 10, + + /// + /// Contains: Normals, User defined attributes. + /// + NormalUserAttributes = ChunkTypeExtensions._vertex + 11, + + /// + /// Contains: Normals, System defined attributes. + /// + NormalAttributes = ChunkTypeExtensions._vertex + 12, + + /// + /// Contains: Normals, Diffuse colors (RGB565), Specular colors (RGB565). + /// + NormalDiffuseSpecular5 = ChunkTypeExtensions._vertex + 13, + + /// + /// Contains: Normals, Diffuse colors (RGB4444), Specular colors (RGB565). + /// + NormalDiffuseSpecular4 = ChunkTypeExtensions._vertex + 14, + + /// + /// Contains: Normals, Diffuse intensity (16-bit), Specular intensity (16-bit). + /// + NormalIntensity = ChunkTypeExtensions._vertex + 15, + + /// + /// Contains: 32 Bit vertex normals (first 2 bits unused, each channel takes 10). + /// + Normal32 = ChunkTypeExtensions._vertex + 16, + + /// + /// Contains: 32 Bit vertex normals (first 2 bits unused, each channel takes 10), Diffuse color (BGRA32). + /// + Normal32Diffuse = ChunkTypeExtensions._vertex + 17, + + /// + /// Contains: 32 Bit vertex normals (first 2 bits unused, each channel takes 10), user attributes. + /// + Normal32UserAttributes = ChunkTypeExtensions._vertex + 18, + + /// + /// End marker chunk. + /// + End = 255 + } +} diff --git a/src/SA3D.Modeling/Mesh/Chunk/WeightStatus.cs b/src/SA3D.Modeling/Mesh/Chunk/WeightStatus.cs new file mode 100644 index 0000000..3127210 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Chunk/WeightStatus.cs @@ -0,0 +1,23 @@ +namespace SA3D.Modeling.Mesh.Chunk +{ + /// + /// Vertex chunk weight status. + /// + public enum WeightStatus + { + /// + /// Start of a weighted model (replaces cached vertices). + /// + Start, + + /// + /// Middle of a weighted model (adds onto cached vertices). + /// + Middle, + + /// + /// End of a weighted model (adds onto cached vertices and normalizes them). + /// + End + } +} diff --git a/src/SA3D.Modeling/Mesh/Converters/BasicConverter.cs b/src/SA3D.Modeling/Mesh/Converters/BasicConverter.cs new file mode 100644 index 0000000..6202acd --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Converters/BasicConverter.cs @@ -0,0 +1,417 @@ +using SA3D.Common; +using SA3D.Common.Lookup; +using SA3D.Modeling.Mesh.Basic; +using SA3D.Modeling.Mesh.Basic.Polygon; +using SA3D.Modeling.Mesh.Buffer; +using SA3D.Modeling.Mesh.Weighted; +using SA3D.Modeling.ObjectData; +using SA3D.Modeling.Strippify; +using SA3D.Modeling.Structs; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace SA3D.Modeling.Mesh.Converters +{ + /// + /// Provides buffer conversion methods for BASIC + /// + internal static class BasicConverter + { + #region Convert To Basic + + private static BasicMaterial ConvertToBasicMaterial(BufferMaterial mat) + { + return new(BasicMaterial.DefaultValues) + { + DiffuseColor = mat.Diffuse, + SpecularColor = mat.Specular, + SpecularExponent = mat.SpecularExponent, + TextureID = mat.TextureIndex, + FilterMode = mat.TextureFiltering, + MipmapDistanceMultiplier = mat.MipmapDistanceMultiplier, + SuperSample = mat.AnisotropicFiltering, + ClampU = mat.ClampU, + ClampV = mat.ClampV, + MirrorU = mat.MirrorU, + MirrorV = mat.MirrorV, + UseAlpha = mat.UseAlpha, + SourceAlpha = mat.SourceBlendMode, + DestinationAlpha = mat.DestinationBlendmode, + DoubleSided = !mat.BackfaceCulling, + + IgnoreLighting = mat.NoLighting, + IgnoreSpecular = mat.NoSpecular, + UseTexture = mat.UseTexture, + EnvironmentMap = mat.NormalMapping + }; + } + + private static void ConvertToBasicStrips( + BufferCorner[][] strips, + bool[] reversedStrips, + out IBasicPolygon[] polygons, + out Vector2[] texcoords, + out Color[] colors) + { + polygons = new IBasicPolygon[strips.Length]; + + int cornerCount = strips.Sum(x => x.Length); + texcoords = new Vector2[cornerCount]; + colors = new Color[cornerCount]; + + int absoluteIndex = 0; + + for(int i = 0; i < strips.Length; i++) + { + BufferCorner[] strip = strips[i]; + + BasicMultiPolygon polygon = new((ushort)strip.Length, reversedStrips[i]); + + for(int j = 0; j < strip.Length; j++, absoluteIndex++) + { + BufferCorner corner = strip[j]; + + polygon.Indices[j] = corner.VertexIndex; + colors[absoluteIndex] = corner.Color; + texcoords[absoluteIndex] = corner.Texcoord; + } + + polygons[i] = polygon; + } + } + + private static void ConvertToBasicTriangles( + BufferCorner[] corners, + out IBasicPolygon[] polygons, + out Vector2[] texcoords, + out Color[] colors) + { + polygons = new IBasicPolygon[corners.Length / 3]; + texcoords = new Vector2[corners.Length]; + colors = new Color[corners.Length]; + + int absoluteIndex = 0; + + for(int i = 0; i < polygons.Length; i++) + { + BufferCorner corner1 = corners[absoluteIndex]; + colors[absoluteIndex] = corner1.Color; + texcoords[absoluteIndex] = corner1.Texcoord; + absoluteIndex++; + + BufferCorner corner2 = corners[absoluteIndex]; + colors[absoluteIndex] = corner2.Color; + texcoords[absoluteIndex] = corner2.Texcoord; + absoluteIndex++; + + BufferCorner corner3 = corners[absoluteIndex]; + colors[absoluteIndex] = corner3.Color; + texcoords[absoluteIndex] = corner3.Texcoord; + absoluteIndex++; + + polygons[i] = new BasicTriangle(corner1.VertexIndex, corner2.VertexIndex, corner3.VertexIndex); + } + } + + private static BasicMesh ConvertToBasicMesh(BufferCorner[] bCorners, bool hasColors, int index, string identifier) + { + BufferCorner[][] strips = TriangleStrippifier.Global.StrippifyNoDegen(bCorners, out bool[] reversed); + + int triangleByteLength = bCorners.Length * 2; + int stripByteLength = (strips.Length + strips.Sum(x => x.Length)) * 2; + + BasicPolygonType type; + IBasicPolygon[] polygons; + Vector2[] texcoords; + Color[] colors; + + if(stripByteLength < triangleByteLength) + { + type = BasicPolygonType.TriangleStrips; + ConvertToBasicStrips(strips, reversed, out polygons, out texcoords, out colors); + } + else + { + type = BasicPolygonType.Triangles; + ConvertToBasicTriangles(bCorners, out polygons, out texcoords, out colors); + } + + bool hasTexcoords = texcoords.Any(x => x != default); + + BasicMesh basicmesh = new(type, polygons, (ushort)index, false, hasColors, hasTexcoords); + + if(hasColors) + { + basicmesh.Colors = new LabeledArray("vcolor_" + index + "_" + identifier, colors); + } + + if(hasTexcoords) + { + basicmesh.Texcoords = new LabeledArray("uv_" + index + "_" + identifier, texcoords); + } + + return basicmesh; + } + + private static BasicAttach OptimizeBasicVertices(BasicAttach attach) + { + PositionNormal[] vertices = new PositionNormal[attach.Positions.Length]; + + for(int i = 0; i < vertices.Length; i++) + { + vertices[i] = new(attach.Positions[i], attach.Normals[i]); + } + + if(!DistinctMap.TryCreateDistinctMap(vertices, out DistinctMap distinctMap)) + { + return attach; + } + + LabeledArray positions = new(attach.Positions.Label, distinctMap.Map!.Length); + LabeledArray normals = new(attach.Normals.Label, positions.Length); + + for(int i = 0; i < positions.Length; i++) + { + PositionNormal pn = distinctMap.Values[i]; + positions[i] = pn.position; + normals[i] = pn.normal; + } + + foreach(BasicMesh mesh in attach.Meshes) + { + foreach(IBasicPolygon polygon in mesh.Polygons) + { + for(int i = 0; i < polygon.NumIndices; i++) + { + polygon[i] = distinctMap[polygon[i]]; + } + } + } + + return new BasicAttach(positions, normals, attach.Meshes, attach.Materials); + } + + public static void ConvertWeightedToBasic( + Node model, + WeightedMesh[] meshData, + bool optimize, + bool ignoreWeights) + { + if(meshData.Any(x => x.IsWeighted) && !ignoreWeights) + { + throw new FormatException("Model is weighted, cannot convert to BASIC format!"); + } + + Node[] nodes = model.GetTreeNodes(); + BasicAttach?[] attaches = new BasicAttach[nodes.Length]; + + meshData = WeightedMesh.MergeAtRoots(meshData); + + foreach(WeightedMesh weightedAttach in meshData) + { + Vector3[] positions = new Vector3[weightedAttach.Vertices.Length]; + Vector3[] normals = new Vector3[positions.Length]; + string identifier = StringExtensions.GenerateIdentifier(); + + for(int i = 0; i < positions.Length; i++) + { + WeightedVertex vtx = weightedAttach.Vertices[i]; + + positions[i] = vtx.Position; + normals[i] = vtx.Normal; + } + + // putting together polygons + BasicMesh[] meshes = new BasicMesh[weightedAttach.TriangleSets.Length]; + BasicMaterial[] materials = new BasicMaterial[meshes.Length]; + + for(int i = 0; i < meshes.Length; i++) + { + meshes[i] = ConvertToBasicMesh(weightedAttach.TriangleSets[i], weightedAttach.HasColors, i, identifier); + materials[i] = ConvertToBasicMaterial(weightedAttach.Materials[i]); + } + + BasicAttach result = new(positions, normals, meshes, materials); + + if(optimize) + { + result = OptimizeBasicVertices(result); + } + + result.Label = weightedAttach.Label ?? "BASIC_" + StringExtensions.GenerateIdentifier(); + + foreach(int index in weightedAttach.RootIndices) + { + attaches[index] = result; + } + } + + model.ClearAttachesFromTree(); + + // Linking the attaches to the nodes + for(int i = 0; i < nodes.Length; i++) + { + nodes[i].Attach = attaches[i]; + } + } + + #endregion + + #region Convert to Buffer + + private static BufferMaterial ConvertToBufferMaterial(BasicMaterial mat) + { + return new(BufferMaterial.DefaultValues) + { + Diffuse = mat.DiffuseColor, + Specular = mat.SpecularColor, + SpecularExponent = mat.SpecularExponent, + TextureIndex = mat.TextureID, + TextureFiltering = mat.FilterMode, + MipmapDistanceMultiplier = mat.MipmapDistanceMultiplier, + AnisotropicFiltering = mat.SuperSample, + ClampU = mat.ClampU, + ClampV = mat.ClampV, + MirrorU = mat.MirrorU, + MirrorV = mat.MirrorV, + UseAlpha = mat.UseAlpha, + SourceBlendMode = mat.SourceAlpha, + DestinationBlendmode = mat.DestinationAlpha, + BackfaceCulling = !mat.DoubleSided, + + NoLighting = mat.IgnoreLighting, + NoSpecular = mat.IgnoreSpecular, + UseTexture = mat.UseTexture, + NormalMapping = mat.EnvironmentMap + }; + } + + private static void ConvertPolygons(BasicMesh mesh, out BufferCorner[] corners, out uint[]? indexList) + { + if(mesh.PolygonType is BasicPolygonType.NPoly or BasicPolygonType.TriangleStrips) + { + indexList = null; + + IEnumerable polys = mesh.Polygons.Cast(); + + BufferCorner[][] strips = new BufferCorner[polys.Count()][]; + bool[] reversed = new bool[strips.Length]; + + int stripNum = 0; + int absoluteIndex = 0; + + foreach(BasicMultiPolygon poly in polys) + { + BufferCorner[] strip = new BufferCorner[poly.Indices.Length]; + + for(int i = 0; i < strip.Length; i++, absoluteIndex++) + { + strip[i] = new BufferCorner( + poly.Indices[i], + mesh.Colors?[absoluteIndex] ?? BufferMesh.DefaultColor, + mesh.Texcoords?[absoluteIndex] ?? Vector2.Zero); + } + + strips[stripNum] = strip; + reversed[stripNum] = poly.Reversed; + stripNum++; + } + + corners = TriangleStrippifier.JoinStrips(strips, reversed); + } + else + { + int absoluteIndex = 0; + corners = new BufferCorner[mesh.PolygonCornerCount]; + + foreach(IBasicPolygon triangle in mesh.Polygons) + { + foreach(ushort index in triangle) + { + corners[absoluteIndex] = new BufferCorner( + index, + mesh.Colors?[absoluteIndex] ?? BufferMesh.DefaultColor, + mesh.Texcoords?[absoluteIndex] ?? Vector2.Zero); + absoluteIndex++; + } + } + + if(mesh.PolygonType == BasicPolygonType.Quads) + { + indexList = new uint[mesh.Polygons.Length * 6]; + + for(uint i = 0, q = 0; i < corners.Length; i += 4, q += 6) + { + indexList[q] = i; + indexList[q + 1] = i + 1; + indexList[q + 2] = i + 2; + + indexList[q + 3] = i + 2; + indexList[q + 4] = i + 1; + indexList[q + 5] = i + 3; + } + } + else + { + indexList = null; + } + } + + } + + public static BufferMesh[] ConvertBasicToBuffer(BasicAttach attach, bool optimize) + { + BufferVertex[] verts = new BufferVertex[attach.Positions.Length]; + for(ushort i = 0; i < verts.Length; i++) + { + verts[i] = new BufferVertex(attach.Positions[i], attach.Normals[i], i); + } + + bool hasNormals = attach.Normals.Any(x => !x.Equals(BufferMesh.DefaultNormal)); + + List meshes = new(); + foreach(BasicMesh mesh in attach.Meshes) + { + // creating the material + BufferMaterial bMat = ConvertToBufferMaterial( + mesh.MaterialIndex < attach.Materials.Length + ? attach.Materials[mesh.MaterialIndex] + : BasicMaterial.DefaultValues); + + ConvertPolygons(mesh, out BufferCorner[] corners, out uint[]? indexList); + bool strippified = mesh.PolygonType is BasicPolygonType.TriangleStrips or BasicPolygonType.NPoly; + + // first mesh includes vertex data + BufferMesh bmesh = meshes.Count == 0 + ? new(verts, bMat, corners, indexList, strippified, false, hasNormals, mesh.Colors != null, 0, 0) + : new(bMat, corners, indexList, strippified, mesh.Colors != null, 0); + + if(optimize) + { + bmesh.OptimizePolygons(); + } + + meshes.Add(bmesh); + } + + return optimize ? BufferMesh.Optimize(meshes) : meshes.ToArray(); + } + + /// + /// Generates Buffer meshes for all attaches in the model + /// + /// The tip of the model hierarchy to convert + /// Whether the buffer model should be optimized + public static void BufferBasicModel(Node model, bool optimize = true) + { + foreach(Attach atc in model.GetTreeAttaches()) + { + atc.MeshData = ConvertBasicToBuffer((BasicAttach)atc, optimize); + } + } + + #endregion + } +} diff --git a/src/SA3D.Modeling/Mesh/Converters/ChunkConverter.cs b/src/SA3D.Modeling/Mesh/Converters/ChunkConverter.cs new file mode 100644 index 0000000..b9ad25c --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Converters/ChunkConverter.cs @@ -0,0 +1,902 @@ +using SA3D.Common; +using SA3D.Modeling.Mesh.Buffer; +using SA3D.Modeling.Mesh.Chunk; +using SA3D.Modeling.Mesh.Chunk.PolyChunks; +using SA3D.Modeling.Mesh.Chunk.Structs; +using SA3D.Modeling.Mesh.Weighted; +using SA3D.Modeling.ObjectData; +using SA3D.Modeling.Strippify; +using SA3D.Modeling.Structs; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Numerics; + +namespace SA3D.Modeling.Mesh.Converters +{ + internal static class ChunkConverter + { + private readonly struct ChunkResult : IOffsetableAttachResult + { + public string Label { get; } + public int VertexCount { get; } + public bool Weighted { get; } + public int[] AttachIndices { get; } + public ChunkAttach[] Attaches { get; } + + Attach[] IOffsetableAttachResult.Attaches => Attaches; + + public ChunkResult(string label, int vertexCount, bool weighted, int[] attachIndices, ChunkAttach[] attaches) + { + Label = label; + VertexCount = vertexCount; + Weighted = weighted; + AttachIndices = attachIndices; + Attaches = attaches; + } + + public void ModifyVertexOffset(int offset) + { + foreach(ChunkAttach attach in Attaches) + { + if(attach.VertexChunks != null) + { + foreach(VertexChunk vtx in attach.VertexChunks.OfType()) + { + vtx.IndexOffset += (ushort)offset; + } + } + + if(attach.PolyChunks != null) + { + foreach(StripChunk stripChunk in attach.PolyChunks.OfType()) + { + foreach(ChunkStrip strip in stripChunk.Strips) + { + for(int i = 0; i < strip.Corners.Length; i++) + { + strip.Corners[i].Index += (ushort)offset; + } + } + } + } + } + } + } + + private class OffsettableChunkConverter : OffsetableAttachConverter + { + private readonly struct IndexedWeightVertex + { + public readonly int index; + public readonly WeightedVertex vertex; + + public IndexedWeightVertex(int index, WeightedVertex vertex) + { + this.index = index; + this.vertex = vertex; + } + + public override string ToString() + { + return $"{index} {vertex}"; + } + } + + private readonly struct BinaryWeightColorVertex : IEquatable + { + public readonly int nodeIndex; + public readonly Vector3 position; + public readonly Color color; + + public BinaryWeightColorVertex(int nodeIndex, Vector3 position, Color color) + { + this.nodeIndex = nodeIndex; + this.position = position; + this.color = color; + } + + public override bool Equals(object? obj) + { + return obj is BinaryWeightColorVertex vertex && + nodeIndex == vertex.nodeIndex && + position.Equals(vertex.position) && + color.Equals(vertex.color); + } + + bool IEquatable.Equals(BinaryWeightColorVertex other) + { + return Equals(other); + } + + public override int GetHashCode() + { + return HashCode.Combine(nodeIndex, position, color); + } + } + + + private static ChunkResult ConvertWeightedBinaryColored(WeightedMesh wba) + { + List vertices = new(); + ChunkCorner[][] cornerSets = new ChunkCorner[wba.TriangleSets.Length][]; + + // Get every vertex per corner + for(int i = 0; i < wba.TriangleSets.Length; i++) + { + BufferCorner[] bufferCorners = wba.TriangleSets[i]; + ChunkCorner[] corners = new ChunkCorner[bufferCorners.Length]; + for(int j = 0; j < bufferCorners.Length; j++) + { + BufferCorner bc = bufferCorners[j]; + corners[j] = new() + { + Index = (ushort)vertices.Count, + Texcoord = bc.Texcoord + }; + + WeightedVertex vertex = wba.Vertices[bc.VertexIndex]; + vertices.Add(new(vertex.GetMaxWeightIndex(), vertex.Position, bc.Color)); + } + + cornerSets[i] = corners; + } + + // first, get rid of all duplicate vertices + DistinctMap distinctVerts = vertices.CreateDistinctMap(); + (int index, BinaryWeightColorVertex vert)[] sortedVertices = new (int index, BinaryWeightColorVertex)[distinctVerts.Values.Count]; + + for(int i = 0; i < sortedVertices.Length; i++) + { + sortedVertices[i] = (i, distinctVerts.Values[i]); + } + + // now sort the vertices by node index + sortedVertices = sortedVertices.OrderBy(x => x.vert.nodeIndex).ToArray(); + + // Create a vertex chunk per node index + List<(int nodeIndex, VertexChunk chunk)> vertexChunks = new(); + + int currentNodeIndex = -1; + List chunkVertices = new(); + ushort currentVertexOffset = 0; + int[] sortedVertMap = new int[sortedVertices.Length]; + for(int i = 0; i < sortedVertices.Length; i++) + { + (int index, BinaryWeightColorVertex vert) vert = sortedVertices[i]; + if(vert.vert.nodeIndex != currentNodeIndex) + { + if(chunkVertices.Count > 0) + { + vertexChunks.Add((currentNodeIndex, new( + VertexChunkType.Diffuse, + WeightStatus.Start, + currentVertexOffset, + chunkVertices.ToArray()))); + } + + currentVertexOffset = (ushort)i; + chunkVertices.Clear(); + currentNodeIndex = vert.vert.nodeIndex; + } + + chunkVertices.Add(new(vert.vert.position, vert.vert.color, Color.ColorWhite)); + sortedVertMap[vert.index] = i; + } + + vertexChunks.Add((currentNodeIndex, new( + VertexChunkType.Diffuse, + WeightStatus.Start, + currentVertexOffset, + chunkVertices.ToArray()))); + + // get the poly chunks + List polyChunks = new(); + for(int i = 0; i < cornerSets.Length; i++) + { + ChunkCorner[] corners = cornerSets[i]; + for(int j = 0; j < corners.Length; j++) + { + int index = distinctVerts[corners[j].Index]; + corners[j].Index = (ushort)sortedVertMap[index]; + } + + polyChunks.AddRange(CreateStripChunk(corners, wba.Materials[i], wba.WriteSpecular)); + } + + // assemble the attaches + List nodeAttachIndices = new(); + List attaches = new(); + + for(int i = 0; i < vertexChunks.Count - 1; i++) + { + (int nodeIndex, VertexChunk chunks) = vertexChunks[i]; + nodeAttachIndices.Add(nodeIndex); + attaches.Add(new(new[] { chunks }, null)); + } + + (int lastNodeindex, VertexChunk lastVertexChunk) = vertexChunks[^1]; + nodeAttachIndices.Add(lastNodeindex); + attaches.Add(new(new[] { lastVertexChunk }, polyChunks.ToArray())); + + return new( + wba.Label ?? "CHUNK_" + StringExtensions.GenerateIdentifier(), + sortedVertices.Length, + true, + nodeAttachIndices.ToArray(), + attaches.ToArray()); + } + + private static ChunkResult ConvertWeighted(WeightedMesh wba) + { + + List singleWeights = new(); + List multiWeights = new(); + + for(int i = 0; i < wba.Vertices.Length; i++) + { + WeightedVertex vtx = wba.Vertices[i]; + int weightCount = vtx.GetWeightCount(); + if(weightCount == 0) + { + throw new InvalidDataException("Vertex has no specified weights"); + } + else if(weightCount == 1) + { + singleWeights.Add(new(i, vtx)); + } + else + { + multiWeights.Add(new(i, vtx)); + } + } + + singleWeights = singleWeights.OrderBy(x => x.vertex.GetFirstWeightIndex()).ThenBy(x => x.index).ToList(); + int multiWeightOffset = singleWeights.Count; + + int[] firstWeightIndices = multiWeights.Select(x => x.vertex.GetFirstWeightIndex()).ToArray(); + int[] lastWeightIndices = multiWeights.Select(x => x.vertex.GetLastWeightIndex()).ToArray(); + + // grouping the vertices together by node + List<(int nodeIndex, VertexChunk[] chunks)> vertexChunks = new(); + + foreach(int nodeIndex in wba.DependingNodeIndices.Order()) + { + List chunks = new(); + + // find out if any singleWeights belong to the node index + int singleWeightIndexOffset = 0; + List singleWeightVerts = new(); + for(int i = 0; i < singleWeights.Count; i++) + { + WeightedVertex vert = singleWeights[i].vertex; + bool contains = vert.Weights![nodeIndex] > 0f; + if(contains) + { + if(singleWeightVerts.Count == 0) + { + singleWeightIndexOffset = i; + } + + Vector3 pos = new(vert.Position.X, vert.Position.Y, vert.Position.Z); + singleWeightVerts.Add(new(pos, vert.Normal, (ushort)i, 1f)); + } + + if(!contains && singleWeightVerts.Count > 0) + { + break; + } + } + + if(singleWeightVerts.Count > 0) + { + chunks.Add( + new VertexChunk( + VertexChunkType.Normal, + WeightStatus.Start, + (ushort)singleWeightIndexOffset, + singleWeightVerts.ToArray())); + } + + // now the ones with weights. we differentiate between + // those that initiate and those that continue + List initWeightsVerts = new(); + List continueWeightsVerts = new(); + List endWeightsVerts = new(); + + for(int i = 0; i < multiWeights.Count; i++) + { + WeightedVertex vert = multiWeights[i].vertex; + + float weight = vert.Weights![nodeIndex]; + if(weight == 0f) + { + continue; + } + + ChunkVertex chunkVert = new( + vert.Position, + vert.Normal, + (ushort)(i + multiWeightOffset), + weight); + + if(firstWeightIndices[i] == nodeIndex) + { + initWeightsVerts.Add(chunkVert); + } + else if(lastWeightIndices[i] == nodeIndex) + { + endWeightsVerts.Add(chunkVert); + } + else + { + continueWeightsVerts.Add(chunkVert); + } + } + + if(initWeightsVerts.Count > 0) + { + chunks.Add( + new VertexChunk( + VertexChunkType.NormalAttributes, + WeightStatus.Start, 0, + initWeightsVerts.ToArray())); + } + + if(continueWeightsVerts.Count > 0) + { + chunks.Add( + new VertexChunk( + VertexChunkType.NormalAttributes, + WeightStatus.Middle, 0, + continueWeightsVerts.ToArray())); + } + + if(endWeightsVerts.Count > 0) + { + chunks.Add( + new VertexChunk( + VertexChunkType.NormalAttributes, + WeightStatus.End, 0, + endWeightsVerts.ToArray())); + } + + vertexChunks.Add((nodeIndex, chunks.ToArray())); + } + + // mapping the indices for the polygons + ushort[] indexMap = new ushort[wba.Vertices.Length]; + for(int i = 0; i < singleWeights.Count; i++) + { + indexMap[singleWeights[i].index] = (ushort)i; + } + + for(int i = 0; i < multiWeights.Count; i++) + { + indexMap[multiWeights[i].index] = (ushort)(i + multiWeightOffset); + } + + // assemble the polygon chunks + List polyChunks = new(); + for(int i = 0; i < wba.TriangleSets.Length; i++) + { + // mapping the triangles to the chunk format + BufferCorner[] bufferCorners = wba.TriangleSets[i]; + ChunkCorner[] corners = new ChunkCorner[bufferCorners.Length]; + for(int j = 0; j < bufferCorners.Length; j++) + { + BufferCorner bc = bufferCorners[j]; + corners[j] = new() + { + Index = indexMap[bc.VertexIndex], + Texcoord = bc.Texcoord + }; + } + + polyChunks.AddRange(CreateStripChunk(corners, wba.Materials[i], wba.WriteSpecular)); + } + + // assemble the attaches + List nodeAttachIndices = new(); + List attaches = new(); + + for(int i = 0; i < vertexChunks.Count - 1; i++) + { + (int nodeIndex, VertexChunk[] chunks) = vertexChunks[i]; + nodeAttachIndices.Add(nodeIndex); + attaches.Add(new(chunks, null)); + } + + (int lastNodeindex, VertexChunk[] lastVertexChunk) = vertexChunks[^1]; + nodeAttachIndices.Add(lastNodeindex); + attaches.Add(new(lastVertexChunk, polyChunks.ToArray())); + + return new( + wba.Label ?? "CHUNK_" + StringExtensions.GenerateIdentifier(), + wba.Vertices.Length, + true, + nodeAttachIndices.ToArray(), + attaches.ToArray()); + } + + protected override ChunkResult ConvertWeighted(WeightedMesh wba, bool optimize) + { + bool binaryWeighted = wba.HasColors; + if(binaryWeighted && !wba.ForceVertexColors) + { + foreach(WeightedVertex vertex in wba.Vertices) + { + if(vertex.IsWeighted()) + { + binaryWeighted = false; + break; + } + } + } + + if(binaryWeighted) + { + return ConvertWeightedBinaryColored(wba); + } + else + { + return ConvertWeighted(wba); + } + } + + protected override ChunkResult ConvertWeightless(WeightedMesh wba, bool optimize) + { + ChunkVertex[] vertices; + ChunkCorner[][] cornerSets = new ChunkCorner[wba.TriangleSets.Length][]; + + VertexChunkType type; + if(wba.HasColors) + { + type = VertexChunkType.Diffuse; + List colorVertices = new(); + for(int i = 0; i < wba.TriangleSets.Length; i++) + { + BufferCorner[] bufferCorners = wba.TriangleSets[i]; + ChunkCorner[] corners = new ChunkCorner[bufferCorners.Length]; + for(int j = 0; j < bufferCorners.Length; j++) + { + BufferCorner bc = bufferCorners[j]; + corners[j] = new() + { + Index = (ushort)colorVertices.Count, + Texcoord = bc.Texcoord + }; + + WeightedVertex vertex = wba.Vertices[bc.VertexIndex]; + colorVertices.Add(new(vertex.Position, bc.Color, Color.ColorWhite)); + } + + cornerSets[i] = corners; + } + + // first, get rid of all duplicate vertices + if(colorVertices.TryCreateDistinctMap(out DistinctMap distinctVerts)) + { + for(int i = 0; i < cornerSets.Length; i++) + { + ChunkCorner[] corners = cornerSets[i]; + for(int j = 0; j < corners.Length; j++) + { + corners[j].Index = distinctVerts[corners[j].Index]; + } + } + } + + vertices = distinctVerts.ValueArray; + } + else + { + type = VertexChunkType.Normal; + vertices = new ChunkVertex[wba.Vertices.Length]; + // converting the vertices 1:1, with normal information + for(int i = 0; i < wba.Vertices.Length; i++) + { + WeightedVertex vert = wba.Vertices[i]; + Vector3 position = new(vert.Position.X, vert.Position.Y, vert.Position.Z); + vertices[i] = new(position, vert.Normal); + } + + for(int i = 0; i < wba.TriangleSets.Length; i++) + { + BufferCorner[] bufferCorners = wba.TriangleSets[i]; + ChunkCorner[] corners = new ChunkCorner[bufferCorners.Length]; + for(int j = 0; j < bufferCorners.Length; j++) + { + BufferCorner bc = bufferCorners[j]; + corners[j] = new() + { + Index = bc.VertexIndex, + Texcoord = bc.Texcoord + }; + } + + cornerSets[i] = corners; + } + } + + VertexChunk vtxChunk = new(type, WeightStatus.Start, 0, vertices); + List polyChunks = new(); + for(int i = 0; i < cornerSets.Length; i++) + { + polyChunks.AddRange(CreateStripChunk(cornerSets[i], wba.Materials[i], wba.WriteSpecular)); + } + + return new( + wba.Label ?? "CHUNK_" + StringExtensions.GenerateIdentifier(), + vertices.Length, + false, + wba.RootIndices.ToArray(), + new[] { + new ChunkAttach( + new[] { vtxChunk }, + polyChunks.ToArray()) + }); + } + + private static PolyChunk[] CreateStripChunk(ChunkCorner[] corners, BufferMaterial material, bool writeSpecular) + { + ChunkCorner[][] stripCorners = TriangleStrippifier.Global.StrippifyNoDegen(corners, out bool[] reversed); + ChunkStrip[] strips = new ChunkStrip[stripCorners.Length]; + + for(int i = 0; i < strips.Length; i++) + { + strips[i] = new(stripCorners[i], reversed[i]); + } + + bool hasUV = material.UseTexture && !material.NormalMapping; + PolyChunkType stripType = hasUV ? PolyChunkType.Strip_Tex : PolyChunkType.Strip_Blank; + + StripChunk stripchunk = new(stripType, strips, 0) + { + FlatShading = material.Flat, + IgnoreAmbient = material.NoAmbient, + IgnoreLight = material.NoLighting, + IgnoreSpecular = material.NoSpecular, + EnvironmentMapping = material.NormalMapping, + UseAlpha = material.UseAlpha, + DoubleSide = !material.BackfaceCulling + }; + + TextureChunk textureChunk = new() + { + ClampU = material.ClampU, + ClampV = material.ClampV, + MirrorU = material.MirrorU, + MirrorV = material.MirrorV, + FilterMode = material.TextureFiltering, + SuperSample = material.AnisotropicFiltering, + TextureID = (ushort)material.TextureIndex + }; + + MaterialChunk materialChunk = new() + { + SourceAlpha = material.SourceBlendMode, + DestinationAlpha = material.DestinationBlendmode, + Diffuse = material.Diffuse, + Ambient = material.Ambient, + }; + + if(writeSpecular) + { + materialChunk.Specular = material.Specular; + materialChunk.SpecularExponent = (byte)material.SpecularExponent; + } + + return new PolyChunk[] { materialChunk, textureChunk, stripchunk }; + } + + protected override void CorrectSpace(Attach attach, Matrix4x4 vertexMatrix) + { + ChunkAttach chunkAttach = (ChunkAttach)attach; + if(chunkAttach.VertexChunks == null) + { + return; + } + + foreach(VertexChunk? vtxChunk in chunkAttach.VertexChunks) + { + if(vtxChunk == null) + { + continue; + } + + for(int j = 0; j < vtxChunk.Vertices.Length; j++) + { + ChunkVertex vertex = vtxChunk.Vertices[j]; + + vertex.Position = Vector3.Transform(vertex.Position, vertexMatrix); + vertex.Normal = Vector3.TransformNormal(vertex.Normal, vertexMatrix); + + vtxChunk.Vertices[j] = vertex; + } + } + } + + protected override ChunkResult WeightedClone(string label, int vertexCount, int[] attachIndices, Attach[] attaches) + { + return new( + label, + vertexCount, + true, + attachIndices, + attaches.Cast().ToArray()); + } + + protected override Attach CombineAttaches(List attaches, string label) + { + List vertexChunks = new(); + List polyChunks = new(); + + foreach(ChunkAttach atc in attaches.Cast()) + { + if(atc.VertexChunks != null) + { + vertexChunks.AddRange(atc.VertexChunks); + } + + if(atc.PolyChunks != null) + { + polyChunks.AddRange(atc.PolyChunks); + } + } + + return new ChunkAttach(vertexChunks.ToArray(), polyChunks.ToArray()) { Label = label }; + } + } + + public static void ConvertWeightedToChunk(Node model, WeightedMesh[] meshData, bool optimize) + { + new OffsettableChunkConverter().Convert(model, meshData, optimize); + } + + #region Convert to Buffer + + private static BufferCorner[] ConvertStripChunk(StripChunk chunk, ChunkVertex[] vertexCache) + { + bool hasColor = chunk.HasColors; + + BufferCorner[][] bufferStrips = new BufferCorner[chunk.Strips.Length][]; + bool[] reversed = new bool[bufferStrips.Length]; + + for(int i = 0; i < chunk.Strips.Length; i++) + { + ChunkStrip strip = chunk.Strips[i]; + BufferCorner[] bufferStrip = new BufferCorner[strip.Corners.Length]; + + for(int j = 0; j < strip.Corners.Length; j++) + { + ChunkCorner corner = strip.Corners[j]; + + Color color = hasColor + ? corner.Color + : vertexCache[corner.Index].Diffuse; + + bufferStrip[j] = new(corner.Index, color, corner.Texcoord); + } + + bufferStrips[i] = bufferStrip; + reversed[i] = strip.Reversed; + } + + return TriangleStrippifier.JoinStrips(bufferStrips, reversed); + } + + public static void BufferChunkModel(Node model, bool optimize) + { + BufferMaterial material = BufferMaterial.DefaultValues; + + ChunkVertex[] vertexCache = new ChunkVertex[0x10000]; + Dictionary activeChunks = GetActivePolyChunks(model); + + foreach(ChunkAttach atc in model.GetTreeAttachEnumerable().OfType()) + { + List meshes = new(); + + BufferVertex[]? vertices = null; + bool continueWeight = false; + bool hasVertexNormals = false; + bool hasVertexColors = false; + ushort vertexWriteOffset = 0; + + if(atc.VertexChunks != null) + { + for(int i = 0; i < atc.VertexChunks.Length; i++) + { + VertexChunk? cnk = atc.VertexChunks[i]; + + if(cnk == null) + { + continue; + } + + List vertexList = new(); + if(!cnk.HasWeight) + { + for(int j = 0; j < cnk.Vertices.Length; j++) + { + ChunkVertex vtx = cnk.Vertices[j]; + vertexCache[j + cnk.IndexOffset] = vtx; + vertexList.Add(new BufferVertex(vtx.Position, vtx.Normal, (ushort)j)); + } + } + else + { + + for(int j = 0; j < cnk.Vertices.Length; j++) + { + ChunkVertex vtx = cnk.Vertices[j]; + vertexCache[vtx.Index + cnk.IndexOffset] = vtx; + vertexList.Add(new BufferVertex(vtx.Position, vtx.Normal, vtx.Index, vtx.Weight)); + } + } + + vertices = vertexList.ToArray(); + continueWeight = cnk.WeightStatus != WeightStatus.Start; + hasVertexNormals = cnk.HasNormals; + hasVertexColors |= cnk.HasDiffuseColors; + vertexWriteOffset = cnk.IndexOffset; + + // if not last + if(i < atc.VertexChunks.Length - 1) + { + meshes.Add(new BufferMesh(vertices, continueWeight, hasVertexNormals, vertexWriteOffset)); + } + } + } + + if(activeChunks.TryGetValue(atc, out PolyChunk?[]? polyChunks)) + { + foreach(PolyChunk? chunk in polyChunks) + { + switch(chunk) + { + case BlendAlphaChunk blendAlphaChunk: + material.SourceBlendMode = blendAlphaChunk.SourceAlpha; + material.DestinationBlendmode = blendAlphaChunk.DestinationAlpha; + break; + case MipmapDistanceMultiplierChunk mmdmChunk: + material.MipmapDistanceMultiplier = mmdmChunk.MipmapDistanceMultiplier; + break; + case SpecularExponentChunk specularExponentChunk: + material.SpecularExponent = specularExponentChunk.SpecularExponent; + break; + case TextureChunk textureChunk: + material.TextureIndex = textureChunk.TextureID; + material.MirrorU = textureChunk.MirrorU; + material.MirrorV = textureChunk.MirrorV; + material.ClampU = textureChunk.ClampU; + material.ClampV = textureChunk.ClampV; + material.AnisotropicFiltering = textureChunk.SuperSample; + material.TextureFiltering = textureChunk.FilterMode; + break; + case MaterialChunk materialChunk: + material.SourceBlendMode = materialChunk.SourceAlpha; + material.DestinationBlendmode = materialChunk.DestinationAlpha; + + if(materialChunk.Diffuse.HasValue) + { + material.Diffuse = materialChunk.Diffuse.Value; + } + + if(materialChunk.Ambient.HasValue) + { + material.Ambient = materialChunk.Ambient.Value; + } + + if(materialChunk.Specular.HasValue) + { + material.Specular = materialChunk.Specular.Value; + material.SpecularExponent = materialChunk.SpecularExponent; + } + + break; + case StripChunk stripChunk: + material.Flat = stripChunk.FlatShading; + material.NoAmbient = stripChunk.IgnoreAmbient; + material.NoLighting = stripChunk.IgnoreLight; + material.NoSpecular = stripChunk.IgnoreSpecular; + material.NormalMapping = stripChunk.EnvironmentMapping; + material.UseTexture = stripChunk.TexcoordCount > 0 || stripChunk.EnvironmentMapping; + material.UseAlpha = stripChunk.UseAlpha; + material.BackfaceCulling = !stripChunk.DoubleSide; + + BufferCorner[] corners = ConvertStripChunk(stripChunk, vertexCache); + + bool hasColor = stripChunk.HasColors || hasVertexColors; + + if(corners.Length > 0) + { + if(vertices != null) + { + meshes.Add(new BufferMesh(vertices, material, corners, null, true, continueWeight, hasVertexNormals, hasColor, vertexWriteOffset, 0)); + vertices = null; + } + else + { + meshes.Add(new BufferMesh(material, corners, null, true, hasColor, 0)); + } + } + + break; + default: + break; + } + } + + } + + if(vertices != null) + { + meshes.Add(new BufferMesh(vertices, continueWeight, hasVertexNormals, vertexWriteOffset)); + } + + atc.MeshData = optimize ? BufferMesh.Optimize(meshes) : meshes.ToArray(); + } + } + + #endregion + + public static Dictionary GetActivePolyChunks(Node model) + { + Dictionary result = new(); + List[] polyChunkCache = Array.Empty>(); + + foreach(ChunkAttach attach in model.GetTreeAttachEnumerable().OfType()) + { + if(attach.PolyChunks == null) + { + continue; + } + + List active = new(); + + int cacheID = -1; + foreach(PolyChunk? polyChunk in attach.PolyChunks) + { + switch(polyChunk) + { + case CacheListChunk cache: + cacheID = cache.List; + + if(polyChunkCache.Length <= cacheID) + { + Array.Resize(ref polyChunkCache, cacheID + 1); + } + + polyChunkCache[cacheID] = new List(); + break; + case DrawListChunk draw: + active.AddRange(polyChunkCache[draw.List]); + break; + default: + if(cacheID > -1) + { + polyChunkCache[cacheID].Add(polyChunk); + } + else + { + active.Add(polyChunk); + } + + break; + } + + } + + + if(active.Count > 0) + { + result.Add(attach, active.ToArray()); + } + } + + return result; + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Converters/FromWeightedConverter.cs b/src/SA3D.Modeling/Mesh/Converters/FromWeightedConverter.cs new file mode 100644 index 0000000..514d37e --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Converters/FromWeightedConverter.cs @@ -0,0 +1,227 @@ +using SA3D.Common; +using SA3D.Modeling.Mesh.Buffer; +using SA3D.Modeling.Mesh.Weighted; +using SA3D.Modeling.ObjectData; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace SA3D.Modeling.Mesh.Converters +{ + internal static class FromWeightedConverter + { + private readonly struct BufferResult : IOffsetableAttachResult + { + public string Label { get; } + public int VertexCount { get; } + public bool Weighted { get; } + public int[] AttachIndices { get; } + public Attach[] Attaches { get; } + + public BufferResult(string label, int vertexCount, bool weighted, int[] attachIndices, Attach[] attaches) + { + Label = label; + VertexCount = vertexCount; + Weighted = weighted; + AttachIndices = attachIndices; + Attaches = attaches; + } + + public void ModifyVertexOffset(int offset) + { + foreach(Attach atc in Attaches) + { + foreach(BufferMesh bm in atc.MeshData) + { + bm.VertexWriteOffset = (ushort)(bm.VertexWriteOffset + offset); + bm.VertexReadOffset = (ushort)(bm.VertexReadOffset + offset); + } + } + } + } + + private class OffsettableBufferConverter : OffsetableAttachConverter + { + protected override BufferResult ConvertWeighted(WeightedMesh wba, bool optimize) + { + List<(int nodeIndex, BufferMesh[])> meshSets = new(); + int[] weightInits = wba.Vertices.Select(x => x.GetFirstWeightIndex()).ToArray(); + + foreach(int nodeIndex in wba.DependingNodeIndices) + { + List initVerts = new(); + List continueVerts = new(); + + for(int i = 0; i < wba.Vertices.Length; i++) + { + WeightedVertex wVert = wba.Vertices[i]; + + float weight = wVert.Weights![nodeIndex]; + if(weight == 0) + { + continue; + } + + BufferVertex vert = new(wVert.Position, wVert.Normal, (ushort)i, weight); + + if(weightInits[i] == nodeIndex) + { + initVerts.Add(vert); + } + else + { + continueVerts.Add(vert); + } + } + + List vertexMeshes = new(); + + if(initVerts.Count > 0) + { + vertexMeshes.Add(new(initVerts.ToArray(), false, true, 0)); + } + + if(continueVerts.Count > 0) + { + vertexMeshes.Add(new(continueVerts.ToArray(), true, true, 0)); + } + + meshSets.Add((nodeIndex, vertexMeshes.ToArray())); + } + + BufferMesh[] polyMeshes = GetPolygonMeshes(wba); + + if(optimize) + { + foreach(BufferMesh polyMesh in polyMeshes) + { + polyMesh.OptimizePolygons(); + } + } + + int[] nodeIndices = new int[meshSets.Count]; + Attach[] attaches = new Attach[meshSets.Count]; + + for(int i = 0; i < meshSets.Count - 1; i++) + { + (int nodeIndex, BufferMesh[] vertexMeshes) = meshSets[i]; + nodeIndices[i] = nodeIndex; + attaches[i] = new(vertexMeshes); + } + + int lastIndex = meshSets.Count - 1; + (int lastNodeIndex, BufferMesh[] lastMeshes) = meshSets[lastIndex]; + nodeIndices[lastIndex] = lastNodeIndex; + + List meshes = new(); + meshes.AddRange(lastMeshes); + meshes.AddRange(polyMeshes); + attaches[lastIndex] = new(meshes.ToArray()); + + return new( + wba.Label ?? "BUFFER_" + StringExtensions.GenerateIdentifier(), + wba.Vertices.Length, + true, + nodeIndices, + attaches); + } + + protected override BufferResult ConvertWeightless(WeightedMesh wba, bool optimize) + { + List meshes = new(); + + BufferVertex[] vertices = new BufferVertex[wba.Vertices.Length]; + + for(int i = 0; i < vertices.Length; i++) + { + WeightedVertex wVert = wba.Vertices[i]; + vertices[i] = new(wVert.Position, wVert.Normal, (ushort)i); + } + + BufferMesh[] polygonMeshes = GetPolygonMeshes(wba); + + meshes.Add(new(vertices, false, true, 0)); + + meshes.AddRange(polygonMeshes); + + BufferMesh[] result = BufferMesh.Optimize(meshes); + + return new( + wba.Label ?? "BUFFER_" + StringExtensions.GenerateIdentifier(), + vertices.Length, + false, + wba.RootIndices.ToArray(), + new Attach[] { new(result) }); + } + + private static BufferMesh[] GetPolygonMeshes(WeightedMesh wba) + { + List result = new(); + + for(int i = 0; i < wba.TriangleSets.Length; i++) + { + BufferMesh mesh = new( + wba.Materials[i], + (BufferCorner[])wba.TriangleSets[i].Clone(), + null, + false, + wba.HasColors, + 0); + + result.Add(mesh); + } + + return result.ToArray(); + } + + protected override void CorrectSpace(Attach attach, Matrix4x4 vertexMatrix) + { + foreach(BufferMesh mesh in attach.MeshData) + { + if(mesh.Vertices == null) + { + continue; + } + + for(int j = 0; j < mesh.Vertices.Length; j++) + { + BufferVertex vertex = mesh.Vertices[j]; + + vertex.Position = Vector3.Transform(vertex.Position, vertexMatrix); + vertex.Normal = Vector3.TransformNormal(vertex.Normal, vertexMatrix); + + mesh.Vertices[j] = vertex; + } + } + } + + protected override BufferResult WeightedClone(string label, int vertexCount, int[] attachIndices, Attach[] attaches) + { + return new( + label, + vertexCount, + true, + attachIndices, + attaches); + } + + protected override Attach CombineAttaches(List attaches, string label) + { + List meshes = new(); + + foreach(Attach atc in attaches) + { + meshes.AddRange(atc.MeshData); + } + + return new Attach(meshes.ToArray()) { Label = label }; + } + + } + + public static void Convert(Node model, WeightedMesh[] meshData, bool optimize) + { + new OffsettableBufferConverter().Convert(model, meshData, optimize); + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Converters/GCConverter.cs b/src/SA3D.Modeling/Mesh/Converters/GCConverter.cs new file mode 100644 index 0000000..e45bb37 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Converters/GCConverter.cs @@ -0,0 +1,584 @@ +using SA3D.Common; +using SA3D.Modeling.Mesh.Buffer; +using SA3D.Modeling.Mesh.Gamecube; +using SA3D.Modeling.Mesh.Gamecube.Enums; +using SA3D.Modeling.Mesh.Gamecube.Parameters; +using SA3D.Modeling.Mesh.Weighted; +using SA3D.Modeling.ObjectData; +using SA3D.Modeling.Structs; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace SA3D.Modeling.Mesh.Converters +{ + /// + /// Provides buffer conversion methods for GC + /// + internal static class GCConverter + { + public static void ConvertWeightedToGC(Node model, WeightedMesh[] meshData, bool optimize, bool ignoreWeights) + { + if(meshData.Any(x => x.IsWeighted) && !ignoreWeights) + { + throw new FormatException("Model is weighted, cannot convert to basic format!"); + } + + Node[] nodes = model.GetTreeNodes(); + GCAttach[] attaches = new GCAttach[nodes.Length]; + List rootlessAttaches = new(); + + meshData = WeightedMesh.MergeAtRoots(meshData); + + foreach(WeightedMesh weightedMesh in meshData) + { + Vector3[] positionData = new Vector3[weightedMesh.Vertices.Length]; + Vector3[] normalData = new Vector3[positionData.Length]; + + for(int i = 0; i < positionData.Length; i++) + { + WeightedVertex vtx = weightedMesh.Vertices[i]; + + positionData[i] = vtx.Position; + normalData[i] = vtx.Normal; + } + + // getting the corner information + int cornerCount = 0; + for(int i = 0; i < weightedMesh.TriangleSets.Length; i++) + { + cornerCount += weightedMesh.TriangleSets[i].Length; + } + + Vector2[] texcoordData = new Vector2[cornerCount]; + Color[] colorData = new Color[cornerCount]; + GCCorner[][] polygonData = new GCCorner[weightedMesh.TriangleSets.Length][]; + + ushort cornerIndex = 0; + for(int i = 0; i < polygonData.Length; i++) + { + BufferCorner[] bufferCorners = weightedMesh.TriangleSets[i]; + GCCorner[] meshCorners = new GCCorner[bufferCorners.Length]; + for(int j = 0; j < bufferCorners.Length; j++) + { + BufferCorner bcorner = bufferCorners[j]; + + texcoordData[cornerIndex] = bcorner.Texcoord; + colorData[cornerIndex] = bcorner.Color; + + meshCorners[j] = new GCCorner() + { + PositionIndex = bcorner.VertexIndex, + NormalIndex = bcorner.VertexIndex, + TexCoord0Index = cornerIndex, + Color0Index = cornerIndex + }; + + cornerIndex++; + } + + // correcting the culling order + for(int j = 0; j < meshCorners.Length; j += 3) + { + (meshCorners[j], meshCorners[j + 1]) = (meshCorners[j + 1], meshCorners[j]); + } + + polygonData[i] = meshCorners; + } + + bool hasUVs = texcoordData.Any(x => x != default); + + // Puttin together the vertex sets + GCVertexSet[] vertexData = new GCVertexSet[2 + (hasUVs ? 1 : 0)]; + + vertexData[0] = GCVertexSet.CreatePositionSet(positionData); + + vertexData[1] = weightedMesh.HasColors + ? GCVertexSet.CreateColor0Set(colorData) + : GCVertexSet.CreateNormalSet(normalData); + + if(hasUVs) + { + vertexData[2] = GCVertexSet.CreateTexcoord0Set(texcoordData); + } + + // stitching polygons together + GCMesh ProcessBufferMesh(GCCorner[] corners, BufferMaterial material, ref BufferMaterial activeMaterial, ref bool first) + { + // generating parameter info + List parameters = new(); + + if(first) + { + GCIndexFormat indexFormat = default; + + foreach(GCVertexSet set in vertexData) + { + parameters.Add(new GCVertexFormatParameter() + { + VertexType = set.Type, + VertexStructType = set.StructType, + VertexDataType = set.DataType + }); + + uint flag = 1u << ((int)set.Type * 2); + + // Mark as use vertex data + indexFormat |= (GCIndexFormat)(flag << 1); + + if(set.DataLength > 256) + { + // mark as large index + indexFormat |= (GCIndexFormat)flag; + } + } + + parameters.Add(new GCIndexFormatParameter() { IndexFormat = indexFormat }); + + activeMaterial = material; + + parameters.Add(new GCLightingParameter() + { + LightingAttributes = weightedMesh.HasColors + ? GCLightingParameter.DefaultColorParam + : GCLightingParameter.DefaultNormalParam, + ShadowStencil = material.GCShadowStencil + }); + + } + + if(first || activeMaterial.GCShadowStencil != material.GCShadowStencil) + { + parameters.Add(new GCLightingParameter() + { + LightingAttributes = weightedMesh.HasColors + ? GCLightingParameter.DefaultColorParam + : GCLightingParameter.DefaultNormalParam, + ShadowStencil = material.GCShadowStencil + }); + } + + if(first + || activeMaterial.SourceBlendMode != material.SourceBlendMode + || activeMaterial.DestinationBlendmode != material.DestinationBlendmode) + { + parameters.Add(new GCBlendAlphaParameter() + { + SourceAlpha = material.SourceBlendMode, + DestinationAlpha = material.DestinationBlendmode + }); + } + + if(first || activeMaterial.Ambient != material.Ambient) + { + parameters.Add(new GCAmbientColorParameter() + { + AmbientColor = material.Ambient + }); + } + + if(first + || activeMaterial.TextureIndex != material.TextureIndex + || activeMaterial.MirrorU != material.MirrorU + || activeMaterial.MirrorV != material.MirrorV + || activeMaterial.ClampU != material.ClampU + || activeMaterial.ClampV != material.ClampV) + { + GCTextureParameter texParam = new() + { + TextureID = (ushort)material.TextureIndex + }; + + if(!material.ClampU) + { + texParam.Tiling |= GCTileMode.RepeatU; + } + + if(!material.ClampV) + { + texParam.Tiling |= GCTileMode.RepeatV; + } + + if(material.MirrorU) + { + texParam.Tiling |= GCTileMode.MirrorU; + } + + if(material.MirrorV) + { + texParam.Tiling |= GCTileMode.MirrorV; + } + + parameters.Add(texParam); + } + + if(first) + { + parameters.Add(GCUnknownParameter.DefaultValues); + } + + if(first + || activeMaterial.GCTexCoordID != material.GCTexCoordID + || activeMaterial.GCTexCoordType != material.GCTexCoordType + || activeMaterial.GCTexCoordSource != material.GCTexCoordSource + || activeMaterial.GCMatrixID != material.GCMatrixID) + { + parameters.Add(new GCTexCoordParameter() + { + TexCoordID = material.GCTexCoordID, + TexCoordType = material.GCTexCoordType, + TexCoordSource = material.GCTexCoordSource, + MatrixID = material.GCMatrixID + }); + } + + activeMaterial = material; + first = false; + + List polygons = new(); + + // note: a single triangle polygon can only carry 0xFFFF corners, so about 22k tris + if(corners.Length > 0xFFFF) + { + int remainingLength = corners.Length; + int offset = 0; + while(remainingLength > 0) + { + GCCorner[] finalCorners = new GCCorner[Math.Max(0xFFFF, remainingLength)]; + Array.Copy(corners, offset, finalCorners, 0, finalCorners.Length); + offset += finalCorners.Length; + remainingLength -= finalCorners.Length; + + GCPolygon triangle = new(GCPolyType.Triangles, finalCorners); + polygons.Add(triangle); + } + } + else + { + polygons.Add(new(GCPolyType.Triangles, corners)); + } + + return new GCMesh(parameters.ToArray(), polygons.ToArray()); + } + + List opaqueMeshIndices = new(); + List translucentMeshIndices = new(); + + for(int i = 0; i < weightedMesh.Materials.Length; i++) + { + (weightedMesh.Materials[i].UseAlpha ? translucentMeshIndices : opaqueMeshIndices).Add(i); + } + + GCMesh[] ProcessBufferMeshes(List meshIndices) + { + BufferMaterial currentMaterial = default; + List result = new(); + bool first = true; + foreach(int index in meshIndices) + { + result.Add( + ProcessBufferMesh( + polygonData[index], + weightedMesh.Materials[index], + ref currentMaterial, + ref first)); + } + + return result.ToArray(); + } + + GCMesh[] opaqueMeshes = ProcessBufferMeshes(opaqueMeshIndices); + GCMesh[] translucentMeshes = ProcessBufferMeshes(translucentMeshIndices); + + GCAttach result = new(vertexData, opaqueMeshes, translucentMeshes) + { + Label = weightedMesh.Label ?? "GC_" + StringExtensions.GenerateIdentifier() + }; + + if(optimize) + { + result.OptimizeVertexData(); + result.OptimizePolygons(); + } + + result.RecalculateBounds(); + + foreach(int index in weightedMesh.RootIndices) + { + attaches[index] = result; + } + } + + model.ClearAttachesFromTree(); + + // Linking the attaches to the nodes + for(int i = 0; i < nodes.Length; i++) + { + nodes[i].Attach = attaches[i]; + } + } + + public static BufferMesh[] ConvertGCToBuffer(GCAttach attach, bool optimize) + { + List meshes = new(); + + DistinctMap? positions = null; + DistinctMap? normals = null; + DistinctMap? colors = null; + DistinctMap? uvs = null; + + + if(attach.VertexData.TryGetValue(GCVertexType.Position, out GCVertexSet? tmp)) + { + if(!optimize || !tmp.Vector3Data.TryCreateDistinctMap(out positions)) + { + positions = new(tmp.Vector3Data, null); + } + } + + if(attach.VertexData.TryGetValue(GCVertexType.Normal, out tmp)) + { + if(!optimize || !tmp.Vector3Data.TryCreateDistinctMap(out normals)) + { + normals = new(tmp.Vector3Data, null); + } + } + + if(attach.VertexData.TryGetValue(GCVertexType.TexCoord0, out tmp)) + { + if(!optimize || !tmp.Vector2Data.TryCreateDistinctMap(out uvs)) + { + uvs = new(tmp.Vector2Data, null); + } + } + + if(attach.VertexData.TryGetValue(GCVertexType.Color0, out tmp)) + { + if(!optimize || !tmp.ColorData.TryCreateDistinctMap(out colors)) + { + colors = new(tmp.ColorData, null); + } + } + + if(positions == null) + { + throw new NullReferenceException("Mandatory positions dont exit"); + } + + + + float uvFac = 1; + + List bufferVertices = new(); + + // if there are no normals, then we can already initialize the entire thing with all positions + Func getVertexIndex; + if(normals == null) + { + for(ushort i = 0; i < positions.Values.Count; i++) + { + bufferVertices.Add(new BufferVertex(positions.Values[i], i)); + } + + getVertexIndex = (pos, nrm) => positions[pos]; + } + else + { + Dictionary vertexIndices = new(); + getVertexIndex = (pos, nrm) => + { + pos = positions[pos]; + nrm = normals[nrm]; + + uint posnrmIndex = pos | ((uint)nrm << 16); + + if(!vertexIndices.TryGetValue(posnrmIndex, out ushort vtxIndex)) + { + vtxIndex = (ushort)bufferVertices.Count; + bufferVertices.Add(new(positions.Values[pos], normals.Values[nrm], vtxIndex)); + vertexIndices.Add(posnrmIndex, vtxIndex); + } + + return vtxIndex; + }; + } + + BufferMaterial material; + + BufferMesh ProcessMesh(GCMesh m) + { + // setting the material properties according to the parameters + foreach(IGCParameter param in m.Parameters) + { + switch(param) + { + case GCVertexFormatParameter vertexFormatParam: + if(vertexFormatParam.VertexType == GCVertexType.TexCoord0 + && (vertexFormatParam.Formatting & 0xF0) == 0) + { + uvFac = 1 << (vertexFormatParam.Formatting & 0x7); + if((vertexFormatParam.Formatting & 0x8) > 0) + { + uvFac = 1 / uvFac; + } + } + + break; + case GCBlendAlphaParameter blendAlphaParam: + material.SourceBlendMode = blendAlphaParam.SourceAlpha; + material.DestinationBlendmode = blendAlphaParam.DestinationAlpha; + break; + + case GCAmbientColorParameter ambientColorParam: + material.Ambient = ambientColorParam.AmbientColor; + break; + + case GCDiffuseColorParameter diffuseColorParam: + material.Ambient = diffuseColorParam.DiffuseColor; + break; + + case GCSpecularColorParameter specularColorParam: + material.Ambient = specularColorParam.SpecularColor; + break; + + case GCTextureParameter textureParam: + material.UseTexture = true; + material.TextureIndex = textureParam.TextureID; + material.ClampU = !textureParam.Tiling.HasFlag(GCTileMode.RepeatU); + material.ClampV = !textureParam.Tiling.HasFlag(GCTileMode.RepeatV); + material.MirrorU = textureParam.Tiling.HasFlag(GCTileMode.MirrorU); + material.MirrorV = textureParam.Tiling.HasFlag(GCTileMode.MirrorV); + break; + + case GCTexCoordParameter texcoordParam: + material.NormalMapping = + texcoordParam.TexCoordSource == + GCTexCoordSource.Normal; + + material.GCMatrixID = texcoordParam.MatrixID; + material.GCTexCoordID = texcoordParam.TexCoordID; + material.GCTexCoordSource = texcoordParam.TexCoordSource; + material.GCTexCoordType = texcoordParam.TexCoordType; + break; + + default: + break; + } + } + + // filtering out the double loops + List corners = new(); + List trianglelist = new(); + + foreach(GCPolygon p in m.Polygons) + { + // inverted culling is done manually in the gc strips, so we have to account for that + bool rev = p.Type != GCPolyType.TriangleStrip || p.Corners[0].PositionIndex != p.Corners[1].PositionIndex; + int offset = rev ? 0 : 1; + uint[] indices = new uint[p.Corners.Length - offset]; + + for(int i = offset; i < p.Corners.Length; i++) + { + GCCorner c = p.Corners[i]; + indices[i - offset] = (uint)corners.Count; + ushort vertexIndex = getVertexIndex(c.PositionIndex, c.NormalIndex); + + Vector2 uv = uvs?.GetValue(c.TexCoord0Index) ?? default; + uv *= uvFac; + Color color = colors?.GetValue(c.Color0Index) ?? Color.ColorWhite; + corners.Add(new BufferCorner(vertexIndex, color, uv)); + } + + // converting indices to triangles + if(p.Type == GCPolyType.Triangles) + { + for(int i = 0; i < indices.Length; i += 3) + { + (indices[i + 1], indices[i]) = (indices[i], indices[i + 1]); + } + + trianglelist.AddRange(indices); + + } + else if(p.Type == GCPolyType.TriangleStrip) + { + uint[] newIndices = new uint[(indices.Length - 2) * 3]; + for(int i = 2, triangleIndex = 0; i < indices.Length; i++, triangleIndex += 3) + { + if(!rev) + { + newIndices[triangleIndex] = indices[i - 2]; + newIndices[triangleIndex + 1] = indices[i - 1]; + } + else + { + newIndices[triangleIndex] = indices[i - 1]; + newIndices[triangleIndex + 1] = indices[i - 2]; + } + + newIndices[triangleIndex + 2] = indices[i]; + rev = !rev; + } + + trianglelist.AddRange(newIndices); + } + else + { + throw new Exception($"Primitive type {p.Type} not a valid triangle format"); + } + } + + return new(material, corners.ToArray(), trianglelist.ToArray(), false, colors != null, 0); + } + + material = new(BufferMaterial.DefaultValues) + { + NoSpecular = true, + NoLighting = colors != null + }; + + foreach(GCMesh m in attach.OpaqueMeshes) + { + meshes.Add(ProcessMesh(m)); + } + + material = new(BufferMaterial.DefaultValues) + { + NoSpecular = true, + NoLighting = colors != null, + UseAlpha = true, + BackfaceCulling = true + }; + + foreach(GCMesh m in attach.TransparentMeshes) + { + meshes.Add(ProcessMesh(m)); + } + + // inject the vertex information into the first mesh + BufferMesh vtxMesh = meshes[0]; + + meshes[0] = new( + bufferVertices.ToArray(), + vtxMesh.Material, + vtxMesh.Corners, + vtxMesh.IndexList, + vtxMesh.Strippified, + false, + normals != null, + vtxMesh.HasColors, + 0, 0); + + return optimize ? BufferMesh.Optimize(meshes) : meshes.ToArray(); + } + + public static void BufferGCModel(Node model, bool optimize) + { + foreach(GCAttach attach in model.GetTreeAttachEnumerable().OfType()) + { + attach.MeshData = ConvertGCToBuffer(attach, optimize); + } + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Converters/IOffsetableAttachResult.cs b/src/SA3D.Modeling/Mesh/Converters/IOffsetableAttachResult.cs new file mode 100644 index 0000000..8dcd4d8 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Converters/IOffsetableAttachResult.cs @@ -0,0 +1,94 @@ +using System.Collections.Generic; +using System.Linq; + +namespace SA3D.Modeling.Mesh.Converters +{ + internal interface IOffsetableAttachResult + { + public string Label { get; } + public int VertexCount { get; } + public bool Weighted { get; } + public int[] AttachIndices { get; } + public Attach[] Attaches { get; } + + public void ModifyVertexOffset(int offset); + + /// + /// Checks for any vertex overlaps in the models and sets their vertex offset accordingly + /// + public static void PlanVertexOffsets(T[] attaches) where T : IOffsetableAttachResult + { + if(attaches.Length == 0) + { + return; + } + + int nodeCount = attaches.Max(x => x.AttachIndices[^1]) + 1; + List<(int start, int end)>[] ranges = new List<(int start, int end)>[nodeCount]; + for(int i = 0; i < nodeCount; i++) + { + ranges[i] = new(); + } + + foreach(IOffsetableAttachResult cr in attaches) + { + int startNode = cr.AttachIndices[0]; + int endNode = cr.AttachIndices[^1]; + + // Map out the blocked regions + Dictionary blocked = new(); + for(int i = startNode; i <= endNode; i++) + { + foreach((int start, int end) r in ranges[i]) + { + if(blocked.TryGetValue(r.start, out int end)) + { + if(r.end > end) + { + blocked[r.start] = r.end; + } + } + else + { + blocked.Add(r.start, r.end); + } + } + } + + // find a free space in the blocked regions + int prevEnd = 0; + foreach(KeyValuePair v in blocked.OrderBy(x => x.Key)) + { + if(prevEnd > v.Key) + { + if(v.Value > prevEnd) + { + prevEnd = v.Value; + } + } + else if(v.Key - prevEnd >= cr.VertexCount) + { + break; + } + else + { + prevEnd = v.Value; + } + } + + // Block the region used by the current attach + for(int i = startNode; i <= endNode; i++) + { + ranges[i].Add((prevEnd, prevEnd + cr.VertexCount)); + } + + // adjust offset if needed + if(prevEnd > 0) + { + cr.ModifyVertexOffset(prevEnd); + } + } + } + + } +} diff --git a/src/SA3D.Modeling/Mesh/Converters/OffsetableAttachConverter.cs b/src/SA3D.Modeling/Mesh/Converters/OffsetableAttachConverter.cs new file mode 100644 index 0000000..f3820fc --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Converters/OffsetableAttachConverter.cs @@ -0,0 +1,113 @@ +using SA3D.Common; +using SA3D.Modeling.Mesh.Weighted; +using SA3D.Modeling.ObjectData; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace SA3D.Modeling.Mesh.Converters +{ + internal abstract class OffsetableAttachConverter where TResult : IOffsetableAttachResult + { + protected abstract TResult ConvertWeightless(WeightedMesh wba, bool optimize); + + protected abstract TResult ConvertWeighted(WeightedMesh wba, bool optimize); + + protected abstract void CorrectSpace(Attach attach, Matrix4x4 vertexMatrix); + + protected abstract TResult WeightedClone(string label, int vertexCount, int[] attachIndices, Attach[] attaches); + + protected abstract Attach CombineAttaches(List attaches, string label); + + public void Convert(Node model, WeightedMesh[] meshData, bool optimize) + { + List results = new(); + (Node node, Matrix4x4 worldMatrix)[] nodeMatrices = model.GetWorldMatrixTree(); + + foreach(WeightedMesh wba in meshData) + { + if(!wba.IsWeighted) + { + results.Add(ConvertWeightless(wba, optimize)); + } + else + { + TResult result = ConvertWeighted(wba, optimize); + + foreach(int rootIndex in wba.RootIndices) + { + int[] attachIndices = new int[result.AttachIndices.Length]; + Attach[] attaches = result.Attaches.ContentClone(); + + Matrix4x4 baseMatrix = nodeMatrices[rootIndex].worldMatrix; + + for(int i = 0; i < attachIndices.Length; i++) + { + int nodeIndex = result.AttachIndices[i] + rootIndex; + attachIndices[i] = nodeIndex; + + Matrix4x4 nodeMatrix = nodeMatrices[nodeIndex].worldMatrix; + Matrix4x4.Invert(nodeMatrix, out Matrix4x4 invNodeMatrix); + Matrix4x4 vertexMatrix = baseMatrix * invNodeMatrix; + + CorrectSpace(attaches[i], vertexMatrix); + } + + string label = result.Label; + if(wba.RootIndices.Count > 1) + { + label += "_" + rootIndex; + } + + results.Add(WeightedClone( + label, + result.VertexCount, + attachIndices, + attaches)); + } + } + } + + IOffsetableAttachResult.PlanVertexOffsets(results.ToArray()); + + List[] nodeAttaches = new List[nodeMatrices.Length]; + for(int i = 0; i < nodeAttaches.Length; i++) + { + nodeAttaches[i] = new(); + } + + foreach(TResult result in results) + { + for(int i = 0; i < result.AttachIndices.Length; i++) + { + nodeAttaches[result.AttachIndices[i]].Add(result.Attaches[i]); + + string attachLabel = result.Label; + if(result.Attaches.Length > 1) + { + attachLabel += "_" + i; + } + + result.Attaches[i].Label = attachLabel; + } + } + + model.ClearAttachesFromTree(); + + for(int i = 0; i < nodeAttaches.Length; i++) + { + List chunks = nodeAttaches[i]; + Node node = nodeMatrices[i].node; + if(chunks.Count == 1) + { + node.Attach = chunks[0]; + } + else + { + string combinedLabel = string.Join('_', chunks.Select(x => x.Label)); + node.Attach = CombineAttaches(chunks, combinedLabel); + } + } + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Converters/ToWeightedConverter.cs b/src/SA3D.Modeling/Mesh/Converters/ToWeightedConverter.cs new file mode 100644 index 0000000..f139842 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Converters/ToWeightedConverter.cs @@ -0,0 +1,510 @@ +using SA3D.Common; +using SA3D.Modeling.Mesh.Buffer; +using SA3D.Modeling.Mesh.Weighted; +using SA3D.Modeling.ObjectData; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace SA3D.Modeling.Mesh.Converters +{ + internal class ToWeightedConverter + { + private readonly Node[] _nodes; + + private readonly Dictionary _worldMatrices; + private readonly List _output; + + /// + /// Mesh index to node index. + ///
Basically used to ignore nodes with no meshdata. + ///
+ private int[] _meshNodeIndexMapping; + + /// + /// First index is vertex index, second is mesh index. + ///
Stores which meshes influence which vertices. + ///
+ private bool[,] _vertexIndexMapping; + + /// + /// Index of the mesh currently being processed. + /// + private int _currentMeshIndex; + + /// + /// Which vertices are used by the current meshes polygon. + /// + private readonly SortedSet _usedVertices; + + private readonly Dictionary _unweighted; + + private bool _finished; + + private ToWeightedConverter(Node model) + { + _nodes = model.GetTreeNodes(); + + _worldMatrices = model.GetWorldMatrixTreeLUT(); + _output = new(); + + _meshNodeIndexMapping = Array.Empty(); + _vertexIndexMapping = new bool[0, 0]; + + _currentMeshIndex = -1; + _usedVertices = new(); + + _unweighted = new(); + } + + private BufferMesh[] GetMeshData(int meshIndex) + { + int nodeIndex = _meshNodeIndexMapping[meshIndex]; + return _nodes[nodeIndex].Attach!.MeshData; + } + + private void Verify() + { + foreach(Node node in _nodes) + { + if(node.Attach == null) + { + continue; + } + + if(node.Attach.MeshData == null) + { + throw new FormatException("Not all attaches have meshdata! Please generate Meshdata before converting"); + } + } + } + + private void Setup() + { + List nodeIndexMeshMap = new(); + + for(int i = 0; i < _nodes.Length; i++) + { + Node node = _nodes[i]; + if(node.Attach == null || node.Attach.MeshData.Length == 0) + { + continue; + } + + nodeIndexMeshMap.Add(i); + } + + _meshNodeIndexMapping = nodeIndexMeshMap.ToArray(); + _vertexIndexMapping = new bool[0x10000, _meshNodeIndexMapping.Length]; + } + + private void MapVertices() + { + BufferMesh[] meshes = GetMeshData(_currentMeshIndex); + + _usedVertices.Clear(); + + foreach(BufferMesh bufferMesh in meshes) + { + if(bufferMesh.Vertices != null) + { + if(bufferMesh.ContinueWeight) + { + foreach(BufferVertex vtx in bufferMesh.Vertices) + { + if(vtx.Weight == 0) + { + continue; + } + + int vertexIndex = vtx.Index + bufferMesh.VertexWriteOffset; + _vertexIndexMapping[vertexIndex, _currentMeshIndex] = true; + } + } + else + { + foreach(BufferVertex vtx in bufferMesh.Vertices) + { + int index = vtx.Index + bufferMesh.VertexWriteOffset; + + Array.Clear(_vertexIndexMapping, index * _meshNodeIndexMapping.Length, _meshNodeIndexMapping.Length); + + if(vtx.Weight > 0) + { + _vertexIndexMapping[index, _currentMeshIndex] = true; + } + } + } + } + + if(bufferMesh.Corners != null) + { + foreach(BufferCorner corner in bufferMesh.Corners) + { + _usedVertices.Add((ushort)(corner.VertexIndex + bufferMesh.VertexReadOffset)); + } + } + } + } + + private int[] GetDependingMeshNodeIndices() + { + SortedSet result = new(); + + foreach(ushort vtxIndex in _usedVertices) + { + for(int i = 0; i < _meshNodeIndexMapping.Length; i++) + { + if(_vertexIndexMapping[vtxIndex, i]) + { + result.Add(i); + } + } + } + + return result.ToArray(); + } + + private int ComputeCommonNodeIndex(SortedSet dependingNodeIndices) + { + Dictionary parentIndices = new(); + for(int i = 0; i < _nodes.Length; i++) + { + parentIndices.Add(_nodes[i], 0); + } + + foreach(int i in dependingNodeIndices) + { + Node? node = _nodes[i]; + while(node != null) + { + parentIndices[node]++; + node = node.Parent; + } + + } + + int target = dependingNodeIndices.Count; + foreach(KeyValuePair t in parentIndices.Reverse()) + { + if(t.Value == target) + { + return Array.IndexOf(_nodes, t.Key); + } + } + + return 0; + } + + private WeightedVertex[] EvaluateNoWeightVertices(int meshNodeIndex) + { + BufferMesh[] meshes = GetMeshData(meshNodeIndex); + + ushort[] indices = _usedVertices.ToArray(); + BufferVertex[] sourceVertices = new BufferVertex[0x10000]; + + foreach(BufferMesh mesh in meshes) + { + if(mesh.Vertices == null) + { + continue; + } + + for(int i = 0; i < mesh.Vertices.Length; i++) + { + BufferVertex sourceVertex = mesh.Vertices[i]; + sourceVertices[sourceVertex.Index + mesh.VertexWriteOffset] = sourceVertex; + } + } + + WeightedVertex[] resultVertices = new WeightedVertex[indices.Length]; + + for(int i = 0; i < indices.Length; i++) + { + int vertexIndex = indices[i]; + BufferVertex sourceVertex = sourceVertices[vertexIndex]; + + WeightedVertex resultVert = new( + sourceVertex.Position, + sourceVertex.Normal); + + resultVertices[i] = resultVert; + } + + return resultVertices; + } + + /// Indices to meshes to use + /// Index to the node that the vertex data should be made relative to. + /// + private WeightedVertex[] EvaluateWeightVertices(int[] meshNodeIndices, int baseNodeIndex) + { + Matrix4x4 baseMatrix = _worldMatrices[_nodes[baseNodeIndex]]; + Matrix4x4.Invert(baseMatrix, out Matrix4x4 invbaseMatrix); + + int meshNodeCount = _meshNodeIndexMapping[meshNodeIndices[^1]] - baseNodeIndex + 1; + WeightedVertex[] vertexBuffer = new WeightedVertex[0x10000]; + + foreach(int meshNodeIndex in meshNodeIndices) + { + int nodeIndex = _meshNodeIndexMapping[meshNodeIndex]; + + Matrix4x4 vertexMatrix = Matrix4x4.Identity; + if(baseNodeIndex != nodeIndex) + { + Node meshNode = _nodes[nodeIndex]; + Matrix4x4 worldMatrix = _worldMatrices[meshNode]; + + vertexMatrix = worldMatrix * invbaseMatrix; + } + + BufferMesh[] meshes = GetMeshData(meshNodeIndex); + int weightIndex = nodeIndex - baseNodeIndex; + + foreach(BufferMesh bufferMesh in meshes) + { + if(bufferMesh.Vertices == null) + { + continue; + } + + foreach(BufferVertex vtx in bufferMesh.Vertices) + { + int index = vtx.Index + bufferMesh.VertexWriteOffset; + if(vtx.Weight == 0.0f) + { + if(!bufferMesh.ContinueWeight) + { + vertexBuffer[index] = new(default, default, meshNodeCount); + } + + continue; + } + + Vector3 pos = Vector3.Transform(vtx.Position, vertexMatrix) * vtx.Weight; + Vector3 nrm = Vector3.TransformNormal(vtx.Normal, vertexMatrix) * vtx.Weight; + + if(bufferMesh.ContinueWeight) + { + vertexBuffer[index].Position += pos; + vertexBuffer[index].Normal += nrm; + } + else + { + vertexBuffer[index] = new(pos, nrm, meshNodeCount); + } + + // Its possible that this mesh continues vertices from another model of which the + // init mesh was not included, so the weights array can be null. + float[]? weights = vertexBuffer[index].Weights; + if(weights != null) + { + weights[weightIndex] = vtx.Weight; + } + + } + } + } + + ushort[] indices = _usedVertices.ToArray(); + WeightedVertex[] resultVertices = new WeightedVertex[indices.Length]; + + for(int i = 0; i < indices.Length; i++) + { + int vertexIndex = indices[i]; + WeightedVertex vertex = vertexBuffer[vertexIndex]; + float sum = vertex.Weights?.Sum() ?? 1f; + + if(sum != 1) + { + vertex.Position /= sum; + vertex.Normal /= sum; + + for(int j = 0; j < vertex.Weights!.Length; j++) + { + vertex.Weights[j] /= sum; + } + } + + resultVertices[i] = vertex; + } + + + + return resultVertices; + } + + private ushort[] GetVertexIndexMap() + { + ushort[] indices = _usedVertices.ToArray(); + + ushort[] result = new ushort[indices[^1] + 1]; + for(ushort i = 0; i < indices.Length; i++) + { + result[indices[i]] = i; + } + + return result; + } + + private (BufferCorner[][] triangleSets, BufferMaterial[] materials, bool hasColors) EvaluatePolygons() + { + BufferMesh[] meshes = GetMeshData(_currentMeshIndex); + + ushort[] vertexIndexMap = GetVertexIndexMap(); + + bool hasColors = false; + List triangleSets = new(); + List materials = new(); + + foreach(BufferMesh bufferMesh in meshes) + { + if(bufferMesh.Corners == null) + { + continue; + } + + hasColors |= bufferMesh.HasColors; + + BufferCorner[] corners = bufferMesh.GetCornerTriangleList(); + + for(int i = 0; i < corners.Length; i++) + { + corners[i].VertexIndex = vertexIndexMap[corners[i].VertexIndex + bufferMesh.VertexReadOffset]; + } + + triangleSets.Add(corners); + materials.Add(bufferMesh.Material); + } + + return (triangleSets.ToArray(), materials.ToArray(), hasColors); + } + + private void EvaluateMesh() + { + int[] dependingMeshNodeIndices = GetDependingMeshNodeIndices(); + int rootNodeIndex = _meshNodeIndexMapping[dependingMeshNodeIndices[0]]; + SortedSet dependingRelativeNodeIndices = new(); + string label; + + WeightedVertex[] vertices; + + if(dependingMeshNodeIndices.Length == 1) + { + if(_unweighted.TryGetValue(_nodes[rootNodeIndex].Attach ?? throw new NullReferenceException(), out WeightedMesh? reuseWBA)) + { + reuseWBA.RootIndices.Add(rootNodeIndex); + return; + } + + vertices = EvaluateNoWeightVertices(dependingMeshNodeIndices[0]); + label = _nodes[rootNodeIndex].Attach!.Label; + } + else + { + SortedSet absoluteDepends = new(dependingMeshNodeIndices.Select(x => _meshNodeIndexMapping[x])); + rootNodeIndex = ComputeCommonNodeIndex(absoluteDepends); + dependingRelativeNodeIndices = new(absoluteDepends.Select(x => x - rootNodeIndex)); + + vertices = EvaluateWeightVertices(dependingMeshNodeIndices, rootNodeIndex); + + label = ""; + string[] attachLabels = absoluteDepends.Select(x => _nodes[x].Attach!.Label).ToArray(); + int longestLabel = attachLabels.Min(x => x.Length); + for(int i = 0; i < longestLabel; i++) + { + char? character = attachLabels[0][i]; + for(int j = 1; j < attachLabels.Length; j++) + { + if(attachLabels[j][i] != character) + { + character = null; + break; + } + } + + if(character == null) + { + break; + } + + label += character; + } + + if(label.Length == 0) + { + label = "Converted_" + StringExtensions.GenerateIdentifier(); + } + else if(_output.Any(x => x.Label == label)) + { + string baseLabel = label; + label += "_2"; + int number = 3; + while(_output.Any(x => x.Label == label)) + { + label = baseLabel + "_" + number; + number += 1; + } + } + } + + (BufferCorner[][] triangleSets, BufferMaterial[] materials, bool hasColors) = EvaluatePolygons(); + + WeightedMesh wba = new( + vertices, + triangleSets, + materials, + new() { rootNodeIndex }, + dependingRelativeNodeIndices, + hasColors + ) + { + Label = label + }; + + if(dependingRelativeNodeIndices.Count == 0) + { + _unweighted.Add(_nodes[rootNodeIndex].Attach ?? throw new NullReferenceException(), wba); + } + + _output.Add(wba); + } + + private WeightedMesh[] Process() + { + if(_finished) + { + return _output.ToArray(); + } + + Verify(); + Setup(); + + foreach(Node node in _nodes) + { + if(node.Attach == null || node.Attach.MeshData.Length == 0) + { + continue; + } + + _currentMeshIndex++; + + MapVertices(); + + if(_usedVertices.Count > 0) + { + EvaluateMesh(); + } + } + + _finished = true; + return _output.ToArray(); + } + + public static WeightedMesh[] ConvertToWeighted(Node model) + { + return new ToWeightedConverter(model).Process(); + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Enums.cs b/src/SA3D.Modeling/Mesh/Enums.cs new file mode 100644 index 0000000..8a417c1 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Enums.cs @@ -0,0 +1,108 @@ +namespace SA3D.Modeling.Mesh +{ + /// + /// The different available Attach formats. + /// + public enum AttachFormat + { + /// + /// Buffer format; Exclusive to this library. + /// + Buffer, + + /// + /// BASIC format. + /// + BASIC, + + /// + /// CHUNK format. + /// + CHUNK, + + /// + /// GC format. + /// + GC + } + + /// + /// Transparency blending + /// + public enum BlendMode : byte + { + /// + /// Blend factor of (0, 0, 0, 0). + ///
D3DBLEND_ZERO + ///
+ Zero = 0, + + /// + /// Blend factor of (1, 1, 1, 1). + ///
D3DBLEND_ONE + ///
+ One = 1, + + /// + /// Blend factor of (Rs; Gs; Bs; As). + ///
D3DBLEND_SRCCOLOR + ///
+ Other = 2, + + /// + /// Blend factor of (1 - Rs, 1- Gs, 1- Bs, 1- As). + ///
D3DBLEND_INVSRCCOLOR + ///
+ OtherInverted = 3, + + /// + /// Blend factor of (As, As, As, As). + ///
D3DBLEND_SRCALPHA + ///
+ SrcAlpha = 4, + + /// + /// Blend factor of (1 - As, 1 - As, 1 - As, 1 - As). + ///
D3DBLEND_INVSRCALPHA + ///
+ SrcAlphaInverted = 5, + + /// + /// Blend factor of (Ad, Ad, Ad, Ad). + ///
D3DBLEND_DESTALPHA + ///
+ DstAlpha = 6, + + /// + /// Blend factor of (1 - Ad, 1 - Ad, 1 - Ad, 1 - Ad). + ///
D3DBLEND_INVDESTALPHA + ///
+ DstAlphaInverted = 7 + } + + /// + /// Texture filtering modes. + /// + public enum FilterMode + { + /// + /// Samples the nearest pixel only. + /// + Nearest = 0, + + /// + /// Linearly interpolates between the pixels. + /// + Bilinear = 1, + + /// + /// Linearly interpolates between the pixels and mitpmaps. + /// + Trilinear = 2, + + /// + /// Mix between bilinear and trilinear (?). + /// + Blend = 3, + } +} diff --git a/src/SA3D.Modeling/Mesh/Gamecube/Enums/GCDataType.cs b/src/SA3D.Modeling/Mesh/Gamecube/Enums/GCDataType.cs new file mode 100644 index 0000000..9eb0553 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Gamecube/Enums/GCDataType.cs @@ -0,0 +1,63 @@ +namespace SA3D.Modeling.Mesh.Gamecube.Enums +{ + /// + /// The component type of the data. + /// + public enum GCDataType : byte + { + /// + /// Equal to . + /// + Unsigned8 = 0, + + /// + /// Equal to . + /// + Signed8 = 1, + + /// + /// Equal to . + /// + Unsigned16 = 2, + + /// + /// Equal to . + /// + Signed16 = 3, + + /// + /// Equal to . + /// + Float32 = 4, + + /// + /// RGB565 struct. + /// + RGB565 = 5, + + /// + /// RGB8 struct. + /// + RGB8 = 6, + + /// + /// RGBX8 struct. + /// + RGBX8 = 7, + + /// + /// RGBA4 struct. + /// + RGBA4 = 8, + + /// + /// RGBA6 struct. + /// + RGBA6 = 9, + + /// + /// RGBA8 struct. + /// + RGBA8 = 10 + } +} diff --git a/src/SA3D.Modeling/Mesh/Gamecube/Enums/GCEnumExtensions.cs b/src/SA3D.Modeling/Mesh/Gamecube/Enums/GCEnumExtensions.cs new file mode 100644 index 0000000..f02bf10 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Gamecube/Enums/GCEnumExtensions.cs @@ -0,0 +1,53 @@ +namespace SA3D.Modeling.Mesh.Gamecube.Enums +{ + /// + /// Extension methods for gamecube enums. + /// + public static class GCEnumExtensions + { + /// + /// Returns the struct size of a struct-data combination + /// + /// Type of structure. + /// Value datatype within the structure + /// The size in bytes. + public static uint GetStructSize(GCStructType structType, GCDataType dataType) + { + uint num_components = structType switch + { + GCStructType.PositionXY + or GCStructType.TexCoordUV => 2, + + GCStructType.PositionXYZ + or GCStructType.NormalXYZ => 3, + + GCStructType.NormalNBT + or GCStructType.NormalNBT3 => 9, + + GCStructType.ColorRGB + or GCStructType.ColorRGBA + or GCStructType.TexCoordU + or _ => 1, + }; + + return (uint)(num_components * dataType switch + { + GCDataType.Unsigned8 + or GCDataType.Signed8 => 1, + + GCDataType.Unsigned16 + or GCDataType.Signed16 + or GCDataType.RGB565 + or GCDataType.RGBA4 => 2, + + GCDataType.RGBA6 => 3, + + GCDataType.Float32 + or GCDataType.RGB8 + or GCDataType.RGBX8 + or GCDataType.RGBA8 + or _ => 4, + }); + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Gamecube/Enums/GCIndexFormat.cs b/src/SA3D.Modeling/Mesh/Gamecube/Enums/GCIndexFormat.cs new file mode 100644 index 0000000..d7ad0c0 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Gamecube/Enums/GCIndexFormat.cs @@ -0,0 +1,142 @@ +using SA3D.Common; +using System; + +namespace SA3D.Modeling.Mesh.Gamecube.Enums +{ + /// + /// Holds information about the triangle lists index formatting of geometry. + /// + [Flags] + public enum GCIndexFormat : uint + { + /// + /// Whether the position matrix ID indices are 16 bit rather than 8 bit. + /// + PositionMatrixIDLargeIndex = Flag32.B0, + + /// + /// Whether the Geometry contains position matrix ID data. + /// + HasPositionMatrixID = Flag32.B1, + + /// + /// Whether the position indices are 16 bit rather than 8 bit. + /// + PositionLargeIndex = Flag32.B2, + + /// + /// Whether the Geometry contains position data. + /// + HasPosition = Flag32.B3, + + /// + /// Whether the normal indices are 16 bit rather than 8 bit. + /// + NormalLargeIndex = Flag32.B4, + + /// + /// Whether the Geometry contains normal data. + /// + HasNormal = Flag32.B5, + + /// + /// Whether the color0 indices are 16 bit rather than 8 bit. + /// + Color0LargeIndex = Flag32.B6, + + /// + /// Whether the Geometry contains color0 data. + /// + HasColor0 = Flag32.B7, + + /// + /// Whether the color1 indices are 16 bit rather than 8 bit. + /// + Color1LargeIndex = Flag32.B8, + + /// + /// Whether the Geometry contains color1 data. + /// + HasColor1 = Flag32.B9, + + /// + /// Whether the texcoord0 indices are 16 bit rather than 8 bit. + /// + TexCoord0LargeIndex = Flag32.B10, + + /// + /// Whether the Geometry contains texcoord0 data. + /// + HasTexCoord0 = Flag32.B11, + + /// + /// Whether the texcoord1 indices are 16 bit rather than 8 bit. + /// + TexCoord1LargeIndex = Flag32.B12, + + /// + /// Whether the Geometry contains texcoord1 data. + /// + HasTexCoord1 = Flag32.B13, + + /// + /// Whether the texcoord2 indices are 16 bit rather than 8 bit. + /// + TexCoord2LargeIndex = Flag32.B14, + + /// + /// Whether the Geometry contains texcoord2 data. + /// + HasTexCoord2 = Flag32.B15, + + /// + /// Whether the texcoord3 indices are 16 bit rather than 8 bit. + /// + TexCoord3LargeIndex = Flag32.B16, + + /// + /// Whether the Geometry contains texcoord3 data. + /// + HasTexCoord3 = Flag32.B17, + + /// + /// Whether the texcoord4 indices are 16 bit rather than 8 bit. + /// + TexCoord4LargeIndex = Flag32.B18, + + /// + /// Whether the Geometry contains texcoord4 data. + /// + HasTexCoord4 = Flag32.B19, + + /// + /// Whether the texcoord5 indices are 16 bit rather than 8 bit. + /// + TexCoord5LargeIndex = Flag32.B20, + + /// + /// Whether the Geometry contains texcoord5 data. + /// + HasTexCoord5 = Flag32.B21, + + /// + /// Whether the texcoord6 indices are 16 bit rather than 8 bit. + /// + TexCoord6LargeIndex = Flag32.B22, + + /// + /// Whether the Geometry contains texcoord6 data. + /// + HasTexCoord6 = Flag32.B23, + + /// + /// Whether the texcoord7 indices are 16 bit rather than 8 bit. + /// + TexCoord7LargeIndex = Flag32.B24, + + /// + /// Whether the Geometry contains texcoord7 data. + /// + HasTexCoord7 = Flag32.B25, + } +} diff --git a/src/SA3D.Modeling/Mesh/Gamecube/Enums/GCParameterType.cs b/src/SA3D.Modeling/Mesh/Gamecube/Enums/GCParameterType.cs new file mode 100644 index 0000000..005f02b --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Gamecube/Enums/GCParameterType.cs @@ -0,0 +1,60 @@ +namespace SA3D.Modeling.Mesh.Gamecube.Enums +{ + /// + /// The types of parameter that exist + /// + public enum GCParameterType : uint + { + /// + /// Stores vertex attribute format information. + /// + VertexFormat = 0, + + /// + /// Stores vertex index attributes. + /// + IndexFormat = 1, + + /// + /// Stores lighting values. + /// + Lighting = 2, + + //Unused = 3, // Yes, number 3 would probably crash the game + + /// + /// Stores blending modes + /// + BlendAlpha = 4, + + /// + /// Stores the ambient color + /// + AmbientColor = 5, + + /// + /// Stores diffuse color (?). + /// + DiffuseColor = 6, + + /// + /// Stores specular color (?). + /// + SpecularColor = 7, + + /// + /// Store texture info. + /// + Texture = 8, + + /// + /// Unknown functionality. + /// + Unknown = 9, + + /// + /// Stores texture coordinate processing parameters. + /// + Texcoord = 10, + } +} diff --git a/src/SA3D.Modeling/Mesh/Gamecube/Enums/GCPolyType.cs b/src/SA3D.Modeling/Mesh/Gamecube/Enums/GCPolyType.cs new file mode 100644 index 0000000..0575e65 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Gamecube/Enums/GCPolyType.cs @@ -0,0 +1,38 @@ +namespace SA3D.Modeling.Mesh.Gamecube.Enums +{ + /// + /// Primitive type on how surface data is stored. + /// + public enum GCPolyType + { + /// + /// Triangle list. + /// + Triangles = 0x90, + + /// + /// Triangle strip. + /// + TriangleStrip = 0x98, + + /// + /// Triangle fan. + /// + TriangleFan = 0xA0, + + /// + /// Edge list. + /// + Lines = 0xA8, + + /// + /// Consecutive edges. + /// + LineStrip = 0xB0, + + /// + /// Lone points in space. + /// + Points = 0xB8 + } +} diff --git a/src/SA3D.Modeling/Mesh/Gamecube/Enums/GCStructType.cs b/src/SA3D.Modeling/Mesh/Gamecube/Enums/GCStructType.cs new file mode 100644 index 0000000..5169387 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Gamecube/Enums/GCStructType.cs @@ -0,0 +1,53 @@ +namespace SA3D.Modeling.Mesh.Gamecube.Enums +{ + /// + /// The component structure of the data. + /// + public enum GCStructType : byte + { + /// + /// 2 component vector position (X, Y). + /// + PositionXY = 0, + + /// + /// 3 Component vector position (X, Y, Z). + /// + PositionXYZ = 1, + + /// + /// 3 Component vector normal (X, Y, Z). + /// + NormalXYZ = 2, + + /// + /// Normal, Binormal and Tangent 3 component vectors (X, Y, Z). + /// + NormalNBT = 3, + + /// + /// Normal, Binormal and Tangent 3 component vectors (X, Y, Z). (NBT"3"?) + /// + NormalNBT3 = 4, + + /// + /// Color with 3 channels. + /// + ColorRGB = 5, + + /// + /// Color with 4 channels. + /// + ColorRGBA = 6, + + /// + /// Single channel texture coordinates. + /// + TexCoordU = 7, + + /// + /// Dual channel texture coordinates. + /// + TexCoordUV = 8 + } +} diff --git a/src/SA3D.Modeling/Mesh/Gamecube/Enums/GCTexCoordSource.cs b/src/SA3D.Modeling/Mesh/Gamecube/Enums/GCTexCoordSource.cs new file mode 100644 index 0000000..5c67b1a --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Gamecube/Enums/GCTexCoordSource.cs @@ -0,0 +1,113 @@ +namespace SA3D.Modeling.Mesh.Gamecube.Enums +{ + /// + /// Input values to use for when calculating texture coordinates. + /// + public enum GCTexCoordSource + { + /// + /// Vertex positions. + /// + Position = 0x0, + + /// + /// Vertex normals. + /// + Normal = 0x1, + + /// + /// Vertex binormals. + /// + Binormal = 0x2, + + /// + /// Vertex tangents. + /// + Tangent = 0x3, + + /// + /// First set of the vertex' texture coordinates. + /// + TexCoord0 = 0x4, + + /// + /// Second set of the vertex' texture coordinates. + /// + TexCoord1 = 0x5, + + /// + /// Third set of the vertex' texture coordinates. + /// + TexCoord2 = 0x6, + + /// + /// Fourth set of the vertex' texture coordinates. + /// + TexCoord3 = 0x7, + + /// + /// Fifth set of the vertex' texture coordinates. + /// + TexCoord4 = 0x8, + + /// + /// Sixth set of the vertex' texture coordinates. + /// + TexCoord5 = 0x9, + + /// + /// Seventh set of the vertex' texture coordinates. + /// + TexCoord6 = 0xA, + + /// + /// Eight set of the vertex' texture coordinates. + /// + TexCoord7 = 0xB, + + /// + /// Bump related 0 (?). + /// + BumpTexCoord0 = 0xC, + + /// + /// Bump related 1 (?). + /// + BumpTexCoord1 = 0xD, + + /// + /// Bump related 2 (?). + /// + BumpTexCoord2 = 0xE, + + /// + /// Bump related 3 (?). + /// + BumpTexCoord3 = 0xF, + + /// + /// Bump related 4 (?). + /// + BumpTexCoord4 = 0x10, + + /// + /// Bump related 5 (?). + /// + BumpTexCoord5 = 0x11, + + /// + /// Bump related 6 (?). + /// + BumpTexCoord6 = 0x12, + + /// + /// First set of the vertex' colors. + /// + Color0 = 0x13, + + /// + /// Second set of the vertex' colors. + /// + Color1 = 0x14, + } +} diff --git a/src/SA3D.Modeling/Mesh/Gamecube/Enums/GCTexCoordType.cs b/src/SA3D.Modeling/Mesh/Gamecube/Enums/GCTexCoordType.cs new file mode 100644 index 0000000..e317c6b --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Gamecube/Enums/GCTexCoordType.cs @@ -0,0 +1,63 @@ +namespace SA3D.Modeling.Mesh.Gamecube.Enums +{ + /// + /// The function type used to generate the texture coordinates. + /// + public enum GCTexCoordType + { + /// + /// Multiplies input by a 3x4 matrix. + /// + Matrix3x4 = 0x0, + + /// + /// Multiplies input by a 2x4 matrix. + /// + Matrix2x4 = 0x1, + + /// + /// Gamecube bump mapping function 0 (?). + /// + Bump0 = 0x2, + + /// + /// Gamecube bump mapping function 1 (?). + /// + Bump1 = 0x3, + + /// + /// Gamecube bump mapping function 2 (?). + /// + Bump2 = 0x4, + + /// + /// Gamecube bump mapping function 3 (?). + /// + Bump3 = 0x5, + + /// + /// Gamecube bump mapping function 4 (?). + /// + Bump4 = 0x6, + + /// + /// Gamecube bump mapping function 5 (?). + /// + Bump5 = 0x7, + + /// + /// Gamecube bump mapping function 6 (?). + /// + Bump6 = 0x8, + + /// + /// Gamecube bump mapping function 7 (?). + /// + Bump7 = 0x9, + + /// + /// Gamecube SRTG function (?). + /// + SRTG = 0xA + } +} diff --git a/src/SA3D.Modeling/Mesh/Gamecube/Enums/GCTexcoordID.cs b/src/SA3D.Modeling/Mesh/Gamecube/Enums/GCTexcoordID.cs new file mode 100644 index 0000000..30de2f9 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Gamecube/Enums/GCTexcoordID.cs @@ -0,0 +1,58 @@ +namespace SA3D.Modeling.Mesh.Gamecube.Enums +{ + /// + /// Output channel to which calculated texture coordinates should be written to. + /// + public enum GCTexCoordID : byte + { + /// + /// First channel. + /// + TexCoord0 = 0, + + /// + /// First channel. + /// + TexCoord1 = 1, + + /// + /// Second channel. + /// + TexCoord2 = 2, + + /// + /// Third channel. + /// + TexCoord3 = 3, + + /// + /// Fourth channel. + /// + TexCoord4 = 4, + + /// + /// Fifth channel. + /// + TexCoord5 = 5, + + /// + /// Sixth channel. + /// + TexCoord6 = 6, + + /// + /// Seventh channel. + /// + TexCoord7 = 7, + + /// + /// Maximum available channel. + /// + TexCoordMax = 8, + + /// + /// No channel. + /// + TexCoordNull = 0xFF, + } +} diff --git a/src/SA3D.Modeling/Mesh/Gamecube/Enums/GCTexcoordMatrix.cs b/src/SA3D.Modeling/Mesh/Gamecube/Enums/GCTexcoordMatrix.cs new file mode 100644 index 0000000..6b7f36c --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Gamecube/Enums/GCTexcoordMatrix.cs @@ -0,0 +1,63 @@ +namespace SA3D.Modeling.Mesh.Gamecube.Enums +{ + /// + /// Matrix slot to use when using or . + /// + public enum GCTexcoordMatrix + { + /// + /// First slot. + /// + Matrix0 = 0, + + /// + /// Second slot. + /// + Matrix1 = 1, + + /// + /// Third slot. + /// + Matrix2 = 2, + + /// + /// Fourth slot. + /// + Matrix3 = 3, + + /// + /// Fifth slot. + /// + Matrix4 = 4, + + /// + /// Sixth slot. + /// + Matrix5 = 5, + + /// + /// Seventh slot. + /// + Matrix6 = 6, + + /// + /// Eight slot. + /// + Matrix7 = 7, + + /// + /// Nineth slot. + /// + Matrix8 = 8, + + /// + /// Tenth slot. + /// + Matrix9 = 9, + + /// + /// Identity matrix. + /// + Identity = 10 + } +} diff --git a/src/SA3D.Modeling/Mesh/Gamecube/Enums/GCTileMode.cs b/src/SA3D.Modeling/Mesh/Gamecube/Enums/GCTileMode.cs new file mode 100644 index 0000000..a4e4c5b --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Gamecube/Enums/GCTileMode.cs @@ -0,0 +1,44 @@ +using SA3D.Common; +using System; + +namespace SA3D.Modeling.Mesh.Gamecube.Enums +{ + /// + /// Gamecube specific texture tiling modes. + /// + [Flags] + public enum GCTileMode : byte + { + /// + /// Repeats texture coordinates outside of 0-1 on the vertical axis. + ///
Has priority over mirroring. + ///
+ RepeatV = Flag8.B0, + + /// + /// Mirrors texture coordinates every second repeated instance on the vertical axis. + /// + MirrorV = Flag8.B1, + + /// + /// Repeats texture coordinates outside of 0-1 on the horizontal axis. + ///
Has priority over mirroring. + ///
+ RepeatU = Flag8.B2, + + /// + /// Mirrors texture coordinates every second repeated instance on the horizontal axis. + /// + MirrorU = Flag8.B3, + + /// + /// Unknown functionality. + /// + Unknown = Flag8.B4, + + /// + /// Mask for all valid values. + /// + Mask = RepeatV | MirrorV | RepeatU | MirrorU | Unknown + } +} diff --git a/src/SA3D.Modeling/Mesh/Gamecube/Enums/GCVertexType.cs b/src/SA3D.Modeling/Mesh/Gamecube/Enums/GCVertexType.cs new file mode 100644 index 0000000..f30dc10 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Gamecube/Enums/GCVertexType.cs @@ -0,0 +1,78 @@ +namespace SA3D.Modeling.Mesh.Gamecube.Enums +{ + /// + /// Indicates which type of data a vertex set stores. + /// + public enum GCVertexType : byte + { + /// + /// Position Matrix Indices. + /// + PositionMatrixID = 0, + + /// + /// Vertex Positions. + /// + Position = 1, + + /// + /// Vertex normals. + /// + Normal = 2, + + /// + /// Vertex colors (First slot). + /// + Color0 = 3, + + /// + /// Vertex colors (Second slot). + /// + Color1 = 4, + + /// + /// Texture Coordinates (First slot). + /// + TexCoord0 = 5, + + /// + /// Texture Coordinates (Second slot). + /// + TexCoord1 = 6, + + /// + /// Texture Coordinates (Third slot). + /// + TexCoord2 = 7, + + /// + /// Texture Coordinates (Fourth slot). + /// + TexCoord3 = 8, + + /// + /// Texture Coordinates (Fifth slot). + /// + TexCoord4 = 9, + + /// + /// Texture Coordinates (Sixth slot). + /// + TexCoord5 = 10, + + /// + /// Texture Coordinates (seventh slot). + /// + TexCoord6 = 11, + + /// + /// Texture Coordinates (eight slot). + /// + TexCoord7 = 12, + + /// + /// Marks end of vertex sets. + /// + End = 255 + } +} diff --git a/src/SA3D.Modeling/Mesh/Gamecube/GCAttach.cs b/src/SA3D.Modeling/Mesh/Gamecube/GCAttach.cs new file mode 100644 index 0000000..f255ef7 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Gamecube/GCAttach.cs @@ -0,0 +1,225 @@ +using SA3D.Common; +using SA3D.Common.IO; +using SA3D.Modeling.Mesh.Buffer; +using SA3D.Modeling.Mesh.Converters; +using SA3D.Modeling.Mesh.Gamecube.Enums; +using SA3D.Modeling.ObjectData.Enums; +using SA3D.Modeling.Structs; +using System; +using System.Collections.Generic; +using System.Linq; +using static SA3D.Common.StringExtensions; + +namespace SA3D.Modeling.Mesh.Gamecube +{ + /// + /// A GC format attach + /// + public sealed class GCAttach : Attach + { + /// + /// Seperate sets of vertex data in this attach. + /// + public Dictionary VertexData { get; } + + /// + /// Meshes with opaque rendering properties. + /// + public GCMesh[] OpaqueMeshes { get; set; } + + /// + /// Meshes with transparent rendering properties. + /// + public GCMesh[] TransparentMeshes { get; set; } + + /// + public override AttachFormat Format + => AttachFormat.GC; + + + /// + /// Creates a new GC attach. + /// + /// Vertex data. + /// Opaque meshes. + /// Transparent meshes. + public GCAttach(Dictionary vertexData, GCMesh[] opaqueMeshes, GCMesh[] transprentMeshes) + { + VertexData = vertexData; + OpaqueMeshes = opaqueMeshes; + TransparentMeshes = transprentMeshes; + Label = "attach_" + GenerateIdentifier(); + } + + internal GCAttach(GCVertexSet[] vertexData, GCMesh[] opaqueMeshes, GCMesh[] transprentMeshes) + { + VertexData = new(); + foreach(GCVertexSet v in vertexData) + { + if(VertexData.ContainsKey(v.Type)) + { + throw new ArgumentException($"Vertexdata contains two sets with the attribute {v.Type}"); + } + + VertexData.Add(v.Type, v); + } + + OpaqueMeshes = opaqueMeshes; + TransparentMeshes = transprentMeshes; + + Label = "attach_" + GenerateIdentifier(); + } + + + /// + public override bool CheckHasWeights() + { + return false; + } + + /// + public override void RecalculateBounds() + { + MeshBounds = Bounds.FromPoints(VertexData[GCVertexType.Position].Vector3Data); + } + + /// + public override bool CanWrite(ModelFormat format) + { + return base.CanWrite(format) || format is ModelFormat.SA2B; + } + + + /// + /// Removes duplicate vertex data. + /// + public void OptimizeVertexData() + { + List allMeshes = new(OpaqueMeshes); + allMeshes.AddRange(TransparentMeshes); + + foreach(GCVertexSet item in VertexData.Values) + { + item.Optimize(allMeshes); + } + } + + /// + /// Optimizes the polygon data of all meshes. + /// + public void OptimizePolygons() + { + for(int i = 0; i < OpaqueMeshes.Length; i++) + { + OpaqueMeshes[i].OptimizePolygons(); + } + + for(int i = 0; i < TransparentMeshes.Length; i++) + { + TransparentMeshes[i].OptimizePolygons(); + } + } + + + /// + /// Converts the gamecube attach to a set of buffer meshes. + /// + /// Whether to optimize the buffer mesh data. + /// The converted buffer meshes. + public BufferMesh[] ConvertToBufferMeshData(bool optimize) + { + return GCConverter.ConvertGCToBuffer(this, optimize); + } + + /// + protected override uint WriteInternal(EndianStackWriter writer, ModelFormat format, PointerLUT lut) + { + uint vtxAddr = GCVertexSet.WriteArray(writer, VertexData.Values.ToArray()); + + byte[] opaqueMeshStructs = GCMesh.WriteArrayContents(writer, OpaqueMeshes); + byte[] transparentMeshStructs = GCMesh.WriteArrayContents(writer, TransparentMeshes); + + uint opaqueAddress = 0; + if(opaqueMeshStructs.Length > 0) + { + opaqueAddress = writer.PointerPosition; + writer.Write(opaqueMeshStructs); + } + + uint transparentAddress = 0; + if(transparentMeshStructs.Length > 0) + { + transparentAddress = writer.PointerPosition; + writer.Write(transparentMeshStructs); + } + + uint address = writer.PointerPosition; + + writer.WriteUInt(vtxAddr); + writer.WriteEmpty(4); + writer.WriteUInt(opaqueAddress); + writer.WriteUInt(transparentAddress); + writer.WriteUShort((ushort)OpaqueMeshes.Length); + writer.WriteUShort((ushort)TransparentMeshes.Length); + MeshBounds.Write(writer); + return address; + } + + /// + /// Reads a gamecube attach off an endian stack reader. + /// + /// The reader to read from. + /// Address at which to start reading. + /// Pointer references to utilize. + /// The gamecube attach that was read. + public static GCAttach Read(EndianStackReader reader, uint address, PointerLUT lut) + { + GCAttach onRead() + { + uint vertexAddress = reader.ReadPointer(address); + // uint gap = data.ReadPointer(address + 4); + uint opaqueAddress = reader.ReadPointer(address + 8); + uint transparentAddress = reader.ReadPointer(address + 12); + + int opaqueCount = reader.ReadShort(address + 16); + int transparentCount = reader.ReadShort(address + 18); + address += 20; + Bounds bounds = Bounds.Read(reader, ref address); + + GCVertexSet[] vertexData = GCVertexSet.ReadArray(reader, vertexAddress); + GCMesh[] opaqueMeshes = GCMesh.ReadArray(reader, opaqueAddress, opaqueCount); + GCMesh[] transparentMeshes = GCMesh.ReadArray(reader, transparentAddress, transparentCount); + + return new GCAttach(vertexData, opaqueMeshes, transparentMeshes) + { + MeshBounds = bounds + }; + } + + return lut.GetAddLabeledValue(address, "attach_", onRead); + } + + + /// + public override GCAttach Clone() + { + Dictionary vertexSets = new(); + foreach(KeyValuePair t in VertexData) + { + vertexSets.Add(t.Key, t.Value.Clone()); + } + + return new GCAttach(vertexSets, OpaqueMeshes.ContentClone(), TransparentMeshes.ContentClone()) + { + Label = Label, + MeshBounds = MeshBounds + }; + } + + /// + public override string ToString() + { + return $"{Label} - GC: {VertexData.Count} - {OpaqueMeshes.Length} - {TransparentMeshes.Length}"; + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Gamecube/GCCorner.cs b/src/SA3D.Modeling/Mesh/Gamecube/GCCorner.cs new file mode 100644 index 0000000..707a75e --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Gamecube/GCCorner.cs @@ -0,0 +1,151 @@ +using SA3D.Modeling.Mesh.Gamecube.Enums; +using System; +using System.Runtime.InteropServices; + +namespace SA3D.Modeling.Mesh.Gamecube +{ + /// + /// A single corner of a polygon, called loop + /// + [StructLayout(LayoutKind.Sequential, Pack = 2)] + public struct GCCorner : IEquatable + { + /// + /// The index to . + /// + public ushort PositionMatrixIDIndex { get; set; } + + /// + /// The index to . + /// + public ushort PositionIndex { get; set; } + + /// + /// The index to . + /// + public ushort NormalIndex { get; set; } + + /// + /// The index to . + /// + public ushort Color0Index { get; set; } + + /// + /// The index to . + /// + public ushort Color1Index { get; set; } + + /// + /// The index to . + /// + public ushort TexCoord0Index { get; set; } + + /// + /// The index to . + /// + public ushort TexCoord1Index { get; set; } + + /// + /// The index to . + /// + public ushort TexCoord2Index { get; set; } + + /// + /// The index to . + /// + public ushort TexCoord3Index { get; set; } + + /// + /// The index to . + /// + public ushort TexCoord4Index { get; set; } + + /// + /// The index to . + /// + public ushort TexCoord5Index { get; set; } + + /// + /// The index to . + /// + public ushort TexCoord6Index { get; set; } + + /// + /// The index to . + /// + public ushort TexCoord7Index { get; set; } + + + /// + public override readonly bool Equals(object? obj) + { + return obj is GCCorner corner && + PositionMatrixIDIndex == corner.PositionMatrixIDIndex && + PositionIndex == corner.PositionIndex && + NormalIndex == corner.NormalIndex && + Color0Index == corner.Color0Index && + Color1Index == corner.Color1Index && + TexCoord0Index == corner.TexCoord0Index && + TexCoord1Index == corner.TexCoord1Index && + TexCoord2Index == corner.TexCoord2Index && + TexCoord3Index == corner.TexCoord3Index && + TexCoord4Index == corner.TexCoord4Index && + TexCoord5Index == corner.TexCoord5Index && + TexCoord6Index == corner.TexCoord6Index && + TexCoord7Index == corner.TexCoord7Index; + } + + /// + public override readonly int GetHashCode() + { + HashCode hash = new(); + hash.Add(PositionMatrixIDIndex); + hash.Add(PositionIndex); + hash.Add(NormalIndex); + hash.Add(Color0Index); + hash.Add(Color1Index); + hash.Add(TexCoord0Index); + hash.Add(TexCoord1Index); + hash.Add(TexCoord2Index); + hash.Add(TexCoord3Index); + hash.Add(TexCoord4Index); + hash.Add(TexCoord5Index); + hash.Add(TexCoord6Index); + hash.Add(TexCoord7Index); + return hash.ToHashCode(); + } + + readonly bool IEquatable.Equals(GCCorner other) + { + return Equals(other); + } + + /// + /// Compares two GC corners for equality. + /// + /// Lefthand corner. + /// Righthand corner. + /// Whether the two corners are equal. + public static bool operator ==(GCCorner left, GCCorner right) + { + return left.Equals(right); + } + + /// + /// Compares two GC corners for inequality. + /// + /// Lefthand corner. + /// Righthand corner. + /// Whether the two corners are inequal. + public static bool operator !=(GCCorner left, GCCorner right) + { + return !(left == right); + } + + /// + public override readonly string ToString() + { + return $"({PositionIndex}, {NormalIndex}, {Color0Index}, {TexCoord0Index})"; + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Gamecube/GCMesh.cs b/src/SA3D.Modeling/Mesh/Gamecube/GCMesh.cs new file mode 100644 index 0000000..e20329e --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Gamecube/GCMesh.cs @@ -0,0 +1,277 @@ +using SA3D.Common; +using SA3D.Common.IO; +using SA3D.Modeling.Mesh.Gamecube.Enums; +using SA3D.Modeling.Mesh.Gamecube.Parameters; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace SA3D.Modeling.Mesh.Gamecube +{ + /// + /// A single mesh, with its own parameter and primitive data
+ ///
+ public class GCMesh : ICloneable + { + /// + /// The data parameters. + /// + public IGCParameter[] Parameters { get; set; } + + /// + /// The polygon data. + /// + public GCPolygon[] Polygons { get; set; } + + /// + /// The index attributes of this mesh. If it has no , it will return null. + /// + public GCIndexFormat? IndexFormat + { + get + { + if(Parameters.FirstOrDefault(x => x.Type == GCParameterType.IndexFormat) is GCIndexFormatParameter p) + { + return p.IndexFormat; + } + + return null; + } + } + + + /// + /// Create a new mesh from existing polygons and parameters + /// + /// Polygon data. + /// Data parameters. + public GCMesh(IGCParameter[] parameters, GCPolygon[] polygons) + { + Parameters = parameters; + Polygons = polygons; + } + + + /// + /// Optimizes the polygon data in the mesh by strippifying. + /// + public void OptimizePolygons() + { + // getting the current triangles + List triangles = new(); + foreach(GCPolygon p in Polygons) + { + if(p.Type == GCPolyType.Triangles) + { + triangles.AddRange(p.Corners); + } + else if(p.Type == GCPolyType.TriangleStrip) + { + bool rev = p.Corners[0].PositionIndex == p.Corners[1].PositionIndex; + for(int i = rev ? 3 : 2; i < p.Corners.Length; i++) + { + if(rev) + { + triangles.Add(p.Corners[i - 1]); + triangles.Add(p.Corners[i - 2]); + } + else + { + triangles.Add(p.Corners[i - 2]); + triangles.Add(p.Corners[i - 1]); + } + + triangles.Add(p.Corners[i]); + rev = !rev; + } + } + } + + GCCorner[][] strips = Strippify.TriangleStrippifier.Global.Strippify(triangles.ToArray()); + + // putting them all together + List polygons = new(); + List singleTris = new(); + + for(int i = 0; i < strips.Length; i++) + { + GCCorner[] strip = strips[i]; + if(strip.Length == 3) + { + singleTris.AddRange(strip); + } + else + { + polygons.Add(new(GCPolyType.TriangleStrip, strip)); + } + } + + if(singleTris.Count > 0) + { + polygons.Add(new(GCPolyType.Triangles, singleTris.ToArray())); + } + + Polygons = polygons.ToArray(); + } + + + /// + /// Writes the parameters to an endian stack writer. + /// + /// The writer to write to. + /// The address at which the parameters were written. + public uint WriteParameters(EndianStackWriter writer) + { + uint result = writer.PointerPosition; + foreach(IGCParameter p in Parameters) + { + p.Write(writer); + } + + return result; + } + + /// + /// Writes the polygons to an endian stack writer. + /// + /// The writer to write to. + /// Index format reference to use for writing polygon indices. If the mesh contains their own index format, it will be replaced. + /// The address at which the polygons were written. + public uint WritePolygons(EndianStackWriter writer, ref GCIndexFormat indexFormat) + { + GCIndexFormat? t = IndexFormat; + if(t.HasValue) + { + indexFormat = t.Value; + } + + uint result = writer.PointerPosition; + + foreach(GCPolygon p in Polygons) + { + p.Write(writer, indexFormat); + } + + writer.Align(0x20); + + return result; + } + + /// + /// Writes the parameters and polygons of a mesh array to an endian stack writer and returns the header array data. + /// + /// The writer to write to. + /// The meshes to write. + /// + public static byte[] WriteArrayContents(EndianStackWriter writer, GCMesh[] meshes) + { + uint[] headerData = new uint[meshes.Length * 4]; + + for(int i = 0, h = 0; i < meshes.Length; i++, h += 4) + { + headerData[h] = meshes[i].WriteParameters(writer); + headerData[h + 1] = (uint)meshes[i].Parameters.Length; + } + + writer.Align(0x20); + + GCIndexFormat indexFormat = GCIndexFormat.HasPosition | GCIndexFormat.PositionLargeIndex; + + for(int i = 0, h = 2; i < meshes.Length; i++, h += 4) + { + uint polyAddress = meshes[i].WritePolygons(writer, ref indexFormat); + headerData[h] = polyAddress; + headerData[h + 1] = writer.PointerPosition - polyAddress; + } + + byte[] result = new byte[headerData.Length * sizeof(uint)]; + System.Buffer.BlockCopy(headerData, 0, result, 0, result.Length); + + return result; + } + + /// + /// Reads gamecube mesh data off an endian stack reader. + /// + /// The reader to read from. + /// Address at which to start reading. + /// Index format to use. + /// The read GC mesh. + public static GCMesh Read(EndianStackReader reader, uint address, ref GCIndexFormat indexFormat) + { + uint parameters_addr = reader.ReadPointer(address); + int parameters_count = reader.ReadInt(address + 4); + + uint primitives_addr = reader.ReadPointer(address + 8); + int primitives_size = reader.ReadInt(address + 12); + + List parameters = new(); + for(int i = 0; i < parameters_count; i++) + { + parameters.Add(IGCParameter.Read(reader, parameters_addr)); + parameters_addr += 8; + } + + if(parameters.FirstOrDefault(x => x.Type == GCParameterType.IndexFormat) is GCIndexFormatParameter p) + { + indexFormat = p.IndexFormat; + } + + List primitives = new(); + uint end_pos = (uint)(primitives_addr + primitives_size); + + while(primitives_addr < end_pos) + { + // if the primitive isnt valid + if(reader[primitives_addr] == 0) + { + break; + } + + primitives.Add(GCPolygon.Read(reader, ref primitives_addr, indexFormat)); + } + + return new GCMesh(parameters.ToArray(), primitives.ToArray()); + } + + /// + /// Reads an array of meshes off an endian stack readaer. + /// + /// The reader to read from. + /// Address at which to start reading. + /// Number of meshes in the array to read. + /// The mesh array that was read. + public static GCMesh[] ReadArray(EndianStackReader reader, uint address, int count) + { + GCIndexFormat indexAttribs = GCIndexFormat.HasPosition; + + GCMesh[] result = new GCMesh[count]; + for(int i = 0; i < count; i++, address += 16) + { + result[i] = Read(reader, address, ref indexAttribs); + } + + return result; + } + + object ICloneable.Clone() + { + return Clone(); + } + + /// + /// Creates a clone of the mesh. + /// + /// The cloned mesh. + public GCMesh Clone() + { + return new((IGCParameter[])Parameters.Clone(), Polygons.ContentClone()); + } + + /// + public override string ToString() + { + return (IndexFormat.HasValue ? ((uint)IndexFormat.Value).ToString("X8") : "NULL") + $" - {Parameters.Length} - {Polygons.Length}"; + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Gamecube/GCPolygon.cs b/src/SA3D.Modeling/Mesh/Gamecube/GCPolygon.cs new file mode 100644 index 0000000..3b25f92 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Gamecube/GCPolygon.cs @@ -0,0 +1,185 @@ +using SA3D.Common.IO; +using SA3D.Modeling.Mesh.Gamecube.Enums; +using System; +using System.Collections.Generic; + +namespace SA3D.Modeling.Mesh.Gamecube +{ + /// + /// A collection of corners forming polygons + /// + public readonly struct GCPolygon : ICloneable + { + /// + /// The way in which polygons are being stored. + /// + public GCPolyType Type { get; } + + /// + /// Corners making up the polygons. + /// + public GCCorner[] Corners { get; } + + /// + /// Create a new empty Primitive + /// + /// The type of primitive. + /// Corners making up the polygons. + public GCPolygon(GCPolyType type, GCCorner[] corners) + { + Type = type; + Corners = corners; + } + + private unsafe delegate void ReadIndex(EndianStackReader reader, ushort* destination, ref uint address); + + + /// + /// Reads a gamecube polygon off an endian stack reader. Advances the pointer by the number of bytes read. + /// + /// The reader to read from. + /// Address at which to start reading. + /// Polygon index formatting. + public static unsafe GCPolygon Read(EndianStackReader reader, ref uint address, GCIndexFormat indexFormat) + { + reader.PushBigEndian(true); + + GCPolyType type = (GCPolyType)reader[address]; + ushort vtxCount = reader.ReadUShort(address + 1); + + static void Read8(EndianStackReader reader, ushort* destination, ref uint address) + { + *destination = reader[address]; + address++; + } + + static void Read16(EndianStackReader reader, ushort* destination, ref uint address) + { + *destination = reader.ReadUShort(address); + address += 2; + } + + List<(ReadIndex read, uint fieldOffset)> readList = new(); + + uint attributes = (uint)indexFormat; + for(uint i = 1, outOffset = 0; i <= 0x2000; i <<= 2, outOffset++) + { + if((attributes & (i << 1)) == 0) + { + continue; + } + + if((attributes & i) == 0) + { + readList.Add((Read8, outOffset)); + } + else + { + readList.Add((Read16, outOffset)); + } + } + + address += 3; + + (ReadIndex read, uint fieldOffset)[] readArray = readList.ToArray(); + GCCorner[] corners = new GCCorner[vtxCount]; + + fixed(GCCorner* corner = &corners[0]) + { + GCCorner* current = corner; + for(int i = 0; i < vtxCount; i++, current++) + { + for(int j = 0; j < readArray.Length; j++) + { + (ReadIndex read, uint fieldOffset) = readArray[j]; + read(reader, ((ushort*)current) + fieldOffset, ref address); + } + } + } + + reader.PopEndian(); + return new GCPolygon(type, corners); + } + + /// + /// Writes the gamecube polygon to an endian stack writer. + /// + /// The writer to write to. + /// Polygon index formatting. + public readonly unsafe void Write(EndianStackWriter writer, GCIndexFormat indexFormat) + { + writer.PushBigEndian(true); + + writer.WriteByte((byte)Type); + writer.WriteUShort((ushort)Corners.Length); + + static void Write8(EndianStackWriter writer, ushort value) + { + writer.WriteByte((byte)value); + } + + static void Write16(EndianStackWriter writer, ushort value) + { + writer.WriteUShort(value); + } + + List<(Action write, uint fieldOffset)> writeList = new(); + + uint attributes = (uint)indexFormat; + for(uint i = 1, outOffset = 0; i <= 0x2000; i <<= 2, outOffset++) + { + if((attributes & (i << 1)) == 0) + { + continue; + } + + if((attributes & i) == 0) + { + writeList.Add((Write8, outOffset)); + } + else + { + writeList.Add((Write16, outOffset)); + } + } + + (Action write, uint fieldOffset)[] writeArray = writeList.ToArray(); + + fixed(GCCorner* corner = &Corners[0]) + { + GCCorner* current = corner; + for(int i = 0; i < Corners.Length; i++, current++) + { + for(int j = 0; j < writeArray.Length; j++) + { + (Action write, uint fieldOffset) = writeArray[j]; + write(writer, *(((ushort*)current) + fieldOffset)); + } + } + } + + writer.PopEndian(); + } + + + readonly object ICloneable.Clone() + { + return Clone(); + } + + /// + /// Creates a deep clone of the polygon. + /// + /// The cloned polygon + public readonly GCPolygon Clone() + { + return new(Type, (GCCorner[])Corners.Clone()); + } + + /// + public override readonly string ToString() + { + return $"{Type}: {Corners.Length}"; + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Gamecube/GCVertexSet.cs b/src/SA3D.Modeling/Mesh/Gamecube/GCVertexSet.cs new file mode 100644 index 0000000..8fbf85d --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Gamecube/GCVertexSet.cs @@ -0,0 +1,451 @@ +using SA3D.Common; +using SA3D.Common.IO; +using SA3D.Modeling.Mesh.Gamecube.Enums; +using SA3D.Modeling.Mesh.Gamecube.Parameters; +using SA3D.Modeling.Structs; +using System; +using System.Collections.Generic; +using System.Numerics; + +namespace SA3D.Modeling.Mesh.Gamecube +{ + /// + /// A vertex data set, which can hold various types of data + /// + public class GCVertexSet : ICloneable + { + /// + /// Null vertex set. + /// + public static readonly GCVertexSet EndVertexSet + = new(GCVertexType.End, default, default, null); + + private Array? _data; + + /// + /// The type of vertex data that is stored. + /// + public GCVertexType Type { get; } + + /// + /// The datatype as which the data is stored. + /// + public GCDataType DataType { get; } + + /// + /// The structure in which the data is stored. + /// + public GCStructType StructType { get; } + + /// + /// The size of a single element in the list in bytes + /// + public uint StructSize => GCEnumExtensions.GetStructSize(StructType, DataType); + + + /// + /// Vector3 data. + /// + public Vector3[] Vector3Data + { + get + { + if(_data is not Vector3[] v3data) + { + throw new InvalidOperationException("VertexSet does not contain Vector3 data!"); + } + + return v3data; + } + } + + /// + /// Vector2 data. + /// + public Vector2[] Vector2Data + { + get + { + if(_data is not Vector2[] uvdata) + { + throw new InvalidOperationException("VertexSet does not contain Vector2 data!"); + } + + return uvdata; + } + } + + /// + /// Color data. + /// + public Color[] ColorData + { + get + { + if(_data is not Color[] coldata) + { + throw new InvalidOperationException("VertexSet does not contain Color data!"); + } + + return coldata; + } + } + + + /// + /// Number of entries in the data. + /// + public int DataLength + => _data?.Length ?? 0; + + + private GCVertexSet(GCVertexType attribute, GCDataType dataType, GCStructType structType, Array? data) + { + Type = attribute; + DataType = dataType; + StructType = structType; + _data = data; + } + + + /// + /// Creates a new vertex set for position data. + /// + /// The position data. + /// The created vertex set. + public static GCVertexSet CreatePositionSet(Vector3[] positions) + { + return new GCVertexSet(GCVertexType.Position, GCDataType.Float32, GCStructType.PositionXYZ, positions); + } + + /// + /// Creates a new vertex set for normal data. + /// + /// The normal data. + /// The created vertex set. + public static GCVertexSet CreateNormalSet(Vector3[] normals) + { + return new GCVertexSet(GCVertexType.Normal, GCDataType.Float32, GCStructType.NormalXYZ, normals); + } + + /// + /// Creates a new vertex set for texcoord0 data. + /// + /// The texcoord data. + /// The created vertex set. + public static GCVertexSet CreateTexcoord0Set(Vector2[] texcoords) + { + return new GCVertexSet(GCVertexType.TexCoord0, GCDataType.Signed16, GCStructType.TexCoordUV, texcoords); + } + + /// + /// Creates a new vertex set for color0 data. + /// + /// The color data. + /// The created vertex set. + public static GCVertexSet CreateColor0Set(Color[] colors) + { + return new GCVertexSet(GCVertexType.Color0, GCDataType.RGBA8, GCStructType.ColorRGBA, colors); + } + + /// + /// Removes double values from the data. Can correct affected indices in mesh polygons. + /// + /// The meshes to correct the polygons of. + /// + public unsafe void Optimize(IEnumerable? meshes) + { + if(_data == null) + { + return; + } + + int prevLength = _data.Length; + + static int[]? OptimizeData(T[] data, ref Array source) where T : IEquatable + { + if(data.TryCreateDistinctMap(out DistinctMap mapping)) + { + source = mapping.ValueArray; + } + + return mapping.Map; + } + + int[]? map = _data switch + { + Vector3[] vector3Data => OptimizeData(vector3Data, ref _data), + Vector2[] vector2Data => OptimizeData(vector2Data, ref _data), + Color[] colorData => OptimizeData(colorData, ref _data), + _ => throw new NotSupportedException(), + }; + + if(map is null || meshes == null) + { + return; + } + + int fieldOffset = (int)Type; + + + foreach(GCMesh mesh in meshes) + { + for(int i = 0; i < mesh.Polygons.Length; i++) + { + int count = mesh.Polygons[i].Corners.Length; + fixed(GCCorner* corner = &mesh.Polygons[i].Corners[0]) + { + // Offsetting it so that its on the correct field for every element. + // We just keep it as a GCCorner* so that ++ moves forward a full element. + GCCorner* current = (GCCorner*)(((ushort*)corner) + fieldOffset); + + for(int j = 0; j < count; j++, current++) + { + *(ushort*)current = (ushort)map[*(ushort*)current]; + } + } + } + } + + if(prevLength > 256 && _data.Length <= 256) + { + GCIndexFormat useLargeIndex = (GCIndexFormat)(1 << (fieldOffset * 2)); + GCIndexFormat hasData = (GCIndexFormat)((uint)useLargeIndex << 1); + useLargeIndex = ~useLargeIndex; + + foreach(GCMesh mesh in meshes) + { + for(int i = 0; i < mesh.Parameters.Length; i++) + { + if(mesh.Parameters[i] is not GCIndexFormatParameter param + || !param.IndexFormat.HasFlag(hasData)) + { + continue; + } + + param.IndexFormat &= useLargeIndex; + mesh.Parameters[i] = param; + } + } + } + } + + + /// + /// Writes the data behind the set to the endian stack writer. + /// + /// The writer to write to. + /// Address at whicht the data was written. + /// + public uint WriteData(EndianStackWriter writer) + { + uint result = writer.PointerPosition; + + switch(Type) + { + case GCVertexType.Position: + case GCVertexType.Normal: + foreach(Vector3 vec in Vector3Data) + { + writer.WriteVector3(vec); + } + + break; + case GCVertexType.Color0: + case GCVertexType.Color1: + foreach(Color col in ColorData) + { + writer.WriteColor(col, ColorIOType.RGBA8); + } + + break; + case GCVertexType.TexCoord0: + case GCVertexType.TexCoord1: + case GCVertexType.TexCoord2: + case GCVertexType.TexCoord3: + case GCVertexType.TexCoord4: + case GCVertexType.TexCoord5: + case GCVertexType.TexCoord6: + case GCVertexType.TexCoord7: + foreach(Vector2 uv in Vector2Data) + { + writer.WriteVector2(uv * 255f, FloatIOType.Short); + } + + break; + case GCVertexType.PositionMatrixID: + case GCVertexType.End: + default: + throw new FormatException($"Vertex type invalid: {Type}"); + } + + return result; + } + + /// + /// Writes the sets structure/header to an endian stack writer. + /// + /// The writer to write to. + /// The address at which the sets data is located. + public void WriteHeader(EndianStackWriter writer, uint dataAddress) + { + writer.WriteByte((byte)Type); + writer.WriteByte((byte)StructSize); + writer.WriteUShort((ushort)DataLength); + + uint structure = (uint)StructType; + structure |= (uint)((byte)DataType << 4); + writer.WriteUInt(structure); + + writer.WriteUInt(dataAddress); + writer.WriteUInt((uint)(DataLength * StructSize)); + } + + /// + /// Writes an array of vertex sets to an endian stack writer. Includes end marker. + /// + /// The writer to write to. + /// The vertex sets to write. + /// Address at which the vertex set headers were written. + public static uint WriteArray(EndianStackWriter writer, GCVertexSet[] vertexSets) + { + uint[] dataAddresses = new uint[vertexSets.Length]; + for(int i = 0; i < vertexSets.Length; i++) + { + dataAddresses[i] = vertexSets[i].WriteData(writer); + } + + uint result = writer.PointerPosition; + + for(int i = 0; i < vertexSets.Length; i++) + { + vertexSets[i].WriteHeader(writer, dataAddresses[i]); + } + + // End marker + writer.WriteByte(0xFF); + writer.WriteEmpty(15); + + return result; + } + + /// + /// Read a vertex set off an endian data reader. + /// + /// The reader to read from. + /// Address at which to start reading. + /// The vertex set that was read. + public static GCVertexSet Read(EndianStackReader reader, uint address) + { + GCVertexType attribute = (GCVertexType)reader[address]; + if(attribute == GCVertexType.End) + { + return EndVertexSet; + } + + uint structure = reader.ReadUInt(address + 4); + GCStructType structType = (GCStructType)(structure & 0x0F); + GCDataType dataType = (GCDataType)((structure >> 4) & 0x0F); + uint structSize = GCEnumExtensions.GetStructSize(structType, dataType); + + if(reader[address + 1] != structSize) + { + throw new Exception($"Read structure size doesnt match calculated structure size: {reader[address + 1]} != {structSize}"); + } + + // reading the data + int count = reader.ReadUShort(address + 2); + uint dataAddr = reader.ReadPointer(address + 8); + + Array setdata; + + switch(attribute) + { + case GCVertexType.Position: + case GCVertexType.Normal: + Vector3[] vector3Data = new Vector3[count]; + for(int i = 0; i < count; i++) + { + vector3Data[i] = reader.ReadVector3(ref dataAddr); + } + + setdata = vector3Data; + break; + case GCVertexType.Color0: + case GCVertexType.Color1: + Color[] colorData = new Color[count]; + for(int i = 0; i < count; i++) + { + colorData[i] = reader.ReadColor(ref dataAddr, ColorIOType.RGBA8); + } + + setdata = colorData; + break; + case GCVertexType.TexCoord0: + case GCVertexType.TexCoord1: + case GCVertexType.TexCoord2: + case GCVertexType.TexCoord3: + case GCVertexType.TexCoord4: + case GCVertexType.TexCoord5: + case GCVertexType.TexCoord6: + case GCVertexType.TexCoord7: + Vector2[] uvData = new Vector2[count]; + for(int i = 0; i < count; i++) + { + uvData[i] = reader.ReadVector2(ref dataAddr, FloatIOType.Short) / 255f; + } + + setdata = uvData; + break; + case GCVertexType.PositionMatrixID: + throw new NotSupportedException(); + case GCVertexType.End: + default: + throw new ArgumentException($"Vertex type invalid: {attribute}"); + } + + return new GCVertexSet(attribute, dataType, structType, setdata); + } + + /// + /// Reads an array of vertex sets off an endian stack reader. Stops reading when an end marker set is encountered. + /// + /// The reader to read from. + /// Address at which to start reading. + /// The vertex sets that were read. + public static GCVertexSet[] ReadArray(EndianStackReader reader, uint address) + { + List result = new(); + GCVertexSet vertexSet = Read(reader, address); + while(vertexSet.Type != GCVertexType.End) + { + result.Add(vertexSet); + address += 16; + vertexSet = Read(reader, address); + } + + return result.ToArray(); + } + + + object ICloneable.Clone() + { + return Clone(); + } + + /// + /// Creates a deep clone of the vertex set. + /// + /// + public GCVertexSet Clone() + { + return new(Type, DataType, StructType, (Array?)_data?.Clone() ?? null); + } + + /// + public override string ToString() + { + return $"{Type}: {DataLength}"; + } + } +} \ No newline at end of file diff --git a/src/SA3D.Modeling/Mesh/Gamecube/Parameters/GCAmbientColorParameter.cs b/src/SA3D.Modeling/Mesh/Gamecube/Parameters/GCAmbientColorParameter.cs new file mode 100644 index 0000000..51d06bb --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Gamecube/Parameters/GCAmbientColorParameter.cs @@ -0,0 +1,37 @@ +using SA3D.Modeling.Mesh.Gamecube.Enums; +using SA3D.Modeling.Structs; + +namespace SA3D.Modeling.Mesh.Gamecube.Parameters +{ + /// + /// Ambient color of the geometry. + /// + public struct GCAmbientColorParameter : IGCParameter + { + /// + /// Ambient color parameter with the color white. + /// + public static readonly GCAmbientColorParameter White = new() { Data = uint.MaxValue }; + + /// + public readonly GCParameterType Type => GCParameterType.AmbientColor; + + /// + public uint Data { get; set; } + + /// + /// Ambient color of the mesh. + /// + public Color AmbientColor + { + get => new() { RGBA = Data }; + set => Data = value.RGBA; + } + + /// + public override string ToString() + { + return $"Ambient color: {AmbientColor}"; + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Gamecube/Parameters/GCBlendAlphaParameter.cs b/src/SA3D.Modeling/Mesh/Gamecube/Parameters/GCBlendAlphaParameter.cs new file mode 100644 index 0000000..acea9c2 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Gamecube/Parameters/GCBlendAlphaParameter.cs @@ -0,0 +1,46 @@ +using SA3D.Modeling.Mesh.Gamecube.Enums; + +namespace SA3D.Modeling.Mesh.Gamecube.Parameters +{ + /// + /// The blending information for the surface of the geometry + /// + public struct GCBlendAlphaParameter : IGCParameter + { + /// + /// Blend alpha parameter with default values. + /// + public static readonly GCBlendAlphaParameter DefaultBlendParameter + = new() { SourceAlpha = BlendMode.SrcAlpha, DestinationAlpha = BlendMode.SrcAlphaInverted }; + + /// + public readonly GCParameterType Type => GCParameterType.BlendAlpha; + + /// + public uint Data { get; set; } + + /// + /// Source pixel blendmode. + /// + public BlendMode SourceAlpha + { + readonly get => (BlendMode)((Data >> 11) & 7); + set => Data = (Data & 0xFFFFC7FF) | (((uint)value & 7) << 11); + } + + /// + /// Destination pixel blendmode. + /// + public BlendMode DestinationAlpha + { + readonly get => (BlendMode)((Data >> 8) & 7); + set => Data = (Data & 0xFFFFF8FF) | (((uint)value & 7) << 8); + } + + /// + public override readonly string ToString() + { + return $"Blendalpha: {SourceAlpha} -> {DestinationAlpha}"; + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Gamecube/Parameters/GCDiffuseColorParameter.cs b/src/SA3D.Modeling/Mesh/Gamecube/Parameters/GCDiffuseColorParameter.cs new file mode 100644 index 0000000..2a7f5f1 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Gamecube/Parameters/GCDiffuseColorParameter.cs @@ -0,0 +1,37 @@ +using SA3D.Modeling.Mesh.Gamecube.Enums; +using SA3D.Modeling.Structs; + +namespace SA3D.Modeling.Mesh.Gamecube.Parameters +{ + /// + /// Diffuse color of the geometry. + /// + public struct GCDiffuseColorParameter : IGCParameter + { + /// + /// Diffuse color parameter with the color white. + /// + public static readonly GCDiffuseColorParameter White = new() { Data = uint.MaxValue }; + + /// + public readonly GCParameterType Type => GCParameterType.DiffuseColor; + + /// + public uint Data { get; set; } + + /// + /// Diffuse color of the mesh. + /// + public Color DiffuseColor + { + get => new() { RGBA = Data }; + set => Data = value.RGBA; + } + + /// + public override string ToString() + { + return $"Diffuse color: {DiffuseColor}"; + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Gamecube/Parameters/GCIndexFormatParameter.cs b/src/SA3D.Modeling/Mesh/Gamecube/Parameters/GCIndexFormatParameter.cs new file mode 100644 index 0000000..c5b2dc0 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Gamecube/Parameters/GCIndexFormatParameter.cs @@ -0,0 +1,31 @@ +using SA3D.Modeling.Mesh.Gamecube.Enums; + +namespace SA3D.Modeling.Mesh.Gamecube.Parameters +{ + /// + /// Holds information about the triangle lists of geometry. + /// + public struct GCIndexFormatParameter : IGCParameter + { + /// + public readonly GCParameterType Type => GCParameterType.IndexFormat; + + /// + public uint Data { get; set; } + + /// + /// Holds information about the triangle lists of geometry. + /// + public GCIndexFormat IndexFormat + { + readonly get => (GCIndexFormat)Data; + set => Data = (uint)value; + } + + /// + public override readonly string ToString() + { + return $"Index Format: {(uint)IndexFormat}"; + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Gamecube/Parameters/GCLightingParameter.cs b/src/SA3D.Modeling/Mesh/Gamecube/Parameters/GCLightingParameter.cs new file mode 100644 index 0000000..744922d --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Gamecube/Parameters/GCLightingParameter.cs @@ -0,0 +1,69 @@ +using SA3D.Modeling.Mesh.Gamecube.Enums; + +namespace SA3D.Modeling.Mesh.Gamecube.Parameters +{ + /// + /// Holds lighting information + /// + public struct GCLightingParameter : IGCParameter + { + /// + /// Default lighting parameter values for geometry using vertex colors. + /// + public static ushort DefaultColorParam = 0xB11; + + /// + /// Default lighting parameter values for geometry using normals (shading). + /// + public static ushort DefaultNormalParam = 0x211; + + /// + public readonly GCParameterType Type => GCParameterType.Lighting; + + /// + public uint Data { get; set; } + + /// + /// Lighting attributes. Pretty much unknown how they work. + /// + public ushort LightingAttributes + { + readonly get => (ushort)(Data & 0xFFFF); + set => Data = (Data & 0xFFFF0000) | value; + } + + /// + /// Which shadow stencil the geometry should use. (?) + ///
Ranges from 0 - 15. + ///
+ public byte ShadowStencil + { + readonly get => (byte)((Data >> 16) & 0xF); + set => Data = (Data & 0xFFF0FFFF) | (uint)((value & 0xF) << 16); + } + + /// + /// Unknown functionality. + /// + public byte Unknown1 + { + readonly get => (byte)((Data >> 20) & 0xF); + set => Data = (Data & 0xFF0FFFFF) | (uint)((value & 0xF) << 20); + } + + /// + /// Unknown functionality. + /// + public byte Unknown2 + { + readonly get => (byte)(Data >> 24); + set => Data = (Data & 0x00FFFFFF) | (uint)(value << 24); + } + + /// + public override readonly string ToString() + { + return $"Lighting: {LightingAttributes} - {ShadowStencil} - {Unknown1} - {Unknown2}"; + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Gamecube/Parameters/GCParameterExtensions.cs b/src/SA3D.Modeling/Mesh/Gamecube/Parameters/GCParameterExtensions.cs new file mode 100644 index 0000000..d717f41 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Gamecube/Parameters/GCParameterExtensions.cs @@ -0,0 +1,22 @@ +using SA3D.Common.IO; +namespace SA3D.Modeling.Mesh.Gamecube.Parameters +{ + /// + /// Extension methods for + /// + public static class GCParameterExtensions + { + /// + /// Writes a GC parameter to an endian stack writer. + /// + /// The parameter to write + /// The writer to write to + public static void Write(this IGCParameter parameter, EndianStackWriter writer) + { + writer.WriteByte((byte)parameter.Type); + writer.WriteEmpty(3); + writer.WriteUInt(parameter.Data); + } + + } +} diff --git a/src/SA3D.Modeling/Mesh/Gamecube/Parameters/GCSpecularColorParameter.cs b/src/SA3D.Modeling/Mesh/Gamecube/Parameters/GCSpecularColorParameter.cs new file mode 100644 index 0000000..b85f36e --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Gamecube/Parameters/GCSpecularColorParameter.cs @@ -0,0 +1,37 @@ +using SA3D.Modeling.Mesh.Gamecube.Enums; +using SA3D.Modeling.Structs; + +namespace SA3D.Modeling.Mesh.Gamecube.Parameters +{ + /// + /// Specular color of the geometry. + /// + public struct GCSpecularColorParameter : IGCParameter + { + /// + /// Specular color parameter with the color white. + /// + public static readonly GCSpecularColorParameter White = new() { Data = uint.MaxValue }; + + /// + public readonly GCParameterType Type => GCParameterType.SpecularColor; + + /// + public uint Data { get; set; } + + /// + /// Specular color of the mesh. + /// + public Color SpecularColor + { + get => new() { RGBA = Data }; + set => Data = value.RGBA; + } + + /// + public override string ToString() + { + return $"Specular color: {SpecularColor}"; + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Gamecube/Parameters/GCTexCoordParameter.cs b/src/SA3D.Modeling/Mesh/Gamecube/Parameters/GCTexCoordParameter.cs new file mode 100644 index 0000000..e864561 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Gamecube/Parameters/GCTexCoordParameter.cs @@ -0,0 +1,83 @@ +using SA3D.Modeling.Mesh.Gamecube.Enums; + +namespace SA3D.Modeling.Mesh.Gamecube.Parameters +{ + /// + /// Determines where or how the geometry gets the texture coordinates. + /// + public struct GCTexCoordParameter : IGCParameter + { + /// + /// Default values parameter. + /// + public static readonly GCTexCoordParameter DefaultValues = new() + { + TexCoordID = GCTexCoordID.TexCoord0, + TexCoordType = GCTexCoordType.Matrix2x4, + TexCoordSource = GCTexCoordSource.TexCoord0, + MatrixID = GCTexcoordMatrix.Identity + }; + + /// + /// Environment mapping parameter. + /// + public static readonly GCTexCoordParameter EnvironmentMapValues = new() + { + TexCoordID = GCTexCoordID.TexCoord0, + TexCoordType = GCTexCoordType.Matrix3x4, + TexCoordSource = GCTexCoordSource.Normal, + MatrixID = GCTexcoordMatrix.Matrix4 + }; + + + /// + public readonly GCParameterType Type => GCParameterType.Texcoord; + + /// + public uint Data { get; set; } + + + /// + /// Output channel to which calculated texture coordinates should be written to. + /// + public GCTexCoordID TexCoordID + { + readonly get => (GCTexCoordID)((Data >> 16) & 0xFF); + set => Data = (Data & 0xFF00FFFF) | ((uint)value << 16); + } + + /// + /// The function type used to generate the texture coordinates. + /// + public GCTexCoordType TexCoordType + { + readonly get => (GCTexCoordType)((Data >> 12) & 0xF); + set => Data = (Data & 0xFFFF0FFF) | ((uint)value << 12); + } + + /// + /// Input values to use for when calculating texture coordinates. + /// + public GCTexCoordSource TexCoordSource + { + readonly get => (GCTexCoordSource)((Data >> 4) & 0xFF); + set => Data = (Data & 0xFFFFF00F) | ((uint)value << 4); + } + + /// + /// Matrix slot to use when using or . + /// + public GCTexcoordMatrix MatrixID + { + readonly get => (GCTexcoordMatrix)(Data & 0xF); + set => Data = (Data & 0xFFFFFFF0) | (uint)value; + } + + + /// + public override readonly string ToString() + { + return $"Texcoord: {TexCoordID} - {TexCoordType} - {TexCoordSource} - {MatrixID}"; + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Gamecube/Parameters/GCTextureParameter.cs b/src/SA3D.Modeling/Mesh/Gamecube/Parameters/GCTextureParameter.cs new file mode 100644 index 0000000..21dc554 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Gamecube/Parameters/GCTextureParameter.cs @@ -0,0 +1,40 @@ +using SA3D.Modeling.Mesh.Gamecube.Enums; + +namespace SA3D.Modeling.Mesh.Gamecube.Parameters +{ + /// + /// Texture information for the geometry + /// + public struct GCTextureParameter : IGCParameter + { + /// + public readonly GCParameterType Type => GCParameterType.Texture; + + /// + public uint Data { get; set; } + + /// + /// Texture index. + /// + public ushort TextureID + { + readonly get => (ushort)(Data & 0xFFFF); + set => Data = (Data & 0xFFFF0000) | value; + } + + /// + /// Texture tiling properties. + /// + public GCTileMode Tiling + { + readonly get => (GCTileMode)(Data >> 16); + set => Data = (Data & 0xFFFF) | ((uint)value << 16); + } + + /// + public override readonly string ToString() + { + return $"Texture: {TextureID} - {(uint)Tiling}"; + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Gamecube/Parameters/GCUnknownParameter.cs b/src/SA3D.Modeling/Mesh/Gamecube/Parameters/GCUnknownParameter.cs new file mode 100644 index 0000000..c897045 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Gamecube/Parameters/GCUnknownParameter.cs @@ -0,0 +1,45 @@ +using SA3D.Modeling.Mesh.Gamecube.Enums; + +namespace SA3D.Modeling.Mesh.Gamecube.Parameters +{ + /// + /// Unknown but not unused. + /// + public struct GCUnknownParameter : IGCParameter + { + /// + /// Parameter with default values. + /// + public static readonly GCUnknownParameter DefaultValues = new() { Unknown1 = 4 }; + + /// + public readonly GCParameterType Type => GCParameterType.Unknown; + + /// + public uint Data { get; set; } + + /// + /// No idea what this does. + /// + public ushort Unknown1 + { + readonly get => (ushort)(Data & 0xFFFF); + set => Data = (Data & 0xFFFF0000) | value; + } + + /// + /// No idea what this does. + /// + public ushort Unknown2 + { + readonly get => (ushort)(Data >> 16); + set => Data = (Data & 0xFFFF) | ((uint)value << 16); + } + + /// + public override readonly string ToString() + { + return $"Unknown: {Unknown1} - {Unknown2}"; + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Gamecube/Parameters/GCVertexFormatParameter.cs b/src/SA3D.Modeling/Mesh/Gamecube/Parameters/GCVertexFormatParameter.cs new file mode 100644 index 0000000..278aee2 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Gamecube/Parameters/GCVertexFormatParameter.cs @@ -0,0 +1,58 @@ +using SA3D.Modeling.Mesh.Gamecube.Enums; + +namespace SA3D.Modeling.Mesh.Gamecube.Parameters +{ + /// + /// Parameter determining which types of vertex data is used by geometry. + /// + public struct GCVertexFormatParameter : IGCParameter + { + /// + public readonly GCParameterType Type => GCParameterType.VertexFormat; + + /// + public uint Data { get; set; } + + /// + /// The attribute type that this parameter applies for. + /// + public GCVertexType VertexType + { + readonly get => (GCVertexType)(Data >> 16); + set => Data = (Data & 0xFFFF) | ((uint)value << 16); + } + + /// + /// Vertex struct type being utilized. + /// + public GCStructType VertexStructType + { + readonly get => (GCStructType)((Data >> 12) & 0xF); + set => Data = (Data & 0xF000) | (((uint)value) << 12); + } + + /// + /// Vertex data type being utilized. + /// + public GCDataType VertexDataType + { + readonly get => (GCDataType)((Data >> 8) & 0xF); + set => Data = (Data & 0xF00) | (((uint)value) << 8); + } + + /// + /// Formatting info. + /// + public byte Formatting + { + readonly get => (byte)(Data & 0xFF); + set => Data = (Data & 0xFFFFFF00) | value; + } + + /// + public override readonly string ToString() + { + return $"Vertex Format: {VertexType} - {VertexStructType} - {VertexDataType} - {Formatting:X2}"; + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Gamecube/Parameters/IGCParameter.cs b/src/SA3D.Modeling/Mesh/Gamecube/Parameters/IGCParameter.cs new file mode 100644 index 0000000..c9a0ea1 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Gamecube/Parameters/IGCParameter.cs @@ -0,0 +1,53 @@ +using SA3D.Common.IO; +using SA3D.Modeling.Mesh.Gamecube.Enums; +using System; + +namespace SA3D.Modeling.Mesh.Gamecube.Parameters +{ + /// + /// Base interface for all GC parameter types. + ///
Used to store geometry information (like materials). + ///
+ public interface IGCParameter + { + /// + /// The type of parameter. + /// + public GCParameterType Type { get; } + + /// + /// All parameter data is stored in these 4 bytes. + /// + public uint Data { get; set; } + + /// + /// Reads a parameter from an endian stack reader. + /// + /// The reader to read from. + /// Address at which the parameter is located + /// The parameter that was read. + public static IGCParameter Read(EndianStackReader reader, uint address) + { + GCParameterType paramType = (GCParameterType)reader[address]; + + IGCParameter result = paramType switch + { + GCParameterType.VertexFormat => new GCVertexFormatParameter(), + GCParameterType.IndexFormat => new GCIndexFormatParameter(), + GCParameterType.Lighting => new GCLightingParameter(), + GCParameterType.BlendAlpha => new GCBlendAlphaParameter(), + GCParameterType.AmbientColor => new GCAmbientColorParameter(), + GCParameterType.DiffuseColor => new GCDiffuseColorParameter(), + GCParameterType.SpecularColor => new GCSpecularColorParameter(), + GCParameterType.Texture => new GCTextureParameter(), + GCParameterType.Unknown => new GCUnknownParameter(), + GCParameterType.Texcoord => new GCTexCoordParameter(), + _ => throw new NotSupportedException($"GC parameter type {paramType} not supported.") + }; + + result.Data = reader.ReadUInt(address + 4); + + return result; + } + } +} diff --git a/src/SA3D.Modeling/Mesh/Weighted/BufferMode.cs b/src/SA3D.Modeling/Mesh/Weighted/BufferMode.cs new file mode 100644 index 0000000..ed735ce --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Weighted/BufferMode.cs @@ -0,0 +1,23 @@ +namespace SA3D.Modeling.Mesh.Weighted +{ + /// + /// Buffer mode for meshdata. + /// + public enum BufferMode + { + /// + /// Uses preexisting buffer data. + /// + None, + + /// + /// Generates buffer meshdata. + /// + Generate, + + /// + /// Generates optimized buffer meshdata. + /// + GenerateOptimized + } +} diff --git a/src/SA3D.Modeling/Mesh/Weighted/WeightedMesh.cs b/src/SA3D.Modeling/Mesh/Weighted/WeightedMesh.cs new file mode 100644 index 0000000..fe1e7cf --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Weighted/WeightedMesh.cs @@ -0,0 +1,536 @@ +using SA3D.Common; +using SA3D.Common.Lookup; +using SA3D.Modeling.Mesh.Buffer; +using SA3D.Modeling.Mesh.Converters; +using SA3D.Modeling.ObjectData; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace SA3D.Modeling.Mesh.Weighted +{ + /// + /// Helper class for model conversions + /// + public class WeightedMesh : ICloneable + { + /// + /// Label of the weighted mesh, if available. + /// + public string? Label { get; set; } + + /// + /// Vertex data. + /// + public WeightedVertex[] Vertices { get; } + + /// + /// Triangle sets. + /// + public BufferCorner[][] TriangleSets { get; private set; } + + /// + /// Materials for each triangle set. + /// + public BufferMaterial[] Materials { get; private set; } + + /// + /// Indices to nodes at which this mesh is used. + ///
That means if this mesh has 2 root nodes, then it is rendered 2 times; Once for every root node. + ///
+ public HashSet RootIndices { get; } + + /// + /// Indices to nodes influencing this mesh, relative to the root node. + /// + public SortedSet DependingNodeIndices { get; } + + /// + /// Whether the model is influenced by multiple nodes. + /// + public bool IsWeighted => DependingNodeIndices.Count > 0; + + /// + /// Whether the model has colors (even if all colors are white). + /// + public bool HasColors { get; } + + /// + /// If true, when converting to Chunk, the final model keeps vertex colors, + /// even if weights are involved (binary weights are produced as a result). + /// + public bool ForceVertexColors { get; set; } + + /// + /// Whether Specular colors should be written to the models. + /// + public bool WriteSpecular { get; set; } + + + internal WeightedMesh( + WeightedVertex[] vertices, + BufferCorner[][] triangleSets, + BufferMaterial[] materials, + HashSet rootIndices, + SortedSet dependingNodeIndices, + bool hasColors) + { + Vertices = vertices; + TriangleSets = triangleSets; + Materials = materials; + RootIndices = rootIndices; + DependingNodeIndices = dependingNodeIndices; + HasColors = hasColors; + } + + /// + /// Creates a new weighted mesh from model data. + /// + /// Vertex data. + /// Triangle sets. + /// Materials for each triangle set. + /// Whether the model contains colors. + /// The created weighted mesh. + public static WeightedMesh Create( + WeightedVertex[] vertices, + BufferCorner[][] triangleSets, + BufferMaterial[] materials, + bool hasColors) + { + SortedSet dependingNodes = new(); + + bool storesWeights = vertices.Any(x => x.Weights != null); + + if(storesWeights && vertices.Any(x => x.Weights == null)) + { + throw new ArgumentException("Vertex weights are inconsistent! All weights must be either null or not!"); + } + + if(storesWeights) + { + foreach(WeightedVertex vtx in vertices) + { + for(int i = 0; i < vtx.Weights!.Length; i++) + { + if(vtx.Weights[i] > 0) + { + dependingNodes.Add(i); + } + } + } + + // Only root node is used, we can remove the weight arrays. + if(dependingNodes.Count == 1 && dependingNodes.Contains(0)) + { + dependingNodes.Clear(); + WeightedVertex[] cleanedVerts = new WeightedVertex[vertices.Length]; + + for(int i = 0; i < vertices.Length; i++) + { + WeightedVertex vert = vertices[i]; + cleanedVerts[i] = new(vert.Position, vert.Normal); + } + + vertices = cleanedVerts; + } + } + + return new( + vertices, + triangleSets, + materials, + new(), + dependingNodes, + hasColors); + } + + + #region Conversion + + /// + /// Attempts to convert an attach to a weighted mesh in local space. Weight information that is dependend on other attaches gets lost. + /// + /// Attach to convert. + /// How to handle buffered mesh data of the attach. + /// The converted weight mesh. + public static WeightedMesh FromAttach(Attach attach, BufferMode bufferMode) + { + Node dummy = new() + { + Attach = attach + }; + + WeightedMesh weightedMesh = FromModel(dummy, bufferMode)[0]; + weightedMesh.RootIndices.Clear(); + + return weightedMesh; + } + + /// + /// Converts buffer meshdata from attaches of a model to weighted meshes. + /// + /// Model to convert. + /// How to handle buffered mesh data of the model. + /// The converted weighted meshes. + public static WeightedMesh[] FromModel(Node model, BufferMode bufferMode) + { + switch(bufferMode) + { + case BufferMode.Generate: + model.BufferMeshData(false); + break; + case BufferMode.GenerateOptimized: + model.BufferMeshData(true); + break; + case BufferMode.None: + default: + break; + } + + WeightedMesh[] result = ToWeightedConverter.ConvertToWeighted(model); + EnsurePolygonsValid(ref result); + return result; + } + + /// + /// Attempts to convert the weighted mesh to a standalone attach. + /// + /// Attach format to convert to. + /// Whether to optimize the mesh info. + /// Whether to ignore losing weight information. + /// The converted attach. + /// + public Attach ToAttach(AttachFormat format, bool optimize, bool ignoreWeights) + { + HashSet backup = new(RootIndices); + RootIndices.Clear(); + RootIndices.Add(0); + + Node dummy = new(); + ToModel(dummy, new[] { this }, format, optimize, ignoreWeights); + + RootIndices.Clear(); + RootIndices.UnionWith(backup); + + return dummy.Attach ?? throw new InvalidOperationException("Attach failed to convert."); + } + + /// + /// Converts weighted meshes to a given attach format and assigns them to a model. + /// + /// Model to assign the converted meshes to. + /// Meshes to convert. + /// Attach format to convert to. + /// Whether to optimize the mesh info. + /// Whether to ignore losing weight information. + public static void ToModel(Node model, WeightedMesh[] meshes, AttachFormat format, bool optimize, bool ignoreWeights = false) + { + EnsurePolygonsValid(ref meshes); + + switch(format) + { + case AttachFormat.Buffer: + FromWeightedConverter.Convert(model, meshes, optimize); + break; + case AttachFormat.BASIC: + BasicConverter.ConvertWeightedToBasic(model, meshes, optimize, ignoreWeights); + break; + case AttachFormat.CHUNK: + ChunkConverter.ConvertWeightedToChunk(model, meshes, optimize); + break; + case AttachFormat.GC: + GCConverter.ConvertWeightedToGC(model, meshes, optimize, ignoreWeights); + break; + default: + throw new ArgumentException("Invalid attach format.", nameof(format)); + } + } + + #endregion + + #region Utilities + + /// + /// Checks for degenerate triangles and removes them. + /// + /// Whether the model contained degenerate triangles that have been removed. + public bool EnsurePolygonsValid() + { + List newTriangleSets = new(); + List newMaterials = new(); + bool changed = false; + + for(int i = 0; i < TriangleSets.Length; i++) + { + List degenerates = new(); + + BufferCorner[] triangles = TriangleSets[i]; + for(int j = 0; j < triangles.Length; j += 3) + { + int index1 = triangles[j].VertexIndex; + int index2 = triangles[j + 1].VertexIndex; + int index3 = triangles[j + 2].VertexIndex; + + if(index1 == index2 + || index2 == index3 + || index3 == index1) + { + degenerates.Add(j); + } + } + + changed |= degenerates.Count > 0; + + if(degenerates.Count == triangles.Length / 3) + { + continue; + } + + newMaterials.Add(Materials[i]); + + if(degenerates.Count == 0) + { + newTriangleSets.Add(triangles); + } + else + { + List newTriangles = new(); + for(int j = 0; j < triangles.Length; j += 3) + { + if(degenerates.Count > 0 && degenerates[0] == j) + { + degenerates.RemoveAt(0); + continue; + } + + newTriangles.Add(triangles[j]); + newTriangles.Add(triangles[j + 1]); + newTriangles.Add(triangles[j + 2]); + } + + newTriangleSets.Add(newTriangles.ToArray()); + } + } + + if(changed) + { + TriangleSets = newTriangleSets.ToArray(); + Materials = newMaterials.ToArray(); + } + + return changed; + } + + /// + /// Checks every mesh for degenerate triangles and removes them. + /// + /// The meshes to correct. + public static void EnsurePolygonsValid(ref WeightedMesh[] meshes) + { + List newWBAs = new(); + bool changed = false; + + foreach(WeightedMesh wba in meshes) + { + if(wba.EnsurePolygonsValid() && wba.TriangleSets.Length == 0) + { + changed = true; + continue; + } + + newWBAs.Add(wba); + } + + if(changed) + { + meshes = newWBAs.ToArray(); + } + } + + /// + /// Combines vertex and polygon data of multiple weighted meshes together. + /// + /// The weighted meshes to merge. + /// The merged mesh. + /// + public static WeightedMesh MergeWeightedMeshes(IEnumerable meshes) + { + int count = meshes.Count(); + if(count == 0) + { + throw new ArgumentException("No attaches in enumerable", nameof(meshes)); + } + else if(count == 1) + { + return meshes.First(); + } + + List vertices = new(); + List corners = new(); + List materials = new(); + SortedSet dependingNodes = new(); + bool hasColors = false; + + int maxNode = meshes.Max(x => x.DependingNodeIndices.Max); + int weightNum = maxNode + 1; + + foreach(WeightedMesh mesh in meshes) + { + materials.AddRange(mesh.Materials); + hasColors |= mesh.HasColors; + + if(vertices.Count > 0) + { + int vertexOffset = vertices.Count; + foreach(BufferCorner[] wbaTriangles in mesh.TriangleSets) + { + BufferCorner[] outTriangles = new BufferCorner[wbaTriangles.Length]; + for(int i = 0; i < wbaTriangles.Length; i++) + { + outTriangles[i].VertexIndex = (ushort)(wbaTriangles[i].VertexIndex + vertexOffset); + } + + corners.Add(outTriangles); + } + } + else + { + corners.AddRange(mesh.TriangleSets.ContentClone()); + } + + if(maxNode == 0) + { + vertices.AddRange(mesh.Vertices); + } + else if(maxNode == mesh.DependingNodeIndices.Max) + { + vertices.AddRange(mesh.Vertices.ContentClone()); + dependingNodes.UnionWith(mesh.DependingNodeIndices); + } + else if(mesh.DependingNodeIndices.Count == 0) + { + for(int i = 0; i < mesh.Vertices.Length; i++) + { + WeightedVertex vert = mesh.Vertices[i]; + WeightedVertex newVert = new(vert.Position, vert.Normal, weightNum); + newVert.Weights![0] = 1; + vertices.Add(newVert); + } + + dependingNodes.Add(0); + } + else + { + for(int i = 0; i < mesh.Vertices.Length; i++) + { + WeightedVertex vert = mesh.Vertices[i]; + WeightedVertex newVert = new(vert.Position, vert.Normal, weightNum); + + Array.Copy(vert.Weights!, newVert.Weights!, vert.Weights!.Length); + + vertices.Add(newVert); + } + + dependingNodes.UnionWith(mesh.DependingNodeIndices); + } + } + + return new( + vertices.ToArray(), + corners.ToArray(), + materials.ToArray(), + new(), + dependingNodes, + hasColors); + } + + /// + /// Merges weighted meshes at their specified roots. + /// + /// The meshes to merge. + /// The merged meshes. + public static WeightedMesh[] MergeAtRoots(WeightedMesh[] meshes) + { + List?[] sharedRoots = new List[meshes.Max(x => x.RootIndices.Max()) + 1]; + + foreach(WeightedMesh mesh in meshes) + { + foreach(int rootIndex in mesh.RootIndices) + { + List? list = sharedRoots[rootIndex]; + + if(list == null) + { + list = new(); + sharedRoots[rootIndex] = list; + } + + list.Add(mesh); + } + } + + if(!sharedRoots.Any(x => x?.Count > 1)) + { + return meshes; + } + + List result = new(); + + for(int i = 0; i < sharedRoots.Length; i++) + { + List? item = sharedRoots[i]; + if(item == null) + { + continue; + } + + if(item.Count == 1) + { + if(item[0].RootIndices.Count == 1) + { + result.Add(item[0]); + } + else + { + // TODO this should probably be improved for edge cases where weighted meshes thrice, of which 2 instances can be reused. + WeightedMesh clonedMesh = item[0].Clone(); + clonedMesh.RootIndices.Clear(); + clonedMesh.RootIndices.Add(i); + result.Add(clonedMesh); + } + + continue; + } + + WeightedMesh merged = MergeWeightedMeshes(item); + merged.RootIndices.Add(i); + result.Add(merged); + } + + return result.ToArray(); + } + + + object ICloneable.Clone() + { + return Clone(); + } + + /// + /// Creates a deep clone of the mesh. + /// + /// The cloned mesh. + public WeightedMesh Clone() + { + return new( + Vertices.ContentClone(), + TriangleSets.ContentClone(), + Materials.ToArray(), + new(RootIndices), + new(DependingNodeIndices), + HasColors); + } + + #endregion + } +} diff --git a/src/SA3D.Modeling/Mesh/Weighted/WeightedVertex.cs b/src/SA3D.Modeling/Mesh/Weighted/WeightedVertex.cs new file mode 100644 index 0000000..64a4321 --- /dev/null +++ b/src/SA3D.Modeling/Mesh/Weighted/WeightedVertex.cs @@ -0,0 +1,313 @@ +using System; +using System.Collections.Generic; +using System.Numerics; + +namespace SA3D.Modeling.Mesh.Weighted +{ + /// + /// Vertex with a position, direction and weight values. + /// + public struct WeightedVertex : IEquatable, ICloneable + { + /// + /// Position in 3D space. + /// + public Vector3 Position { get; set; } + + /// + /// Normalized direction. + /// + public Vector3 Normal { get; set; } + + /// + /// Weights per node relative to the base node of the weighted mesh that this vertex belongs to. + /// + public float[]? Weights { get; } + + + /// + /// Create a new vertex with no weight values. + /// + /// Position in 3D space. + /// Normalized direction. + public WeightedVertex(Vector3 position, Vector3 normal) + { + Position = position; + Normal = normal; + Weights = null; + } + + /// + /// Create a new vertex with empty weight values. + /// + /// Position in 3D space. + /// Normalized direction. + /// Number of nodes that this vertex covers. + public WeightedVertex(Vector3 position, Vector3 normal, int nodeCount) + { + Position = position; + Normal = normal; + Weights = new float[nodeCount]; + } + + /// + /// Create a new vertex. + /// + /// Position in 3D space. + /// Normalized direction. + /// Weight values to use. + public WeightedVertex(Vector3 position, Vector3 normal, float[]? weights) + { + Position = position; + Normal = normal; + Weights = weights; + } + + + /// + /// Returns a clone of the vertex + /// + /// + public readonly WeightedVertex Normalized() + { + return new(Position, Vector3.Normalize(Normal), (float[]?)Weights?.Clone()); + } + + /// + /// Checks whether the vertex has more than one weight. + /// + /// Whether the vertex has more than one weight. + public readonly bool IsWeighted() + { + if(Weights == null) + { + return false; + } + + bool oneWeight = false; + for(int i = 0; i < Weights.Length; i++) + { + if(Weights[i] > 0f) + { + if(oneWeight) + { + return true; + } + + oneWeight = true; + } + } + + return false; + } + + /// + /// Gets the number of weight values greater than 0. + /// + /// The number of weight values greater than 0. + public readonly int GetWeightCount() + { + if(Weights == null) + { + return 0; + } + + int count = 0; + for(int i = 0; i < Weights.Length; i++) + { + float weight = Weights[i]; + if(weight > 0f) + { + count++; + } + } + + return count; + } + + /// + /// Compiles an array of node indices mapped to their weight values for weights greater than 0. + /// + /// The mapped weight values. + /// + public readonly (int nodeIndex, float weight)[] GetWeightMap() + { + if(Weights == null) + { + throw new InvalidOperationException(); + } + + List<(int nodeIndex, float weight)> result = new(); + for(int i = 0; i < Weights.Length; i++) + { + float weight = Weights[i]; + if(weight > 0f) + { + result.Add((i, weight)); + } + } + + return result.ToArray(); + } + + /// + /// Determines the index of the first weight greater than 0. + /// + /// The index of the first weight greater than 0 + /// + public readonly int GetFirstWeightIndex() + { + if(Weights == null) + { + throw new InvalidOperationException(); + } + + for(int i = 0; i < Weights.Length; i++) + { + if(Weights[i] > 0f) + { + return i; + } + } + + return -1; + } + + /// + /// Determines the index of the last weight greater than 0. + /// + /// The index of the last weight greater than 0 + /// + public readonly int GetLastWeightIndex() + { + if(Weights == null) + { + throw new InvalidOperationException(); + } + + for(int i = Weights.Length - 1; i >= 0; i--) + { + if(Weights[i] > 0f) + { + return i; + } + } + + return -1; + } + + /// + /// Determines the index of the greatest weight greater than 0. If all weights are 0, it will return -1. + /// + /// The index of the greatest weight greater than 0. + /// + public readonly int GetMaxWeightIndex() + { + if(Weights == null) + { + throw new InvalidOperationException(); + } + + int result = -1; + float weightCheck = 0; + for(int i = 0; i < Weights.Length; i++) + { + float weight = Weights[i]; + if(weight > weightCheck) + { + weightCheck = weight; + result = i; + } + } + + return result; + } + + #region Comparisons + + /// + public override readonly bool Equals(object? obj) + { + return obj is WeightedVertex vertex && + Position.Equals(vertex.Position) && + Normal.Equals(vertex.Normal) && + EqualityComparer.Default.Equals(Weights, vertex.Weights); + } + + /// + public override readonly int GetHashCode() + { + return HashCode.Combine(Position, Normal, Weights); + } + + readonly bool IEquatable.Equals(WeightedVertex other) + { + return Equals(other); + } + + /// + /// Compares two weighted vertices for equality. + /// + /// Lefthand vertex. + /// Righthand vertex. + /// Whether the vertices are equal. + public static bool operator ==(WeightedVertex left, WeightedVertex right) + { + return left.Equals(right); + } + + /// + /// Compares two weighted vertices for inequality. + /// + /// Lefthand vertex. + /// Righthand vertex. + /// Whether the vertices are inequal. + public static bool operator !=(WeightedVertex left, WeightedVertex right) + { + return !(left == right); + } + + #endregion + + /// + /// Creates a clone of the vertex. + /// + /// The cloned vertex. + public readonly WeightedVertex Clone() + { + return new(Position, Normal, (float[]?)Weights?.Clone()); + } + + readonly object ICloneable.Clone() + { + return Clone(); + } + + /// + public override readonly string ToString() + { + if(Weights == null) + { + return "NULL"; + } + + int weightCount = 0; + string result = ""; + + for(int i = 0; i < Weights.Length; i++) + { + float weight = Weights[i]; + if(weight == 0f) + { + continue; + } + + weightCount++; + result += i + ", "; + } + + return $"{weightCount} - {result}"; + } + + } +} diff --git a/src/SA3D.Modeling/ObjectData/Enums/EnumExtensions.cs b/src/SA3D.Modeling/ObjectData/Enums/EnumExtensions.cs new file mode 100644 index 0000000..c2a14da --- /dev/null +++ b/src/SA3D.Modeling/ObjectData/Enums/EnumExtensions.cs @@ -0,0 +1,196 @@ +namespace SA3D.Modeling.ObjectData.Enums +{ + /// + /// Extension methods for object data enums. + /// + public static class EnumExtensions + { + private static readonly (SA1SurfaceAttributes sa1, SurfaceAttributes universal)[] _sa1SurfaceAttributeMapping = new[] + { + ( SA1SurfaceAttributes.Solid, SurfaceAttributes.Solid ), + ( SA1SurfaceAttributes.Water, SurfaceAttributes.Water ), + ( SA1SurfaceAttributes.NoFriction, SurfaceAttributes.NoFriction ), + ( SA1SurfaceAttributes.NoAcceleration, SurfaceAttributes.NoAcceleration ), + + ( SA1SurfaceAttributes.LowAcceleration, SurfaceAttributes.LowAcceleration ), + ( SA1SurfaceAttributes.UseSkyDrawDistance, SurfaceAttributes.UseSkyDrawDistance ), + ( SA1SurfaceAttributes.CannotLand, SurfaceAttributes.CannotLand ), + ( SA1SurfaceAttributes.IncreasedAcceleration, SurfaceAttributes.IncreasedAcceleration ), + + ( SA1SurfaceAttributes.Diggable, SurfaceAttributes.Diggable ), + ( SA1SurfaceAttributes.Unknown9, SurfaceAttributes.SA1_Unknown9 ), + ( SA1SurfaceAttributes.Waterfall, SurfaceAttributes.Waterfall ), + ( SA1SurfaceAttributes.Unknown11, SurfaceAttributes.SA1_Unknown11 ), + + ( SA1SurfaceAttributes.Unclimbable, SurfaceAttributes.Unclimbable ), + ( SA1SurfaceAttributes.Chaos0Land, SurfaceAttributes.Chaos0Land ), + ( SA1SurfaceAttributes.Stairs, SurfaceAttributes.Stairs ), + ( SA1SurfaceAttributes.Unknown15, SurfaceAttributes.SA1_Unknown15 ), + + ( SA1SurfaceAttributes.Hurt, SurfaceAttributes.Hurt ), + ( SA1SurfaceAttributes.TubeAcceleration, SurfaceAttributes.TubeAcceleration ), + ( SA1SurfaceAttributes.LowDepth, SurfaceAttributes.LowDepth ), + ( SA1SurfaceAttributes.Unknown19, SurfaceAttributes.SA1_Unknown19 ), + + ( SA1SurfaceAttributes.Footprints, SurfaceAttributes.Footprints ), + ( SA1SurfaceAttributes.Accelerate, SurfaceAttributes.Accelerate ), + ( SA1SurfaceAttributes.WaterCollision, SurfaceAttributes.WaterCollision ), + ( SA1SurfaceAttributes.Gravity, SurfaceAttributes.Gravity ), + + ( SA1SurfaceAttributes.NoZWrite, SurfaceAttributes.NoZWrite ), + ( SA1SurfaceAttributes.DrawByMesh, SurfaceAttributes.DrawByMesh ), + ( SA1SurfaceAttributes.EnableManipulation, SurfaceAttributes.EnableManipulation ), + ( SA1SurfaceAttributes.DynamicCollision, SurfaceAttributes.DynamicCollision ), + + ( SA1SurfaceAttributes.TransformBounds, SurfaceAttributes.TransformBounds ), + ( SA1SurfaceAttributes.BoundsRadiusSmall, SurfaceAttributes.BoundsRadiusSmall ), + ( SA1SurfaceAttributes.BoundsRadiusTiny, SurfaceAttributes.BoundsRadiusTiny ), + ( SA1SurfaceAttributes.Visible, SurfaceAttributes.Visible ), + }; + + private static readonly (SA2SurfaceAttributes sa2, SurfaceAttributes universal)[] _sa2SurfaceAttributeMapping = new[] + { + ( SA2SurfaceAttributes.Solid, SurfaceAttributes.Solid ), + ( SA2SurfaceAttributes.Water, SurfaceAttributes.Water ), + ( SA2SurfaceAttributes.NoFriction, SurfaceAttributes.NoFriction ), + ( SA2SurfaceAttributes.NoAcceleration, SurfaceAttributes.NoAcceleration ), + + ( SA2SurfaceAttributes.LowAcceleration, SurfaceAttributes.LowAcceleration ), + ( SA2SurfaceAttributes.Diggable, SurfaceAttributes.Diggable ), + ( SA2SurfaceAttributes.Unknown6, SurfaceAttributes.SA2_Unknown6 ), + ( SA2SurfaceAttributes.Unclimbable, SurfaceAttributes.Unclimbable ), + + ( SA2SurfaceAttributes.Stairs, SurfaceAttributes.Stairs ), + ( SA2SurfaceAttributes.Unknown9, SurfaceAttributes.SA2_Unknown9 ), + ( SA2SurfaceAttributes.Hurt, SurfaceAttributes.Hurt ), + ( SA2SurfaceAttributes.Footprints, SurfaceAttributes.Footprints ), + + ( SA2SurfaceAttributes.CannotLand, SurfaceAttributes.CannotLand ), + ( SA2SurfaceAttributes.WaterNoAlpha, SurfaceAttributes.WaterNoAlpha ), + ( SA2SurfaceAttributes.Unknown14, SurfaceAttributes.SA2_Unknown14 ), + ( SA2SurfaceAttributes.NoShadows, SurfaceAttributes.NoShadows ), + + ( SA2SurfaceAttributes.Unknown16, SurfaceAttributes.SA2_Unknown16 ), + ( SA2SurfaceAttributes.Unknown17, SurfaceAttributes.SA2_Unknown17 ), + ( SA2SurfaceAttributes.Unknown18, SurfaceAttributes.SA2_Unknown18 ), + ( SA2SurfaceAttributes.Gravity, SurfaceAttributes.Gravity ), + + ( SA2SurfaceAttributes.TubeAcceleration, SurfaceAttributes.TubeAcceleration ), + ( SA2SurfaceAttributes.IncreasedAcceleration, SurfaceAttributes.IncreasedAcceleration ), + ( SA2SurfaceAttributes.NoFog, SurfaceAttributes.NoFog ), + ( SA2SurfaceAttributes.UseSkyDrawDistance, SurfaceAttributes.UseSkyDrawDistance ), + + ( SA2SurfaceAttributes.EasyDraw, SurfaceAttributes.EasyDraw ), + ( SA2SurfaceAttributes.Unknown25, SurfaceAttributes.SA2_Unknown25 ), + ( SA2SurfaceAttributes.Unknown26, SurfaceAttributes.SA2_Unknown26 ), + ( SA2SurfaceAttributes.DynamicCollision, SurfaceAttributes.DynamicCollision ), + + ( SA2SurfaceAttributes.TransformBounds, SurfaceAttributes.TransformBounds ), + ( SA2SurfaceAttributes.BoundsRadiusSmall, SurfaceAttributes.BoundsRadiusSmall ), + ( SA2SurfaceAttributes.BoundsRadiusTiny, SurfaceAttributes.BoundsRadiusTiny ), + ( SA2SurfaceAttributes.Visible, SurfaceAttributes.Visible ), + }; + + /// + /// Converts from SA1 surface flags to the combined surface flags. + /// + /// Attributes to convert. + /// The converted attributes. + public static SurfaceAttributes ToUniversal(this SA1SurfaceAttributes attributes) + { + SurfaceAttributes result = 0; + + foreach((SA1SurfaceAttributes sa1, SurfaceAttributes universal) in _sa1SurfaceAttributeMapping) + { + if(attributes.HasFlag(sa1)) + { + result |= universal; + } + } + + return result; + } + + /// + /// Converts from SA2 surface flags to the combined surface flags. + /// + /// Attributes to convert. + /// The converted attributes. + public static SurfaceAttributes ToUniversal(this SA2SurfaceAttributes attributes) + { + SurfaceAttributes result = 0; + + foreach((SA2SurfaceAttributes sa2, SurfaceAttributes universal) in _sa2SurfaceAttributeMapping) + { + if(attributes.HasFlag(sa2)) + { + result |= universal; + } + } + + return result; + } + + /// + /// Converts from the combined surface flags to SA1 surface flags. + /// + /// Attributes to convert. + /// The converted attributes. + public static SA1SurfaceAttributes ToSA1(this SurfaceAttributes attributes) + { + SA1SurfaceAttributes result = 0; + + foreach((SA1SurfaceAttributes sa1, SurfaceAttributes universal) in _sa1SurfaceAttributeMapping) + { + if(attributes.HasFlag(universal)) + { + result |= sa1; + } + } + + return result; + } + + /// + /// Converts from the combined surface flags to SA2 surface flags. + /// + /// Attributes to convert. + /// The converted attributes. + public static SA2SurfaceAttributes ToSA2(this SurfaceAttributes attributes) + { + SA2SurfaceAttributes result = 0; + + foreach((SA2SurfaceAttributes sa2, SurfaceAttributes universal) in _sa2SurfaceAttributeMapping) + { + if(attributes.HasFlag(universal)) + { + result |= sa2; + } + } + + return result; + } + + /// + /// Checks whether the surface attributes enable rendering. + /// + /// The attributes to check. + /// + public static bool CheckIsVisual(this SurfaceAttributes attributes) + { + return attributes.HasFlag(SurfaceAttributes.Visible); + } + + /// + /// Checks whether the surface attributes use collision behavior. + /// + /// The attributes to check. + /// + public static bool CheckIsCollision(this SurfaceAttributes attributes) + { + return attributes.HasFlag(SurfaceAttributes.Solid) + || attributes.HasFlag(SurfaceAttributes.Water) + || attributes.HasFlag(SurfaceAttributes.WaterNoAlpha); + } + } +} diff --git a/src/SA3D.Modeling/ObjectData/Enums/LandTableAttributes.cs b/src/SA3D.Modeling/ObjectData/Enums/LandTableAttributes.cs new file mode 100644 index 0000000..df0d690 --- /dev/null +++ b/src/SA3D.Modeling/ObjectData/Enums/LandTableAttributes.cs @@ -0,0 +1,28 @@ +namespace SA3D.Modeling.ObjectData.Enums +{ + /// + /// Landtable attributes. + /// + public enum LandtableAttributes : uint + { + /// + /// Enables geometry motions. + /// + EnableMotions = 0x1, + + /// + /// Utilizes specified texture list. + /// + LoadTexlist = 0x2, + + /// + /// Specifies a custom draw distance. + /// + CustomDrawDistance = 0x4, + + /// + /// Loads texture from file. + /// + LoadTextureFile = 0x8, + } +} diff --git a/src/SA3D.Modeling/ObjectData/Enums/ModelFormat.cs b/src/SA3D.Modeling/ObjectData/Enums/ModelFormat.cs new file mode 100644 index 0000000..43438ca --- /dev/null +++ b/src/SA3D.Modeling/ObjectData/Enums/ModelFormat.cs @@ -0,0 +1,33 @@ +namespace SA3D.Modeling.ObjectData.Enums +{ + /// + /// Model data formats. + /// + public enum ModelFormat : int + { + /// + /// Sonic Adventure 1; Uses BASIC. + /// + SA1 = 0, + + /// + /// Sonic Adventure DX; Uses BASIC. + /// + SADX = 1, + + /// + /// Sonic Adventure 2; Uses CHUNK (and BASIC). + /// + SA2 = 2, + + /// + /// Sonic Adventure 2 Battle; Uses GC (and BASIC). + /// + SA2B = 3, + + /// + /// SA3D in-house API format. + /// + Buffer = 4 + } +} diff --git a/src/SA3D.Modeling/ObjectData/Enums/NodeAttributes.cs b/src/SA3D.Modeling/ObjectData/Enums/NodeAttributes.cs new file mode 100644 index 0000000..2c5f4fd --- /dev/null +++ b/src/SA3D.Modeling/ObjectData/Enums/NodeAttributes.cs @@ -0,0 +1,67 @@ +using SA3D.Common; +using System; + +namespace SA3D.Modeling.ObjectData.Enums +{ + /// + /// Node attributes + /// + [Flags] + public enum NodeAttributes : uint + { + /// + /// The nodes position has no effect on the transforms. + /// + NoPosition = Flag32.B0, + + /// + /// The nodes rotation has no effect on the transforms. + /// + NoRotation = Flag32.B1, + + /// + /// The nodes scale has no effect on the transforms. + /// + NoScale = Flag32.B2, + + /// + /// Node should be skipped for attach related evaluations. Required if node has no attach. + /// + SkipDraw = Flag32.B3, + + /// + /// Child of the node is skipped. Required if the node has no child. + /// + SkipChildren = Flag32.B4, + + /// + /// Euler angles are applied in ZYX order, instead of XYZ. + /// + RotateZYX = Flag32.B5, + + /// + /// Ignore this node (but not its children) entirely for when evaluation an animation. + /// + NoAnimate = Flag32.B6, + + /// + /// Ignore this node (but not its children) entirely for when evaluation a shape animation. + /// + NoMorph = Flag32.B7, + + /// + /// ? + /// + Clip = Flag32.B8, + + /// + /// ? + /// + Modifier = Flag32.B9, + + /// + /// Node uses a quaternion instead of euler angle for rotation information. + /// + UseQuaternionRotation = Flag32.B10 + } +} diff --git a/src/SA3D.Modeling/ObjectData/Enums/RotationUpdateMode.cs b/src/SA3D.Modeling/ObjectData/Enums/RotationUpdateMode.cs new file mode 100644 index 0000000..3f40702 --- /dev/null +++ b/src/SA3D.Modeling/ObjectData/Enums/RotationUpdateMode.cs @@ -0,0 +1,23 @@ +namespace SA3D.Modeling.ObjectData.Enums +{ + /// + /// Determines how the rotation values of a node should be handled after the rotation order has been changed. + /// + public enum RotationUpdateMode + { + /// + /// Keeps the euler angles but updates the quaternion rotation based on the new order. + /// + UpdateQuaternion, + + /// + /// Keeps the quaternion rotation but updates the euler to represent the same final rotation based on the new order. + /// + UpdateEuler, + + /// + /// Keeps animation values as they are. (not recommended) + /// + Keep, + } +} diff --git a/src/SA3D.Modeling/ObjectData/Enums/SA1SurfaceAttributes.cs b/src/SA3D.Modeling/ObjectData/Enums/SA1SurfaceAttributes.cs new file mode 100644 index 0000000..92a6f2c --- /dev/null +++ b/src/SA3D.Modeling/ObjectData/Enums/SA1SurfaceAttributes.cs @@ -0,0 +1,174 @@ +using SA3D.Common; +using System; + +namespace SA3D.Modeling.ObjectData.Enums +{ + /// + /// SA1 geometry surface information. + /// + [Flags] + public enum SA1SurfaceAttributes : uint + { + /// + /// Makes the geometry collidable with. + /// + Solid = Flag32.B0, + + /// + /// Adds water ripple effect when passing through geometry. + /// + Water = Flag32.B1, + + /// + /// Surface has no (or less) friction. Player will slip around, like on ice. + /// + NoFriction = Flag32.B2, + + /// + /// Reduces the players acceleration to 0. + /// + NoAcceleration = Flag32.B3, + + /// + /// Reduces the players acceleration. + /// + LowAcceleration = Flag32.B4, + + /// + /// Uses skybox drawdistance. + /// + UseSkyDrawDistance = Flag32.B5, + + /// + /// Surface will prevent players from standing on it. + /// + CannotLand = Flag32.B6, + + /// + /// Increases the players acceleration. + /// + IncreasedAcceleration = Flag32.B7, + + /// + /// Lets the player dig on the surface. + /// + Diggable = Flag32.B8, + + /// + /// Unknown functionality. + /// + Unknown9 = Flag32.B9, + + /// + /// Force alpha sorting; Disable Z Write when used together with Water; Force disable Z write in all levels except Lost World 2 + /// + Waterfall = Flag32.B10, + + /// + /// Unknown functionality. + /// + Unknown11 = Flag32.B11, + + /// + /// Surface cannot be climbed by players. + /// + Unclimbable = Flag32.B12, + + /// + /// Turns off Visible when Chaos 0 jumps up a pole + /// + Chaos0Land = Flag32.B13, + + /// + /// The player wont slip on the surface, regardless of how steep it is. + /// + Stairs = Flag32.B14, + + /// + /// Unknown functionality. + /// + Unknown15 = Flag32.B15, + + /// + /// Damages the player on contact. + /// + Hurt = Flag32.B16, + + /// + /// Tube acceleration (?). + /// + TubeAcceleration = Flag32.B17, + + /// + /// Affects drawing queue (?). + /// + LowDepth = Flag32.B18, + + /// + /// Unknown functionality. + /// + Unknown19 = Flag32.B19, + + /// + /// Renders footprints when running on the surface. + /// + Footprints = Flag32.B20, + + /// + /// Accelerates the player (?). + /// + Accelerate = Flag32.B21, + + /// + /// Changes player state to swimming when in contact. + /// + WaterCollision = Flag32.B22, + + /// + /// Keeps the player stuck to surfaces by altering gravitional direction (?). + /// + Gravity = Flag32.B23, + + /// + /// Does not write to the Z buffer. + /// + NoZWrite = Flag32.B24, + + /// + /// Sorts the transparent meshes within the attaches, instead of grouping them all together for sorting (?). + /// + DrawByMesh = Flag32.B25, + + /// + /// Dont store vertices in buffer. + /// + EnableManipulation = Flag32.B26, + + /// + /// Allows for transforming collisions. + /// + DynamicCollision = Flag32.B27, + + /// + /// Bounds center is offset from 0,0,0 and gets influenced by rotation and scale. + /// + TransformBounds = Flag32.B28, + + /// + /// Bounds radius is > 10 and <= 20 units. + ///
If combined with , its >= 3. + ///
+ BoundsRadiusSmall = Flag32.B29, + + /// + /// Bounds radius is > 3 and <= 10 units. + ///
If combined with , its >= 3. + ///
+ BoundsRadiusTiny = Flag32.B30, + + /// + /// Enables rendering for the geometry. + /// + Visible = Flag32.B31 + } +} diff --git a/src/SA3D.Modeling/ObjectData/Enums/SA2SurfaceAttributes.cs b/src/SA3D.Modeling/ObjectData/Enums/SA2SurfaceAttributes.cs new file mode 100644 index 0000000..3b1ca53 --- /dev/null +++ b/src/SA3D.Modeling/ObjectData/Enums/SA2SurfaceAttributes.cs @@ -0,0 +1,174 @@ +using SA3D.Common; +using System; + +namespace SA3D.Modeling.ObjectData.Enums +{ + /// + /// SA2 geometry surface information. + /// + [Flags] + public enum SA2SurfaceAttributes : uint + { + /// + /// Makes the geometry collidable with. + /// + Solid = Flag32.B0, + + /// + /// Adds water ripple effect when passing through geometry. + /// + Water = Flag32.B1, + + /// + /// Surface has no (or less) friction. Player will slip around, like on ice. + /// + NoFriction = Flag32.B2, + + /// + /// Reduces the players acceleration to 0. + /// + NoAcceleration = Flag32.B3, + + /// + /// Reduces the players acceleration. + /// + LowAcceleration = Flag32.B4, + + /// + /// Lets the player dig on the surface. + /// + Diggable = Flag32.B5, + + /// + /// Unknown functionality. + /// + Unknown6 = Flag32.B6, + + /// + /// Surface cannot be climbed by players. + /// + Unclimbable = Flag32.B7, + + /// + /// The player wont slip on the surface, regardless of how steep it is. + /// + Stairs = Flag32.B8, + + /// + /// Unknown functionality. + /// + Unknown9 = Flag32.B9, + + /// + /// Damages the player on contact. + /// + Hurt = Flag32.B10, + + /// + /// Renders footprints when running on the surface. + /// + Footprints = Flag32.B11, + + /// + /// Surface will prevent players from standing on it. + /// + CannotLand = Flag32.B12, + + /// + /// Adds water ripple effect when passing through geometry, but doesn't mess with the transparent queue (?). + /// + WaterNoAlpha = Flag32.B13, + + /// + /// Unknown functionality. + /// + Unknown14 = Flag32.B14, + + /// + /// Surface will not render shadows. + /// + NoShadows = Flag32.B15, + + /// + /// Unknown functionality. + /// + Unknown16 = Flag32.B16, + + /// + /// Unknown functionality. + /// + Unknown17 = Flag32.B17, + + /// + /// Unknown functionality. + /// + Unknown18 = Flag32.B18, + + /// + /// Keeps the player stuck to surfaces by altering gravitional direction (?). + /// + Gravity = Flag32.B19, + + /// + /// Tube acceleration (?). + /// + TubeAcceleration = Flag32.B20, + + /// + /// Increases the players acceleration. + /// + IncreasedAcceleration = Flag32.B21, + + /// + /// Surface ignores fog when rendering. + /// + NoFog = Flag32.B22, + + /// + /// Uses skybox drawdistance. + /// + UseSkyDrawDistance = Flag32.B23, + + /// + /// Uses easy drawing function (dreamcast functionality). + /// + EasyDraw = Flag32.B24, + + /// + /// Unknown functionality. + /// + Unknown25 = Flag32.B25, + + /// + /// Unknown functionality. + /// + Unknown26 = Flag32.B26, + + /// + /// Moving collision + /// + DynamicCollision = Flag32.B27, + + /// + /// Bounds center is offset from 0,0,0 and gets influenced by rotation and scale. + /// + TransformBounds = Flag32.B28, + + /// + /// Bounds radius is > 10 and <= 20 units. + ///
If combined with , its >= 3. + ///
+ BoundsRadiusSmall = Flag32.B29, + + /// + /// Bounds radius is > 3 and <= 10 units. + ///
If combined with , its >= 3. + ///
+ BoundsRadiusTiny = Flag32.B30, + + /// + /// Enables rendering for the geometry. + /// + Visible = Flag32.B31 + } +} diff --git a/src/SA3D.Modeling/ObjectData/Enums/SurfaceAttributes.cs b/src/SA3D.Modeling/ObjectData/Enums/SurfaceAttributes.cs new file mode 100644 index 0000000..9745b98 --- /dev/null +++ b/src/SA3D.Modeling/ObjectData/Enums/SurfaceAttributes.cs @@ -0,0 +1,356 @@ +using SA3D.Common; +using System; + +namespace SA3D.Modeling.ObjectData.Enums +{ + /// + /// Geometry surface information
+ /// Combination of and + ///
+ [Flags] + public enum SurfaceAttributes : ulong + { + /// + /// Enables rendering for the geometry. + ///
SA1 & SA2 + ///
+ Visible = Flag64.B0, + + /// + /// Makes the geometry collidable with. + ///
SA1 & SA2 + ///
+ Solid = Flag64.B1, + + /// + /// Adds water ripple effect when passing through geometry. + ///
SA1 & SA2 + ///
+ Water = Flag64.B2, + + /// + /// Adds water ripple effect when passing through geometry, but doesn't mess with the transparent queue (?). + ///
SA2 + ///
+ WaterNoAlpha = Flag64.B3, + + /// + /// Bounds center is offset from 0,0,0 and gets influenced by rotation and scale. + ///
SA1 & SA2 + ///
+ TransformBounds = Flag64.B4, + + /// + /// Bounds radius is > 10 and <= 20 units. + ///
If combined with , its >= 3. + ///
SA1 & SA2 + ///
+ BoundsRadiusSmall = Flag64.B5, + + /// + /// Bounds radius is > 3 and <= 10 units. + ///
If combined with , its >= 3. + ///
SA1 & SA2 + ///
+ BoundsRadiusTiny = Flag64.B6, + + /// + /// Bound transform related attributes. + /// + BoundTransforms = TransformBounds + | BoundsRadiusSmall + | BoundsRadiusTiny, + + + /// + /// Accelerates the player (?). + ///
SA1 + ///
+ Accelerate = Flag64.B10, + + /// + /// Reduces the players acceleration. + ///
SA1 & SA2 + ///
+ LowAcceleration = Flag64.B12, + + /// + /// Reduces the players acceleration to 0. + ///
SA1 & SA2 + ///
+ NoAcceleration = Flag64.B13, + + /// + /// Increases the players acceleration. + ///
SA1 & SA2 + ///
+ IncreasedAcceleration = Flag64.B14, + + /// + /// Tube acceleration (?). + ///
SA1 & SA2 + ///
+ TubeAcceleration = Flag64.B15, + + /// + /// Surface has no (or less) friction. Player will slip around, like on ice. + ///
SA1 & SA2 + ///
+ NoFriction = Flag64.B16, + + /// + /// Surface will prevent players from standing on it. + ///
SA1 & SA2 + ///
+ CannotLand = Flag64.B17, + + /// + /// Surface cannot be climbed by players. + ///
SA1 & SA2 + ///
+ Unclimbable = Flag64.B18, + + /// + /// The player wont slip on the surface, regardless of how steep it is. + ///
SA1 & SA2 + ///
+ Stairs = Flag64.B19, + + /// + /// Lets the player dig on the surface. + ///
SA1 & SA2 + ///
+ Diggable = Flag64.B20, + + /// + /// Damages the player on contact. + /// + Hurt = Flag64.B21, + + /// + /// Allows for transforming collisions. + ///
SA1 & SA2 + ///
+ DynamicCollision = Flag64.B22, + + /// + /// Changes player state to swimming when in contact. + ///
SA1 + ///
+ WaterCollision = Flag64.B23, + + /// + /// Keeps the player stuck to surfaces by altering gravitional direction (?). + ///
SA1 & SA2 + ///
+ Gravity = Flag64.B24, + + /// + /// All attribute values that affect collision or physics. + /// + Collisions = Solid + | Water + | WaterNoAlpha + | Accelerate + | LowAcceleration + | NoAcceleration + | IncreasedAcceleration + | TubeAcceleration + | NoFriction + | CannotLand + | Unclimbable + | Stairs + | Diggable + | Hurt + | DynamicCollision + | WaterCollision + | Gravity, + + + /// + /// Renders footprints when running on the surface. + ///
SA1 & SA2 + ///
+ Footprints = Flag64.B30, + + /// + /// Surface will not render shadows. + ///
SA2 + ///
+ NoShadows = Flag64.B31, + + /// + /// Surface ignores fog when rendering. + ///
SA2 + ///
+ NoFog = Flag64.B32, + + /// + /// Affects drawing queue (?). + ///
SA1 + ///
+ LowDepth = Flag64.B33, + + /// + /// Uses skybox drawdistance. + ///
SA1 & SA2 + ///
+ UseSkyDrawDistance = Flag64.B34, + + /// + /// Uses easy drawing function (dreamcast functionality). + ///
SA2 + ///
+ EasyDraw = Flag64.B35, + + /// + /// Does not write to the Z buffer. + ///
SA1 + ///
+ NoZWrite = Flag64.B36, + + /// + /// Sorts the transparent meshes within the attaches, instead of grouping them all together for sorting (?). + ///
SA1 + ///
+ DrawByMesh = Flag64.B37, + + /// + /// Dont store vertices in buffer. + ///
SA1 + ///
+ EnableManipulation = Flag64.B38, + + /// + /// Force alpha sorting; Disable Z Write when used together with Water; Force disable Z write in all levels except Lost World 2 + ///
SA1 + ///
+ Waterfall = Flag64.B39, + + /// + /// Turns off Visible when Chaos 0 jumps up a pole. + ///
SA1 + ///
+ Chaos0Land = Flag64.B40, + + /// + /// All attribute values that affect visuals. + /// + Visuals = Visible + | Footprints + | NoShadows + | NoFog + | LowDepth + | UseSkyDrawDistance + | EasyDraw + | NoZWrite + | DrawByMesh + | EnableManipulation + | Waterfall + | Chaos0Land, + + + /// + /// Unknown functionality from SA1. + /// + SA1_Unknown9 = Flag64.B52, + + /// + /// Unknown functionality from SA1. + /// + SA1_Unknown11 = Flag64.B53, + + /// + /// Unknown functionality from SA1. + /// + SA1_Unknown15 = Flag64.B54, + + /// + /// Unknown functionality from SA1. + /// + SA1_Unknown19 = Flag64.B55, + + /// + /// All unknown attribute values from SA1 combined. + /// + SA1_Unknowns = SA1_Unknown9 + | SA1_Unknown11 + | SA1_Unknown15 + | SA1_Unknown19, + + + /// + /// Unknown functionality from SA2. + /// + SA2_Unknown6 = Flag64.B56, + + /// + /// Unknown functionality from SA2. + /// + SA2_Unknown9 = Flag64.B57, + + /// + /// Unknown functionality from SA2. + /// + SA2_Unknown14 = Flag64.B58, + + /// + /// Unknown functionality from SA2. + /// + SA2_Unknown16 = Flag64.B59, + + /// + /// Unknown functionality from SA2. + /// + SA2_Unknown17 = Flag64.B60, + + /// + /// Unknown functionality from SA2. + /// + SA2_Unknown18 = Flag64.B61, + + /// + /// Unknown functionality from SA2. + /// + SA2_Unknown25 = Flag64.B62, + + /// + /// Unknown functionality from SA2. + /// + SA2_Unknown26 = Flag64.B63, + + /// + /// All unknown attribute values from SA2 combined. + /// + SA2_Unknowns = SA2_Unknown6 + | SA2_Unknown9 + | SA2_Unknown14 + | SA2_Unknown16 + | SA2_Unknown17 + | SA2_Unknown18 + | SA2_Unknown25 + | SA2_Unknown26, + + + /// + /// All unknown attribute values combined. + /// + Unknowns = SA1_Unknowns | SA2_Unknowns, + + /// + /// All attribute values valid for collision geometry. + /// + CollisionMask = Collisions | Unknowns | BoundTransforms, + + /// + /// All attribute values valid for visual geometry. + /// + VisualMask = Visuals | Unknowns | BoundTransforms, + + /// + /// All valid attribute values combined. + /// + ValidMask = Visuals | Collisions | Unknowns | BoundTransforms, + + } +} diff --git a/src/SA3D.Modeling/ObjectData/Events/AttachUpdatedEvent.cs b/src/SA3D.Modeling/ObjectData/Events/AttachUpdatedEvent.cs new file mode 100644 index 0000000..2e1743c --- /dev/null +++ b/src/SA3D.Modeling/ObjectData/Events/AttachUpdatedEvent.cs @@ -0,0 +1,34 @@ +using SA3D.Modeling.Mesh; +using System; + +namespace SA3D.Modeling.ObjectData.Events +{ + /// + /// Arguments for the . + /// + public class AttachUpdatedEventArgs : EventArgs + { + /// + /// Attach before changing. + /// + public Attach? OldAttach { get; } + + /// + /// Attach after changing. + /// + public Attach? NewAttach { get; } + + internal AttachUpdatedEventArgs(Attach? oldAttach, Attach? newAttach) + { + OldAttach = oldAttach; + NewAttach = newAttach; + } + } + + /// + /// Event raised when the attach of a node gets changed. + /// + /// Node that has raised the event. + /// Arguments passed to the event. + public delegate void AttachUpdatedEventHandler(Node node, AttachUpdatedEventArgs args); +} diff --git a/src/SA3D.Modeling/ObjectData/Events/TransformSet.cs b/src/SA3D.Modeling/ObjectData/Events/TransformSet.cs new file mode 100644 index 0000000..bf7bd04 --- /dev/null +++ b/src/SA3D.Modeling/ObjectData/Events/TransformSet.cs @@ -0,0 +1,67 @@ +using System.Numerics; + +namespace SA3D.Modeling.ObjectData.Events +{ + /// + /// Stores 3D transform data in local space. + /// + public readonly struct TransformSet + { + /// + /// Localspace matrix. + /// + public Matrix4x4 LocalMatrix { get; } + + /// + /// Localspace position. + /// + public Vector3 Position { get; } + + /// + /// Localspace euler angles. + /// + public Vector3 Rotation { get; } + + /// + /// Localspace quaternion. + /// + public Quaternion QuaternionRotation { get; } + + /// + /// Localspace scale. + /// + public Vector3 Scale { get; } + + /// + /// Creates a new transform set. + /// + /// Localspace matrix. + /// Localspace position. + /// Localspace euler angles. + /// Localspace quaternion. + /// Localspace scale. + public TransformSet(Matrix4x4 matrix, Vector3 position, Vector3 rotation, Quaternion quaternionRotation, Vector3 scale) + { + LocalMatrix = matrix; + Position = position; + Rotation = rotation; + QuaternionRotation = quaternionRotation; + Scale = scale; + } + + /// + /// Creates a new transform set from a nodes transform properties. + /// + /// The node to get the transform properties of. + /// The transform set. + public static TransformSet FromNode(Node node) + { + return new( + node.LocalMatrix, + node.Position, + node.EulerRotation, + node.QuaternionRotation, + node.Scale); + } + } +} diff --git a/src/SA3D.Modeling/ObjectData/Events/TransformsUpdatedEvent.cs b/src/SA3D.Modeling/ObjectData/Events/TransformsUpdatedEvent.cs new file mode 100644 index 0000000..d2c597e --- /dev/null +++ b/src/SA3D.Modeling/ObjectData/Events/TransformsUpdatedEvent.cs @@ -0,0 +1,39 @@ +using System; + +namespace SA3D.Modeling.ObjectData.Events +{ + /// + /// Event arguments for . + /// + public sealed class TransformsUpdatedEventArgs : EventArgs + { + /// + /// Transforms before changing. + /// + public TransformSet OldTransforms { get; } + + /// + /// Transforms after changing. + /// + public TransformSet NewTransforms { get; } + + /// + /// The updated transform values. + /// + public UpdatedTransformValue UpdatedValues { get; } + + internal TransformsUpdatedEventArgs(TransformSet oldTransforms, TransformSet newTransforms, UpdatedTransformValue updatedValues) + { + OldTransforms = oldTransforms; + NewTransforms = newTransforms; + UpdatedValues = updatedValues; + } + } + + /// + /// Event raised when any transform property of a node gets changed. + /// + /// Node that has raised the event. + /// Arguments passed to the event. + public delegate void TransformsUpdatedEventHandler(Node node, TransformsUpdatedEventArgs args); +} diff --git a/src/SA3D.Modeling/ObjectData/Events/UpdatedTransformValue.cs b/src/SA3D.Modeling/ObjectData/Events/UpdatedTransformValue.cs new file mode 100644 index 0000000..236b754 --- /dev/null +++ b/src/SA3D.Modeling/ObjectData/Events/UpdatedTransformValue.cs @@ -0,0 +1,31 @@ +using System; + +namespace SA3D.Modeling.ObjectData.Events +{ + /// + /// Determines which transform property triggered the event. + /// + [Flags] + public enum UpdatedTransformValue : int + { + /// + /// Position was updated. + /// + Position = 0x01, + + /// + /// Rotation was updated. + /// + Rotation = 0x02, + + /// + /// Scale was updated. + /// + Scale = 0x4, + + /// + /// "RotateZYX" was updated. + /// + RotateMode = 0x08 + } +} diff --git a/src/SA3D.Modeling/ObjectData/LandEntry.cs b/src/SA3D.Modeling/ObjectData/LandEntry.cs new file mode 100644 index 0000000..5269cab --- /dev/null +++ b/src/SA3D.Modeling/ObjectData/LandEntry.cs @@ -0,0 +1,235 @@ +using SA3D.Common.IO; +using SA3D.Modeling.Mesh; +using SA3D.Modeling.ObjectData.Enums; +using SA3D.Modeling.ObjectData.Events; +using SA3D.Modeling.Structs; +using System; +using System.Numerics; +using static SA3D.Common.StringExtensions; + +namespace SA3D.Modeling.ObjectData +{ + /// + /// Stage Geometry + /// + public class LandEntry + { + private Node _model; + + /// + /// Model behind the landentry. + /// + public Node Model + { + get => _model; + set + { + _model.OnTransformsUpdated -= OnTransformsUpdated; + _model.OnAttachUpdated -= OnAttachUpdated; + + _model = value; + + _model.OnTransformsUpdated += OnTransformsUpdated; + _model.OnAttachUpdated += OnAttachUpdated; + } + } + + /// + /// World space bounds. + ///
Get automatically updated when the transforms change. + ///
+ public Bounds ModelBounds { get; set; } + + /// + /// Geometry behavior attributes. + /// + public SurfaceAttributes SurfaceAttributes { get; set; } + + /// + /// Block mapping bits + /// + public uint BlockBit { get; set; } + + /// + /// No idea what this does at all, might be unused + /// + public uint Unknown { get; set; } + + + /// + /// Creates a new landentry object. + /// + /// Model behind the landentry. + /// Geometry behavior attributes. + public LandEntry(Node node, SurfaceAttributes surfaceAttributes) + { + _model = node; + _model.OnTransformsUpdated += OnTransformsUpdated; + _model.OnAttachUpdated += OnAttachUpdated; + + SurfaceAttributes = surfaceAttributes; + + UpdateBounds(); + } + + private LandEntry(Node model, SurfaceAttributes attribs, uint blockbit, uint unknown, Bounds modelBounds) + { + _model = model; + _model.OnTransformsUpdated += OnTransformsUpdated; + _model.OnAttachUpdated += OnAttachUpdated; + + SurfaceAttributes = attribs; + BlockBit = blockbit; + Unknown = unknown; + ModelBounds = modelBounds; + } + + + /// + /// Creates a new land entry from an attach. + /// + /// + /// Geometry behavior attributes. + /// + public static LandEntry CreateWithAttach(Attach attach, SurfaceAttributes surfaceAttributes) + { + Node node = new() + { + Attach = attach, + Label = "col_" + GenerateIdentifier() + }; + + return new(node, surfaceAttributes); + } + + + private void OnAttachUpdated(Node node, AttachUpdatedEventArgs args) + { + UpdateBounds(); + } + + private void OnTransformsUpdated(Node node, TransformsUpdatedEventArgs args) + { + UpdateBounds(); + } + + /// + /// Copies the Attach-bounds and applies the landentries transform matrix to them + /// + public void UpdateBounds() + { + if(Model.Attach == null) + { + ModelBounds = default; + return; + } + + Vector3 position = Vector3.Transform(Model.Attach.MeshBounds.Position, Model.QuaternionRotation) + Model.Position; + float radius = Model.Attach.MeshBounds.Radius * Model.Scale.GreatestValue(); + ModelBounds = new(position, radius); + } + + + /// + /// Reads a landentry off an endian stack reader. + /// + /// The reader to read from. + /// Address at which to start reading. + /// Format to read model data in. + /// Landtable format that the landentry belongs to. + /// Pointer references to utilize. + /// The land entry that was read. + public static LandEntry Read(EndianStackReader reader, uint address, ModelFormat modelFormat, ModelFormat tableFormat, PointerLUT lut) + { + Bounds bounds = Bounds.Read(reader, ref address); + if(tableFormat < ModelFormat.SA2) + { + address += 8; //sa1 has unused radius y and radius z values + } + + uint modelAddress = reader.ReadPointer(address); + Node model = Node.Read(reader, modelAddress, modelFormat, lut); + + uint unknown = 0; + uint blockBit; + + SurfaceAttributes attribs; + if(tableFormat == ModelFormat.Buffer) + { + unknown = reader.ReadUInt(address + 4); + blockBit = reader.ReadUInt(address + 8); + attribs = (SurfaceAttributes)reader.ReadULong(address + 12); + } + else if(tableFormat >= ModelFormat.SA2) + { + unknown = reader.ReadUInt(address + 4); + blockBit = reader.ReadUInt(address + 8); + attribs = ((SA2SurfaceAttributes)reader.ReadUInt(address + 12)).ToUniversal(); + } + else + { + blockBit = reader.ReadUInt(address + 4); + attribs = ((SA1SurfaceAttributes)reader.ReadUInt(address + 8)).ToUniversal(); + } + + return new(model, attribs, blockBit, unknown, bounds); + } + + /// + /// Writes the land entry to an endian stack writer. + /// + /// Does not write the node itself. + /// The writer to write to. + /// Landtable format. + /// Pointer references to utilize + public void Write(EndianStackWriter writer, ModelFormat format, PointerLUT lut) + { + if(!lut.Nodes.TryGetAddress(Model, out uint modelAddress)) + { + throw new InvalidOperationException("Model has not been written!"); + } + + ModelBounds.Write(writer); + if(format is ModelFormat.SA1 or ModelFormat.SADX) + { + writer.WriteEmpty(8); // unused radius y and radius z values + } + + writer.WriteUInt(modelAddress); + + if(format is ModelFormat.Buffer) + { + writer.WriteUInt(Unknown); + writer.WriteUInt(BlockBit); + writer.WriteULong((ulong)SurfaceAttributes); + } + else if(format is ModelFormat.SA2 or ModelFormat.SA2B) + { + writer.WriteUInt(Unknown); + writer.WriteUInt(BlockBit); + writer.WriteUInt((uint)SurfaceAttributes.ToSA2()); + } + else // SA1 or SADX + { + writer.WriteUInt(BlockBit); + writer.WriteUInt((uint)SurfaceAttributes.ToSA1()); + } + } + + + /// + /// Creates a copy of the landentry copies the node tree but reuses attaches. + /// + /// + public LandEntry Copy() + { + return new(Model.DeepSimpleCopy(), SurfaceAttributes, BlockBit, Unknown, ModelBounds); + } + + /// + public override string ToString() + { + return Model.ToString(); + } + } +} diff --git a/src/SA3D.Modeling/ObjectData/LandTable.cs b/src/SA3D.Modeling/ObjectData/LandTable.cs new file mode 100644 index 0000000..54b58fd --- /dev/null +++ b/src/SA3D.Modeling/ObjectData/LandTable.cs @@ -0,0 +1,558 @@ +using SA3D.Common.IO; +using SA3D.Common.Lookup; +using SA3D.Modeling.Animation; +using SA3D.Modeling.Mesh; +using SA3D.Modeling.Mesh.Basic; +using SA3D.Modeling.ObjectData.Enums; +using SA3D.Modeling.Structs; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using static SA3D.Common.StringExtensions; + +namespace SA3D.Modeling.ObjectData +{ + /// + /// Stage geometry information + /// + public class LandTable : ILabel + { + #region Properties + + /// + /// Landtable name / C struct label + /// + public string Label { get; set; } + + /// + /// Level geometry + /// + public ILabeledArray Geometry { get; set; } + + /// + /// Geometry animations (sa1) + /// + public ILabeledArray GeometryAnimations { get; set; } + + /// + /// Landtable attributes + /// + public LandtableAttributes Attributes { get; set; } + + /// + /// Draw distance + /// + public float DrawDistance { get; set; } + + /// + /// Texture file name + /// + public string? TextureFileName { get; set; } + + /// + /// Texture list pointer + /// + public uint TexListPtr { get; set; } + + /// + /// Format of the landtable + /// + public ModelFormat Format { get; private set; } + + #endregion + + + /// + /// Creates a new Landtable. + /// + /// Level geometry. + /// Geometry animations. + /// Landtable Format. + public LandTable(ILabeledArray geometry, ILabeledArray geometryAnimations, ModelFormat format) + { + string identifier = GenerateIdentifier(); + + Label = "landtable_" + identifier; + Geometry = geometry; + GeometryAnimations = geometryAnimations; + + Format = format; + } + + /// + /// Creates a new Landtable. + /// + /// Level geometry. + /// Landtable Format. + public LandTable(ILabeledArray geometry, ModelFormat format) + : this(geometry, new LabeledArray(0), format) { } + + + /// + /// Generates buffer mesh data for the attaches of every land entry. + /// + /// Whether to optimize vertex and polygon data of the buffered meshes. + public void BufferMeshData(bool optimize) + { + HashSet buffered = new(); + + foreach(LandEntry landEntry in Geometry) + { + Node node = landEntry.Model; + + if(node.Next != null || node.Child != null) + { + node.BufferMeshData(optimize); + } + + if(node.Attach == null || buffered.Contains(node.Attach)) + { + continue; + } + + node.BufferMeshData(optimize); + buffered.Add(node.Attach); + } + } + + /// + /// Converts the the model data and land entry structure to a different model format. + /// + /// The model format to convert to. + /// Whether to optimize the converted model data. + /// Whether to convert data even if it already is the same format. + /// Whether to generate mesh data after conversion. + public void ConvertToFormat(ModelFormat newFormat, bool optimize, bool forceUpdate = false, bool updateBuffer = false) + { + if(newFormat == Format && !forceUpdate) + { + return; + } + + AttachFormat newAtcFormat = newFormat switch + { + ModelFormat.SA1 or ModelFormat.SADX => AttachFormat.BASIC, + ModelFormat.SA2 => AttachFormat.CHUNK, + ModelFormat.SA2B => AttachFormat.GC, + ModelFormat.Buffer or _ => AttachFormat.Buffer, + }; + + void convertModels(AttachFormat format, IEnumerable landEntries) + { + Dictionary convertedAttaches = new(); + + foreach(LandEntry landEntry in Geometry) + { + Node node = landEntry.Model; + + if(node.Next != null || node.Child != null) + { + node.ConvertAttachFormat(format, optimize, false, forceUpdate, updateBuffer); + } + + if(node.Attach == null) + { + continue; + } + else if(convertedAttaches.TryGetValue(node.Attach, out Attach? convertedAttach)) + { + node.Attach = convertedAttach; + } + else + { + Attach previous = node.Attach; + node.ConvertAttachFormat(format, optimize, false, forceUpdate, updateBuffer); + convertedAttaches.Add(previous, node.Attach); + } + } + } + + if(newAtcFormat is AttachFormat.Buffer) + { + BufferMeshData(optimize); + } + else if(newAtcFormat is AttachFormat.BASIC) + { + convertModels(newAtcFormat, Geometry); + } + else if(Format is ModelFormat.SA2 or ModelFormat.SA2B) + { + // we only need to convert visual geometry, since we are converting between SA2 formats here. + convertModels(newAtcFormat, Geometry.Where(x => x.Model.Attach is not BasicAttach)); + } + else + { + // converting from some hybrid format to SA2/B format + List visual = new(); + List collision = new(); + + foreach(LandEntry le in Geometry) + { + bool isCollision = le.SurfaceAttributes.CheckIsCollision(); + bool isVisual = !isCollision || le.SurfaceAttributes.HasFlag(SurfaceAttributes.Visible); + + if(isVisual && isCollision) + { + LandEntry collisionLE = le.Copy(); + LandEntry visualLE = le; + + collisionLE.SurfaceAttributes &= SurfaceAttributes.CollisionMask; + visualLE.SurfaceAttributes &= SurfaceAttributes.VisualMask; + + collision.Add(collisionLE); + visual.Add(visualLE); + } + else if(isVisual) + { + visual.Add(le); + } + else if(isCollision) + { + collision.Add(le); + } + + } + + convertModels(newAtcFormat, visual); + convertModels(AttachFormat.BASIC, collision); + + visual.AddRange(collision); + + Geometry = new LabeledArray(Geometry.Label, visual.ToArray()); + } + + Format = newFormat; + } + + /// + /// Sorts land entries to be viable for SA2 / SA2B export. + /// + public void SortLandEntries() + { + if(Format is not ModelFormat.SA2 or ModelFormat.SA2B) + { + return; + } + + LandEntry[] newOrder = new LandEntry[Geometry.Length]; + int count = 0; + + foreach(LandEntry le in Geometry) + { + if(le.Model.Attach?.Format == AttachFormat.BASIC) + { + continue; + } + + newOrder[count] = le; + count++; + } + + foreach(LandEntry le in Geometry) + { + if(!(le.Model.Attach?.Format == AttachFormat.BASIC)) + { + continue; + } + + newOrder[count] = le; + count++; + } + + IList list = Geometry; + if(list.IsReadOnly) + { + Geometry = new LabeledReadOnlyArray(Geometry.Label, newOrder); + } + else + { + for(int i = 0; i < newOrder.Length; i++) + { + list[i] = newOrder[i]; + } + } + } + + + /// + /// Writes the landtable to a stream + /// + /// Output stream + /// + public uint Write(EndianStackWriter writer, PointerLUT lut) + { + if(Format is ModelFormat.SA2 or ModelFormat.SA2B) + { + return lut.GetAddAddress(this, () => WriteSA2(writer, lut)); + } + else + { + return lut.GetAddAddress(this, () => WriteSA1(writer, lut)); + } + + } + + private uint WriteSA2(EndianStackWriter writer, PointerLUT lut) + { + ushort visualCount = 0; + bool visualFinished = false; + + // verifying order of land entries + foreach(LandEntry le in Geometry) + { + AttachFormat? attachFormat = le.Model.GetAttachFormat(); + + if(visualFinished && attachFormat != null && attachFormat != AttachFormat.BASIC) + { + throw new FormatException("Landtable entries are not ordered propertly! Visual models (BASIC) need to come after visual models."); + } + else if(!visualFinished) + { + if(attachFormat == AttachFormat.BASIC) + { + visualFinished = true; + } + else + { + visualCount++; + } + } + } + + ModelFormat format = Format; + int entryNum = 0; + foreach(LandEntry landEntry in Geometry) + { + if(entryNum == visualCount) + { + format = ModelFormat.SA1; + } + + landEntry.Model.Write(writer, format, lut); + entryNum++; + } + + uint geomAddr = lut.GetAddAddress(Geometry, () => WriteGeometry(writer, lut)); + uint texNameAddr = WriteTextureName(writer); + + uint result = writer.PointerPosition; + + writer.WriteUShort((ushort)Geometry.Length); + writer.WriteUShort(visualCount); + writer.WriteEmpty(8); // todo: figure out what these do + writer.WriteFloat(DrawDistance); + writer.WriteUInt(geomAddr); + writer.WriteEmpty(4); // unused geometry animations + writer.WriteUInt(texNameAddr); + writer.WriteUInt(TexListPtr); + + return result; + } + + private uint WriteSA1(EndianStackWriter writer, PointerLUT lut) + { + foreach(LandEntry le in Geometry) + { + le.Model.Write(writer, Format, lut); + } + + foreach(LandEntryMotion lem in GeometryAnimations) + { + lem.WriteData(writer, Format, lut); + } + + uint geomAddr = lut.GetAddAddress(Geometry, () => WriteGeometry(writer, lut)); + + uint animAddr = 0; + if(GeometryAnimations.Length > 0) + { + uint onWriteAnimations() + { + uint result = writer.PointerPosition; + + foreach(LandEntryMotion lem in GeometryAnimations) + { + lem.Write(writer, lut); + } + + return result; + } + + animAddr = lut.GetAddAddress(GeometryAnimations, onWriteAnimations); + } + + uint texNameAddr = WriteTextureName(writer); + + uint result = writer.PointerPosition; + + writer.WriteUShort((ushort)Geometry.Length); + writer.WriteUShort((ushort)GeometryAnimations.Length); + writer.WriteUInt((uint)Attributes); + writer.WriteFloat(DrawDistance); + writer.WriteUInt(geomAddr); + writer.WriteUInt(animAddr); + writer.WriteUInt(texNameAddr); + writer.WriteUInt(TexListPtr); + writer.WriteEmpty(8); // two unused pointers + + return result; + } + + private uint WriteGeometry(EndianStackWriter writer, PointerLUT lut) + { + uint result = writer.PointerPosition; + + foreach(LandEntry le in Geometry) + { + le.Write(writer, Format, lut); + } + + return result; + } + + private uint WriteTextureName(EndianStackWriter writer) + { + uint texNameAddr = 0; + if(TextureFileName != null) + { + texNameAddr = writer.PointerPosition; + writer.Write(Encoding.ASCII.GetBytes(TextureFileName + '\0')); + writer.Align(4); + } + + return texNameAddr; + } + + + /// + /// Reads a landtable from a byte array + /// + /// + /// + /// + /// + /// + public static LandTable Read(EndianStackReader data, uint address, ModelFormat format, PointerLUT lut) + { + LandTable onRead() + { + float radius; + LandtableAttributes attribs = 0; + + string identifier = GenerateIdentifier(); + + ILabeledArray geometry; + ILabeledArray anim; + + string texName = ""; + uint texListPtr; + + ushort geometryCount = data.ReadUShort(address); + + ushort nonBasicCount; + uint geometryLoc; + uint geometrySize; + short animCount; + uint animAddr; + uint texNameLoc; + uint texlistLoc; + + switch(format) + { + case ModelFormat.SA1: + case ModelFormat.SADX: + case ModelFormat.Buffer: + attribs = (LandtableAttributes)data.ReadUInt(address + 4); + radius = data.ReadFloat(address + 8); + + geometryLoc = address + 0xC; + geometrySize = 0x24; + nonBasicCount = geometryCount; + + animCount = data.ReadShort(address + 2); + animAddr = data.ReadPointer(address + 0x10); + + texNameLoc = address + 0x14; + texlistLoc = address + 0x18; + break; + case ModelFormat.SA2: + case ModelFormat.SA2B: + radius = data.ReadFloat(address + 0xC); + + geometryLoc = address + 0x10; + geometrySize = 0x20; + nonBasicCount = data.ReadUShort(address + 2); + + animAddr = 0; + animCount = 0; + + texNameLoc = address + 0x18; + texlistLoc = address + 0x1C; + break; + default: + throw new InvalidDataException("Landtable format not valid"); + } + + LabeledArray onReadGeometry(uint geometryAddr) + { + LabeledArray result = new(geometryCount); + + for(int i = 0; i < geometryCount; i++) + { + result[i] = LandEntry.Read(data, geometryAddr, i >= nonBasicCount ? ModelFormat.SA1 : format, format, lut); + geometryAddr += geometrySize; + } + + return result; + } + + geometry = data.TryReadPointer(geometryLoc, out uint geometryAddr) + ? lut.GetAddLabeledValue(geometryAddr, "collist_", onReadGeometry) + : (ILabeledArray)new LabeledArray("collist_" + identifier, 0); + + LabeledArray onReadAnims() + { + LabeledArray result = new(animCount); + + for(int i = 0; i < animCount; i++) + { + result[i] = LandEntryMotion.Read(data, animAddr, format, lut); + animAddr += LandEntryMotion.StructSize; + } + + return result; + } + + anim = animAddr != 0 + ? lut.GetAddLabeledValue(animAddr, "animlist_", onReadAnims) + : (ILabeledArray)new LabeledArray("animlist_" + identifier, 0); + + if(data.TryReadPointer(texNameLoc, out uint texNameAddr)) + { + texName = data.ReadNullterminatedString(texNameAddr, Encoding.ASCII); + } + + texListPtr = data.ReadUInt(texlistLoc); + + return new LandTable(geometry, anim, format) + { + DrawDistance = radius, + Attributes = attribs, + TexListPtr = texListPtr, + TextureFileName = texName + }; + } + + return lut.GetAddLabeledValue(address, "landtable_", onRead); + } + + + /// + public override string ToString() + { + return $"{Format} LandTable"; + } + } +} diff --git a/src/SA3D.Modeling/ObjectData/Node.Attach.cs b/src/SA3D.Modeling/ObjectData/Node.Attach.cs new file mode 100644 index 0000000..342420c --- /dev/null +++ b/src/SA3D.Modeling/ObjectData/Node.Attach.cs @@ -0,0 +1,197 @@ +using SA3D.Modeling.Mesh; +using SA3D.Modeling.Mesh.Converters; +using SA3D.Modeling.ObjectData.Events; +using System; +using System.Linq; + +namespace SA3D.Modeling.ObjectData +{ + public partial class Node + { + private Attach? _attach; + + + /// + /// Mesh data attached to this node. + /// + public Attach? Attach + { + get => _attach; + set + { + if(value == _attach) + { + return; + } + + AttachFormat? format = GetAttachFormat(); + + if(value != null && format != null && format != value.Format) + { + throw new FormatException($"Node uses {format} attaches, and the attach that is being set is of type {value.Format}! Cannot set attach!"); + } + + Attach? previous = _attach; + _attach = value; + + OnAttachUpdated?.Invoke(this, new(previous, _attach)); + } + } + + /// + /// Raised when the attach of the node changes. + /// + public event AttachUpdatedEventHandler? OnAttachUpdated; + + + /// + /// Determines Attach format across the whole tree that this node belongs to. + /// + public AttachFormat? GetAttachFormat() + { + foreach(Node node in GetTreeNodeEnumerable()) + { + if(node.Attach != null) + { + return node.Attach.Format; + } + } + + return null; + } + + private void CheckAttachCompatibility(Node other) + { + AttachFormat? format = GetAttachFormat(); + + if(other.CheckHasBranchAttaches() + && format != null + && other.GetAttachFormat() != format) + { + throw new InvalidOperationException("The node you are trying to insert has an incompatible attach format!"); + } + } + + /// + /// Checks if or any node directly below has an attach. + /// + public bool CheckHasBranchAttaches() + { + return GetBranchNodeEnumerable(false).Any(x => x._attach != null); + } + + /// + /// Removes all of the attaches from the tree, allowing for changing the attach format. + /// + public void ClearAttachesFromTree() + { + foreach(Node node in GetTreeNodeEnumerable()) + { + if(node._attach != null) + { + Attach? previous = node._attach; + node._attach = null; + node.OnAttachUpdated?.Invoke(node, new(previous, null)); + } + } + } + + /// + /// Whether this node tree has weighted attaches + /// + public bool CheckHasTreeWeightedMesh() + { + return GetTreeAttachEnumerable().Any(x => x.CheckHasWeights()); + } + + + /// + /// Generates buffer mesh data for the attaches in the entire tree. + /// + /// Whether to optimize vertex and polygon data of the buffered meshes. + public void BufferMeshData(bool optimize) + { + AttachFormat? format = GetAttachFormat(); + if(format == null) + { + return; + } + + Node rootNode = GetRootNode(); + + switch(format) + { + case AttachFormat.BASIC: + BasicConverter.BufferBasicModel(rootNode, optimize); + break; + case AttachFormat.CHUNK: + ChunkConverter.BufferChunkModel(rootNode, optimize); + break; + case AttachFormat.GC: + GCConverter.BufferGCModel(rootNode, optimize); + break; + case AttachFormat.Buffer: + default: + break; + } + } + + /// + /// Converts the entire Model to a different attach format. + /// + /// The attach format to convert to. + /// Whether to optimize the converted data. + /// Convert regardless of weight information being lost. + /// Force conversion, even if the attach format ends up being the same. + /// Whether to generate buffer mesh data after conversion. + public void ConvertAttachFormat( + AttachFormat newAttachFormat, + bool optimize, + bool ignoreWeights = false, + bool forceUpdate = false, + bool updateBuffer = false) + { + AttachFormat? format = GetAttachFormat(); + if(format == null || (newAttachFormat == format && !forceUpdate)) + { + return; + } + + if(newAttachFormat == AttachFormat.Buffer) + { + BufferMeshData(optimize); + return; + } + + Node rootNode = GetRootNode(); + + Mesh.Weighted.WeightedMesh[] weightedMeshes = ToWeightedConverter.ConvertToWeighted(rootNode); + + if(weightedMeshes.Length == 0) + { + throw new InvalidOperationException("No weighted meshes have been generated! Did you perhaps forget to buffer the mesh data?"); + } + + switch(newAttachFormat) + { + case AttachFormat.BASIC: + BasicConverter.ConvertWeightedToBasic(rootNode, weightedMeshes, optimize, ignoreWeights); + break; + case AttachFormat.CHUNK: + ChunkConverter.ConvertWeightedToChunk(rootNode, weightedMeshes, optimize); + break; + case AttachFormat.GC: + GCConverter.ConvertWeightedToGC(rootNode, weightedMeshes, optimize, ignoreWeights); + break; + case AttachFormat.Buffer: + default: + break; + } + + if(updateBuffer) + { + BufferMeshData(optimize); + } + } + } +} diff --git a/src/SA3D.Modeling/ObjectData/Node.Attributes.cs b/src/SA3D.Modeling/ObjectData/Node.Attributes.cs new file mode 100644 index 0000000..ff5b8f4 --- /dev/null +++ b/src/SA3D.Modeling/ObjectData/Node.Attributes.cs @@ -0,0 +1,156 @@ +using SA3D.Modeling.ObjectData.Enums; +using System.Numerics; + +namespace SA3D.Modeling.ObjectData +{ + public partial class Node + { + /// + /// Various additional info for the node. + /// + public NodeAttributes Attributes { get; private set; } + + + /// + /// The nodes position has no effect on the transforms. + /// + public bool NoPosition + { + get => GetNodeAttribute(NodeAttributes.NoPosition); + set => SetNodeAttribute(NodeAttributes.NoPosition, value); + } + + /// + /// The nodes rotation has no effect on the transforms. + /// + public bool NoRotation + { + get => GetNodeAttribute(NodeAttributes.NoRotation); + set => SetNodeAttribute(NodeAttributes.NoRotation, value); + } + + /// + /// The nodes scale has no effect on the transforms. + /// + public bool NoScale + { + get => GetNodeAttribute(NodeAttributes.NoScale); + set => SetNodeAttribute(NodeAttributes.NoScale, value); + } + + /// + /// Node should be skipped for attach related evaluations. Required if node has no attach. + /// + public bool SkipDraw + { + get => GetNodeAttribute(NodeAttributes.SkipDraw); + set => SetNodeAttribute(NodeAttributes.SkipDraw, value); + } + + /// + /// Child of the node is skipped. Required if the node has no child. + /// + public bool SkipChildren + { + get => GetNodeAttribute(NodeAttributes.SkipChildren); + set => SetNodeAttribute(NodeAttributes.SkipChildren, value); + } + + /// + /// Whether euler angles are applied in ZYX order, instead of XYZ. + /// + public bool RotateZYX + { + get => GetNodeAttribute(NodeAttributes.RotateZYX); + private set => SetNodeAttribute(NodeAttributes.RotateZYX, value); + } + + /// + /// If enabled, the node will be ignored by animations (but not its child or successor). + /// + public bool NoAnimate + { + get => GetNodeAttribute(NodeAttributes.NoAnimate); + set => SetNodeAttribute(NodeAttributes.NoAnimate, value); + } + + /// + /// If enabled, the node will be ignored by shape animations (but not its child or successor). + /// + public bool NoMorph + { + get => GetNodeAttribute(NodeAttributes.NoMorph); + set => SetNodeAttribute(NodeAttributes.NoMorph, value); + } + + /// + /// Whether the node uses quaternion rotations. + /// + public bool UseQuaternionRotation + { + get => GetNodeAttribute(NodeAttributes.UseQuaternionRotation); + set => SetNodeAttribute(NodeAttributes.UseQuaternionRotation, value); + } + + + private void SetNodeAttribute(NodeAttributes attribute, bool state) + { + if(state) + { + Attributes |= attribute; + } + else + { + Attributes &= ~attribute; + } + } + + private bool GetNodeAttribute(NodeAttributes attribute) + { + return Attributes.HasFlag(attribute); + } + + /// + /// Automatically fills in attributes based on other properties of the node. + /// + public void AutoNodeAttributes() + { + NoPosition = Position == Vector3.Zero; + NoScale = Scale == Vector3.One; + NoRotation = EulerRotation == Vector3.Zero; + SkipChildren = Child == null; + SkipDraw = Attach == null; + } + + /// + /// Sets the rotation order. + /// + /// New rotation order state. + /// Determines how the rotation values of a node should be handled after the rotation order has been changed. + public void SetRotationZYX(bool newValue, RotationUpdateMode mode = RotationUpdateMode.UpdateEuler) + { + if(RotateZYX == newValue) + { + return; + } + + RotateZYX = newValue; + + if(mode != RotationUpdateMode.Keep) + { + UpdateTransforms(null, null, null, null, mode); + } + } + + /// + /// Sets all node attributes at the same time. + /// + /// The new attributes to set. + /// Determines how the rotation values of a node should be handled after the rotation order has been changed. + public void SetAllNodeAttributes(NodeAttributes attributes, RotationUpdateMode rotationUpdateMode = RotationUpdateMode.UpdateEuler) + { + SetRotationZYX(attributes.HasFlag(NodeAttributes.RotateZYX), rotationUpdateMode); + Attributes = attributes; + } + } +} diff --git a/src/SA3D.Modeling/ObjectData/Node.Enumerate.cs b/src/SA3D.Modeling/ObjectData/Node.Enumerate.cs new file mode 100644 index 0000000..3a9c541 --- /dev/null +++ b/src/SA3D.Modeling/ObjectData/Node.Enumerate.cs @@ -0,0 +1,232 @@ +using SA3D.Modeling.Mesh; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace SA3D.Modeling.ObjectData +{ + public partial class Node : IEnumerable + { + #region Base + + /// + /// Returns an enumerators that iterates over the direct children of this node. + /// + public IEnumerator GetEnumerator() + { + Node? current = Child; + while(current != null) + { + yield return current; + current = current.Next; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Returns all children of this node in an array. + /// + /// The child array. + public Node[] GetChildren() + { + return this.ToArray(); + } + + /// + /// Iterates over the branch, starting at this node. + ///
First , then . + ///
+ /// Iterate over the sibling nodes of node too (starts at the root sibling). + public IEnumerable GetBranchNodeEnumerable(bool includeSiblings) + { + Stack nodeStack = new(); + + if(!includeSiblings) + { + yield return this; + if(Child != null) + { + nodeStack.Push(Child); + } + } + else + { + nodeStack.Push(GetRootSibling()); + } + + while(nodeStack.TryPop(out Node? node)) + { + yield return node; + + if(node.Next != null) + { + nodeStack.Push(node.Next); + } + + if(node.Child != null) + { + nodeStack.Push(node.Child); + } + } + } + + /// + /// Counts the number of nodes in the branch, starting at this node. + /// + /// Iterate over the sibling nodes of node too (starts at the root sibling). + public int GetBranchNodeCount(bool includeSiblings) + { + return GetBranchNodeEnumerable(includeSiblings).Count(); + } + + /// + /// Returns the nodes in the branch, starting at this node. + ///
First , then . + ///
+ /// Iterate over the sibling nodes of node too (starts at the root sibling). + public Node[] GetBranchNodes(bool includeSiblings) + { + return GetBranchNodeEnumerable(includeSiblings).ToArray(); + } + + + /// + /// Iterates over the entire tree, starting at the root node. + ///
First , then . + ///
+ public IEnumerable GetTreeNodeEnumerable() + { + // Branch gets the root sibling either way, so its enough calling the root parent. + return GetRootParent().GetBranchNodeEnumerable(true); + } + + /// + /// Returns the number of nodes in the entire tree, starting at the root node. + /// + /// + public int GetTreeNodeCount() + { + return GetTreeNodeEnumerable().Count(); + } + + /// + /// Returns the entire tree, starting at the root node. + ///
First , then . + ///
+ public Node[] GetTreeNodes() + { + return GetTreeNodeEnumerable().ToArray(); + } + + #endregion + + #region Animate + + /// + /// Iterates over the entire tree, starting at the root node. + ///
Includes only nodes where is . + ///
First , then . + ///
+ public IEnumerable GetAnimTreeNodeEnumerable() + { + return GetTreeNodeEnumerable().Where(x => !x.NoAnimate); + } + + /// + /// Returns the number of nodes in the entire tree, starting at the root node. + ///
Includes only nodes where is . + ///
+ /// + public int GetAnimTreeNodeCount() + { + return GetAnimTreeNodeEnumerable().Count(); + } + + /// + /// Returns the entire tree, starting at the root node. + ///
Includes only nodes where is . + ///
First , then . + ///
+ public Node[] GetAnimTreeNodes() + { + return GetAnimTreeNodeEnumerable().ToArray(); + } + + #endregion + + #region Morph + + /// + /// Iterates over the entire tree, starting at the root node. + ///
Includes only nodes where is . + ///
First , then . + ///
+ public IEnumerable GetMorphTreeNodeEnumerable() + { + return GetTreeNodeEnumerable().Where(x => !x.NoMorph); + } + + /// + /// Returns the number of nodes in the entire tree, starting at the root node. + ///
Includes only nodes where is . + ///
+ /// + public int GetMorphTreeNodeCount() + { + return GetMorphTreeNodeEnumerable().Count(); + } + + /// + /// Returns the entire tree, starting at the root node. + ///
Includes only nodes where is . + ///
First , then . + ///
+ public Node[] GetMorphTreeNodes() + { + return GetMorphTreeNodeEnumerable().ToArray(); + } + + #endregion + + #region Attaches + + /// + /// Iterates over the attaches of the entire tree, starting at the root node. + ///
First , then . + ///
+ public IEnumerable GetTreeAttachEnumerable() + { + foreach(Node node in GetTreeNodeEnumerable()) + { + if(node.Attach != null) + { + yield return node.Attach; + } + } + } + + /// + /// Returns the number of attaches in the entire tree, starting at the root node. + /// + /// + public int GetTreeAttachCount() + { + return GetTreeAttachEnumerable().Count(); + } + + /// + /// Returns the attaches in the entire tree, starting at the root node. + ///
First , then . + ///
+ public Attach[] GetTreeAttaches() + { + return GetTreeAttachEnumerable().ToArray(); + } + + #endregion + } +} diff --git a/src/SA3D.Modeling/ObjectData/Node.Transforms.cs b/src/SA3D.Modeling/ObjectData/Node.Transforms.cs new file mode 100644 index 0000000..4747d6a --- /dev/null +++ b/src/SA3D.Modeling/ObjectData/Node.Transforms.cs @@ -0,0 +1,211 @@ +using SA3D.Modeling.ObjectData.Enums; +using SA3D.Modeling.ObjectData.Events; +using SA3D.Modeling.Structs; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace SA3D.Modeling.ObjectData +{ + public partial class Node + { + private Vector3 _position; + private Vector3 _eulerRotation; + private Quaternion _quaternionRotation = Quaternion.Identity; + private Vector3 _scale = Vector3.One; + + /// + /// Position in localspace. + /// + public Vector3 Position + { + get => _position; + set => UpdateTransforms(value, null, null, null, RotationUpdateMode.Keep); + } + + /// + /// Euler angles rotation in localspace (radians). + ///
Affects . + ///
+ public Vector3 EulerRotation + { + get => _eulerRotation; + set => UpdateTransforms(null, value, null, null, RotationUpdateMode.Keep); + } + + /// + /// Quaternion rotation in local space. + ///
Affects . + ///
+ public Quaternion QuaternionRotation + { + get => _quaternionRotation; + set => UpdateTransforms(null, null, value, null, RotationUpdateMode.Keep); + } + + /// + /// Scale in local space. + /// + public Vector3 Scale + { + get => _scale; + set => UpdateTransforms(null, null, null, value, RotationUpdateMode.Keep); + } + + /// + /// Matrix representation of the localspace transform channels. + /// + public Matrix4x4 LocalMatrix { get; private set; } = Matrix4x4.Identity; + + /// + /// Raised whenever any transform property changes. + /// + public event TransformsUpdatedEventHandler? OnTransformsUpdated; + + + private void UpdateTransforms(Vector3? position, Vector3? eulerRotation, Quaternion? quaternionRotation, Vector3? scale, RotationUpdateMode rotationUpdateMode) + { + TransformSet oldTransforms = TransformSet.FromNode(this); + UpdatedTransformValue updated = default; + + if(position != null) + { + _position = position.Value; + updated |= UpdatedTransformValue.Position; + } + + if(eulerRotation != null) + { + _eulerRotation = eulerRotation.Value; + _quaternionRotation = QuaternionUtilities.EulerToQuaternion(_eulerRotation, RotateZYX); + updated |= UpdatedTransformValue.Rotation; + } + + if(quaternionRotation != null) + { + _quaternionRotation = quaternionRotation.Value; + _eulerRotation = QuaternionUtilities.QuaternionToEuler(_quaternionRotation, RotateZYX); + updated |= UpdatedTransformValue.Rotation; + } + + if(scale != null) + { + _scale = scale.Value; + updated |= UpdatedTransformValue.Scale; + } + + switch(rotationUpdateMode) + { + case RotationUpdateMode.UpdateQuaternion: + _quaternionRotation = QuaternionUtilities.EulerToQuaternion(_eulerRotation, RotateZYX); + break; + case RotationUpdateMode.UpdateEuler: + _eulerRotation = QuaternionUtilities.QuaternionToEuler(_quaternionRotation, RotateZYX); + break; + case RotationUpdateMode.Keep: + default: + break; + } + + if(updated == default) + { + return; + } + + LocalMatrix = MatrixUtilities.CreateTransformMatrix(_position, _quaternionRotation, _scale); + + TransformSet newTransforms = TransformSet.FromNode(this); + OnTransformsUpdated?.Invoke(this, new(oldTransforms, newTransforms, updated)); + } + + /// + /// Updates multiple transforms at the same time. + /// + /// New position. + /// New euler angles. + /// New scale. + public void UpdateTransforms(Vector3? position, Vector3? eulerRotation, Vector3? scale) + { + UpdateTransforms(position, eulerRotation, null, scale, RotationUpdateMode.Keep); + } + + /// + /// Updates multiply transforms at the same time. + /// + /// New position. + /// New quaternion rotation. + /// New scale. + public void UpdateTransforms(Vector3? position, Quaternion? quaternionRotation, Vector3? scale) + { + UpdateTransforms(position, null, quaternionRotation, scale, RotationUpdateMode.Keep); + } + + + /// + /// Calculates the world matrix for this object (recursive). + /// + /// + public Matrix4x4 GetWorldMatrix() + { + Matrix4x4 local = LocalMatrix; + + if(Parent != null) + { + local *= Parent.GetWorldMatrix(); + } + + return local; + } + + /// + /// Returns an enumerable that iterates over the entire tree, starting at this node. + ///
Works like , but includes the world matrix too without recursive calculations. + ///
+ public IEnumerable<(Node node, Matrix4x4 worldMatrix)> GetWorldMatrixTreeEnumerator() + { + Stack<(Node node, Matrix4x4 parentWorldMatrix)> nmStack = new(); + + nmStack.Push((this, Matrix4x4.Identity)); + + while(nmStack.TryPop(out (Node node, Matrix4x4 parentWorldMatrix) nm)) + { + Matrix4x4 worldMatrix = nm.node.LocalMatrix * nm.parentWorldMatrix; + yield return (nm.node, worldMatrix); + + if(nm.node.Next != null) + { + nmStack.Push((nm.node.Next, nm.parentWorldMatrix)); + } + + if(nm.node.Child != null) + { + nmStack.Push((nm.node.Child, worldMatrix)); + } + } + } + + /// + /// Returns the node tree with their corresponding world matrices. Does not utilize recursiveness. + /// + public (Node node, Matrix4x4 worldMatrix)[] GetWorldMatrixTree() + { + return GetWorldMatrixTreeEnumerator().ToArray(); + } + + /// + /// Returns the node tree with their corresponding world matrices in a dictionary. Does not utilize recursiveness. + /// + /// + public Dictionary GetWorldMatrixTreeLUT() + { + Dictionary result = new(); + foreach((Node node, Matrix4x4 worldMatrix) in GetWorldMatrixTreeEnumerator()) + { + result.Add(node, worldMatrix); + } + + return result; + } + + } +} diff --git a/src/SA3D.Modeling/ObjectData/Node.Tree.cs b/src/SA3D.Modeling/ObjectData/Node.Tree.cs new file mode 100644 index 0000000..86b0662 --- /dev/null +++ b/src/SA3D.Modeling/ObjectData/Node.Tree.cs @@ -0,0 +1,411 @@ +using System; +using System.Linq; + +namespace SA3D.Modeling.ObjectData +{ + public partial class Node + { + /// + /// Direct child of the node. + /// + public Node? Child { get; private set; } + + /// + /// Parent of the node. + /// + public Node? Parent { get; private set; } + + /// + /// The succeeding node. + /// + public Node? Next { get; private set; } + + /// + /// The preceeding node. + /// + public Node? Previous { get; private set; } + + /// + /// Number of direct children this node has. + /// + public int ChildCount => this.Count(); + + + /// + /// Whether this node has a previous or next node. + /// + public bool HasSiblings => Next != null || Previous != null; + + /// + /// Whether this node has a parent and/or siblings. + /// + public bool HasImmediates => Parent != null || HasSiblings; + + + /// + /// Returns child at a specific index. + /// + /// Index of the child to get. + /// + /// + public Node this[int index] + { + get + { + if(index < 0) + { + throw new IndexOutOfRangeException($"Index {index} out of range! Must be a positive value!"); + } + + if(Child == null) + { + throw new IndexOutOfRangeException($"Index {index} out of range! Node has no children!"); + } + + Node target = Child; + for(int i = 0; i < index; i++) + { + if(target.Next == null) + { + throw new IndexOutOfRangeException($"Index {index} out of range! Maximum valid index: {i}"); + } + + target = target.Next; + } + + return target; + } + } + + /// + /// Returns the root parent of this tree. + /// + public Node GetRootParent() + { + Node root = this; + while(root.Parent != null) + { + root = root.Parent; + } + + return root; + } + + /// + /// Returns the root sibling of this node. + /// + /// + public Node GetRootSibling() + { + Node root = this; + while(root.Previous != null) + { + root = root.Previous; + } + + return root; + } + + /// + /// Returns the absolute root of the node tree (root sibling of the root parent). + /// + /// + public Node GetRootNode() + { + return GetRootParent().GetRootSibling(); + } + + + /// + /// Detaches the node from its parent and siblings. Children will be kept. + /// + public void Detach() + { + if(!HasImmediates) + { + return; + } + + if(Previous != null) + { + Previous.Next = Next; + } + else if(Parent != null) + { + Parent.Child = Next; + } + + if(Next != null) + { + Next.Previous = Previous; + } + + Parent = null; + Next = null; + Previous = null; + } + + /// + /// Detaches all children from this node. + /// + /// Whether to keep the sibling relationships between the detached child nodes. + public void DetachChildren(bool remainSiblings) + { + if(Child == null) + { + return; + } + + Node? current = Child; + while(current != null) + { + Node? next = current.Next; + + current.Parent = null; + + if(!remainSiblings) + { + current.Next = null; + current.Previous = null; + } + + current = next; + } + + Child = null; + } + + /// + /// Detaches the successor node from this node. + /// + /// Whether to keep the sibling relationships between the detached successor nodes. + public void DetachSuccessors(bool remainSiblings) + { + if(Next == null) + { + return; + } + + Node? current = Next; + while(current != null) + { + Node? next = current.Next; + + if(current.Parent != null) + { + current.Parent = null; + } + + if(!remainSiblings) + { + current.Next = null; + current.Previous = null; + } + + current = next; + } + + Next.Previous = null; + Next = null; + } + + + /// + /// Inserts a before node. + ///
( .Previous = ). + ///
+ /// Runs on node. + /// The node to insert. + /// + public void InsertBefore(Node node) + { + CheckAttachCompatibility(node); + + node.Detach(); + + if(Previous != null) + { + Previous.Next = node; + } + else if(Parent != null) + { + Parent.Child = node; + } + + node.Parent = Parent; + node.Next = this; + Previous = node; + } + + /// + /// Inserts a after node. + ///
( .Next = ). + ///
+ /// Runs on node. + /// The node to insert. + /// + public void InsertAfter(Node node) + { + CheckAttachCompatibility(node); + + node.Detach(); + + if(Next != null) + { + Next.Previous = node; + } + + node.Parent = Parent; + node.Previous = this; + Next = node; + } + + /// + /// Inserts the node after the last child node. + /// + /// The node to append. + public void AppendChild(Node node) + { + if(Child == null) + { + CheckAttachCompatibility(node); + node.Detach(); + + node.Parent = this; + Child = node; + + return; + } + + Node target = Child; + while(target.Next != null) + { + target = target.Next; + } + + node.InsertAfter(target); + } + + /// + /// Inserts the node to be a specific child index. + /// + /// Index at which to insert the node. + /// The node to insert. + /// + public void InsertChild(int index, Node node) + { + if(index < 0) + { + throw new IndexOutOfRangeException($"Index {index} out of range! Must be a positive value!"); + } + + Node? previous = null; + Node? target = Child; + for(int i = 0; i < index; i++) + { + if(target == null) + { + throw new IndexOutOfRangeException($"Index {index} out of range! Maximum valid insert index: {i}"); + } + + previous = target; + target = target.Next; + } + + + if(target != null) + { + target.InsertBefore(node); + } + else if(previous != null) + { + previous.InsertAfter(node); + } + else + { + AppendChild(node); + } + } + + + /// + /// Replaces the child of node. Old and new child will keep sibling relationships. + /// + /// The new child to set + public void SetChild(Node? node) + { + if(Child == node) + { + return; + } + + if(node?.Previous != null) + { + throw new InvalidOperationException("The node you are trying to set has a previous node, only root siblings can be set as a direct child!"); + } + + if(node != null) + { + CheckAttachCompatibility(node); + } + + DetachChildren(true); + + if(node == null) + { + return; + } + + Node? current = node; + while(current != null) + { + current.Parent = this; + current = current.Next; + } + + Child = node; + } + + /// + /// Replaces the successor of node. Old and new child will keep their own successors. + /// + /// The new successor to set. + public void SetNext(Node? node) + { + if(Child == node) + { + return; + } + + if(node?.Parent != null) + { + throw new InvalidOperationException("The node you are trying to set has a parent node, only parentless nodes can be set as a direct successor!"); + } + + if(node != null) + { + CheckAttachCompatibility(node); + } + + DetachSuccessors(true); + + if(node == null) + { + return; + } + + if(Parent != null) + { + Node? current = node; + while(current != null) + { + current.Parent = Parent; + current = current.Next; + } + } + + Next = node; + node.Previous = this; + } + } +} diff --git a/src/SA3D.Modeling/ObjectData/Node.cs b/src/SA3D.Modeling/ObjectData/Node.cs new file mode 100644 index 0000000..601e156 --- /dev/null +++ b/src/SA3D.Modeling/ObjectData/Node.cs @@ -0,0 +1,239 @@ +using SA3D.Common; +using SA3D.Common.IO; +using SA3D.Common.Lookup; +using SA3D.Modeling.ObjectData.Enums; +using SA3D.Modeling.Structs; +using System; +using System.Collections.Generic; +using System.Numerics; + +namespace SA3D.Modeling.ObjectData +{ + /// + /// Hierarchy object making up models. + /// + public partial class Node : ILabel + { + /// + /// Byte size of a node structure. + /// + public const uint StructSize = 0x34; + + /// + public string Label { get; set; } + + + /// + /// Creates a new blank node. + /// + public Node() + { + Label = "object_" + StringExtensions.GenerateIdentifier(); + } + + + /// + /// Writes the node and its contents to an endian stack writer. + /// + /// The writer to write to. + /// Format to write the model in. + /// Pointer references to utilize. + /// The address at which the node was written. + /// + public uint Write(EndianStackWriter writer, ModelFormat format, PointerLUT lut) + { + uint onWrite() + { + uint childAddress = Child?.Write(writer, format, lut) ?? 0; + uint nextAddress = Next?.Write(writer, format, lut) ?? 0; + uint attachAddress = Attach?.Write(writer, format, lut) ?? 0; + + uint result = writer.PointerPosition; + + writer.WriteUInt((uint)Attributes); + writer.WriteUInt(attachAddress); + + writer.WriteVector3(Position); + + if(UseQuaternionRotation) + { + writer.WriteFloat(QuaternionRotation.X); + writer.WriteFloat(QuaternionRotation.Y); + writer.WriteFloat(QuaternionRotation.Z); + } + else + { + writer.WriteVector3(EulerRotation, FloatIOType.BAMS32); + } + + writer.WriteVector3(Scale); + + writer.WriteUInt(childAddress); + writer.WriteUInt(nextAddress); + + if(UseQuaternionRotation) + { + writer.WriteFloat(QuaternionRotation.W); + } + else + { + writer.WriteEmpty(4); + } + + return result; + } + + return lut.GetAddAddress(this, onWrite); + } + + /// + /// Reads a node and its contents off an endian stack reader. + /// + /// The reader to read from. + /// Address at which to start reading. + /// Format of the model to read. + /// Pointer references to utilize. + /// The node that was read. + public static Node Read(EndianStackReader reader, uint address, ModelFormat format, PointerLUT lut) + { + Node onRead() + { + Node result = new(); + + NodeAttributes attributes = (NodeAttributes)reader.ReadUInt(address); + result.SetAllNodeAttributes(attributes, RotationUpdateMode.Keep); + + if(reader.TryReadPointer(address + 4, out uint attachAddress)) + { + result.Attach = Mesh.Attach.Read(reader, attachAddress, format, lut); + } + + address += 8; + Vector3 position = reader.ReadVector3(ref address); + Vector3? eulerRotation = null; + Quaternion? quaternionRotation = null; + + if(result.UseQuaternionRotation) + { + Vector3 vectorPart = reader.ReadVector3(ref address); + float scalaPart = reader.ReadFloat(address + 20); + + quaternionRotation = new(vectorPart, scalaPart); + } + else + { + eulerRotation = reader.ReadVector3(ref address, FloatIOType.BAMS32); + } + + Vector3 scale = reader.ReadVector3(ref address); + + result.UpdateTransforms(position, eulerRotation, quaternionRotation, scale, RotationUpdateMode.Keep); + + if(reader.TryReadPointer(address, out uint childAddr)) + { + result.SetChild(Read(reader, childAddr, format, lut)); + } + + if(reader.TryReadPointer(address + 4, out uint siblingAddr)) + { + result.SetNext(Read(reader, siblingAddr, format, lut)); + } + + return result; + } + + return lut.GetAddLabeledValue(address, "object_", onRead); + } + + + /// + /// Creates a shallow copy of the node with no relationships. + /// + /// The cloned node. + public Node SimpleCopy() + { + Node result = (Node)MemberwiseClone(); + + result.Parent = null; + result.Child = null; + result.Previous = null; + result.Next = null; + + return result; + } + + /// + /// Creates a copy of the node with no relationships and a deep cloned attach. + /// + /// The cloned node. + public Node AttachCopy() + { + Node result = SimpleCopy(); + if(result.Attach != null) + { + result.Attach = result.Attach.Clone(); + } + + return result; + } + + /// + /// Duplicated the node in place and inserts it after the original node. + /// + public Node Duplicate() + { + Node result = SimpleCopy(); + result.Label += "_Clone"; + InsertAfter(result); + return result; + } + + private Node BaseClone(Func cloneFunc) + { + Dictionary clones = new(); + + foreach(Node node in GetTreeNodeEnumerable()) + { + clones.Add(node, cloneFunc(node)); + } + + foreach(KeyValuePair item in clones) + { + Node original = item.Key; + Node clone = item.Value; + + clone.Parent = original.Parent == null ? null : clones[original.Parent]; + clone.Child = original.Child == null ? null : clones[original.Child]; + clone.Previous = original.Previous == null ? null : clones[original.Previous]; + clone.Next = original.Next == null ? null : clones[original.Next]; + } + + return clones[this]; + } + + /// + /// Clones the entire tree and returns the clone of the node that the calling node. Attaches are reused. + /// + /// The cloned instance of the calling node + public Node DeepSimpleCopy() + { + return BaseClone((n) => n.SimpleCopy()); + } + + /// + /// Clones the entire tree including attaches and returns the clone of the node that the calling node. + /// + /// The cloned instance of the calling node + public Node DeepAttachCopy() + { + return BaseClone((n) => n.AttachCopy()); + } + + + /// + public override string ToString() + { + return Attach == null ? $"{Label} - /" : $"{Label} - {Attach}"; + } + } +} diff --git a/src/SA3D.Modeling/PublicAPI/net7.0/PublicAPI.Shipped.txt b/src/SA3D.Modeling/PublicAPI/net7.0/PublicAPI.Shipped.txt new file mode 100644 index 0000000..91b0e1a --- /dev/null +++ b/src/SA3D.Modeling/PublicAPI/net7.0/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable \ No newline at end of file diff --git a/src/SA3D.Modeling/PublicAPI/net7.0/PublicAPI.Unshipped.txt b/src/SA3D.Modeling/PublicAPI/net7.0/PublicAPI.Unshipped.txt new file mode 100644 index 0000000..d082b59 --- /dev/null +++ b/src/SA3D.Modeling/PublicAPI/net7.0/PublicAPI.Unshipped.txt @@ -0,0 +1,1878 @@ +abstract SA3D.Modeling.Mesh.Chunk.PolyChunk.ByteSize.get -> uint +abstract SA3D.Modeling.Mesh.Chunk.PolyChunk.InternalWrite(SA3D.Common.IO.EndianStackWriter! writer) -> void +abstract SA3D.Modeling.Mesh.Chunk.PolyChunks.SizedChunk.Size.get -> ushort +const SA3D.Modeling.Animation.Motion.StructSize = 16 -> uint +const SA3D.Modeling.Mesh.Basic.BasicMaterial.StructSize = 20 -> uint +const SA3D.Modeling.Mesh.Basic.BasicMesh.StructSize = 24 -> uint +const SA3D.Modeling.Mesh.Basic.BasicMesh.StructSizeDX = 28 -> uint +const SA3D.Modeling.Mesh.Buffer.BufferCorner.StructSize = 14 -> uint +const SA3D.Modeling.Mesh.Buffer.BufferCorner.StructSizeNoColor = 10 -> uint +const SA3D.Modeling.Mesh.Buffer.BufferMaterial.StructSize = 32 -> uint +const SA3D.Modeling.ObjectData.Node.StructSize = 52 -> uint +override SA3D.Modeling.Animation.Motion.ToString() -> string! +override SA3D.Modeling.File.ModelFile.ToString() -> string! +override SA3D.Modeling.Mesh.Attach.ToString() -> string! +override SA3D.Modeling.Mesh.Basic.BasicAttach.CanWrite(SA3D.Modeling.ObjectData.Enums.ModelFormat format) -> bool +override SA3D.Modeling.Mesh.Basic.BasicAttach.CheckHasWeights() -> bool +override SA3D.Modeling.Mesh.Basic.BasicAttach.Clone() -> SA3D.Modeling.Mesh.Attach! +override SA3D.Modeling.Mesh.Basic.BasicAttach.Format.get -> SA3D.Modeling.Mesh.AttachFormat +override SA3D.Modeling.Mesh.Basic.BasicAttach.RecalculateBounds() -> void +override SA3D.Modeling.Mesh.Basic.BasicAttach.ToString() -> string! +override SA3D.Modeling.Mesh.Basic.BasicMaterial.Equals(object? obj) -> bool +override SA3D.Modeling.Mesh.Basic.BasicMaterial.GetHashCode() -> int +override SA3D.Modeling.Mesh.Basic.BasicMaterial.ToString() -> string! +override SA3D.Modeling.Mesh.Basic.Polygon.BasicMultiPolygon.ToString() -> string! +override SA3D.Modeling.Mesh.Basic.Polygon.BasicQuad.ToString() -> string! +override SA3D.Modeling.Mesh.Basic.Polygon.BasicTriangle.ToString() -> string! +override SA3D.Modeling.Mesh.Buffer.BufferCorner.Equals(object? obj) -> bool +override SA3D.Modeling.Mesh.Buffer.BufferCorner.GetHashCode() -> int +override SA3D.Modeling.Mesh.Buffer.BufferCorner.ToString() -> string! +override SA3D.Modeling.Mesh.Buffer.BufferMesh.ToString() -> string! +override SA3D.Modeling.Mesh.Buffer.BufferVertex.Equals(object? obj) -> bool +override SA3D.Modeling.Mesh.Buffer.BufferVertex.GetHashCode() -> int +override SA3D.Modeling.Mesh.Buffer.BufferVertex.ToString() -> string! +override SA3D.Modeling.Mesh.Chunk.ChunkAttach.CanWrite(SA3D.Modeling.ObjectData.Enums.ModelFormat format) -> bool +override SA3D.Modeling.Mesh.Chunk.ChunkAttach.CheckHasWeights() -> bool +override SA3D.Modeling.Mesh.Chunk.ChunkAttach.Clone() -> SA3D.Modeling.Mesh.Chunk.ChunkAttach! +override SA3D.Modeling.Mesh.Chunk.ChunkAttach.Format.get -> SA3D.Modeling.Mesh.AttachFormat +override SA3D.Modeling.Mesh.Chunk.ChunkAttach.RecalculateBounds() -> void +override SA3D.Modeling.Mesh.Chunk.ChunkAttach.ToString() -> string! +override SA3D.Modeling.Mesh.Chunk.PolyChunk.ToString() -> string! +override SA3D.Modeling.Mesh.Chunk.PolyChunks.BitsChunk.ByteSize.get -> uint +override SA3D.Modeling.Mesh.Chunk.PolyChunks.BitsChunk.InternalWrite(SA3D.Common.IO.EndianStackWriter! writer) -> void +override SA3D.Modeling.Mesh.Chunk.PolyChunks.BlendAlphaChunk.ToString() -> string! +override SA3D.Modeling.Mesh.Chunk.PolyChunks.CacheListChunk.ToString() -> string! +override SA3D.Modeling.Mesh.Chunk.PolyChunks.DrawListChunk.ToString() -> string! +override SA3D.Modeling.Mesh.Chunk.PolyChunks.MaterialBumpChunk.InternalWrite(SA3D.Common.IO.EndianStackWriter! writer) -> void +override SA3D.Modeling.Mesh.Chunk.PolyChunks.MaterialBumpChunk.Size.get -> ushort +override SA3D.Modeling.Mesh.Chunk.PolyChunks.MaterialChunk.InternalWrite(SA3D.Common.IO.EndianStackWriter! writer) -> void +override SA3D.Modeling.Mesh.Chunk.PolyChunks.MaterialChunk.Size.get -> ushort +override SA3D.Modeling.Mesh.Chunk.PolyChunks.MipmapDistanceMultiplierChunk.ToString() -> string! +override SA3D.Modeling.Mesh.Chunk.PolyChunks.SizedChunk.InternalWrite(SA3D.Common.IO.EndianStackWriter! writer) -> void +override SA3D.Modeling.Mesh.Chunk.PolyChunks.SpecularExponentChunk.ToString() -> string! +override SA3D.Modeling.Mesh.Chunk.PolyChunks.StripChunk.Clone() -> SA3D.Modeling.Mesh.Chunk.PolyChunks.StripChunk! +override SA3D.Modeling.Mesh.Chunk.PolyChunks.StripChunk.InternalWrite(SA3D.Common.IO.EndianStackWriter! writer) -> void +override SA3D.Modeling.Mesh.Chunk.PolyChunks.StripChunk.Size.get -> ushort +override SA3D.Modeling.Mesh.Chunk.PolyChunks.StripChunk.ToString() -> string! +override SA3D.Modeling.Mesh.Chunk.PolyChunks.TextureChunk.ByteSize.get -> uint +override SA3D.Modeling.Mesh.Chunk.PolyChunks.TextureChunk.InternalWrite(SA3D.Common.IO.EndianStackWriter! writer) -> void +override SA3D.Modeling.Mesh.Chunk.PolyChunks.TextureChunk.ToString() -> string! +override SA3D.Modeling.Mesh.Chunk.PolyChunks.VolumeChunk.Clone() -> SA3D.Modeling.Mesh.Chunk.PolyChunks.VolumeChunk! +override SA3D.Modeling.Mesh.Chunk.PolyChunks.VolumeChunk.InternalWrite(SA3D.Common.IO.EndianStackWriter! writer) -> void +override SA3D.Modeling.Mesh.Chunk.PolyChunks.VolumeChunk.Size.get -> ushort +override SA3D.Modeling.Mesh.Chunk.PolyChunks.VolumeChunk.ToString() -> string! +override SA3D.Modeling.Mesh.Chunk.Structs.ChunkCorner.Equals(object? obj) -> bool +override SA3D.Modeling.Mesh.Chunk.Structs.ChunkCorner.GetHashCode() -> int +override SA3D.Modeling.Mesh.Chunk.Structs.ChunkCorner.ToString() -> string! +override SA3D.Modeling.Mesh.Chunk.Structs.ChunkStrip.ToString() -> string! +override SA3D.Modeling.Mesh.Chunk.Structs.ChunkVertex.Equals(object? obj) -> bool +override SA3D.Modeling.Mesh.Chunk.Structs.ChunkVertex.GetHashCode() -> int +override SA3D.Modeling.Mesh.Chunk.Structs.ChunkVertex.ToString() -> string! +override SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeQuad.ToString() -> string! +override SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeTriangle.ToString() -> string! +override SA3D.Modeling.Mesh.Chunk.VertexChunk.ToString() -> string! +override SA3D.Modeling.Mesh.Gamecube.GCAttach.CanWrite(SA3D.Modeling.ObjectData.Enums.ModelFormat format) -> bool +override SA3D.Modeling.Mesh.Gamecube.GCAttach.CheckHasWeights() -> bool +override SA3D.Modeling.Mesh.Gamecube.GCAttach.Clone() -> SA3D.Modeling.Mesh.Gamecube.GCAttach! +override SA3D.Modeling.Mesh.Gamecube.GCAttach.Format.get -> SA3D.Modeling.Mesh.AttachFormat +override SA3D.Modeling.Mesh.Gamecube.GCAttach.RecalculateBounds() -> void +override SA3D.Modeling.Mesh.Gamecube.GCAttach.ToString() -> string! +override SA3D.Modeling.Mesh.Gamecube.GCCorner.Equals(object? obj) -> bool +override SA3D.Modeling.Mesh.Gamecube.GCCorner.GetHashCode() -> int +override SA3D.Modeling.Mesh.Gamecube.GCCorner.ToString() -> string! +override SA3D.Modeling.Mesh.Gamecube.GCMesh.ToString() -> string! +override SA3D.Modeling.Mesh.Gamecube.GCPolygon.ToString() -> string! +override SA3D.Modeling.Mesh.Gamecube.GCVertexSet.ToString() -> string! +override SA3D.Modeling.Mesh.Gamecube.Parameters.GCAmbientColorParameter.ToString() -> string! +override SA3D.Modeling.Mesh.Gamecube.Parameters.GCBlendAlphaParameter.ToString() -> string! +override SA3D.Modeling.Mesh.Gamecube.Parameters.GCDiffuseColorParameter.ToString() -> string! +override SA3D.Modeling.Mesh.Gamecube.Parameters.GCIndexFormatParameter.ToString() -> string! +override SA3D.Modeling.Mesh.Gamecube.Parameters.GCLightingParameter.ToString() -> string! +override SA3D.Modeling.Mesh.Gamecube.Parameters.GCSpecularColorParameter.ToString() -> string! +override SA3D.Modeling.Mesh.Gamecube.Parameters.GCTexCoordParameter.ToString() -> string! +override SA3D.Modeling.Mesh.Gamecube.Parameters.GCTextureParameter.ToString() -> string! +override SA3D.Modeling.Mesh.Gamecube.Parameters.GCUnknownParameter.ToString() -> string! +override SA3D.Modeling.Mesh.Gamecube.Parameters.GCVertexFormatParameter.ToString() -> string! +override SA3D.Modeling.Mesh.Weighted.WeightedVertex.Equals(object? obj) -> bool +override SA3D.Modeling.Mesh.Weighted.WeightedVertex.GetHashCode() -> int +override SA3D.Modeling.Mesh.Weighted.WeightedVertex.ToString() -> string! +override SA3D.Modeling.ObjectData.LandEntry.ToString() -> string! +override SA3D.Modeling.ObjectData.LandTable.ToString() -> string! +override SA3D.Modeling.ObjectData.Node.ToString() -> string! +override SA3D.Modeling.Structs.Bounds.ToString() -> string! +override SA3D.Modeling.Structs.Color.Equals(object? obj) -> bool +override SA3D.Modeling.Structs.Color.GetHashCode() -> int +override SA3D.Modeling.Structs.Color.ToString() -> string! +override SA3D.Modeling.Structs.PointerLUT.AddEntry(uint address, object! value) -> void +override sealed SA3D.Modeling.Mesh.Chunk.PolyChunks.SizedChunk.ByteSize.get -> uint +SA3D.Modeling.Animation.EnumExtensions +SA3D.Modeling.Animation.Frame +SA3D.Modeling.Animation.Frame.Angle.get -> float? +SA3D.Modeling.Animation.Frame.Angle.set -> void +SA3D.Modeling.Animation.Frame.Color.get -> SA3D.Modeling.Structs.Color? +SA3D.Modeling.Animation.Frame.Color.set -> void +SA3D.Modeling.Animation.Frame.EulerRotation.get -> System.Numerics.Vector3? +SA3D.Modeling.Animation.Frame.EulerRotation.set -> void +SA3D.Modeling.Animation.Frame.Frame() -> void +SA3D.Modeling.Animation.Frame.FrameTime.get -> float +SA3D.Modeling.Animation.Frame.FrameTime.set -> void +SA3D.Modeling.Animation.Frame.Intensity.get -> float? +SA3D.Modeling.Animation.Frame.Intensity.set -> void +SA3D.Modeling.Animation.Frame.Normal.get -> System.Numerics.Vector3[]? +SA3D.Modeling.Animation.Frame.Normal.set -> void +SA3D.Modeling.Animation.Frame.Point.get -> System.Numerics.Vector2? +SA3D.Modeling.Animation.Frame.Point.set -> void +SA3D.Modeling.Animation.Frame.Position.get -> System.Numerics.Vector3? +SA3D.Modeling.Animation.Frame.Position.set -> void +SA3D.Modeling.Animation.Frame.QuaternionRotation.get -> System.Numerics.Quaternion? +SA3D.Modeling.Animation.Frame.QuaternionRotation.set -> void +SA3D.Modeling.Animation.Frame.Roll.get -> float? +SA3D.Modeling.Animation.Frame.Roll.set -> void +SA3D.Modeling.Animation.Frame.Scale.get -> System.Numerics.Vector3? +SA3D.Modeling.Animation.Frame.Scale.set -> void +SA3D.Modeling.Animation.Frame.Spotlight.get -> SA3D.Modeling.Animation.Spotlight? +SA3D.Modeling.Animation.Frame.Spotlight.set -> void +SA3D.Modeling.Animation.Frame.Target.get -> System.Numerics.Vector3? +SA3D.Modeling.Animation.Frame.Target.set -> void +SA3D.Modeling.Animation.Frame.Vector.get -> System.Numerics.Vector3? +SA3D.Modeling.Animation.Frame.Vector.set -> void +SA3D.Modeling.Animation.Frame.Vertex.get -> System.Numerics.Vector3[]? +SA3D.Modeling.Animation.Frame.Vertex.set -> void +SA3D.Modeling.Animation.InterpolationMode +SA3D.Modeling.Animation.InterpolationMode.Linear = 0 -> SA3D.Modeling.Animation.InterpolationMode +SA3D.Modeling.Animation.InterpolationMode.Spline = 1 -> SA3D.Modeling.Animation.InterpolationMode +SA3D.Modeling.Animation.InterpolationMode.User = 2 -> SA3D.Modeling.Animation.InterpolationMode +SA3D.Modeling.Animation.KeyframeAttributes +SA3D.Modeling.Animation.KeyframeAttributes.Angle = 256 -> SA3D.Modeling.Animation.KeyframeAttributes +SA3D.Modeling.Animation.KeyframeAttributes.EulerRotation = 2 -> SA3D.Modeling.Animation.KeyframeAttributes +SA3D.Modeling.Animation.KeyframeAttributes.Intensity = 1024 -> SA3D.Modeling.Animation.KeyframeAttributes +SA3D.Modeling.Animation.KeyframeAttributes.LightColor = 512 -> SA3D.Modeling.Animation.KeyframeAttributes +SA3D.Modeling.Animation.KeyframeAttributes.Normal = 32 -> SA3D.Modeling.Animation.KeyframeAttributes +SA3D.Modeling.Animation.KeyframeAttributes.Point = 4096 -> SA3D.Modeling.Animation.KeyframeAttributes +SA3D.Modeling.Animation.KeyframeAttributes.Position = 1 -> SA3D.Modeling.Animation.KeyframeAttributes +SA3D.Modeling.Animation.KeyframeAttributes.QuaternionRotation = 8192 -> SA3D.Modeling.Animation.KeyframeAttributes +SA3D.Modeling.Animation.KeyframeAttributes.Roll = 128 -> SA3D.Modeling.Animation.KeyframeAttributes +SA3D.Modeling.Animation.KeyframeAttributes.Scale = 4 -> SA3D.Modeling.Animation.KeyframeAttributes +SA3D.Modeling.Animation.KeyframeAttributes.Spot = 2048 -> SA3D.Modeling.Animation.KeyframeAttributes +SA3D.Modeling.Animation.KeyframeAttributes.Target = 64 -> SA3D.Modeling.Animation.KeyframeAttributes +SA3D.Modeling.Animation.KeyframeAttributes.Vector = 8 -> SA3D.Modeling.Animation.KeyframeAttributes +SA3D.Modeling.Animation.KeyframeAttributes.Vertex = 16 -> SA3D.Modeling.Animation.KeyframeAttributes +SA3D.Modeling.Animation.Keyframes +SA3D.Modeling.Animation.Keyframes.Angle.get -> System.Collections.Generic.SortedDictionary! +SA3D.Modeling.Animation.Keyframes.EnsureNodeKeyframes(SA3D.Modeling.ObjectData.Node! node, SA3D.Modeling.Animation.KeyframeAttributes targets, uint endFrame) -> void +SA3D.Modeling.Animation.Keyframes.EulerRotation.get -> System.Collections.Generic.SortedDictionary! +SA3D.Modeling.Animation.Keyframes.GetFrameAt(float frame) -> SA3D.Modeling.Animation.Frame +SA3D.Modeling.Animation.Keyframes.HasKeyframes.get -> bool +SA3D.Modeling.Animation.Keyframes.Intensity.get -> System.Collections.Generic.SortedDictionary! +SA3D.Modeling.Animation.Keyframes.KeyframeCount.get -> uint +SA3D.Modeling.Animation.Keyframes.Keyframes() -> void +SA3D.Modeling.Animation.Keyframes.LightColor.get -> System.Collections.Generic.SortedDictionary! +SA3D.Modeling.Animation.Keyframes.Normal.get -> System.Collections.Generic.SortedDictionary!>! +SA3D.Modeling.Animation.Keyframes.Optimize(float generalThreshold, float quaternionThreshold, float colorThreshold, bool asDegrees, uint? start = null, uint? end = null) -> void +SA3D.Modeling.Animation.Keyframes.Point.get -> System.Collections.Generic.SortedDictionary! +SA3D.Modeling.Animation.Keyframes.Position.get -> System.Collections.Generic.SortedDictionary! +SA3D.Modeling.Animation.Keyframes.QuaternionRotation.get -> System.Collections.Generic.SortedDictionary! +SA3D.Modeling.Animation.Keyframes.Roll.get -> System.Collections.Generic.SortedDictionary! +SA3D.Modeling.Animation.Keyframes.Scale.get -> System.Collections.Generic.SortedDictionary! +SA3D.Modeling.Animation.Keyframes.Spot.get -> System.Collections.Generic.SortedDictionary! +SA3D.Modeling.Animation.Keyframes.Target.get -> System.Collections.Generic.SortedDictionary! +SA3D.Modeling.Animation.Keyframes.Type.get -> SA3D.Modeling.Animation.KeyframeAttributes +SA3D.Modeling.Animation.Keyframes.Vector.get -> System.Collections.Generic.SortedDictionary! +SA3D.Modeling.Animation.Keyframes.Vertex.get -> System.Collections.Generic.SortedDictionary!>! +SA3D.Modeling.Animation.Keyframes.Write(SA3D.Common.IO.EndianStackWriter! writer, SA3D.Modeling.Animation.KeyframeAttributes writeAttributes, SA3D.Modeling.Structs.PointerLUT! lut, bool shortRot = false) -> (uint address, uint count)[]! +SA3D.Modeling.Animation.LandEntryMotion +SA3D.Modeling.Animation.LandEntryMotion.Frame.get -> float +SA3D.Modeling.Animation.LandEntryMotion.Frame.set -> void +SA3D.Modeling.Animation.LandEntryMotion.LandEntryMotion(float frame, float step, float maxFrame, SA3D.Modeling.Animation.NodeMotion! nodeMotion, uint textureListPointer) -> void +SA3D.Modeling.Animation.LandEntryMotion.LandEntryMotion(float frame, float step, float maxFrame, SA3D.Modeling.ObjectData.Node! model, SA3D.Modeling.Animation.Motion! motion, uint textureListPointer) -> void +SA3D.Modeling.Animation.LandEntryMotion.LandEntryMotion(float frame, float step, float maxFrame, SA3D.Modeling.ObjectData.Node! model, SA3D.Modeling.Animation.NodeMotion! nodeMotion, uint textureListPointer) -> void +SA3D.Modeling.Animation.LandEntryMotion.MaxFrame.get -> float +SA3D.Modeling.Animation.LandEntryMotion.MaxFrame.set -> void +SA3D.Modeling.Animation.LandEntryMotion.Model.get -> SA3D.Modeling.ObjectData.Node! +SA3D.Modeling.Animation.LandEntryMotion.Model.set -> void +SA3D.Modeling.Animation.LandEntryMotion.NodeMotion.get -> SA3D.Modeling.Animation.NodeMotion! +SA3D.Modeling.Animation.LandEntryMotion.NodeMotion.set -> void +SA3D.Modeling.Animation.LandEntryMotion.Step.get -> float +SA3D.Modeling.Animation.LandEntryMotion.Step.set -> void +SA3D.Modeling.Animation.LandEntryMotion.TextureListPointer.get -> uint +SA3D.Modeling.Animation.LandEntryMotion.TextureListPointer.set -> void +SA3D.Modeling.Animation.LandEntryMotion.Write(SA3D.Common.IO.EndianStackWriter! writer, SA3D.Modeling.Structs.PointerLUT! lut) -> void +SA3D.Modeling.Animation.LandEntryMotion.WriteData(SA3D.Common.IO.EndianStackWriter! writer, SA3D.Modeling.ObjectData.Enums.ModelFormat format, SA3D.Modeling.Structs.PointerLUT! lut) -> void +SA3D.Modeling.Animation.Motion +SA3D.Modeling.Animation.Motion.EnsureNodeKeyframes(SA3D.Modeling.ObjectData.Node! model, SA3D.Modeling.Animation.KeyframeAttributes targetTypes, bool createKeyframes) -> void +SA3D.Modeling.Animation.Motion.GetFrameCount() -> uint +SA3D.Modeling.Animation.Motion.InterpolationMode.get -> SA3D.Modeling.Animation.InterpolationMode +SA3D.Modeling.Animation.Motion.InterpolationMode.set -> void +SA3D.Modeling.Animation.Motion.IsCameraMotion.get -> bool +SA3D.Modeling.Animation.Motion.IsLightMotion.get -> bool +SA3D.Modeling.Animation.Motion.IsNodeMotion.get -> bool +SA3D.Modeling.Animation.Motion.IsShapeMotion.get -> bool +SA3D.Modeling.Animation.Motion.IsSpotLightMotion.get -> bool +SA3D.Modeling.Animation.Motion.Keyframes.get -> System.Collections.Generic.Dictionary! +SA3D.Modeling.Animation.Motion.KeyframeTypes.get -> SA3D.Modeling.Animation.KeyframeAttributes +SA3D.Modeling.Animation.Motion.Label.get -> string! +SA3D.Modeling.Animation.Motion.Label.set -> void +SA3D.Modeling.Animation.Motion.ManualKeyframeTypes.get -> SA3D.Modeling.Animation.KeyframeAttributes +SA3D.Modeling.Animation.Motion.ManualKeyframeTypes.set -> void +SA3D.Modeling.Animation.Motion.ModelCount.get -> uint +SA3D.Modeling.Animation.Motion.ModelCount.set -> void +SA3D.Modeling.Animation.Motion.Motion() -> void +SA3D.Modeling.Animation.Motion.Optimize(float generalThreshold, float quaternionThreshold, float colorThreshold, bool asDegrees, uint? start = null, uint? end = null) -> void +SA3D.Modeling.Animation.Motion.ShortRot.get -> bool +SA3D.Modeling.Animation.Motion.ShortRot.set -> void +SA3D.Modeling.Animation.Motion.Write(SA3D.Common.IO.EndianStackWriter! writer, SA3D.Modeling.Structs.PointerLUT! lut) -> uint +SA3D.Modeling.Animation.NodeMotion +SA3D.Modeling.Animation.NodeMotion.Animation.get -> SA3D.Modeling.Animation.Motion! +SA3D.Modeling.Animation.NodeMotion.Animation.set -> void +SA3D.Modeling.Animation.NodeMotion.Label.get -> string! +SA3D.Modeling.Animation.NodeMotion.Label.set -> void +SA3D.Modeling.Animation.NodeMotion.Model.get -> SA3D.Modeling.ObjectData.Node! +SA3D.Modeling.Animation.NodeMotion.Model.set -> void +SA3D.Modeling.Animation.NodeMotion.NodeMotion(SA3D.Modeling.ObjectData.Node! model, SA3D.Modeling.Animation.Motion! animation) -> void +SA3D.Modeling.Animation.NodeMotion.Write(SA3D.Common.IO.EndianStackWriter! writer, SA3D.Modeling.ObjectData.Enums.ModelFormat format, SA3D.Modeling.Structs.PointerLUT! lut) -> uint +SA3D.Modeling.Animation.Spotlight +SA3D.Modeling.Animation.Spotlight.far -> float +SA3D.Modeling.Animation.Spotlight.insideAngle -> float +SA3D.Modeling.Animation.Spotlight.near -> float +SA3D.Modeling.Animation.Spotlight.outsideAngle -> float +SA3D.Modeling.Animation.Spotlight.Spotlight() -> void +SA3D.Modeling.Animation.Spotlight.Write(SA3D.Common.IO.EndianStackWriter! writer) -> void +SA3D.Modeling.Animation.Utilities.KeyframeRotationUtils +SA3D.Modeling.File.AnimationFile +SA3D.Modeling.File.AnimationFile.Animation.get -> SA3D.Modeling.Animation.Motion! +SA3D.Modeling.File.AnimationFile.MetaData.get -> SA3D.Modeling.File.MetaData! +SA3D.Modeling.File.AnimationFile.Write(SA3D.Common.IO.EndianStackWriter! writer) -> void +SA3D.Modeling.File.AnimationFile.WriteToData() -> byte[]! +SA3D.Modeling.File.AnimationFile.WriteToFile(string! filepath) -> void +SA3D.Modeling.File.LevelFile +SA3D.Modeling.File.LevelFile.Level.get -> SA3D.Modeling.ObjectData.LandTable! +SA3D.Modeling.File.LevelFile.MetaData.get -> SA3D.Modeling.File.MetaData! +SA3D.Modeling.File.LevelFile.Write(SA3D.Common.IO.EndianStackWriter! writer) -> void +SA3D.Modeling.File.LevelFile.WriteToData() -> byte[]! +SA3D.Modeling.File.LevelFile.WriteToFile(string! filepath) -> void +SA3D.Modeling.File.MetaBlockType +SA3D.Modeling.File.MetaBlockType.ActionName = 1094931534 -> SA3D.Modeling.File.MetaBlockType +SA3D.Modeling.File.MetaBlockType.Animation = 1296649793 -> SA3D.Modeling.File.MetaBlockType +SA3D.Modeling.File.MetaBlockType.Author = 1213486401 -> SA3D.Modeling.File.MetaBlockType +SA3D.Modeling.File.MetaBlockType.Description = 1129530692 -> SA3D.Modeling.File.MetaBlockType +SA3D.Modeling.File.MetaBlockType.End = 4476485 -> SA3D.Modeling.File.MetaBlockType +SA3D.Modeling.File.MetaBlockType.Label = 1279410508 -> SA3D.Modeling.File.MetaBlockType +SA3D.Modeling.File.MetaBlockType.Morph = 1179799373 -> SA3D.Modeling.File.MetaBlockType +SA3D.Modeling.File.MetaBlockType.ObjectName = 1329744462 -> SA3D.Modeling.File.MetaBlockType +SA3D.Modeling.File.MetaBlockType.Texture = 5784916 -> SA3D.Modeling.File.MetaBlockType +SA3D.Modeling.File.MetaBlockType.Tool = 1280266068 -> SA3D.Modeling.File.MetaBlockType +SA3D.Modeling.File.MetaData +SA3D.Modeling.File.MetaData.ActionName.get -> string? +SA3D.Modeling.File.MetaData.ActionName.set -> void +SA3D.Modeling.File.MetaData.AnimFiles.get -> System.Collections.Generic.List! +SA3D.Modeling.File.MetaData.Author.get -> string? +SA3D.Modeling.File.MetaData.Author.set -> void +SA3D.Modeling.File.MetaData.Description.get -> string? +SA3D.Modeling.File.MetaData.Description.set -> void +SA3D.Modeling.File.MetaData.Labels.get -> System.Collections.Generic.Dictionary! +SA3D.Modeling.File.MetaData.Labels.set -> void +SA3D.Modeling.File.MetaData.MetaData() -> void +SA3D.Modeling.File.MetaData.MorphFiles.get -> System.Collections.Generic.List! +SA3D.Modeling.File.MetaData.ObjectName.get -> string? +SA3D.Modeling.File.MetaData.ObjectName.set -> void +SA3D.Modeling.File.MetaData.Other.get -> System.Collections.Generic.Dictionary! +SA3D.Modeling.File.MetaData.Other.set -> void +SA3D.Modeling.File.MetaData.Write(SA3D.Common.IO.EndianStackWriter! writer) -> uint +SA3D.Modeling.File.ModelFile +SA3D.Modeling.File.ModelFile.Format.get -> SA3D.Modeling.ObjectData.Enums.ModelFormat +SA3D.Modeling.File.ModelFile.MetaData.get -> SA3D.Modeling.File.MetaData! +SA3D.Modeling.File.ModelFile.Model.get -> SA3D.Modeling.ObjectData.Node! +SA3D.Modeling.File.ModelFile.NJFile.get -> bool +SA3D.Modeling.File.ModelFile.Write(SA3D.Common.IO.EndianStackWriter! writer) -> void +SA3D.Modeling.File.ModelFile.WriteToData() -> byte[]! +SA3D.Modeling.File.ModelFile.WriteToFile(string! filepath) -> void +SA3D.Modeling.Mesh.Attach +SA3D.Modeling.Mesh.Attach.Attach() -> void +SA3D.Modeling.Mesh.Attach.Attach(SA3D.Modeling.Mesh.Buffer.BufferMesh![]! meshdata) -> void +SA3D.Modeling.Mesh.Attach.GetDisplayMeshes() -> (SA3D.Modeling.Mesh.Buffer.BufferMesh![]! opaque, SA3D.Modeling.Mesh.Buffer.BufferMesh![]! transparent) +SA3D.Modeling.Mesh.Attach.Label.get -> string! +SA3D.Modeling.Mesh.Attach.Label.set -> void +SA3D.Modeling.Mesh.Attach.MeshBounds.get -> SA3D.Modeling.Structs.Bounds +SA3D.Modeling.Mesh.Attach.MeshBounds.set -> void +SA3D.Modeling.Mesh.Attach.MeshData.get -> SA3D.Modeling.Mesh.Buffer.BufferMesh![]! +SA3D.Modeling.Mesh.Attach.MeshData.set -> void +SA3D.Modeling.Mesh.Attach.Write(SA3D.Common.IO.EndianStackWriter! writer, SA3D.Modeling.ObjectData.Enums.ModelFormat format, SA3D.Modeling.Structs.PointerLUT! lut) -> uint +SA3D.Modeling.Mesh.AttachFormat +SA3D.Modeling.Mesh.AttachFormat.BASIC = 1 -> SA3D.Modeling.Mesh.AttachFormat +SA3D.Modeling.Mesh.AttachFormat.Buffer = 0 -> SA3D.Modeling.Mesh.AttachFormat +SA3D.Modeling.Mesh.AttachFormat.CHUNK = 2 -> SA3D.Modeling.Mesh.AttachFormat +SA3D.Modeling.Mesh.AttachFormat.GC = 3 -> SA3D.Modeling.Mesh.AttachFormat +SA3D.Modeling.Mesh.Basic.BasicAttach +SA3D.Modeling.Mesh.Basic.BasicAttach.BasicAttach(SA3D.Common.Lookup.ILabeledArray! positions, SA3D.Common.Lookup.ILabeledArray! normals, SA3D.Common.Lookup.ILabeledArray! meshes, SA3D.Common.Lookup.ILabeledArray! materials) -> void +SA3D.Modeling.Mesh.Basic.BasicAttach.BasicAttach(System.Numerics.Vector3[]! positions, System.Numerics.Vector3[]! normals, SA3D.Modeling.Mesh.Basic.BasicMesh![]! meshes, SA3D.Modeling.Mesh.Basic.BasicMaterial[]! materials) -> void +SA3D.Modeling.Mesh.Basic.BasicAttach.ConvertToBufferMeshData(bool optimize) -> SA3D.Modeling.Mesh.Buffer.BufferMesh![]! +SA3D.Modeling.Mesh.Basic.BasicAttach.Materials.get -> SA3D.Common.Lookup.ILabeledArray! +SA3D.Modeling.Mesh.Basic.BasicAttach.Meshes.get -> SA3D.Common.Lookup.ILabeledArray! +SA3D.Modeling.Mesh.Basic.BasicAttach.Normals.get -> SA3D.Common.Lookup.ILabeledArray! +SA3D.Modeling.Mesh.Basic.BasicAttach.Positions.get -> SA3D.Common.Lookup.ILabeledArray! +SA3D.Modeling.Mesh.Basic.BasicMaterial +SA3D.Modeling.Mesh.Basic.BasicMaterial.Attributes.get -> uint +SA3D.Modeling.Mesh.Basic.BasicMaterial.Attributes.set -> void +SA3D.Modeling.Mesh.Basic.BasicMaterial.BasicMaterial() -> void +SA3D.Modeling.Mesh.Basic.BasicMaterial.BasicMaterial(SA3D.Modeling.Mesh.Basic.BasicMaterial template) -> void +SA3D.Modeling.Mesh.Basic.BasicMaterial.ClampU.get -> bool +SA3D.Modeling.Mesh.Basic.BasicMaterial.ClampU.set -> void +SA3D.Modeling.Mesh.Basic.BasicMaterial.ClampV.get -> bool +SA3D.Modeling.Mesh.Basic.BasicMaterial.ClampV.set -> void +SA3D.Modeling.Mesh.Basic.BasicMaterial.DestinationAlpha.get -> SA3D.Modeling.Mesh.BlendMode +SA3D.Modeling.Mesh.Basic.BasicMaterial.DestinationAlpha.set -> void +SA3D.Modeling.Mesh.Basic.BasicMaterial.DiffuseColor.get -> SA3D.Modeling.Structs.Color +SA3D.Modeling.Mesh.Basic.BasicMaterial.DiffuseColor.set -> void +SA3D.Modeling.Mesh.Basic.BasicMaterial.DoubleSided.get -> bool +SA3D.Modeling.Mesh.Basic.BasicMaterial.DoubleSided.set -> void +SA3D.Modeling.Mesh.Basic.BasicMaterial.EnvironmentMap.get -> bool +SA3D.Modeling.Mesh.Basic.BasicMaterial.EnvironmentMap.set -> void +SA3D.Modeling.Mesh.Basic.BasicMaterial.FilterMode.get -> SA3D.Modeling.Mesh.FilterMode +SA3D.Modeling.Mesh.Basic.BasicMaterial.FilterMode.set -> void +SA3D.Modeling.Mesh.Basic.BasicMaterial.FlatShading.get -> bool +SA3D.Modeling.Mesh.Basic.BasicMaterial.FlatShading.set -> void +SA3D.Modeling.Mesh.Basic.BasicMaterial.IgnoreLighting.get -> bool +SA3D.Modeling.Mesh.Basic.BasicMaterial.IgnoreLighting.set -> void +SA3D.Modeling.Mesh.Basic.BasicMaterial.IgnoreSpecular.get -> bool +SA3D.Modeling.Mesh.Basic.BasicMaterial.IgnoreSpecular.set -> void +SA3D.Modeling.Mesh.Basic.BasicMaterial.MipmapDistanceMultiplier.get -> float +SA3D.Modeling.Mesh.Basic.BasicMaterial.MipmapDistanceMultiplier.set -> void +SA3D.Modeling.Mesh.Basic.BasicMaterial.MirrorU.get -> bool +SA3D.Modeling.Mesh.Basic.BasicMaterial.MirrorU.set -> void +SA3D.Modeling.Mesh.Basic.BasicMaterial.MirrorV.get -> bool +SA3D.Modeling.Mesh.Basic.BasicMaterial.MirrorV.set -> void +SA3D.Modeling.Mesh.Basic.BasicMaterial.PickStatus.get -> bool +SA3D.Modeling.Mesh.Basic.BasicMaterial.PickStatus.set -> void +SA3D.Modeling.Mesh.Basic.BasicMaterial.SourceAlpha.get -> SA3D.Modeling.Mesh.BlendMode +SA3D.Modeling.Mesh.Basic.BasicMaterial.SourceAlpha.set -> void +SA3D.Modeling.Mesh.Basic.BasicMaterial.SpecularColor.get -> SA3D.Modeling.Structs.Color +SA3D.Modeling.Mesh.Basic.BasicMaterial.SpecularColor.set -> void +SA3D.Modeling.Mesh.Basic.BasicMaterial.SpecularExponent.get -> float +SA3D.Modeling.Mesh.Basic.BasicMaterial.SpecularExponent.set -> void +SA3D.Modeling.Mesh.Basic.BasicMaterial.SuperSample.get -> bool +SA3D.Modeling.Mesh.Basic.BasicMaterial.SuperSample.set -> void +SA3D.Modeling.Mesh.Basic.BasicMaterial.TextureID.get -> uint +SA3D.Modeling.Mesh.Basic.BasicMaterial.TextureID.set -> void +SA3D.Modeling.Mesh.Basic.BasicMaterial.UseAlpha.get -> bool +SA3D.Modeling.Mesh.Basic.BasicMaterial.UseAlpha.set -> void +SA3D.Modeling.Mesh.Basic.BasicMaterial.UserAttributes.get -> byte +SA3D.Modeling.Mesh.Basic.BasicMaterial.UserAttributes.set -> void +SA3D.Modeling.Mesh.Basic.BasicMaterial.UseTexture.get -> bool +SA3D.Modeling.Mesh.Basic.BasicMaterial.UseTexture.set -> void +SA3D.Modeling.Mesh.Basic.BasicMaterial.Write(SA3D.Common.IO.EndianStackWriter! writer) -> void +SA3D.Modeling.Mesh.Basic.BasicMesh +SA3D.Modeling.Mesh.Basic.BasicMesh.BasicMesh(SA3D.Modeling.Mesh.Basic.Polygon.BasicPolygonType polygonType, SA3D.Modeling.Mesh.Basic.Polygon.IBasicPolygon![]! polygons, ushort materialIndex, bool hasNormal, bool hasColor, bool hasTexcoords) -> void +SA3D.Modeling.Mesh.Basic.BasicMesh.BasicMesh(ushort materialID, SA3D.Modeling.Mesh.Basic.Polygon.BasicPolygonType polyType, SA3D.Common.Lookup.LabeledReadOnlyArray! polys, SA3D.Common.Lookup.ILabeledArray? normals, SA3D.Common.Lookup.ILabeledArray? colors, SA3D.Common.Lookup.ILabeledArray? texcoords) -> void +SA3D.Modeling.Mesh.Basic.BasicMesh.Clone() -> SA3D.Modeling.Mesh.Basic.BasicMesh! +SA3D.Modeling.Mesh.Basic.BasicMesh.Colors.get -> SA3D.Common.Lookup.ILabeledArray? +SA3D.Modeling.Mesh.Basic.BasicMesh.Colors.set -> void +SA3D.Modeling.Mesh.Basic.BasicMesh.MaterialIndex.get -> ushort +SA3D.Modeling.Mesh.Basic.BasicMesh.MaterialIndex.set -> void +SA3D.Modeling.Mesh.Basic.BasicMesh.Normals.get -> SA3D.Common.Lookup.ILabeledArray? +SA3D.Modeling.Mesh.Basic.BasicMesh.Normals.set -> void +SA3D.Modeling.Mesh.Basic.BasicMesh.PolyAttributes.get -> uint +SA3D.Modeling.Mesh.Basic.BasicMesh.PolyAttributes.set -> void +SA3D.Modeling.Mesh.Basic.BasicMesh.PolygonCornerCount.get -> int +SA3D.Modeling.Mesh.Basic.BasicMesh.Polygons.get -> SA3D.Common.Lookup.LabeledReadOnlyArray! +SA3D.Modeling.Mesh.Basic.BasicMesh.PolygonType.get -> SA3D.Modeling.Mesh.Basic.Polygon.BasicPolygonType +SA3D.Modeling.Mesh.Basic.BasicMesh.Texcoords.get -> SA3D.Common.Lookup.ILabeledArray? +SA3D.Modeling.Mesh.Basic.BasicMesh.Texcoords.set -> void +SA3D.Modeling.Mesh.Basic.BasicMesh.WriteData(SA3D.Common.IO.EndianStackWriter! writer, SA3D.Modeling.Structs.PointerLUT! lut) -> void +SA3D.Modeling.Mesh.Basic.BasicMesh.WriteMeshset(SA3D.Common.IO.EndianStackWriter! writer, bool DX, SA3D.Modeling.Structs.PointerLUT! lut) -> void +SA3D.Modeling.Mesh.Basic.Polygon.BasicMultiPolygon +SA3D.Modeling.Mesh.Basic.Polygon.BasicMultiPolygon.BasicMultiPolygon() -> void +SA3D.Modeling.Mesh.Basic.Polygon.BasicMultiPolygon.BasicMultiPolygon(uint size, bool reversed) -> void +SA3D.Modeling.Mesh.Basic.Polygon.BasicMultiPolygon.BasicMultiPolygon(ushort[]! indices, bool reversed) -> void +SA3D.Modeling.Mesh.Basic.Polygon.BasicMultiPolygon.Clone() -> object! +SA3D.Modeling.Mesh.Basic.Polygon.BasicMultiPolygon.GetEnumerator() -> System.Collections.Generic.IEnumerator! +SA3D.Modeling.Mesh.Basic.Polygon.BasicMultiPolygon.Indices.get -> ushort[]! +SA3D.Modeling.Mesh.Basic.Polygon.BasicMultiPolygon.Indices.set -> void +SA3D.Modeling.Mesh.Basic.Polygon.BasicMultiPolygon.NumIndices.get -> int +SA3D.Modeling.Mesh.Basic.Polygon.BasicMultiPolygon.Reversed.get -> bool +SA3D.Modeling.Mesh.Basic.Polygon.BasicMultiPolygon.Reversed.set -> void +SA3D.Modeling.Mesh.Basic.Polygon.BasicMultiPolygon.Size.get -> uint +SA3D.Modeling.Mesh.Basic.Polygon.BasicMultiPolygon.this[int index].get -> ushort +SA3D.Modeling.Mesh.Basic.Polygon.BasicMultiPolygon.this[int index].set -> void +SA3D.Modeling.Mesh.Basic.Polygon.BasicMultiPolygon.Write(SA3D.Common.IO.EndianStackWriter! writer) -> void +SA3D.Modeling.Mesh.Basic.Polygon.BasicPolygonType +SA3D.Modeling.Mesh.Basic.Polygon.BasicPolygonType.NPoly = 2 -> SA3D.Modeling.Mesh.Basic.Polygon.BasicPolygonType +SA3D.Modeling.Mesh.Basic.Polygon.BasicPolygonType.Quads = 1 -> SA3D.Modeling.Mesh.Basic.Polygon.BasicPolygonType +SA3D.Modeling.Mesh.Basic.Polygon.BasicPolygonType.Triangles = 0 -> SA3D.Modeling.Mesh.Basic.Polygon.BasicPolygonType +SA3D.Modeling.Mesh.Basic.Polygon.BasicPolygonType.TriangleStrips = 3 -> SA3D.Modeling.Mesh.Basic.Polygon.BasicPolygonType +SA3D.Modeling.Mesh.Basic.Polygon.BasicQuad +SA3D.Modeling.Mesh.Basic.Polygon.BasicQuad.BasicQuad() -> void +SA3D.Modeling.Mesh.Basic.Polygon.BasicQuad.BasicQuad(ushort index1, ushort index2, ushort index3, ushort index4) -> void +SA3D.Modeling.Mesh.Basic.Polygon.BasicQuad.Clone() -> object! +SA3D.Modeling.Mesh.Basic.Polygon.BasicQuad.GetEnumerator() -> System.Collections.Generic.IEnumerator! +SA3D.Modeling.Mesh.Basic.Polygon.BasicQuad.Index1.get -> ushort +SA3D.Modeling.Mesh.Basic.Polygon.BasicQuad.Index1.set -> void +SA3D.Modeling.Mesh.Basic.Polygon.BasicQuad.Index2.get -> ushort +SA3D.Modeling.Mesh.Basic.Polygon.BasicQuad.Index2.set -> void +SA3D.Modeling.Mesh.Basic.Polygon.BasicQuad.Index3.get -> ushort +SA3D.Modeling.Mesh.Basic.Polygon.BasicQuad.Index3.set -> void +SA3D.Modeling.Mesh.Basic.Polygon.BasicQuad.Index4.get -> ushort +SA3D.Modeling.Mesh.Basic.Polygon.BasicQuad.Index4.set -> void +SA3D.Modeling.Mesh.Basic.Polygon.BasicQuad.NumIndices.get -> int +SA3D.Modeling.Mesh.Basic.Polygon.BasicQuad.Size.get -> uint +SA3D.Modeling.Mesh.Basic.Polygon.BasicQuad.this[int index].get -> ushort +SA3D.Modeling.Mesh.Basic.Polygon.BasicQuad.this[int index].set -> void +SA3D.Modeling.Mesh.Basic.Polygon.BasicQuad.Write(SA3D.Common.IO.EndianStackWriter! writer) -> void +SA3D.Modeling.Mesh.Basic.Polygon.BasicTriangle +SA3D.Modeling.Mesh.Basic.Polygon.BasicTriangle.BasicTriangle() -> void +SA3D.Modeling.Mesh.Basic.Polygon.BasicTriangle.BasicTriangle(ushort index1, ushort index2, ushort index3) -> void +SA3D.Modeling.Mesh.Basic.Polygon.BasicTriangle.Clone() -> object! +SA3D.Modeling.Mesh.Basic.Polygon.BasicTriangle.GetEnumerator() -> System.Collections.Generic.IEnumerator! +SA3D.Modeling.Mesh.Basic.Polygon.BasicTriangle.Index1.get -> ushort +SA3D.Modeling.Mesh.Basic.Polygon.BasicTriangle.Index1.set -> void +SA3D.Modeling.Mesh.Basic.Polygon.BasicTriangle.Index2.get -> ushort +SA3D.Modeling.Mesh.Basic.Polygon.BasicTriangle.Index2.set -> void +SA3D.Modeling.Mesh.Basic.Polygon.BasicTriangle.Index3.get -> ushort +SA3D.Modeling.Mesh.Basic.Polygon.BasicTriangle.Index3.set -> void +SA3D.Modeling.Mesh.Basic.Polygon.BasicTriangle.NumIndices.get -> int +SA3D.Modeling.Mesh.Basic.Polygon.BasicTriangle.Size.get -> uint +SA3D.Modeling.Mesh.Basic.Polygon.BasicTriangle.this[int index].get -> ushort +SA3D.Modeling.Mesh.Basic.Polygon.BasicTriangle.this[int index].set -> void +SA3D.Modeling.Mesh.Basic.Polygon.BasicTriangle.Write(SA3D.Common.IO.EndianStackWriter! writer) -> void +SA3D.Modeling.Mesh.Basic.Polygon.IBasicPolygon +SA3D.Modeling.Mesh.Basic.Polygon.IBasicPolygon.NumIndices.get -> int +SA3D.Modeling.Mesh.Basic.Polygon.IBasicPolygon.Read(SA3D.Common.IO.EndianStackReader! reader, ref uint address, SA3D.Modeling.Mesh.Basic.Polygon.BasicPolygonType type) -> SA3D.Modeling.Mesh.Basic.Polygon.IBasicPolygon! +SA3D.Modeling.Mesh.Basic.Polygon.IBasicPolygon.Size.get -> uint +SA3D.Modeling.Mesh.Basic.Polygon.IBasicPolygon.this[int index].get -> ushort +SA3D.Modeling.Mesh.Basic.Polygon.IBasicPolygon.this[int index].set -> void +SA3D.Modeling.Mesh.Basic.Polygon.IBasicPolygon.Write(SA3D.Common.IO.EndianStackWriter! writer) -> void +SA3D.Modeling.Mesh.BlendMode +SA3D.Modeling.Mesh.BlendMode.DstAlpha = 6 -> SA3D.Modeling.Mesh.BlendMode +SA3D.Modeling.Mesh.BlendMode.DstAlphaInverted = 7 -> SA3D.Modeling.Mesh.BlendMode +SA3D.Modeling.Mesh.BlendMode.One = 1 -> SA3D.Modeling.Mesh.BlendMode +SA3D.Modeling.Mesh.BlendMode.Other = 2 -> SA3D.Modeling.Mesh.BlendMode +SA3D.Modeling.Mesh.BlendMode.OtherInverted = 3 -> SA3D.Modeling.Mesh.BlendMode +SA3D.Modeling.Mesh.BlendMode.SrcAlpha = 4 -> SA3D.Modeling.Mesh.BlendMode +SA3D.Modeling.Mesh.BlendMode.SrcAlphaInverted = 5 -> SA3D.Modeling.Mesh.BlendMode +SA3D.Modeling.Mesh.BlendMode.Zero = 0 -> SA3D.Modeling.Mesh.BlendMode +SA3D.Modeling.Mesh.Buffer.BufferCorner +SA3D.Modeling.Mesh.Buffer.BufferCorner.BufferCorner() -> void +SA3D.Modeling.Mesh.Buffer.BufferCorner.BufferCorner(ushort vertexIndex) -> void +SA3D.Modeling.Mesh.Buffer.BufferCorner.BufferCorner(ushort vertexIndex, SA3D.Modeling.Structs.Color color, System.Numerics.Vector2 texcoord) -> void +SA3D.Modeling.Mesh.Buffer.BufferCorner.Color.get -> SA3D.Modeling.Structs.Color +SA3D.Modeling.Mesh.Buffer.BufferCorner.Color.set -> void +SA3D.Modeling.Mesh.Buffer.BufferCorner.Texcoord.get -> System.Numerics.Vector2 +SA3D.Modeling.Mesh.Buffer.BufferCorner.Texcoord.set -> void +SA3D.Modeling.Mesh.Buffer.BufferCorner.VertexIndex.get -> ushort +SA3D.Modeling.Mesh.Buffer.BufferCorner.VertexIndex.set -> void +SA3D.Modeling.Mesh.Buffer.BufferCorner.Write(SA3D.Common.IO.EndianStackWriter! writer, bool writeColor) -> void +SA3D.Modeling.Mesh.Buffer.BufferMaterial +SA3D.Modeling.Mesh.Buffer.BufferMaterial.Ambient.get -> SA3D.Modeling.Structs.Color +SA3D.Modeling.Mesh.Buffer.BufferMaterial.Ambient.set -> void +SA3D.Modeling.Mesh.Buffer.BufferMaterial.AnisotropicFiltering.get -> bool +SA3D.Modeling.Mesh.Buffer.BufferMaterial.AnisotropicFiltering.set -> void +SA3D.Modeling.Mesh.Buffer.BufferMaterial.Attributes.get -> SA3D.Modeling.Mesh.Buffer.MaterialAttributes +SA3D.Modeling.Mesh.Buffer.BufferMaterial.Attributes.set -> void +SA3D.Modeling.Mesh.Buffer.BufferMaterial.BackfaceCulling.get -> bool +SA3D.Modeling.Mesh.Buffer.BufferMaterial.BackfaceCulling.set -> void +SA3D.Modeling.Mesh.Buffer.BufferMaterial.BufferMaterial() -> void +SA3D.Modeling.Mesh.Buffer.BufferMaterial.BufferMaterial(SA3D.Modeling.Mesh.Buffer.BufferMaterial template) -> void +SA3D.Modeling.Mesh.Buffer.BufferMaterial.ClampU.get -> bool +SA3D.Modeling.Mesh.Buffer.BufferMaterial.ClampU.set -> void +SA3D.Modeling.Mesh.Buffer.BufferMaterial.ClampV.get -> bool +SA3D.Modeling.Mesh.Buffer.BufferMaterial.ClampV.set -> void +SA3D.Modeling.Mesh.Buffer.BufferMaterial.DestinationBlendmode.get -> SA3D.Modeling.Mesh.BlendMode +SA3D.Modeling.Mesh.Buffer.BufferMaterial.DestinationBlendmode.set -> void +SA3D.Modeling.Mesh.Buffer.BufferMaterial.Diffuse.get -> SA3D.Modeling.Structs.Color +SA3D.Modeling.Mesh.Buffer.BufferMaterial.Diffuse.set -> void +SA3D.Modeling.Mesh.Buffer.BufferMaterial.Flat.get -> bool +SA3D.Modeling.Mesh.Buffer.BufferMaterial.Flat.set -> void +SA3D.Modeling.Mesh.Buffer.BufferMaterial.GamecubeData.get -> uint +SA3D.Modeling.Mesh.Buffer.BufferMaterial.GamecubeData.set -> void +SA3D.Modeling.Mesh.Buffer.BufferMaterial.GCMatrixID.get -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexcoordMatrix +SA3D.Modeling.Mesh.Buffer.BufferMaterial.GCMatrixID.set -> void +SA3D.Modeling.Mesh.Buffer.BufferMaterial.GCShadowStencil.get -> byte +SA3D.Modeling.Mesh.Buffer.BufferMaterial.GCShadowStencil.set -> void +SA3D.Modeling.Mesh.Buffer.BufferMaterial.GCTexCoordID.get -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordID +SA3D.Modeling.Mesh.Buffer.BufferMaterial.GCTexCoordID.set -> void +SA3D.Modeling.Mesh.Buffer.BufferMaterial.GCTexCoordSource.get -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource +SA3D.Modeling.Mesh.Buffer.BufferMaterial.GCTexCoordSource.set -> void +SA3D.Modeling.Mesh.Buffer.BufferMaterial.GCTexCoordType.get -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordType +SA3D.Modeling.Mesh.Buffer.BufferMaterial.GCTexCoordType.set -> void +SA3D.Modeling.Mesh.Buffer.BufferMaterial.HasAttributes(SA3D.Modeling.Mesh.Buffer.MaterialAttributes attrib) -> bool +SA3D.Modeling.Mesh.Buffer.BufferMaterial.MipmapDistanceMultiplier.get -> float +SA3D.Modeling.Mesh.Buffer.BufferMaterial.MipmapDistanceMultiplier.set -> void +SA3D.Modeling.Mesh.Buffer.BufferMaterial.MirrorU.get -> bool +SA3D.Modeling.Mesh.Buffer.BufferMaterial.MirrorU.set -> void +SA3D.Modeling.Mesh.Buffer.BufferMaterial.MirrorV.get -> bool +SA3D.Modeling.Mesh.Buffer.BufferMaterial.MirrorV.set -> void +SA3D.Modeling.Mesh.Buffer.BufferMaterial.NoAmbient.get -> bool +SA3D.Modeling.Mesh.Buffer.BufferMaterial.NoAmbient.set -> void +SA3D.Modeling.Mesh.Buffer.BufferMaterial.NoLighting.get -> bool +SA3D.Modeling.Mesh.Buffer.BufferMaterial.NoLighting.set -> void +SA3D.Modeling.Mesh.Buffer.BufferMaterial.NormalMapping.get -> bool +SA3D.Modeling.Mesh.Buffer.BufferMaterial.NormalMapping.set -> void +SA3D.Modeling.Mesh.Buffer.BufferMaterial.NoSpecular.get -> bool +SA3D.Modeling.Mesh.Buffer.BufferMaterial.NoSpecular.set -> void +SA3D.Modeling.Mesh.Buffer.BufferMaterial.SetAttributes(SA3D.Modeling.Mesh.Buffer.MaterialAttributes attrib, bool state) -> void +SA3D.Modeling.Mesh.Buffer.BufferMaterial.SourceBlendMode.get -> SA3D.Modeling.Mesh.BlendMode +SA3D.Modeling.Mesh.Buffer.BufferMaterial.SourceBlendMode.set -> void +SA3D.Modeling.Mesh.Buffer.BufferMaterial.Specular.get -> SA3D.Modeling.Structs.Color +SA3D.Modeling.Mesh.Buffer.BufferMaterial.Specular.set -> void +SA3D.Modeling.Mesh.Buffer.BufferMaterial.SpecularExponent.get -> float +SA3D.Modeling.Mesh.Buffer.BufferMaterial.SpecularExponent.set -> void +SA3D.Modeling.Mesh.Buffer.BufferMaterial.TextureFiltering.get -> SA3D.Modeling.Mesh.FilterMode +SA3D.Modeling.Mesh.Buffer.BufferMaterial.TextureFiltering.set -> void +SA3D.Modeling.Mesh.Buffer.BufferMaterial.TextureIndex.get -> uint +SA3D.Modeling.Mesh.Buffer.BufferMaterial.TextureIndex.set -> void +SA3D.Modeling.Mesh.Buffer.BufferMaterial.UseAlpha.get -> bool +SA3D.Modeling.Mesh.Buffer.BufferMaterial.UseAlpha.set -> void +SA3D.Modeling.Mesh.Buffer.BufferMaterial.UseTexture.get -> bool +SA3D.Modeling.Mesh.Buffer.BufferMaterial.UseTexture.set -> void +SA3D.Modeling.Mesh.Buffer.BufferMaterial.Write(SA3D.Common.IO.EndianStackWriter! writer) -> void +SA3D.Modeling.Mesh.Buffer.BufferMesh +SA3D.Modeling.Mesh.Buffer.BufferMesh.BufferMesh(SA3D.Modeling.Mesh.Buffer.BufferMaterial material, SA3D.Modeling.Mesh.Buffer.BufferCorner[]! corners, uint[]? indexList, bool strippified, bool hasColors, ushort vertexReadOffset) -> void +SA3D.Modeling.Mesh.Buffer.BufferMesh.BufferMesh(SA3D.Modeling.Mesh.Buffer.BufferVertex[]! vertices, bool continueWeight, bool hasNormals, ushort vertexWriteOffset) -> void +SA3D.Modeling.Mesh.Buffer.BufferMesh.BufferMesh(SA3D.Modeling.Mesh.Buffer.BufferVertex[]? vertices, SA3D.Modeling.Mesh.Buffer.BufferMaterial material, SA3D.Modeling.Mesh.Buffer.BufferCorner[]? corners, uint[]? indexList, bool strippified, bool continueWeight, bool hasNormals, bool hasColors, ushort vertexWriteOffset, ushort vertexReadOffset) -> void +SA3D.Modeling.Mesh.Buffer.BufferMesh.Clone() -> SA3D.Modeling.Mesh.Buffer.BufferMesh! +SA3D.Modeling.Mesh.Buffer.BufferMesh.ContinueWeight.get -> bool +SA3D.Modeling.Mesh.Buffer.BufferMesh.Corners.get -> SA3D.Modeling.Mesh.Buffer.BufferCorner[]? +SA3D.Modeling.Mesh.Buffer.BufferMesh.GetCornerTriangleList() -> SA3D.Modeling.Mesh.Buffer.BufferCorner[]! +SA3D.Modeling.Mesh.Buffer.BufferMesh.GetIndexTriangleList() -> uint[]! +SA3D.Modeling.Mesh.Buffer.BufferMesh.HasColors.get -> bool +SA3D.Modeling.Mesh.Buffer.BufferMesh.HasNormals.get -> bool +SA3D.Modeling.Mesh.Buffer.BufferMesh.IndexList.get -> uint[]? +SA3D.Modeling.Mesh.Buffer.BufferMesh.Material.get -> SA3D.Modeling.Mesh.Buffer.BufferMaterial +SA3D.Modeling.Mesh.Buffer.BufferMesh.OptimizePolygons() -> void +SA3D.Modeling.Mesh.Buffer.BufferMesh.Strippified.get -> bool +SA3D.Modeling.Mesh.Buffer.BufferMesh.VertexReadOffset.get -> ushort +SA3D.Modeling.Mesh.Buffer.BufferMesh.VertexWriteOffset.get -> ushort +SA3D.Modeling.Mesh.Buffer.BufferMesh.Vertices.get -> SA3D.Modeling.Mesh.Buffer.BufferVertex[]? +SA3D.Modeling.Mesh.Buffer.BufferMesh.Write(SA3D.Common.IO.EndianStackWriter! writer) -> uint +SA3D.Modeling.Mesh.Buffer.BufferVertex +SA3D.Modeling.Mesh.Buffer.BufferVertex.BufferVertex() -> void +SA3D.Modeling.Mesh.Buffer.BufferVertex.BufferVertex(System.Numerics.Vector3 position, System.Numerics.Vector3 normal, ushort index) -> void +SA3D.Modeling.Mesh.Buffer.BufferVertex.BufferVertex(System.Numerics.Vector3 position, System.Numerics.Vector3 normal, ushort index, float weight) -> void +SA3D.Modeling.Mesh.Buffer.BufferVertex.BufferVertex(System.Numerics.Vector3 position, ushort index) -> void +SA3D.Modeling.Mesh.Buffer.BufferVertex.Equals(SA3D.Modeling.Mesh.Buffer.BufferVertex other) -> bool +SA3D.Modeling.Mesh.Buffer.BufferVertex.Index.get -> ushort +SA3D.Modeling.Mesh.Buffer.BufferVertex.Index.set -> void +SA3D.Modeling.Mesh.Buffer.BufferVertex.Normal.get -> System.Numerics.Vector3 +SA3D.Modeling.Mesh.Buffer.BufferVertex.Normal.set -> void +SA3D.Modeling.Mesh.Buffer.BufferVertex.Position.get -> System.Numerics.Vector3 +SA3D.Modeling.Mesh.Buffer.BufferVertex.Position.set -> void +SA3D.Modeling.Mesh.Buffer.BufferVertex.Weight.get -> float +SA3D.Modeling.Mesh.Buffer.BufferVertex.Weight.set -> void +SA3D.Modeling.Mesh.Buffer.BufferVertex.Write(SA3D.Common.IO.EndianStackWriter! writer, bool writeNormal) -> void +SA3D.Modeling.Mesh.Buffer.MaterialAttributes +SA3D.Modeling.Mesh.Buffer.MaterialAttributes.AnisotropicFiltering = 2 -> SA3D.Modeling.Mesh.Buffer.MaterialAttributes +SA3D.Modeling.Mesh.Buffer.MaterialAttributes.BackfaceCulling = 4096 -> SA3D.Modeling.Mesh.Buffer.MaterialAttributes +SA3D.Modeling.Mesh.Buffer.MaterialAttributes.ClampU = 4 -> SA3D.Modeling.Mesh.Buffer.MaterialAttributes +SA3D.Modeling.Mesh.Buffer.MaterialAttributes.ClampV = 8 -> SA3D.Modeling.Mesh.Buffer.MaterialAttributes +SA3D.Modeling.Mesh.Buffer.MaterialAttributes.Flat = 1024 -> SA3D.Modeling.Mesh.Buffer.MaterialAttributes +SA3D.Modeling.Mesh.Buffer.MaterialAttributes.MirrorU = 16 -> SA3D.Modeling.Mesh.Buffer.MaterialAttributes +SA3D.Modeling.Mesh.Buffer.MaterialAttributes.MirrorV = 32 -> SA3D.Modeling.Mesh.Buffer.MaterialAttributes +SA3D.Modeling.Mesh.Buffer.MaterialAttributes.NoAmbient = 256 -> SA3D.Modeling.Mesh.Buffer.MaterialAttributes +SA3D.Modeling.Mesh.Buffer.MaterialAttributes.NoLighting = 128 -> SA3D.Modeling.Mesh.Buffer.MaterialAttributes +SA3D.Modeling.Mesh.Buffer.MaterialAttributes.NormalMapping = 64 -> SA3D.Modeling.Mesh.Buffer.MaterialAttributes +SA3D.Modeling.Mesh.Buffer.MaterialAttributes.NoSpecular = 512 -> SA3D.Modeling.Mesh.Buffer.MaterialAttributes +SA3D.Modeling.Mesh.Buffer.MaterialAttributes.UseAlpha = 2048 -> SA3D.Modeling.Mesh.Buffer.MaterialAttributes +SA3D.Modeling.Mesh.Buffer.MaterialAttributes.UseTexture = 1 -> SA3D.Modeling.Mesh.Buffer.MaterialAttributes +SA3D.Modeling.Mesh.Chunk.ChunkAttach +SA3D.Modeling.Mesh.Chunk.ChunkAttach.ChunkAttach(SA3D.Common.Lookup.ILabeledArray? vertexChunks, SA3D.Common.Lookup.ILabeledArray? polyChunks) -> void +SA3D.Modeling.Mesh.Chunk.ChunkAttach.ChunkAttach(SA3D.Modeling.Mesh.Chunk.VertexChunk?[]? vertexChunks, SA3D.Modeling.Mesh.Chunk.PolyChunk?[]? polyChunks) -> void +SA3D.Modeling.Mesh.Chunk.ChunkAttach.PolyChunks.get -> SA3D.Common.Lookup.ILabeledArray? +SA3D.Modeling.Mesh.Chunk.ChunkAttach.PolyChunks.set -> void +SA3D.Modeling.Mesh.Chunk.ChunkAttach.VertexChunks.get -> SA3D.Common.Lookup.ILabeledArray? +SA3D.Modeling.Mesh.Chunk.ChunkAttach.VertexChunks.set -> void +SA3D.Modeling.Mesh.Chunk.ChunkTypeExtensions +SA3D.Modeling.Mesh.Chunk.PolyChunk +SA3D.Modeling.Mesh.Chunk.PolyChunk.Attributes.get -> byte +SA3D.Modeling.Mesh.Chunk.PolyChunk.Attributes.set -> void +SA3D.Modeling.Mesh.Chunk.PolyChunk.PolyChunk(SA3D.Modeling.Mesh.Chunk.PolyChunkType type) -> void +SA3D.Modeling.Mesh.Chunk.PolyChunk.Type.get -> SA3D.Modeling.Mesh.Chunk.PolyChunkType +SA3D.Modeling.Mesh.Chunk.PolyChunk.Type.set -> void +SA3D.Modeling.Mesh.Chunk.PolyChunk.Write(SA3D.Common.IO.EndianStackWriter! writer, SA3D.Modeling.Structs.PointerLUT! lut) -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.BitsChunk +SA3D.Modeling.Mesh.Chunk.PolyChunks.BitsChunk.BitsChunk(SA3D.Modeling.Mesh.Chunk.PolyChunkType type) -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.BlendAlphaChunk +SA3D.Modeling.Mesh.Chunk.PolyChunks.BlendAlphaChunk.BlendAlphaChunk() -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.BlendAlphaChunk.DestinationAlpha.get -> SA3D.Modeling.Mesh.BlendMode +SA3D.Modeling.Mesh.Chunk.PolyChunks.BlendAlphaChunk.DestinationAlpha.set -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.BlendAlphaChunk.SourceAlpha.get -> SA3D.Modeling.Mesh.BlendMode +SA3D.Modeling.Mesh.Chunk.PolyChunks.BlendAlphaChunk.SourceAlpha.set -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.CacheListChunk +SA3D.Modeling.Mesh.Chunk.PolyChunks.CacheListChunk.CacheListChunk() -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.CacheListChunk.List.get -> byte +SA3D.Modeling.Mesh.Chunk.PolyChunks.CacheListChunk.List.set -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.DrawListChunk +SA3D.Modeling.Mesh.Chunk.PolyChunks.DrawListChunk.DrawListChunk() -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.DrawListChunk.List.get -> byte +SA3D.Modeling.Mesh.Chunk.PolyChunks.DrawListChunk.List.set -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.MaterialBumpChunk +SA3D.Modeling.Mesh.Chunk.PolyChunks.MaterialBumpChunk.DX.get -> ushort +SA3D.Modeling.Mesh.Chunk.PolyChunks.MaterialBumpChunk.DX.set -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.MaterialBumpChunk.DY.get -> ushort +SA3D.Modeling.Mesh.Chunk.PolyChunks.MaterialBumpChunk.DY.set -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.MaterialBumpChunk.DZ.get -> ushort +SA3D.Modeling.Mesh.Chunk.PolyChunks.MaterialBumpChunk.DZ.set -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.MaterialBumpChunk.MaterialBumpChunk() -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.MaterialBumpChunk.UX.get -> ushort +SA3D.Modeling.Mesh.Chunk.PolyChunks.MaterialBumpChunk.UX.set -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.MaterialBumpChunk.UY.get -> ushort +SA3D.Modeling.Mesh.Chunk.PolyChunks.MaterialBumpChunk.UY.set -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.MaterialBumpChunk.UZ.get -> ushort +SA3D.Modeling.Mesh.Chunk.PolyChunks.MaterialBumpChunk.UZ.set -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.MaterialChunk +SA3D.Modeling.Mesh.Chunk.PolyChunks.MaterialChunk.Ambient.get -> SA3D.Modeling.Structs.Color? +SA3D.Modeling.Mesh.Chunk.PolyChunks.MaterialChunk.Ambient.set -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.MaterialChunk.DestinationAlpha.get -> SA3D.Modeling.Mesh.BlendMode +SA3D.Modeling.Mesh.Chunk.PolyChunks.MaterialChunk.DestinationAlpha.set -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.MaterialChunk.Diffuse.get -> SA3D.Modeling.Structs.Color? +SA3D.Modeling.Mesh.Chunk.PolyChunks.MaterialChunk.Diffuse.set -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.MaterialChunk.MaterialChunk() -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.MaterialChunk.Second.get -> bool +SA3D.Modeling.Mesh.Chunk.PolyChunks.MaterialChunk.Second.set -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.MaterialChunk.SourceAlpha.get -> SA3D.Modeling.Mesh.BlendMode +SA3D.Modeling.Mesh.Chunk.PolyChunks.MaterialChunk.SourceAlpha.set -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.MaterialChunk.Specular.get -> SA3D.Modeling.Structs.Color? +SA3D.Modeling.Mesh.Chunk.PolyChunks.MaterialChunk.Specular.set -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.MaterialChunk.SpecularExponent.get -> byte +SA3D.Modeling.Mesh.Chunk.PolyChunks.MaterialChunk.SpecularExponent.set -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.MipmapDistanceMultiplierChunk +SA3D.Modeling.Mesh.Chunk.PolyChunks.MipmapDistanceMultiplierChunk.MipmapDistanceMultiplier.get -> float +SA3D.Modeling.Mesh.Chunk.PolyChunks.MipmapDistanceMultiplierChunk.MipmapDistanceMultiplier.set -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.MipmapDistanceMultiplierChunk.MipmapDistanceMultiplierChunk() -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.SizedChunk +SA3D.Modeling.Mesh.Chunk.PolyChunks.SizedChunk.SizedChunk(SA3D.Modeling.Mesh.Chunk.PolyChunkType type) -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.SpecularExponentChunk +SA3D.Modeling.Mesh.Chunk.PolyChunks.SpecularExponentChunk.SpecularExponent.get -> byte +SA3D.Modeling.Mesh.Chunk.PolyChunks.SpecularExponentChunk.SpecularExponent.set -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.SpecularExponentChunk.SpecularExponentChunk() -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.StripChunk +SA3D.Modeling.Mesh.Chunk.PolyChunks.StripChunk.ChangeType(SA3D.Modeling.Mesh.Chunk.PolyChunkType type) -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.StripChunk.DoubleSide.get -> bool +SA3D.Modeling.Mesh.Chunk.PolyChunks.StripChunk.DoubleSide.set -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.StripChunk.EnvironmentMapping.get -> bool +SA3D.Modeling.Mesh.Chunk.PolyChunks.StripChunk.EnvironmentMapping.set -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.StripChunk.FlatShading.get -> bool +SA3D.Modeling.Mesh.Chunk.PolyChunks.StripChunk.FlatShading.set -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.StripChunk.HasColors.get -> bool +SA3D.Modeling.Mesh.Chunk.PolyChunks.StripChunk.HasHDTexcoords.get -> bool +SA3D.Modeling.Mesh.Chunk.PolyChunks.StripChunk.HasNormals.get -> bool +SA3D.Modeling.Mesh.Chunk.PolyChunks.StripChunk.IgnoreAmbient.get -> bool +SA3D.Modeling.Mesh.Chunk.PolyChunks.StripChunk.IgnoreAmbient.set -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.StripChunk.IgnoreLight.get -> bool +SA3D.Modeling.Mesh.Chunk.PolyChunks.StripChunk.IgnoreLight.set -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.StripChunk.IgnoreSpecular.get -> bool +SA3D.Modeling.Mesh.Chunk.PolyChunks.StripChunk.IgnoreSpecular.set -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.StripChunk.RawSize.get -> uint +SA3D.Modeling.Mesh.Chunk.PolyChunks.StripChunk.StripChunk(SA3D.Modeling.Mesh.Chunk.PolyChunkType type, SA3D.Modeling.Mesh.Chunk.Structs.ChunkStrip[]! strips, int triangleAttributeCount) -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.StripChunk.StripChunk(SA3D.Modeling.Mesh.Chunk.PolyChunkType type, ushort stripCount, int triangleAttributeCount) -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.StripChunk.Strips.get -> SA3D.Modeling.Mesh.Chunk.Structs.ChunkStrip[]! +SA3D.Modeling.Mesh.Chunk.PolyChunks.StripChunk.TexcoordCount.get -> int +SA3D.Modeling.Mesh.Chunk.PolyChunks.StripChunk.TriangleAttributeCount.get -> int +SA3D.Modeling.Mesh.Chunk.PolyChunks.StripChunk.TriangleAttributeCount.set -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.StripChunk.UnknownAttribute.get -> bool +SA3D.Modeling.Mesh.Chunk.PolyChunks.StripChunk.UnknownAttribute.set -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.StripChunk.UseAlpha.get -> bool +SA3D.Modeling.Mesh.Chunk.PolyChunks.StripChunk.UseAlpha.set -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.TextureChunk +SA3D.Modeling.Mesh.Chunk.PolyChunks.TextureChunk.ClampU.get -> bool +SA3D.Modeling.Mesh.Chunk.PolyChunks.TextureChunk.ClampU.set -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.TextureChunk.ClampV.get -> bool +SA3D.Modeling.Mesh.Chunk.PolyChunks.TextureChunk.ClampV.set -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.TextureChunk.Data.get -> ushort +SA3D.Modeling.Mesh.Chunk.PolyChunks.TextureChunk.FilterMode.get -> SA3D.Modeling.Mesh.FilterMode +SA3D.Modeling.Mesh.Chunk.PolyChunks.TextureChunk.FilterMode.set -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.TextureChunk.MipmapDistanceMultiplier.get -> float +SA3D.Modeling.Mesh.Chunk.PolyChunks.TextureChunk.MipmapDistanceMultiplier.set -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.TextureChunk.MirrorU.get -> bool +SA3D.Modeling.Mesh.Chunk.PolyChunks.TextureChunk.MirrorU.set -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.TextureChunk.MirrorV.get -> bool +SA3D.Modeling.Mesh.Chunk.PolyChunks.TextureChunk.MirrorV.set -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.TextureChunk.Second.get -> bool +SA3D.Modeling.Mesh.Chunk.PolyChunks.TextureChunk.Second.set -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.TextureChunk.SuperSample.get -> bool +SA3D.Modeling.Mesh.Chunk.PolyChunks.TextureChunk.SuperSample.set -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.TextureChunk.TextureChunk(bool second = false) -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.TextureChunk.TextureID.get -> ushort +SA3D.Modeling.Mesh.Chunk.PolyChunks.TextureChunk.TextureID.set -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.VolumeChunk +SA3D.Modeling.Mesh.Chunk.PolyChunks.VolumeChunk.PolygonAttributeCount.get -> int +SA3D.Modeling.Mesh.Chunk.PolyChunks.VolumeChunk.PolygonAttributeCount.set -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.VolumeChunk.Polygons.get -> SA3D.Modeling.Mesh.Chunk.Structs.IChunkVolumePolygon![]! +SA3D.Modeling.Mesh.Chunk.PolyChunks.VolumeChunk.RawSize.get -> uint +SA3D.Modeling.Mesh.Chunk.PolyChunks.VolumeChunk.VolumeChunk(SA3D.Modeling.Mesh.Chunk.PolyChunkType type, SA3D.Modeling.Mesh.Chunk.Structs.IChunkVolumePolygon![]! polygons, int polygonAttributeCount) -> void +SA3D.Modeling.Mesh.Chunk.PolyChunks.VolumeChunk.VolumeChunk(SA3D.Modeling.Mesh.Chunk.PolyChunkType type, ushort polygonCount, int polygonAttributeCount) -> void +SA3D.Modeling.Mesh.Chunk.PolyChunkType +SA3D.Modeling.Mesh.Chunk.PolyChunkType.BlendAlpha = 1 -> SA3D.Modeling.Mesh.Chunk.PolyChunkType +SA3D.Modeling.Mesh.Chunk.PolyChunkType.CacheList = 4 -> SA3D.Modeling.Mesh.Chunk.PolyChunkType +SA3D.Modeling.Mesh.Chunk.PolyChunkType.DrawList = 5 -> SA3D.Modeling.Mesh.Chunk.PolyChunkType +SA3D.Modeling.Mesh.Chunk.PolyChunkType.End = 255 -> SA3D.Modeling.Mesh.Chunk.PolyChunkType +SA3D.Modeling.Mesh.Chunk.PolyChunkType.Material_Ambient = 18 -> SA3D.Modeling.Mesh.Chunk.PolyChunkType +SA3D.Modeling.Mesh.Chunk.PolyChunkType.Material_Ambient2 = 26 -> SA3D.Modeling.Mesh.Chunk.PolyChunkType +SA3D.Modeling.Mesh.Chunk.PolyChunkType.Material_AmbientSpecular = 22 -> SA3D.Modeling.Mesh.Chunk.PolyChunkType +SA3D.Modeling.Mesh.Chunk.PolyChunkType.Material_AmbientSpecular2 = 30 -> SA3D.Modeling.Mesh.Chunk.PolyChunkType +SA3D.Modeling.Mesh.Chunk.PolyChunkType.Material_Bump = 24 -> SA3D.Modeling.Mesh.Chunk.PolyChunkType +SA3D.Modeling.Mesh.Chunk.PolyChunkType.Material_Diffuse = 17 -> SA3D.Modeling.Mesh.Chunk.PolyChunkType +SA3D.Modeling.Mesh.Chunk.PolyChunkType.Material_Diffuse2 = 25 -> SA3D.Modeling.Mesh.Chunk.PolyChunkType +SA3D.Modeling.Mesh.Chunk.PolyChunkType.Material_DiffuseAmbient = 19 -> SA3D.Modeling.Mesh.Chunk.PolyChunkType +SA3D.Modeling.Mesh.Chunk.PolyChunkType.Material_DiffuseAmbient2 = 27 -> SA3D.Modeling.Mesh.Chunk.PolyChunkType +SA3D.Modeling.Mesh.Chunk.PolyChunkType.Material_DiffuseAmbientSpecular = 23 -> SA3D.Modeling.Mesh.Chunk.PolyChunkType +SA3D.Modeling.Mesh.Chunk.PolyChunkType.Material_DiffuseAmbientSpecular2 = 31 -> SA3D.Modeling.Mesh.Chunk.PolyChunkType +SA3D.Modeling.Mesh.Chunk.PolyChunkType.Material_DiffuseSpecular = 21 -> SA3D.Modeling.Mesh.Chunk.PolyChunkType +SA3D.Modeling.Mesh.Chunk.PolyChunkType.Material_DiffuseSpecular2 = 29 -> SA3D.Modeling.Mesh.Chunk.PolyChunkType +SA3D.Modeling.Mesh.Chunk.PolyChunkType.Material_Specular = 20 -> SA3D.Modeling.Mesh.Chunk.PolyChunkType +SA3D.Modeling.Mesh.Chunk.PolyChunkType.Material_Specular2 = 28 -> SA3D.Modeling.Mesh.Chunk.PolyChunkType +SA3D.Modeling.Mesh.Chunk.PolyChunkType.MipmapDistanceMultiplier = 2 -> SA3D.Modeling.Mesh.Chunk.PolyChunkType +SA3D.Modeling.Mesh.Chunk.PolyChunkType.Null = 0 -> SA3D.Modeling.Mesh.Chunk.PolyChunkType +SA3D.Modeling.Mesh.Chunk.PolyChunkType.SpecularExponent = 3 -> SA3D.Modeling.Mesh.Chunk.PolyChunkType +SA3D.Modeling.Mesh.Chunk.PolyChunkType.Strip_Blank = 64 -> SA3D.Modeling.Mesh.Chunk.PolyChunkType +SA3D.Modeling.Mesh.Chunk.PolyChunkType.Strip_BlankDouble = 73 -> SA3D.Modeling.Mesh.Chunk.PolyChunkType +SA3D.Modeling.Mesh.Chunk.PolyChunkType.Strip_Color = 70 -> SA3D.Modeling.Mesh.Chunk.PolyChunkType +SA3D.Modeling.Mesh.Chunk.PolyChunkType.Strip_HDTex = 66 -> SA3D.Modeling.Mesh.Chunk.PolyChunkType +SA3D.Modeling.Mesh.Chunk.PolyChunkType.Strip_HDTexColor = 72 -> SA3D.Modeling.Mesh.Chunk.PolyChunkType +SA3D.Modeling.Mesh.Chunk.PolyChunkType.Strip_HDTexDouble = 75 -> SA3D.Modeling.Mesh.Chunk.PolyChunkType +SA3D.Modeling.Mesh.Chunk.PolyChunkType.Strip_HDTexNormal = 69 -> SA3D.Modeling.Mesh.Chunk.PolyChunkType +SA3D.Modeling.Mesh.Chunk.PolyChunkType.Strip_Normal = 67 -> SA3D.Modeling.Mesh.Chunk.PolyChunkType +SA3D.Modeling.Mesh.Chunk.PolyChunkType.Strip_Tex = 65 -> SA3D.Modeling.Mesh.Chunk.PolyChunkType +SA3D.Modeling.Mesh.Chunk.PolyChunkType.Strip_TexColor = 71 -> SA3D.Modeling.Mesh.Chunk.PolyChunkType +SA3D.Modeling.Mesh.Chunk.PolyChunkType.Strip_TexDouble = 74 -> SA3D.Modeling.Mesh.Chunk.PolyChunkType +SA3D.Modeling.Mesh.Chunk.PolyChunkType.Strip_TexNormal = 68 -> SA3D.Modeling.Mesh.Chunk.PolyChunkType +SA3D.Modeling.Mesh.Chunk.PolyChunkType.TextureID = 8 -> SA3D.Modeling.Mesh.Chunk.PolyChunkType +SA3D.Modeling.Mesh.Chunk.PolyChunkType.TextureID2 = 9 -> SA3D.Modeling.Mesh.Chunk.PolyChunkType +SA3D.Modeling.Mesh.Chunk.PolyChunkType.Volume_Polygon3 = 56 -> SA3D.Modeling.Mesh.Chunk.PolyChunkType +SA3D.Modeling.Mesh.Chunk.PolyChunkType.Volume_Polygon4 = 57 -> SA3D.Modeling.Mesh.Chunk.PolyChunkType +SA3D.Modeling.Mesh.Chunk.PolyChunkType.Volume_Strip = 58 -> SA3D.Modeling.Mesh.Chunk.PolyChunkType +SA3D.Modeling.Mesh.Chunk.Structs.ChunkCorner +SA3D.Modeling.Mesh.Chunk.Structs.ChunkCorner.Attributes1.get -> ushort +SA3D.Modeling.Mesh.Chunk.Structs.ChunkCorner.Attributes1.set -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkCorner.Attributes2.get -> ushort +SA3D.Modeling.Mesh.Chunk.Structs.ChunkCorner.Attributes2.set -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkCorner.Attributes3.get -> ushort +SA3D.Modeling.Mesh.Chunk.Structs.ChunkCorner.Attributes3.set -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkCorner.ChunkCorner() -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkCorner.Color.get -> SA3D.Modeling.Structs.Color +SA3D.Modeling.Mesh.Chunk.Structs.ChunkCorner.Color.set -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkCorner.Index.get -> ushort +SA3D.Modeling.Mesh.Chunk.Structs.ChunkCorner.Index.set -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkCorner.Normal.get -> System.Numerics.Vector3 +SA3D.Modeling.Mesh.Chunk.Structs.ChunkCorner.Normal.set -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkCorner.Texcoord.get -> System.Numerics.Vector2 +SA3D.Modeling.Mesh.Chunk.Structs.ChunkCorner.Texcoord.set -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkCorner.Texcoord2.get -> System.Numerics.Vector2 +SA3D.Modeling.Mesh.Chunk.Structs.ChunkCorner.Texcoord2.set -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkStrip +SA3D.Modeling.Mesh.Chunk.Structs.ChunkStrip.ChunkStrip() -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkStrip.ChunkStrip(SA3D.Modeling.Mesh.Chunk.Structs.ChunkCorner[]! corners, bool reverse) -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkStrip.Clone() -> SA3D.Modeling.Mesh.Chunk.Structs.ChunkStrip +SA3D.Modeling.Mesh.Chunk.Structs.ChunkStrip.Corners.get -> SA3D.Modeling.Mesh.Chunk.Structs.ChunkCorner[]! +SA3D.Modeling.Mesh.Chunk.Structs.ChunkStrip.Reversed.get -> bool +SA3D.Modeling.Mesh.Chunk.Structs.ChunkStrip.Size(int texcoordCount, bool hasNormal, bool hasColor, int triangleAttributeCount) -> uint +SA3D.Modeling.Mesh.Chunk.Structs.ChunkStrip.Write(SA3D.Common.IO.EndianStackWriter! writer, int texcoordCount, bool hdTexcoord, bool hasNormal, bool hasColor, int triangleAttributeCount) -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVertex +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVertex.Attributes.get -> uint +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVertex.Attributes.set -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVertex.ChunkVertex() -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVertex.ChunkVertex(System.Numerics.Vector3 position, SA3D.Modeling.Structs.Color diffuse, SA3D.Modeling.Structs.Color specular) -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVertex.ChunkVertex(System.Numerics.Vector3 position, System.Numerics.Vector3 normal) -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVertex.ChunkVertex(System.Numerics.Vector3 position, System.Numerics.Vector3 normal, uint attribs) -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVertex.ChunkVertex(System.Numerics.Vector3 position, System.Numerics.Vector3 normal, ushort index, float weight) -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVertex.Diffuse.get -> SA3D.Modeling.Structs.Color +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVertex.Diffuse.set -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVertex.Index.get -> ushort +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVertex.Index.set -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVertex.Normal.get -> System.Numerics.Vector3 +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVertex.Normal.set -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVertex.Position.get -> System.Numerics.Vector3 +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVertex.Position.set -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVertex.Specular.get -> SA3D.Modeling.Structs.Color +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVertex.Specular.set -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVertex.Weight.get -> float +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVertex.Weight.set -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeQuad +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeQuad.Attribute1.get -> ushort +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeQuad.Attribute1.set -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeQuad.Attribute2.get -> ushort +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeQuad.Attribute2.set -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeQuad.Attribute3.get -> ushort +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeQuad.Attribute3.set -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeQuad.ChunkVolumeQuad() -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeQuad.ChunkVolumeQuad(ushort index1, ushort index2, ushort index3, ushort index4) -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeQuad.ChunkVolumeQuad(ushort index1, ushort index2, ushort index3, ushort index4, ushort attribute1, ushort attribute2, ushort attribute3) -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeQuad.Clone() -> SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeQuad +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeQuad.Index1.get -> ushort +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeQuad.Index1.set -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeQuad.Index2.get -> ushort +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeQuad.Index2.set -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeQuad.Index3.get -> ushort +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeQuad.Index3.set -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeQuad.Index4.get -> ushort +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeQuad.Index4.set -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeQuad.NumIndices.get -> int +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeQuad.Size(int polygonAttributeCount) -> ushort +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeQuad.this[int index].get -> ushort +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeQuad.this[int index].set -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeQuad.Write(SA3D.Common.IO.EndianStackWriter! writer, int polygonAttributeCount) -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeStrip +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeStrip.ChunkVolumeStrip() -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeStrip.ChunkVolumeStrip(int size, bool reversed) -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeStrip.ChunkVolumeStrip(ushort[]! indices, bool reversed) -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeStrip.Clone() -> SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeStrip +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeStrip.Indices.get -> ushort[]! +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeStrip.NumIndices.get -> int +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeStrip.Reversed.get -> bool +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeStrip.Reversed.set -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeStrip.Size(int polygonAttributeCount) -> ushort +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeStrip.this[int index].get -> ushort +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeStrip.this[int index].set -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeStrip.TriangleAttributes.get -> ushort[,]! +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeStrip.Write(SA3D.Common.IO.EndianStackWriter! writer, int polygonAttributeCount) -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeTriangle +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeTriangle.Attribute1.get -> ushort +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeTriangle.Attribute1.set -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeTriangle.Attribute2.get -> ushort +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeTriangle.Attribute2.set -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeTriangle.Attribute3.get -> ushort +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeTriangle.Attribute3.set -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeTriangle.ChunkVolumeTriangle() -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeTriangle.ChunkVolumeTriangle(ushort index1, ushort index2, ushort index3) -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeTriangle.ChunkVolumeTriangle(ushort index1, ushort index2, ushort index3, ushort attribute1, ushort attribute2, ushort attribute3) -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeTriangle.Clone() -> SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeTriangle +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeTriangle.Index1.get -> ushort +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeTriangle.Index1.set -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeTriangle.Index2.get -> ushort +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeTriangle.Index2.set -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeTriangle.Index3.get -> ushort +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeTriangle.Index3.set -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeTriangle.NumIndices.get -> int +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeTriangle.Size(int polygonAttributeCount) -> ushort +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeTriangle.this[int index].get -> ushort +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeTriangle.this[int index].set -> void +SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeTriangle.Write(SA3D.Common.IO.EndianStackWriter! writer, int polygonAttributeCount) -> void +SA3D.Modeling.Mesh.Chunk.Structs.IChunkVolumePolygon +SA3D.Modeling.Mesh.Chunk.Structs.IChunkVolumePolygon.NumIndices.get -> int +SA3D.Modeling.Mesh.Chunk.Structs.IChunkVolumePolygon.Size(int polygonAttributeCount) -> ushort +SA3D.Modeling.Mesh.Chunk.Structs.IChunkVolumePolygon.this[int index].get -> ushort +SA3D.Modeling.Mesh.Chunk.Structs.IChunkVolumePolygon.this[int index].set -> void +SA3D.Modeling.Mesh.Chunk.Structs.IChunkVolumePolygon.Write(SA3D.Common.IO.EndianStackWriter! writer, int polygonAttributeCount) -> void +SA3D.Modeling.Mesh.Chunk.VertexChunk +SA3D.Modeling.Mesh.Chunk.VertexChunk.Attributes.get -> byte +SA3D.Modeling.Mesh.Chunk.VertexChunk.Clone() -> SA3D.Modeling.Mesh.Chunk.VertexChunk! +SA3D.Modeling.Mesh.Chunk.VertexChunk.HasDiffuseColors.get -> bool +SA3D.Modeling.Mesh.Chunk.VertexChunk.HasNormals.get -> bool +SA3D.Modeling.Mesh.Chunk.VertexChunk.HasSpecularColors.get -> bool +SA3D.Modeling.Mesh.Chunk.VertexChunk.HasWeight.get -> bool +SA3D.Modeling.Mesh.Chunk.VertexChunk.IndexOffset.get -> ushort +SA3D.Modeling.Mesh.Chunk.VertexChunk.IndexOffset.set -> void +SA3D.Modeling.Mesh.Chunk.VertexChunk.Type.get -> SA3D.Modeling.Mesh.Chunk.VertexChunkType +SA3D.Modeling.Mesh.Chunk.VertexChunk.VertexChunk(SA3D.Modeling.Mesh.Chunk.VertexChunkType type, byte attributes, ushort indexOffset, SA3D.Modeling.Mesh.Chunk.Structs.ChunkVertex[]! vertices) -> void +SA3D.Modeling.Mesh.Chunk.VertexChunk.VertexChunk(SA3D.Modeling.Mesh.Chunk.VertexChunkType type, SA3D.Modeling.Mesh.Chunk.WeightStatus weightstatus, ushort indexOffset, SA3D.Modeling.Mesh.Chunk.Structs.ChunkVertex[]! vertices) -> void +SA3D.Modeling.Mesh.Chunk.VertexChunk.Vertices.get -> SA3D.Modeling.Mesh.Chunk.Structs.ChunkVertex[]! +SA3D.Modeling.Mesh.Chunk.VertexChunk.WeightStatus.get -> SA3D.Modeling.Mesh.Chunk.WeightStatus +SA3D.Modeling.Mesh.Chunk.VertexChunk.Write(SA3D.Common.IO.EndianStackWriter! writer) -> void +SA3D.Modeling.Mesh.Chunk.VertexChunkType +SA3D.Modeling.Mesh.Chunk.VertexChunkType.Attributes = 37 -> SA3D.Modeling.Mesh.Chunk.VertexChunkType +SA3D.Modeling.Mesh.Chunk.VertexChunkType.Blank = 34 -> SA3D.Modeling.Mesh.Chunk.VertexChunkType +SA3D.Modeling.Mesh.Chunk.VertexChunkType.BlankVec4 = 32 -> SA3D.Modeling.Mesh.Chunk.VertexChunkType +SA3D.Modeling.Mesh.Chunk.VertexChunkType.Diffuse = 35 -> SA3D.Modeling.Mesh.Chunk.VertexChunkType +SA3D.Modeling.Mesh.Chunk.VertexChunkType.DiffuseSpecular4 = 39 -> SA3D.Modeling.Mesh.Chunk.VertexChunkType +SA3D.Modeling.Mesh.Chunk.VertexChunkType.DiffuseSpecular5 = 38 -> SA3D.Modeling.Mesh.Chunk.VertexChunkType +SA3D.Modeling.Mesh.Chunk.VertexChunkType.End = 255 -> SA3D.Modeling.Mesh.Chunk.VertexChunkType +SA3D.Modeling.Mesh.Chunk.VertexChunkType.Intensity = 40 -> SA3D.Modeling.Mesh.Chunk.VertexChunkType +SA3D.Modeling.Mesh.Chunk.VertexChunkType.Normal = 41 -> SA3D.Modeling.Mesh.Chunk.VertexChunkType +SA3D.Modeling.Mesh.Chunk.VertexChunkType.Normal32 = 48 -> SA3D.Modeling.Mesh.Chunk.VertexChunkType +SA3D.Modeling.Mesh.Chunk.VertexChunkType.Normal32Diffuse = 49 -> SA3D.Modeling.Mesh.Chunk.VertexChunkType +SA3D.Modeling.Mesh.Chunk.VertexChunkType.Normal32UserAttributes = 50 -> SA3D.Modeling.Mesh.Chunk.VertexChunkType +SA3D.Modeling.Mesh.Chunk.VertexChunkType.NormalAttributes = 44 -> SA3D.Modeling.Mesh.Chunk.VertexChunkType +SA3D.Modeling.Mesh.Chunk.VertexChunkType.NormalDiffuse = 42 -> SA3D.Modeling.Mesh.Chunk.VertexChunkType +SA3D.Modeling.Mesh.Chunk.VertexChunkType.NormalDiffuseSpecular4 = 46 -> SA3D.Modeling.Mesh.Chunk.VertexChunkType +SA3D.Modeling.Mesh.Chunk.VertexChunkType.NormalDiffuseSpecular5 = 45 -> SA3D.Modeling.Mesh.Chunk.VertexChunkType +SA3D.Modeling.Mesh.Chunk.VertexChunkType.NormalIntensity = 47 -> SA3D.Modeling.Mesh.Chunk.VertexChunkType +SA3D.Modeling.Mesh.Chunk.VertexChunkType.NormalUserAttributes = 43 -> SA3D.Modeling.Mesh.Chunk.VertexChunkType +SA3D.Modeling.Mesh.Chunk.VertexChunkType.NormalVec4 = 33 -> SA3D.Modeling.Mesh.Chunk.VertexChunkType +SA3D.Modeling.Mesh.Chunk.VertexChunkType.Null = 0 -> SA3D.Modeling.Mesh.Chunk.VertexChunkType +SA3D.Modeling.Mesh.Chunk.VertexChunkType.UserAttributes = 36 -> SA3D.Modeling.Mesh.Chunk.VertexChunkType +SA3D.Modeling.Mesh.Chunk.WeightStatus +SA3D.Modeling.Mesh.Chunk.WeightStatus.End = 2 -> SA3D.Modeling.Mesh.Chunk.WeightStatus +SA3D.Modeling.Mesh.Chunk.WeightStatus.Middle = 1 -> SA3D.Modeling.Mesh.Chunk.WeightStatus +SA3D.Modeling.Mesh.Chunk.WeightStatus.Start = 0 -> SA3D.Modeling.Mesh.Chunk.WeightStatus +SA3D.Modeling.Mesh.FilterMode +SA3D.Modeling.Mesh.FilterMode.Bilinear = 1 -> SA3D.Modeling.Mesh.FilterMode +SA3D.Modeling.Mesh.FilterMode.Blend = 3 -> SA3D.Modeling.Mesh.FilterMode +SA3D.Modeling.Mesh.FilterMode.Nearest = 0 -> SA3D.Modeling.Mesh.FilterMode +SA3D.Modeling.Mesh.FilterMode.Trilinear = 2 -> SA3D.Modeling.Mesh.FilterMode +SA3D.Modeling.Mesh.Gamecube.Enums.GCDataType +SA3D.Modeling.Mesh.Gamecube.Enums.GCDataType.Float32 = 4 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCDataType +SA3D.Modeling.Mesh.Gamecube.Enums.GCDataType.RGB565 = 5 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCDataType +SA3D.Modeling.Mesh.Gamecube.Enums.GCDataType.RGB8 = 6 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCDataType +SA3D.Modeling.Mesh.Gamecube.Enums.GCDataType.RGBA4 = 8 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCDataType +SA3D.Modeling.Mesh.Gamecube.Enums.GCDataType.RGBA6 = 9 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCDataType +SA3D.Modeling.Mesh.Gamecube.Enums.GCDataType.RGBA8 = 10 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCDataType +SA3D.Modeling.Mesh.Gamecube.Enums.GCDataType.RGBX8 = 7 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCDataType +SA3D.Modeling.Mesh.Gamecube.Enums.GCDataType.Signed16 = 3 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCDataType +SA3D.Modeling.Mesh.Gamecube.Enums.GCDataType.Signed8 = 1 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCDataType +SA3D.Modeling.Mesh.Gamecube.Enums.GCDataType.Unsigned16 = 2 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCDataType +SA3D.Modeling.Mesh.Gamecube.Enums.GCDataType.Unsigned8 = 0 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCDataType +SA3D.Modeling.Mesh.Gamecube.Enums.GCEnumExtensions +SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat +SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat.Color0LargeIndex = 64 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat +SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat.Color1LargeIndex = 256 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat +SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat.HasColor0 = 128 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat +SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat.HasColor1 = 512 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat +SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat.HasNormal = 32 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat +SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat.HasPosition = 8 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat +SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat.HasPositionMatrixID = 2 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat +SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat.HasTexCoord0 = 2048 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat +SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat.HasTexCoord1 = 8192 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat +SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat.HasTexCoord2 = 32768 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat +SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat.HasTexCoord3 = 131072 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat +SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat.HasTexCoord4 = 524288 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat +SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat.HasTexCoord5 = 2097152 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat +SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat.HasTexCoord6 = 8388608 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat +SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat.HasTexCoord7 = 33554432 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat +SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat.NormalLargeIndex = 16 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat +SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat.PositionLargeIndex = 4 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat +SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat.PositionMatrixIDLargeIndex = 1 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat +SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat.TexCoord0LargeIndex = 1024 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat +SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat.TexCoord1LargeIndex = 4096 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat +SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat.TexCoord2LargeIndex = 16384 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat +SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat.TexCoord3LargeIndex = 65536 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat +SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat.TexCoord4LargeIndex = 262144 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat +SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat.TexCoord5LargeIndex = 1048576 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat +SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat.TexCoord6LargeIndex = 4194304 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat +SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat.TexCoord7LargeIndex = 16777216 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat +SA3D.Modeling.Mesh.Gamecube.Enums.GCParameterType +SA3D.Modeling.Mesh.Gamecube.Enums.GCParameterType.AmbientColor = 5 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCParameterType +SA3D.Modeling.Mesh.Gamecube.Enums.GCParameterType.BlendAlpha = 4 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCParameterType +SA3D.Modeling.Mesh.Gamecube.Enums.GCParameterType.DiffuseColor = 6 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCParameterType +SA3D.Modeling.Mesh.Gamecube.Enums.GCParameterType.IndexFormat = 1 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCParameterType +SA3D.Modeling.Mesh.Gamecube.Enums.GCParameterType.Lighting = 2 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCParameterType +SA3D.Modeling.Mesh.Gamecube.Enums.GCParameterType.SpecularColor = 7 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCParameterType +SA3D.Modeling.Mesh.Gamecube.Enums.GCParameterType.Texcoord = 10 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCParameterType +SA3D.Modeling.Mesh.Gamecube.Enums.GCParameterType.Texture = 8 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCParameterType +SA3D.Modeling.Mesh.Gamecube.Enums.GCParameterType.Unknown = 9 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCParameterType +SA3D.Modeling.Mesh.Gamecube.Enums.GCParameterType.VertexFormat = 0 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCParameterType +SA3D.Modeling.Mesh.Gamecube.Enums.GCPolyType +SA3D.Modeling.Mesh.Gamecube.Enums.GCPolyType.Lines = 168 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCPolyType +SA3D.Modeling.Mesh.Gamecube.Enums.GCPolyType.LineStrip = 176 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCPolyType +SA3D.Modeling.Mesh.Gamecube.Enums.GCPolyType.Points = 184 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCPolyType +SA3D.Modeling.Mesh.Gamecube.Enums.GCPolyType.TriangleFan = 160 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCPolyType +SA3D.Modeling.Mesh.Gamecube.Enums.GCPolyType.Triangles = 144 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCPolyType +SA3D.Modeling.Mesh.Gamecube.Enums.GCPolyType.TriangleStrip = 152 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCPolyType +SA3D.Modeling.Mesh.Gamecube.Enums.GCStructType +SA3D.Modeling.Mesh.Gamecube.Enums.GCStructType.ColorRGB = 5 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCStructType +SA3D.Modeling.Mesh.Gamecube.Enums.GCStructType.ColorRGBA = 6 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCStructType +SA3D.Modeling.Mesh.Gamecube.Enums.GCStructType.NormalNBT = 3 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCStructType +SA3D.Modeling.Mesh.Gamecube.Enums.GCStructType.NormalNBT3 = 4 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCStructType +SA3D.Modeling.Mesh.Gamecube.Enums.GCStructType.NormalXYZ = 2 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCStructType +SA3D.Modeling.Mesh.Gamecube.Enums.GCStructType.PositionXY = 0 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCStructType +SA3D.Modeling.Mesh.Gamecube.Enums.GCStructType.PositionXYZ = 1 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCStructType +SA3D.Modeling.Mesh.Gamecube.Enums.GCStructType.TexCoordU = 7 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCStructType +SA3D.Modeling.Mesh.Gamecube.Enums.GCStructType.TexCoordUV = 8 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCStructType +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordID +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordID.TexCoord0 = 0 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordID +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordID.TexCoord1 = 1 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordID +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordID.TexCoord2 = 2 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordID +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordID.TexCoord3 = 3 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordID +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordID.TexCoord4 = 4 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordID +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordID.TexCoord5 = 5 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordID +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordID.TexCoord6 = 6 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordID +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordID.TexCoord7 = 7 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordID +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordID.TexCoordMax = 8 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordID +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordID.TexCoordNull = 255 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordID +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexcoordMatrix +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexcoordMatrix.Identity = 10 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexcoordMatrix +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexcoordMatrix.Matrix0 = 0 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexcoordMatrix +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexcoordMatrix.Matrix1 = 1 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexcoordMatrix +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexcoordMatrix.Matrix2 = 2 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexcoordMatrix +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexcoordMatrix.Matrix3 = 3 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexcoordMatrix +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexcoordMatrix.Matrix4 = 4 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexcoordMatrix +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexcoordMatrix.Matrix5 = 5 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexcoordMatrix +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexcoordMatrix.Matrix6 = 6 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexcoordMatrix +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexcoordMatrix.Matrix7 = 7 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexcoordMatrix +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexcoordMatrix.Matrix8 = 8 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexcoordMatrix +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexcoordMatrix.Matrix9 = 9 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexcoordMatrix +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource.Binormal = 2 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource.BumpTexCoord0 = 12 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource.BumpTexCoord1 = 13 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource.BumpTexCoord2 = 14 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource.BumpTexCoord3 = 15 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource.BumpTexCoord4 = 16 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource.BumpTexCoord5 = 17 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource.BumpTexCoord6 = 18 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource.Color0 = 19 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource.Color1 = 20 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource.Normal = 1 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource.Position = 0 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource.Tangent = 3 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource.TexCoord0 = 4 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource.TexCoord1 = 5 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource.TexCoord2 = 6 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource.TexCoord3 = 7 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource.TexCoord4 = 8 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource.TexCoord5 = 9 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource.TexCoord6 = 10 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource.TexCoord7 = 11 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordType +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordType.Bump0 = 2 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordType +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordType.Bump1 = 3 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordType +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordType.Bump2 = 4 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordType +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordType.Bump3 = 5 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordType +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordType.Bump4 = 6 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordType +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordType.Bump5 = 7 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordType +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordType.Bump6 = 8 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordType +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordType.Bump7 = 9 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordType +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordType.Matrix2x4 = 1 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordType +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordType.Matrix3x4 = 0 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordType +SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordType.SRTG = 10 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordType +SA3D.Modeling.Mesh.Gamecube.Enums.GCTileMode +SA3D.Modeling.Mesh.Gamecube.Enums.GCTileMode.Mask = SA3D.Modeling.Mesh.Gamecube.Enums.GCTileMode.RepeatV | SA3D.Modeling.Mesh.Gamecube.Enums.GCTileMode.MirrorV | SA3D.Modeling.Mesh.Gamecube.Enums.GCTileMode.RepeatU | SA3D.Modeling.Mesh.Gamecube.Enums.GCTileMode.MirrorU | SA3D.Modeling.Mesh.Gamecube.Enums.GCTileMode.Unknown -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTileMode +SA3D.Modeling.Mesh.Gamecube.Enums.GCTileMode.MirrorU = 8 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTileMode +SA3D.Modeling.Mesh.Gamecube.Enums.GCTileMode.MirrorV = 2 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTileMode +SA3D.Modeling.Mesh.Gamecube.Enums.GCTileMode.RepeatU = 4 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTileMode +SA3D.Modeling.Mesh.Gamecube.Enums.GCTileMode.RepeatV = 1 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTileMode +SA3D.Modeling.Mesh.Gamecube.Enums.GCTileMode.Unknown = 16 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTileMode +SA3D.Modeling.Mesh.Gamecube.Enums.GCVertexType +SA3D.Modeling.Mesh.Gamecube.Enums.GCVertexType.Color0 = 3 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCVertexType +SA3D.Modeling.Mesh.Gamecube.Enums.GCVertexType.Color1 = 4 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCVertexType +SA3D.Modeling.Mesh.Gamecube.Enums.GCVertexType.End = 255 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCVertexType +SA3D.Modeling.Mesh.Gamecube.Enums.GCVertexType.Normal = 2 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCVertexType +SA3D.Modeling.Mesh.Gamecube.Enums.GCVertexType.Position = 1 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCVertexType +SA3D.Modeling.Mesh.Gamecube.Enums.GCVertexType.PositionMatrixID = 0 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCVertexType +SA3D.Modeling.Mesh.Gamecube.Enums.GCVertexType.TexCoord0 = 5 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCVertexType +SA3D.Modeling.Mesh.Gamecube.Enums.GCVertexType.TexCoord1 = 6 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCVertexType +SA3D.Modeling.Mesh.Gamecube.Enums.GCVertexType.TexCoord2 = 7 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCVertexType +SA3D.Modeling.Mesh.Gamecube.Enums.GCVertexType.TexCoord3 = 8 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCVertexType +SA3D.Modeling.Mesh.Gamecube.Enums.GCVertexType.TexCoord4 = 9 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCVertexType +SA3D.Modeling.Mesh.Gamecube.Enums.GCVertexType.TexCoord5 = 10 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCVertexType +SA3D.Modeling.Mesh.Gamecube.Enums.GCVertexType.TexCoord6 = 11 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCVertexType +SA3D.Modeling.Mesh.Gamecube.Enums.GCVertexType.TexCoord7 = 12 -> SA3D.Modeling.Mesh.Gamecube.Enums.GCVertexType +SA3D.Modeling.Mesh.Gamecube.GCAttach +SA3D.Modeling.Mesh.Gamecube.GCAttach.ConvertToBufferMeshData(bool optimize) -> SA3D.Modeling.Mesh.Buffer.BufferMesh![]! +SA3D.Modeling.Mesh.Gamecube.GCAttach.GCAttach(System.Collections.Generic.Dictionary! vertexData, SA3D.Modeling.Mesh.Gamecube.GCMesh![]! opaqueMeshes, SA3D.Modeling.Mesh.Gamecube.GCMesh![]! transprentMeshes) -> void +SA3D.Modeling.Mesh.Gamecube.GCAttach.OpaqueMeshes.get -> SA3D.Modeling.Mesh.Gamecube.GCMesh![]! +SA3D.Modeling.Mesh.Gamecube.GCAttach.OpaqueMeshes.set -> void +SA3D.Modeling.Mesh.Gamecube.GCAttach.OptimizePolygons() -> void +SA3D.Modeling.Mesh.Gamecube.GCAttach.OptimizeVertexData() -> void +SA3D.Modeling.Mesh.Gamecube.GCAttach.TransparentMeshes.get -> SA3D.Modeling.Mesh.Gamecube.GCMesh![]! +SA3D.Modeling.Mesh.Gamecube.GCAttach.TransparentMeshes.set -> void +SA3D.Modeling.Mesh.Gamecube.GCAttach.VertexData.get -> System.Collections.Generic.Dictionary! +SA3D.Modeling.Mesh.Gamecube.GCCorner +SA3D.Modeling.Mesh.Gamecube.GCCorner.Color0Index.get -> ushort +SA3D.Modeling.Mesh.Gamecube.GCCorner.Color0Index.set -> void +SA3D.Modeling.Mesh.Gamecube.GCCorner.Color1Index.get -> ushort +SA3D.Modeling.Mesh.Gamecube.GCCorner.Color1Index.set -> void +SA3D.Modeling.Mesh.Gamecube.GCCorner.GCCorner() -> void +SA3D.Modeling.Mesh.Gamecube.GCCorner.NormalIndex.get -> ushort +SA3D.Modeling.Mesh.Gamecube.GCCorner.NormalIndex.set -> void +SA3D.Modeling.Mesh.Gamecube.GCCorner.PositionIndex.get -> ushort +SA3D.Modeling.Mesh.Gamecube.GCCorner.PositionIndex.set -> void +SA3D.Modeling.Mesh.Gamecube.GCCorner.PositionMatrixIDIndex.get -> ushort +SA3D.Modeling.Mesh.Gamecube.GCCorner.PositionMatrixIDIndex.set -> void +SA3D.Modeling.Mesh.Gamecube.GCCorner.TexCoord0Index.get -> ushort +SA3D.Modeling.Mesh.Gamecube.GCCorner.TexCoord0Index.set -> void +SA3D.Modeling.Mesh.Gamecube.GCCorner.TexCoord1Index.get -> ushort +SA3D.Modeling.Mesh.Gamecube.GCCorner.TexCoord1Index.set -> void +SA3D.Modeling.Mesh.Gamecube.GCCorner.TexCoord2Index.get -> ushort +SA3D.Modeling.Mesh.Gamecube.GCCorner.TexCoord2Index.set -> void +SA3D.Modeling.Mesh.Gamecube.GCCorner.TexCoord3Index.get -> ushort +SA3D.Modeling.Mesh.Gamecube.GCCorner.TexCoord3Index.set -> void +SA3D.Modeling.Mesh.Gamecube.GCCorner.TexCoord4Index.get -> ushort +SA3D.Modeling.Mesh.Gamecube.GCCorner.TexCoord4Index.set -> void +SA3D.Modeling.Mesh.Gamecube.GCCorner.TexCoord5Index.get -> ushort +SA3D.Modeling.Mesh.Gamecube.GCCorner.TexCoord5Index.set -> void +SA3D.Modeling.Mesh.Gamecube.GCCorner.TexCoord6Index.get -> ushort +SA3D.Modeling.Mesh.Gamecube.GCCorner.TexCoord6Index.set -> void +SA3D.Modeling.Mesh.Gamecube.GCCorner.TexCoord7Index.get -> ushort +SA3D.Modeling.Mesh.Gamecube.GCCorner.TexCoord7Index.set -> void +SA3D.Modeling.Mesh.Gamecube.GCMesh +SA3D.Modeling.Mesh.Gamecube.GCMesh.Clone() -> SA3D.Modeling.Mesh.Gamecube.GCMesh! +SA3D.Modeling.Mesh.Gamecube.GCMesh.GCMesh(SA3D.Modeling.Mesh.Gamecube.Parameters.IGCParameter![]! parameters, SA3D.Modeling.Mesh.Gamecube.GCPolygon[]! polygons) -> void +SA3D.Modeling.Mesh.Gamecube.GCMesh.IndexFormat.get -> SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat? +SA3D.Modeling.Mesh.Gamecube.GCMesh.OptimizePolygons() -> void +SA3D.Modeling.Mesh.Gamecube.GCMesh.Parameters.get -> SA3D.Modeling.Mesh.Gamecube.Parameters.IGCParameter![]! +SA3D.Modeling.Mesh.Gamecube.GCMesh.Parameters.set -> void +SA3D.Modeling.Mesh.Gamecube.GCMesh.Polygons.get -> SA3D.Modeling.Mesh.Gamecube.GCPolygon[]! +SA3D.Modeling.Mesh.Gamecube.GCMesh.Polygons.set -> void +SA3D.Modeling.Mesh.Gamecube.GCMesh.WriteParameters(SA3D.Common.IO.EndianStackWriter! writer) -> uint +SA3D.Modeling.Mesh.Gamecube.GCMesh.WritePolygons(SA3D.Common.IO.EndianStackWriter! writer, ref SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat indexFormat) -> uint +SA3D.Modeling.Mesh.Gamecube.GCPolygon +SA3D.Modeling.Mesh.Gamecube.GCPolygon.Clone() -> SA3D.Modeling.Mesh.Gamecube.GCPolygon +SA3D.Modeling.Mesh.Gamecube.GCPolygon.Corners.get -> SA3D.Modeling.Mesh.Gamecube.GCCorner[]! +SA3D.Modeling.Mesh.Gamecube.GCPolygon.GCPolygon() -> void +SA3D.Modeling.Mesh.Gamecube.GCPolygon.GCPolygon(SA3D.Modeling.Mesh.Gamecube.Enums.GCPolyType type, SA3D.Modeling.Mesh.Gamecube.GCCorner[]! corners) -> void +SA3D.Modeling.Mesh.Gamecube.GCPolygon.Type.get -> SA3D.Modeling.Mesh.Gamecube.Enums.GCPolyType +SA3D.Modeling.Mesh.Gamecube.GCPolygon.Write(SA3D.Common.IO.EndianStackWriter! writer, SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat indexFormat) -> void +SA3D.Modeling.Mesh.Gamecube.GCVertexSet +SA3D.Modeling.Mesh.Gamecube.GCVertexSet.Clone() -> SA3D.Modeling.Mesh.Gamecube.GCVertexSet! +SA3D.Modeling.Mesh.Gamecube.GCVertexSet.ColorData.get -> SA3D.Modeling.Structs.Color[]! +SA3D.Modeling.Mesh.Gamecube.GCVertexSet.DataLength.get -> int +SA3D.Modeling.Mesh.Gamecube.GCVertexSet.DataType.get -> SA3D.Modeling.Mesh.Gamecube.Enums.GCDataType +SA3D.Modeling.Mesh.Gamecube.GCVertexSet.Optimize(System.Collections.Generic.IEnumerable? meshes) -> void +SA3D.Modeling.Mesh.Gamecube.GCVertexSet.StructSize.get -> uint +SA3D.Modeling.Mesh.Gamecube.GCVertexSet.StructType.get -> SA3D.Modeling.Mesh.Gamecube.Enums.GCStructType +SA3D.Modeling.Mesh.Gamecube.GCVertexSet.Type.get -> SA3D.Modeling.Mesh.Gamecube.Enums.GCVertexType +SA3D.Modeling.Mesh.Gamecube.GCVertexSet.Vector2Data.get -> System.Numerics.Vector2[]! +SA3D.Modeling.Mesh.Gamecube.GCVertexSet.Vector3Data.get -> System.Numerics.Vector3[]! +SA3D.Modeling.Mesh.Gamecube.GCVertexSet.WriteData(SA3D.Common.IO.EndianStackWriter! writer) -> uint +SA3D.Modeling.Mesh.Gamecube.GCVertexSet.WriteHeader(SA3D.Common.IO.EndianStackWriter! writer, uint dataAddress) -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.GCAmbientColorParameter +SA3D.Modeling.Mesh.Gamecube.Parameters.GCAmbientColorParameter.AmbientColor.get -> SA3D.Modeling.Structs.Color +SA3D.Modeling.Mesh.Gamecube.Parameters.GCAmbientColorParameter.AmbientColor.set -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.GCAmbientColorParameter.Data.get -> uint +SA3D.Modeling.Mesh.Gamecube.Parameters.GCAmbientColorParameter.Data.set -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.GCAmbientColorParameter.GCAmbientColorParameter() -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.GCAmbientColorParameter.Type.get -> SA3D.Modeling.Mesh.Gamecube.Enums.GCParameterType +SA3D.Modeling.Mesh.Gamecube.Parameters.GCBlendAlphaParameter +SA3D.Modeling.Mesh.Gamecube.Parameters.GCBlendAlphaParameter.Data.get -> uint +SA3D.Modeling.Mesh.Gamecube.Parameters.GCBlendAlphaParameter.Data.set -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.GCBlendAlphaParameter.DestinationAlpha.get -> SA3D.Modeling.Mesh.BlendMode +SA3D.Modeling.Mesh.Gamecube.Parameters.GCBlendAlphaParameter.DestinationAlpha.set -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.GCBlendAlphaParameter.GCBlendAlphaParameter() -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.GCBlendAlphaParameter.SourceAlpha.get -> SA3D.Modeling.Mesh.BlendMode +SA3D.Modeling.Mesh.Gamecube.Parameters.GCBlendAlphaParameter.SourceAlpha.set -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.GCBlendAlphaParameter.Type.get -> SA3D.Modeling.Mesh.Gamecube.Enums.GCParameterType +SA3D.Modeling.Mesh.Gamecube.Parameters.GCDiffuseColorParameter +SA3D.Modeling.Mesh.Gamecube.Parameters.GCDiffuseColorParameter.Data.get -> uint +SA3D.Modeling.Mesh.Gamecube.Parameters.GCDiffuseColorParameter.Data.set -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.GCDiffuseColorParameter.DiffuseColor.get -> SA3D.Modeling.Structs.Color +SA3D.Modeling.Mesh.Gamecube.Parameters.GCDiffuseColorParameter.DiffuseColor.set -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.GCDiffuseColorParameter.GCDiffuseColorParameter() -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.GCDiffuseColorParameter.Type.get -> SA3D.Modeling.Mesh.Gamecube.Enums.GCParameterType +SA3D.Modeling.Mesh.Gamecube.Parameters.GCIndexFormatParameter +SA3D.Modeling.Mesh.Gamecube.Parameters.GCIndexFormatParameter.Data.get -> uint +SA3D.Modeling.Mesh.Gamecube.Parameters.GCIndexFormatParameter.Data.set -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.GCIndexFormatParameter.GCIndexFormatParameter() -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.GCIndexFormatParameter.IndexFormat.get -> SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat +SA3D.Modeling.Mesh.Gamecube.Parameters.GCIndexFormatParameter.IndexFormat.set -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.GCIndexFormatParameter.Type.get -> SA3D.Modeling.Mesh.Gamecube.Enums.GCParameterType +SA3D.Modeling.Mesh.Gamecube.Parameters.GCLightingParameter +SA3D.Modeling.Mesh.Gamecube.Parameters.GCLightingParameter.Data.get -> uint +SA3D.Modeling.Mesh.Gamecube.Parameters.GCLightingParameter.Data.set -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.GCLightingParameter.GCLightingParameter() -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.GCLightingParameter.LightingAttributes.get -> ushort +SA3D.Modeling.Mesh.Gamecube.Parameters.GCLightingParameter.LightingAttributes.set -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.GCLightingParameter.ShadowStencil.get -> byte +SA3D.Modeling.Mesh.Gamecube.Parameters.GCLightingParameter.ShadowStencil.set -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.GCLightingParameter.Type.get -> SA3D.Modeling.Mesh.Gamecube.Enums.GCParameterType +SA3D.Modeling.Mesh.Gamecube.Parameters.GCLightingParameter.Unknown1.get -> byte +SA3D.Modeling.Mesh.Gamecube.Parameters.GCLightingParameter.Unknown1.set -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.GCLightingParameter.Unknown2.get -> byte +SA3D.Modeling.Mesh.Gamecube.Parameters.GCLightingParameter.Unknown2.set -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.GCParameterExtensions +SA3D.Modeling.Mesh.Gamecube.Parameters.GCSpecularColorParameter +SA3D.Modeling.Mesh.Gamecube.Parameters.GCSpecularColorParameter.Data.get -> uint +SA3D.Modeling.Mesh.Gamecube.Parameters.GCSpecularColorParameter.Data.set -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.GCSpecularColorParameter.GCSpecularColorParameter() -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.GCSpecularColorParameter.SpecularColor.get -> SA3D.Modeling.Structs.Color +SA3D.Modeling.Mesh.Gamecube.Parameters.GCSpecularColorParameter.SpecularColor.set -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.GCSpecularColorParameter.Type.get -> SA3D.Modeling.Mesh.Gamecube.Enums.GCParameterType +SA3D.Modeling.Mesh.Gamecube.Parameters.GCTexCoordParameter +SA3D.Modeling.Mesh.Gamecube.Parameters.GCTexCoordParameter.Data.get -> uint +SA3D.Modeling.Mesh.Gamecube.Parameters.GCTexCoordParameter.Data.set -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.GCTexCoordParameter.GCTexCoordParameter() -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.GCTexCoordParameter.MatrixID.get -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexcoordMatrix +SA3D.Modeling.Mesh.Gamecube.Parameters.GCTexCoordParameter.MatrixID.set -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.GCTexCoordParameter.TexCoordID.get -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordID +SA3D.Modeling.Mesh.Gamecube.Parameters.GCTexCoordParameter.TexCoordID.set -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.GCTexCoordParameter.TexCoordSource.get -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordSource +SA3D.Modeling.Mesh.Gamecube.Parameters.GCTexCoordParameter.TexCoordSource.set -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.GCTexCoordParameter.TexCoordType.get -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTexCoordType +SA3D.Modeling.Mesh.Gamecube.Parameters.GCTexCoordParameter.TexCoordType.set -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.GCTexCoordParameter.Type.get -> SA3D.Modeling.Mesh.Gamecube.Enums.GCParameterType +SA3D.Modeling.Mesh.Gamecube.Parameters.GCTextureParameter +SA3D.Modeling.Mesh.Gamecube.Parameters.GCTextureParameter.Data.get -> uint +SA3D.Modeling.Mesh.Gamecube.Parameters.GCTextureParameter.Data.set -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.GCTextureParameter.GCTextureParameter() -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.GCTextureParameter.TextureID.get -> ushort +SA3D.Modeling.Mesh.Gamecube.Parameters.GCTextureParameter.TextureID.set -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.GCTextureParameter.Tiling.get -> SA3D.Modeling.Mesh.Gamecube.Enums.GCTileMode +SA3D.Modeling.Mesh.Gamecube.Parameters.GCTextureParameter.Tiling.set -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.GCTextureParameter.Type.get -> SA3D.Modeling.Mesh.Gamecube.Enums.GCParameterType +SA3D.Modeling.Mesh.Gamecube.Parameters.GCUnknownParameter +SA3D.Modeling.Mesh.Gamecube.Parameters.GCUnknownParameter.Data.get -> uint +SA3D.Modeling.Mesh.Gamecube.Parameters.GCUnknownParameter.Data.set -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.GCUnknownParameter.GCUnknownParameter() -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.GCUnknownParameter.Type.get -> SA3D.Modeling.Mesh.Gamecube.Enums.GCParameterType +SA3D.Modeling.Mesh.Gamecube.Parameters.GCUnknownParameter.Unknown1.get -> ushort +SA3D.Modeling.Mesh.Gamecube.Parameters.GCUnknownParameter.Unknown1.set -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.GCUnknownParameter.Unknown2.get -> ushort +SA3D.Modeling.Mesh.Gamecube.Parameters.GCUnknownParameter.Unknown2.set -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.GCVertexFormatParameter +SA3D.Modeling.Mesh.Gamecube.Parameters.GCVertexFormatParameter.Data.get -> uint +SA3D.Modeling.Mesh.Gamecube.Parameters.GCVertexFormatParameter.Data.set -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.GCVertexFormatParameter.Formatting.get -> byte +SA3D.Modeling.Mesh.Gamecube.Parameters.GCVertexFormatParameter.Formatting.set -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.GCVertexFormatParameter.GCVertexFormatParameter() -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.GCVertexFormatParameter.Type.get -> SA3D.Modeling.Mesh.Gamecube.Enums.GCParameterType +SA3D.Modeling.Mesh.Gamecube.Parameters.GCVertexFormatParameter.VertexDataType.get -> SA3D.Modeling.Mesh.Gamecube.Enums.GCDataType +SA3D.Modeling.Mesh.Gamecube.Parameters.GCVertexFormatParameter.VertexDataType.set -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.GCVertexFormatParameter.VertexStructType.get -> SA3D.Modeling.Mesh.Gamecube.Enums.GCStructType +SA3D.Modeling.Mesh.Gamecube.Parameters.GCVertexFormatParameter.VertexStructType.set -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.GCVertexFormatParameter.VertexType.get -> SA3D.Modeling.Mesh.Gamecube.Enums.GCVertexType +SA3D.Modeling.Mesh.Gamecube.Parameters.GCVertexFormatParameter.VertexType.set -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.IGCParameter +SA3D.Modeling.Mesh.Gamecube.Parameters.IGCParameter.Data.get -> uint +SA3D.Modeling.Mesh.Gamecube.Parameters.IGCParameter.Data.set -> void +SA3D.Modeling.Mesh.Gamecube.Parameters.IGCParameter.Read(SA3D.Common.IO.EndianStackReader! reader, uint address) -> SA3D.Modeling.Mesh.Gamecube.Parameters.IGCParameter! +SA3D.Modeling.Mesh.Gamecube.Parameters.IGCParameter.Type.get -> SA3D.Modeling.Mesh.Gamecube.Enums.GCParameterType +SA3D.Modeling.Mesh.Weighted.BufferMode +SA3D.Modeling.Mesh.Weighted.BufferMode.Generate = 1 -> SA3D.Modeling.Mesh.Weighted.BufferMode +SA3D.Modeling.Mesh.Weighted.BufferMode.GenerateOptimized = 2 -> SA3D.Modeling.Mesh.Weighted.BufferMode +SA3D.Modeling.Mesh.Weighted.BufferMode.None = 0 -> SA3D.Modeling.Mesh.Weighted.BufferMode +SA3D.Modeling.Mesh.Weighted.WeightedMesh +SA3D.Modeling.Mesh.Weighted.WeightedMesh.Clone() -> SA3D.Modeling.Mesh.Weighted.WeightedMesh! +SA3D.Modeling.Mesh.Weighted.WeightedMesh.DependingNodeIndices.get -> System.Collections.Generic.SortedSet! +SA3D.Modeling.Mesh.Weighted.WeightedMesh.EnsurePolygonsValid() -> bool +SA3D.Modeling.Mesh.Weighted.WeightedMesh.ForceVertexColors.get -> bool +SA3D.Modeling.Mesh.Weighted.WeightedMesh.ForceVertexColors.set -> void +SA3D.Modeling.Mesh.Weighted.WeightedMesh.HasColors.get -> bool +SA3D.Modeling.Mesh.Weighted.WeightedMesh.IsWeighted.get -> bool +SA3D.Modeling.Mesh.Weighted.WeightedMesh.Label.get -> string? +SA3D.Modeling.Mesh.Weighted.WeightedMesh.Label.set -> void +SA3D.Modeling.Mesh.Weighted.WeightedMesh.Materials.get -> SA3D.Modeling.Mesh.Buffer.BufferMaterial[]! +SA3D.Modeling.Mesh.Weighted.WeightedMesh.RootIndices.get -> System.Collections.Generic.HashSet! +SA3D.Modeling.Mesh.Weighted.WeightedMesh.ToAttach(SA3D.Modeling.Mesh.AttachFormat format, bool optimize, bool ignoreWeights) -> SA3D.Modeling.Mesh.Attach! +SA3D.Modeling.Mesh.Weighted.WeightedMesh.TriangleSets.get -> SA3D.Modeling.Mesh.Buffer.BufferCorner[]![]! +SA3D.Modeling.Mesh.Weighted.WeightedMesh.Vertices.get -> SA3D.Modeling.Mesh.Weighted.WeightedVertex[]! +SA3D.Modeling.Mesh.Weighted.WeightedMesh.WriteSpecular.get -> bool +SA3D.Modeling.Mesh.Weighted.WeightedMesh.WriteSpecular.set -> void +SA3D.Modeling.Mesh.Weighted.WeightedVertex +SA3D.Modeling.Mesh.Weighted.WeightedVertex.Clone() -> SA3D.Modeling.Mesh.Weighted.WeightedVertex +SA3D.Modeling.Mesh.Weighted.WeightedVertex.GetFirstWeightIndex() -> int +SA3D.Modeling.Mesh.Weighted.WeightedVertex.GetLastWeightIndex() -> int +SA3D.Modeling.Mesh.Weighted.WeightedVertex.GetMaxWeightIndex() -> int +SA3D.Modeling.Mesh.Weighted.WeightedVertex.GetWeightCount() -> int +SA3D.Modeling.Mesh.Weighted.WeightedVertex.GetWeightMap() -> (int nodeIndex, float weight)[]! +SA3D.Modeling.Mesh.Weighted.WeightedVertex.IsWeighted() -> bool +SA3D.Modeling.Mesh.Weighted.WeightedVertex.Normal.get -> System.Numerics.Vector3 +SA3D.Modeling.Mesh.Weighted.WeightedVertex.Normal.set -> void +SA3D.Modeling.Mesh.Weighted.WeightedVertex.Normalized() -> SA3D.Modeling.Mesh.Weighted.WeightedVertex +SA3D.Modeling.Mesh.Weighted.WeightedVertex.Position.get -> System.Numerics.Vector3 +SA3D.Modeling.Mesh.Weighted.WeightedVertex.Position.set -> void +SA3D.Modeling.Mesh.Weighted.WeightedVertex.WeightedVertex() -> void +SA3D.Modeling.Mesh.Weighted.WeightedVertex.WeightedVertex(System.Numerics.Vector3 position, System.Numerics.Vector3 normal) -> void +SA3D.Modeling.Mesh.Weighted.WeightedVertex.WeightedVertex(System.Numerics.Vector3 position, System.Numerics.Vector3 normal, float[]? weights) -> void +SA3D.Modeling.Mesh.Weighted.WeightedVertex.WeightedVertex(System.Numerics.Vector3 position, System.Numerics.Vector3 normal, int nodeCount) -> void +SA3D.Modeling.Mesh.Weighted.WeightedVertex.Weights.get -> float[]? +SA3D.Modeling.ObjectData.Enums.EnumExtensions +SA3D.Modeling.ObjectData.Enums.LandtableAttributes +SA3D.Modeling.ObjectData.Enums.LandtableAttributes.CustomDrawDistance = 4 -> SA3D.Modeling.ObjectData.Enums.LandtableAttributes +SA3D.Modeling.ObjectData.Enums.LandtableAttributes.EnableMotions = 1 -> SA3D.Modeling.ObjectData.Enums.LandtableAttributes +SA3D.Modeling.ObjectData.Enums.LandtableAttributes.LoadTexlist = 2 -> SA3D.Modeling.ObjectData.Enums.LandtableAttributes +SA3D.Modeling.ObjectData.Enums.LandtableAttributes.LoadTextureFile = 8 -> SA3D.Modeling.ObjectData.Enums.LandtableAttributes +SA3D.Modeling.ObjectData.Enums.ModelFormat +SA3D.Modeling.ObjectData.Enums.ModelFormat.Buffer = 4 -> SA3D.Modeling.ObjectData.Enums.ModelFormat +SA3D.Modeling.ObjectData.Enums.ModelFormat.SA1 = 0 -> SA3D.Modeling.ObjectData.Enums.ModelFormat +SA3D.Modeling.ObjectData.Enums.ModelFormat.SA2 = 2 -> SA3D.Modeling.ObjectData.Enums.ModelFormat +SA3D.Modeling.ObjectData.Enums.ModelFormat.SA2B = 3 -> SA3D.Modeling.ObjectData.Enums.ModelFormat +SA3D.Modeling.ObjectData.Enums.ModelFormat.SADX = 1 -> SA3D.Modeling.ObjectData.Enums.ModelFormat +SA3D.Modeling.ObjectData.Enums.NodeAttributes +SA3D.Modeling.ObjectData.Enums.NodeAttributes.Clip = 256 -> SA3D.Modeling.ObjectData.Enums.NodeAttributes +SA3D.Modeling.ObjectData.Enums.NodeAttributes.Modifier = 512 -> SA3D.Modeling.ObjectData.Enums.NodeAttributes +SA3D.Modeling.ObjectData.Enums.NodeAttributes.NoAnimate = 64 -> SA3D.Modeling.ObjectData.Enums.NodeAttributes +SA3D.Modeling.ObjectData.Enums.NodeAttributes.NoMorph = 128 -> SA3D.Modeling.ObjectData.Enums.NodeAttributes +SA3D.Modeling.ObjectData.Enums.NodeAttributes.NoPosition = 1 -> SA3D.Modeling.ObjectData.Enums.NodeAttributes +SA3D.Modeling.ObjectData.Enums.NodeAttributes.NoRotation = 2 -> SA3D.Modeling.ObjectData.Enums.NodeAttributes +SA3D.Modeling.ObjectData.Enums.NodeAttributes.NoScale = 4 -> SA3D.Modeling.ObjectData.Enums.NodeAttributes +SA3D.Modeling.ObjectData.Enums.NodeAttributes.RotateZYX = 32 -> SA3D.Modeling.ObjectData.Enums.NodeAttributes +SA3D.Modeling.ObjectData.Enums.NodeAttributes.SkipChildren = 16 -> SA3D.Modeling.ObjectData.Enums.NodeAttributes +SA3D.Modeling.ObjectData.Enums.NodeAttributes.SkipDraw = 8 -> SA3D.Modeling.ObjectData.Enums.NodeAttributes +SA3D.Modeling.ObjectData.Enums.NodeAttributes.UseQuaternionRotation = 1024 -> SA3D.Modeling.ObjectData.Enums.NodeAttributes +SA3D.Modeling.ObjectData.Enums.RotationUpdateMode +SA3D.Modeling.ObjectData.Enums.RotationUpdateMode.Keep = 2 -> SA3D.Modeling.ObjectData.Enums.RotationUpdateMode +SA3D.Modeling.ObjectData.Enums.RotationUpdateMode.UpdateEuler = 1 -> SA3D.Modeling.ObjectData.Enums.RotationUpdateMode +SA3D.Modeling.ObjectData.Enums.RotationUpdateMode.UpdateQuaternion = 0 -> SA3D.Modeling.ObjectData.Enums.RotationUpdateMode +SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes.Accelerate = 2097152 -> SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes.BoundsRadiusSmall = 536870912 -> SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes.BoundsRadiusTiny = 1073741824 -> SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes.CannotLand = 64 -> SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes.Chaos0Land = 8192 -> SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes.Diggable = 256 -> SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes.DrawByMesh = 33554432 -> SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes.DynamicCollision = 134217728 -> SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes.EnableManipulation = 67108864 -> SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes.Footprints = 1048576 -> SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes.Gravity = 8388608 -> SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes.Hurt = 65536 -> SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes.IncreasedAcceleration = 128 -> SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes.LowAcceleration = 16 -> SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes.LowDepth = 262144 -> SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes.NoAcceleration = 8 -> SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes.NoFriction = 4 -> SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes.NoZWrite = 16777216 -> SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes.Solid = 1 -> SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes.Stairs = 16384 -> SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes.TransformBounds = 268435456 -> SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes.TubeAcceleration = 131072 -> SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes.Unclimbable = 4096 -> SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes.Unknown11 = 2048 -> SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes.Unknown15 = 32768 -> SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes.Unknown19 = 524288 -> SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes.Unknown9 = 512 -> SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes.UseSkyDrawDistance = 32 -> SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes.Visible = 2147483648 -> SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes.Water = 2 -> SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes.WaterCollision = 4194304 -> SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes.Waterfall = 1024 -> SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes.BoundsRadiusSmall = 536870912 -> SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes.BoundsRadiusTiny = 1073741824 -> SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes.CannotLand = 4096 -> SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes.Diggable = 32 -> SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes.DynamicCollision = 134217728 -> SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes.EasyDraw = 16777216 -> SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes.Footprints = 2048 -> SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes.Gravity = 524288 -> SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes.Hurt = 1024 -> SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes.IncreasedAcceleration = 2097152 -> SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes.LowAcceleration = 16 -> SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes.NoAcceleration = 8 -> SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes.NoFog = 4194304 -> SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes.NoFriction = 4 -> SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes.NoShadows = 32768 -> SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes.Solid = 1 -> SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes.Stairs = 256 -> SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes.TransformBounds = 268435456 -> SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes.TubeAcceleration = 1048576 -> SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes.Unclimbable = 128 -> SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes.Unknown14 = 16384 -> SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes.Unknown16 = 65536 -> SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes.Unknown17 = 131072 -> SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes.Unknown18 = 262144 -> SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes.Unknown25 = 33554432 -> SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes.Unknown26 = 67108864 -> SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes.Unknown6 = 64 -> SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes.Unknown9 = 512 -> SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes.UseSkyDrawDistance = 8388608 -> SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes.Visible = 2147483648 -> SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes.Water = 2 -> SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes.WaterNoAlpha = 8192 -> SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.Accelerate = 1024 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.BoundsRadiusSmall = 32 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.BoundsRadiusTiny = 64 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.BoundTransforms = SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.TransformBounds | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.BoundsRadiusSmall | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.BoundsRadiusTiny -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.CannotLand = 131072 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.Chaos0Land = 1099511627776 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.CollisionMask = SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown26 | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.BoundTransforms | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.Collisions | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA1_Unknowns | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown6 | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown9 | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown14 | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown16 | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown17 | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown18 | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown25 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.Collisions = SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.Solid | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.Water | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.WaterNoAlpha | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.Accelerate | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.LowAcceleration | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.NoAcceleration | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.IncreasedAcceleration | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.TubeAcceleration | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.NoFriction | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.CannotLand | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.Unclimbable | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.Stairs | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.Diggable | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.Hurt | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.DynamicCollision | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.WaterCollision | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.Gravity -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.Diggable = 1048576 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.DrawByMesh = 137438953472 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.DynamicCollision = 4194304 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.EasyDraw = 34359738368 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.EnableManipulation = 274877906944 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.Footprints = 1073741824 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.Gravity = 16777216 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.Hurt = 2097152 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.IncreasedAcceleration = 16384 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.LowAcceleration = 4096 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.LowDepth = 8589934592 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.NoAcceleration = 8192 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.NoFog = 4294967296 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.NoFriction = 65536 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.NoShadows = 2147483648 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.NoZWrite = 68719476736 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA1_Unknown11 = 9007199254740992 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA1_Unknown15 = 18014398509481984 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA1_Unknown19 = 36028797018963968 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA1_Unknown9 = 4503599627370496 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA1_Unknowns = SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA1_Unknown9 | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA1_Unknown11 | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA1_Unknown15 | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA1_Unknown19 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown14 = 288230376151711744 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown16 = 576460752303423488 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown17 = 1152921504606846976 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown18 = 2305843009213693952 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown25 = 4611686018427387904 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown26 = 9223372036854775808 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown6 = 72057594037927936 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown9 = 144115188075855872 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknowns = SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown26 | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown6 | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown9 | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown14 | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown16 | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown17 | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown18 | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown25 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.Solid = 2 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.Stairs = 524288 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.TransformBounds = 16 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.TubeAcceleration = 32768 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.Unclimbable = 262144 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.Unknowns = SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown26 | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA1_Unknowns | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown6 | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown9 | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown14 | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown16 | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown17 | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown18 | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown25 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.UseSkyDrawDistance = 17179869184 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.ValidMask = SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown26 | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.BoundTransforms | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.Collisions | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.Visuals | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA1_Unknowns | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown6 | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown9 | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown14 | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown16 | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown17 | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown18 | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown25 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.Visible = 1 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.VisualMask = SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown26 | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.BoundTransforms | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.Visuals | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA1_Unknowns | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown6 | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown9 | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown14 | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown16 | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown17 | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown18 | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.SA2_Unknown25 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.Visuals = SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.Visible | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.Footprints | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.NoShadows | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.NoFog | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.LowDepth | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.UseSkyDrawDistance | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.EasyDraw | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.NoZWrite | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.DrawByMesh | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.EnableManipulation | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.Waterfall | SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.Chaos0Land -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.Water = 4 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.WaterCollision = 8388608 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.Waterfall = 549755813888 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Enums.SurfaceAttributes.WaterNoAlpha = 8 -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.Events.AttachUpdatedEventArgs +SA3D.Modeling.ObjectData.Events.AttachUpdatedEventArgs.NewAttach.get -> SA3D.Modeling.Mesh.Attach? +SA3D.Modeling.ObjectData.Events.AttachUpdatedEventArgs.OldAttach.get -> SA3D.Modeling.Mesh.Attach? +SA3D.Modeling.ObjectData.Events.AttachUpdatedEventHandler +SA3D.Modeling.ObjectData.Events.TransformSet +SA3D.Modeling.ObjectData.Events.TransformSet.LocalMatrix.get -> System.Numerics.Matrix4x4 +SA3D.Modeling.ObjectData.Events.TransformSet.Position.get -> System.Numerics.Vector3 +SA3D.Modeling.ObjectData.Events.TransformSet.QuaternionRotation.get -> System.Numerics.Quaternion +SA3D.Modeling.ObjectData.Events.TransformSet.Rotation.get -> System.Numerics.Vector3 +SA3D.Modeling.ObjectData.Events.TransformSet.Scale.get -> System.Numerics.Vector3 +SA3D.Modeling.ObjectData.Events.TransformSet.TransformSet() -> void +SA3D.Modeling.ObjectData.Events.TransformSet.TransformSet(System.Numerics.Matrix4x4 matrix, System.Numerics.Vector3 position, System.Numerics.Vector3 rotation, System.Numerics.Quaternion quaternionRotation, System.Numerics.Vector3 scale) -> void +SA3D.Modeling.ObjectData.Events.TransformsUpdatedEventArgs +SA3D.Modeling.ObjectData.Events.TransformsUpdatedEventArgs.NewTransforms.get -> SA3D.Modeling.ObjectData.Events.TransformSet +SA3D.Modeling.ObjectData.Events.TransformsUpdatedEventArgs.OldTransforms.get -> SA3D.Modeling.ObjectData.Events.TransformSet +SA3D.Modeling.ObjectData.Events.TransformsUpdatedEventArgs.UpdatedValues.get -> SA3D.Modeling.ObjectData.Events.UpdatedTransformValue +SA3D.Modeling.ObjectData.Events.TransformsUpdatedEventHandler +SA3D.Modeling.ObjectData.Events.UpdatedTransformValue +SA3D.Modeling.ObjectData.Events.UpdatedTransformValue.Position = 1 -> SA3D.Modeling.ObjectData.Events.UpdatedTransformValue +SA3D.Modeling.ObjectData.Events.UpdatedTransformValue.RotateMode = 8 -> SA3D.Modeling.ObjectData.Events.UpdatedTransformValue +SA3D.Modeling.ObjectData.Events.UpdatedTransformValue.Rotation = 2 -> SA3D.Modeling.ObjectData.Events.UpdatedTransformValue +SA3D.Modeling.ObjectData.Events.UpdatedTransformValue.Scale = 4 -> SA3D.Modeling.ObjectData.Events.UpdatedTransformValue +SA3D.Modeling.ObjectData.LandEntry +SA3D.Modeling.ObjectData.LandEntry.BlockBit.get -> uint +SA3D.Modeling.ObjectData.LandEntry.BlockBit.set -> void +SA3D.Modeling.ObjectData.LandEntry.Copy() -> SA3D.Modeling.ObjectData.LandEntry! +SA3D.Modeling.ObjectData.LandEntry.LandEntry(SA3D.Modeling.ObjectData.Node! node, SA3D.Modeling.ObjectData.Enums.SurfaceAttributes surfaceAttributes) -> void +SA3D.Modeling.ObjectData.LandEntry.Model.get -> SA3D.Modeling.ObjectData.Node! +SA3D.Modeling.ObjectData.LandEntry.Model.set -> void +SA3D.Modeling.ObjectData.LandEntry.ModelBounds.get -> SA3D.Modeling.Structs.Bounds +SA3D.Modeling.ObjectData.LandEntry.ModelBounds.set -> void +SA3D.Modeling.ObjectData.LandEntry.SurfaceAttributes.get -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +SA3D.Modeling.ObjectData.LandEntry.SurfaceAttributes.set -> void +SA3D.Modeling.ObjectData.LandEntry.Unknown.get -> uint +SA3D.Modeling.ObjectData.LandEntry.Unknown.set -> void +SA3D.Modeling.ObjectData.LandEntry.UpdateBounds() -> void +SA3D.Modeling.ObjectData.LandEntry.Write(SA3D.Common.IO.EndianStackWriter! writer, SA3D.Modeling.ObjectData.Enums.ModelFormat format, SA3D.Modeling.Structs.PointerLUT! lut) -> void +SA3D.Modeling.ObjectData.LandTable +SA3D.Modeling.ObjectData.LandTable.Attributes.get -> SA3D.Modeling.ObjectData.Enums.LandtableAttributes +SA3D.Modeling.ObjectData.LandTable.Attributes.set -> void +SA3D.Modeling.ObjectData.LandTable.BufferMeshData(bool optimize) -> void +SA3D.Modeling.ObjectData.LandTable.ConvertToFormat(SA3D.Modeling.ObjectData.Enums.ModelFormat newFormat, bool optimize, bool forceUpdate = false, bool updateBuffer = false) -> void +SA3D.Modeling.ObjectData.LandTable.DrawDistance.get -> float +SA3D.Modeling.ObjectData.LandTable.DrawDistance.set -> void +SA3D.Modeling.ObjectData.LandTable.Format.get -> SA3D.Modeling.ObjectData.Enums.ModelFormat +SA3D.Modeling.ObjectData.LandTable.Geometry.get -> SA3D.Common.Lookup.ILabeledArray! +SA3D.Modeling.ObjectData.LandTable.Geometry.set -> void +SA3D.Modeling.ObjectData.LandTable.GeometryAnimations.get -> SA3D.Common.Lookup.ILabeledArray! +SA3D.Modeling.ObjectData.LandTable.GeometryAnimations.set -> void +SA3D.Modeling.ObjectData.LandTable.Label.get -> string! +SA3D.Modeling.ObjectData.LandTable.Label.set -> void +SA3D.Modeling.ObjectData.LandTable.LandTable(SA3D.Common.Lookup.ILabeledArray! geometry, SA3D.Common.Lookup.ILabeledArray! geometryAnimations, SA3D.Modeling.ObjectData.Enums.ModelFormat format) -> void +SA3D.Modeling.ObjectData.LandTable.LandTable(SA3D.Common.Lookup.ILabeledArray! geometry, SA3D.Modeling.ObjectData.Enums.ModelFormat format) -> void +SA3D.Modeling.ObjectData.LandTable.SortLandEntries() -> void +SA3D.Modeling.ObjectData.LandTable.TexListPtr.get -> uint +SA3D.Modeling.ObjectData.LandTable.TexListPtr.set -> void +SA3D.Modeling.ObjectData.LandTable.TextureFileName.get -> string? +SA3D.Modeling.ObjectData.LandTable.TextureFileName.set -> void +SA3D.Modeling.ObjectData.LandTable.Write(SA3D.Common.IO.EndianStackWriter! writer, SA3D.Modeling.Structs.PointerLUT! lut) -> uint +SA3D.Modeling.ObjectData.Node +SA3D.Modeling.ObjectData.Node.AppendChild(SA3D.Modeling.ObjectData.Node! node) -> void +SA3D.Modeling.ObjectData.Node.Attach.get -> SA3D.Modeling.Mesh.Attach? +SA3D.Modeling.ObjectData.Node.Attach.set -> void +SA3D.Modeling.ObjectData.Node.AttachCopy() -> SA3D.Modeling.ObjectData.Node! +SA3D.Modeling.ObjectData.Node.Attributes.get -> SA3D.Modeling.ObjectData.Enums.NodeAttributes +SA3D.Modeling.ObjectData.Node.AutoNodeAttributes() -> void +SA3D.Modeling.ObjectData.Node.BufferMeshData(bool optimize) -> void +SA3D.Modeling.ObjectData.Node.CheckHasBranchAttaches() -> bool +SA3D.Modeling.ObjectData.Node.CheckHasTreeWeightedMesh() -> bool +SA3D.Modeling.ObjectData.Node.Child.get -> SA3D.Modeling.ObjectData.Node? +SA3D.Modeling.ObjectData.Node.ChildCount.get -> int +SA3D.Modeling.ObjectData.Node.ClearAttachesFromTree() -> void +SA3D.Modeling.ObjectData.Node.ConvertAttachFormat(SA3D.Modeling.Mesh.AttachFormat newAttachFormat, bool optimize, bool ignoreWeights = false, bool forceUpdate = false, bool updateBuffer = false) -> void +SA3D.Modeling.ObjectData.Node.DeepAttachCopy() -> SA3D.Modeling.ObjectData.Node! +SA3D.Modeling.ObjectData.Node.DeepSimpleCopy() -> SA3D.Modeling.ObjectData.Node! +SA3D.Modeling.ObjectData.Node.Detach() -> void +SA3D.Modeling.ObjectData.Node.DetachChildren(bool remainSiblings) -> void +SA3D.Modeling.ObjectData.Node.DetachSuccessors(bool remainSiblings) -> void +SA3D.Modeling.ObjectData.Node.Duplicate() -> SA3D.Modeling.ObjectData.Node! +SA3D.Modeling.ObjectData.Node.EulerRotation.get -> System.Numerics.Vector3 +SA3D.Modeling.ObjectData.Node.EulerRotation.set -> void +SA3D.Modeling.ObjectData.Node.GetAnimTreeNodeCount() -> int +SA3D.Modeling.ObjectData.Node.GetAnimTreeNodeEnumerable() -> System.Collections.Generic.IEnumerable! +SA3D.Modeling.ObjectData.Node.GetAnimTreeNodes() -> SA3D.Modeling.ObjectData.Node![]! +SA3D.Modeling.ObjectData.Node.GetAttachFormat() -> SA3D.Modeling.Mesh.AttachFormat? +SA3D.Modeling.ObjectData.Node.GetBranchNodeCount(bool includeSiblings) -> int +SA3D.Modeling.ObjectData.Node.GetBranchNodeEnumerable(bool includeSiblings) -> System.Collections.Generic.IEnumerable! +SA3D.Modeling.ObjectData.Node.GetBranchNodes(bool includeSiblings) -> SA3D.Modeling.ObjectData.Node![]! +SA3D.Modeling.ObjectData.Node.GetChildren() -> SA3D.Modeling.ObjectData.Node![]! +SA3D.Modeling.ObjectData.Node.GetEnumerator() -> System.Collections.Generic.IEnumerator! +SA3D.Modeling.ObjectData.Node.GetMorphTreeNodeCount() -> int +SA3D.Modeling.ObjectData.Node.GetMorphTreeNodeEnumerable() -> System.Collections.Generic.IEnumerable! +SA3D.Modeling.ObjectData.Node.GetMorphTreeNodes() -> SA3D.Modeling.ObjectData.Node![]! +SA3D.Modeling.ObjectData.Node.GetRootNode() -> SA3D.Modeling.ObjectData.Node! +SA3D.Modeling.ObjectData.Node.GetRootParent() -> SA3D.Modeling.ObjectData.Node! +SA3D.Modeling.ObjectData.Node.GetRootSibling() -> SA3D.Modeling.ObjectData.Node! +SA3D.Modeling.ObjectData.Node.GetTreeAttachCount() -> int +SA3D.Modeling.ObjectData.Node.GetTreeAttachEnumerable() -> System.Collections.Generic.IEnumerable! +SA3D.Modeling.ObjectData.Node.GetTreeAttaches() -> SA3D.Modeling.Mesh.Attach![]! +SA3D.Modeling.ObjectData.Node.GetTreeNodeCount() -> int +SA3D.Modeling.ObjectData.Node.GetTreeNodeEnumerable() -> System.Collections.Generic.IEnumerable! +SA3D.Modeling.ObjectData.Node.GetTreeNodes() -> SA3D.Modeling.ObjectData.Node![]! +SA3D.Modeling.ObjectData.Node.GetWorldMatrix() -> System.Numerics.Matrix4x4 +SA3D.Modeling.ObjectData.Node.GetWorldMatrixTree() -> (SA3D.Modeling.ObjectData.Node! node, System.Numerics.Matrix4x4 worldMatrix)[]! +SA3D.Modeling.ObjectData.Node.GetWorldMatrixTreeEnumerator() -> System.Collections.Generic.IEnumerable<(SA3D.Modeling.ObjectData.Node! node, System.Numerics.Matrix4x4 worldMatrix)>! +SA3D.Modeling.ObjectData.Node.GetWorldMatrixTreeLUT() -> System.Collections.Generic.Dictionary! +SA3D.Modeling.ObjectData.Node.HasImmediates.get -> bool +SA3D.Modeling.ObjectData.Node.HasSiblings.get -> bool +SA3D.Modeling.ObjectData.Node.InsertAfter(SA3D.Modeling.ObjectData.Node! node) -> void +SA3D.Modeling.ObjectData.Node.InsertBefore(SA3D.Modeling.ObjectData.Node! node) -> void +SA3D.Modeling.ObjectData.Node.InsertChild(int index, SA3D.Modeling.ObjectData.Node! node) -> void +SA3D.Modeling.ObjectData.Node.Label.get -> string! +SA3D.Modeling.ObjectData.Node.Label.set -> void +SA3D.Modeling.ObjectData.Node.LocalMatrix.get -> System.Numerics.Matrix4x4 +SA3D.Modeling.ObjectData.Node.Next.get -> SA3D.Modeling.ObjectData.Node? +SA3D.Modeling.ObjectData.Node.NoAnimate.get -> bool +SA3D.Modeling.ObjectData.Node.NoAnimate.set -> void +SA3D.Modeling.ObjectData.Node.Node() -> void +SA3D.Modeling.ObjectData.Node.NoMorph.get -> bool +SA3D.Modeling.ObjectData.Node.NoMorph.set -> void +SA3D.Modeling.ObjectData.Node.NoPosition.get -> bool +SA3D.Modeling.ObjectData.Node.NoPosition.set -> void +SA3D.Modeling.ObjectData.Node.NoRotation.get -> bool +SA3D.Modeling.ObjectData.Node.NoRotation.set -> void +SA3D.Modeling.ObjectData.Node.NoScale.get -> bool +SA3D.Modeling.ObjectData.Node.NoScale.set -> void +SA3D.Modeling.ObjectData.Node.OnAttachUpdated -> SA3D.Modeling.ObjectData.Events.AttachUpdatedEventHandler? +SA3D.Modeling.ObjectData.Node.OnTransformsUpdated -> SA3D.Modeling.ObjectData.Events.TransformsUpdatedEventHandler? +SA3D.Modeling.ObjectData.Node.Parent.get -> SA3D.Modeling.ObjectData.Node? +SA3D.Modeling.ObjectData.Node.Position.get -> System.Numerics.Vector3 +SA3D.Modeling.ObjectData.Node.Position.set -> void +SA3D.Modeling.ObjectData.Node.Previous.get -> SA3D.Modeling.ObjectData.Node? +SA3D.Modeling.ObjectData.Node.QuaternionRotation.get -> System.Numerics.Quaternion +SA3D.Modeling.ObjectData.Node.QuaternionRotation.set -> void +SA3D.Modeling.ObjectData.Node.RotateZYX.get -> bool +SA3D.Modeling.ObjectData.Node.Scale.get -> System.Numerics.Vector3 +SA3D.Modeling.ObjectData.Node.Scale.set -> void +SA3D.Modeling.ObjectData.Node.SetAllNodeAttributes(SA3D.Modeling.ObjectData.Enums.NodeAttributes attributes, SA3D.Modeling.ObjectData.Enums.RotationUpdateMode rotationUpdateMode = SA3D.Modeling.ObjectData.Enums.RotationUpdateMode.UpdateEuler) -> void +SA3D.Modeling.ObjectData.Node.SetChild(SA3D.Modeling.ObjectData.Node? node) -> void +SA3D.Modeling.ObjectData.Node.SetNext(SA3D.Modeling.ObjectData.Node? node) -> void +SA3D.Modeling.ObjectData.Node.SetRotationZYX(bool newValue, SA3D.Modeling.ObjectData.Enums.RotationUpdateMode mode = SA3D.Modeling.ObjectData.Enums.RotationUpdateMode.UpdateEuler) -> void +SA3D.Modeling.ObjectData.Node.SimpleCopy() -> SA3D.Modeling.ObjectData.Node! +SA3D.Modeling.ObjectData.Node.SkipChildren.get -> bool +SA3D.Modeling.ObjectData.Node.SkipChildren.set -> void +SA3D.Modeling.ObjectData.Node.SkipDraw.get -> bool +SA3D.Modeling.ObjectData.Node.SkipDraw.set -> void +SA3D.Modeling.ObjectData.Node.this[int index].get -> SA3D.Modeling.ObjectData.Node! +SA3D.Modeling.ObjectData.Node.UpdateTransforms(System.Numerics.Vector3? position, System.Numerics.Quaternion? quaternionRotation, System.Numerics.Vector3? scale) -> void +SA3D.Modeling.ObjectData.Node.UpdateTransforms(System.Numerics.Vector3? position, System.Numerics.Vector3? eulerRotation, System.Numerics.Vector3? scale) -> void +SA3D.Modeling.ObjectData.Node.UseQuaternionRotation.get -> bool +SA3D.Modeling.ObjectData.Node.UseQuaternionRotation.set -> void +SA3D.Modeling.ObjectData.Node.Write(SA3D.Common.IO.EndianStackWriter! writer, SA3D.Modeling.ObjectData.Enums.ModelFormat format, SA3D.Modeling.Structs.PointerLUT! lut) -> uint +SA3D.Modeling.Strippify.TopologyException +SA3D.Modeling.Strippify.TopologyException.TopologyException(string! msg) -> void +SA3D.Modeling.Strippify.TriangleStrippifier +SA3D.Modeling.Strippify.TriangleStrippifier.RaiseTopoError.get -> bool +SA3D.Modeling.Strippify.TriangleStrippifier.RaiseTopoError.set -> void +SA3D.Modeling.Strippify.TriangleStrippifier.Strippify(int[]! triangleList) -> int[]![]! +SA3D.Modeling.Strippify.TriangleStrippifier.Strippify(T[]! triangleList) -> T[]![]! +SA3D.Modeling.Strippify.TriangleStrippifier.Strippify(T[]! triangleList, out SA3D.Common.DistinctMap! distinctMap) -> int[]![]! +SA3D.Modeling.Strippify.TriangleStrippifier.StrippifyNoDegen(T[]! triangleList, out bool[]! reversedStrips) -> T[]![]! +SA3D.Modeling.Strippify.TriangleStrippifier.TriangleStrippifier(bool raiseTopoError = false) -> void +SA3D.Modeling.Structs.Bounds +SA3D.Modeling.Structs.Bounds.Bounds() -> void +SA3D.Modeling.Structs.Bounds.Bounds(System.Numerics.Vector3 position, float radius) -> void +SA3D.Modeling.Structs.Bounds.Matrix.get -> System.Numerics.Matrix4x4 +SA3D.Modeling.Structs.Bounds.Position.get -> System.Numerics.Vector3 +SA3D.Modeling.Structs.Bounds.Position.set -> void +SA3D.Modeling.Structs.Bounds.Radius.get -> float +SA3D.Modeling.Structs.Bounds.Radius.set -> void +SA3D.Modeling.Structs.Bounds.Write(SA3D.Common.IO.EndianStackWriter! writer) -> void +SA3D.Modeling.Structs.Color +SA3D.Modeling.Structs.Color.Alpha.get -> byte +SA3D.Modeling.Structs.Color.Alpha.set -> void +SA3D.Modeling.Structs.Color.AlphaF.get -> float +SA3D.Modeling.Structs.Color.AlphaF.set -> void +SA3D.Modeling.Structs.Color.ARGB.get -> uint +SA3D.Modeling.Structs.Color.ARGB.set -> void +SA3D.Modeling.Structs.Color.ARGB4.get -> ushort +SA3D.Modeling.Structs.Color.ARGB4.set -> void +SA3D.Modeling.Structs.Color.Blue.get -> byte +SA3D.Modeling.Structs.Color.Blue.set -> void +SA3D.Modeling.Structs.Color.BlueF.get -> float +SA3D.Modeling.Structs.Color.BlueF.set -> void +SA3D.Modeling.Structs.Color.Color() -> void +SA3D.Modeling.Structs.Color.Color(byte red, byte green, byte blue) -> void +SA3D.Modeling.Structs.Color.Color(byte red, byte green, byte blue, byte alpha) -> void +SA3D.Modeling.Structs.Color.Color(float red, float green, float blue) -> void +SA3D.Modeling.Structs.Color.Color(float red, float green, float blue, float alpha) -> void +SA3D.Modeling.Structs.Color.Color(System.Numerics.Vector4 vector) -> void +SA3D.Modeling.Structs.Color.FloatVector.get -> System.Numerics.Vector4 +SA3D.Modeling.Structs.Color.FloatVector.set -> void +SA3D.Modeling.Structs.Color.GetLuminance() -> float +SA3D.Modeling.Structs.Color.Green.get -> byte +SA3D.Modeling.Structs.Color.Green.set -> void +SA3D.Modeling.Structs.Color.GreenF.get -> float +SA3D.Modeling.Structs.Color.GreenF.set -> void +SA3D.Modeling.Structs.Color.Hex.get -> string! +SA3D.Modeling.Structs.Color.Hex.set -> void +SA3D.Modeling.Structs.Color.Red.get -> byte +SA3D.Modeling.Structs.Color.Red.set -> void +SA3D.Modeling.Structs.Color.RedF.get -> float +SA3D.Modeling.Structs.Color.RedF.set -> void +SA3D.Modeling.Structs.Color.RGB565.get -> ushort +SA3D.Modeling.Structs.Color.RGB565.set -> void +SA3D.Modeling.Structs.Color.RGBA.get -> uint +SA3D.Modeling.Structs.Color.RGBA.set -> void +SA3D.Modeling.Structs.Color.SystemColor.get -> System.Drawing.Color +SA3D.Modeling.Structs.Color.SystemColor.set -> void +SA3D.Modeling.Structs.ColorIOType +SA3D.Modeling.Structs.ColorIOType.ARGB4 = 2 -> SA3D.Modeling.Structs.ColorIOType +SA3D.Modeling.Structs.ColorIOType.ARGB8_16 = 1 -> SA3D.Modeling.Structs.ColorIOType +SA3D.Modeling.Structs.ColorIOType.ARGB8_32 = 0 -> SA3D.Modeling.Structs.ColorIOType +SA3D.Modeling.Structs.ColorIOType.RGB565 = 3 -> SA3D.Modeling.Structs.ColorIOType +SA3D.Modeling.Structs.ColorIOType.RGBA8 = 4 -> SA3D.Modeling.Structs.ColorIOType +SA3D.Modeling.Structs.ColorIOTypeExtensions +SA3D.Modeling.Structs.DebugStringExtensions +SA3D.Modeling.Structs.EndianIOExtensions +SA3D.Modeling.Structs.EndianIOExtensions.ReadValueAdvanceDelegate +SA3D.Modeling.Structs.EndianIOExtensions.ReadValueDelegate +SA3D.Modeling.Structs.EndianIOExtensions.WriteValueDelegate +SA3D.Modeling.Structs.FloatIOType +SA3D.Modeling.Structs.FloatIOType.BAMS16 = 3 -> SA3D.Modeling.Structs.FloatIOType +SA3D.Modeling.Structs.FloatIOType.BAMS32 = 4 -> SA3D.Modeling.Structs.FloatIOType +SA3D.Modeling.Structs.FloatIOType.Float = 0 -> SA3D.Modeling.Structs.FloatIOType +SA3D.Modeling.Structs.FloatIOType.Integer = 2 -> SA3D.Modeling.Structs.FloatIOType +SA3D.Modeling.Structs.FloatIOType.Short = 1 -> SA3D.Modeling.Structs.FloatIOType +SA3D.Modeling.Structs.FloatIOTypeExtensions +SA3D.Modeling.Structs.MatrixUtilities +SA3D.Modeling.Structs.PointerLUT +SA3D.Modeling.Structs.PointerLUT.Attaches.get -> SA3D.Common.Lookup.PointerDictionary! +SA3D.Modeling.Structs.PointerLUT.Motions.get -> SA3D.Common.Lookup.PointerDictionary! +SA3D.Modeling.Structs.PointerLUT.NodeMotions.get -> SA3D.Common.Lookup.PointerDictionary! +SA3D.Modeling.Structs.PointerLUT.Nodes.get -> SA3D.Common.Lookup.PointerDictionary! +SA3D.Modeling.Structs.PointerLUT.Other.get -> SA3D.Common.Lookup.PointerDictionary! +SA3D.Modeling.Structs.PointerLUT.PointerLUT() -> void +SA3D.Modeling.Structs.PointerLUT.PointerLUT(System.Collections.Generic.Dictionary! labels) -> void +SA3D.Modeling.Structs.PointerLUT.PolyChunks.get -> SA3D.Common.Lookup.PointerDictionary! +SA3D.Modeling.Structs.QuaternionUtilities +SA3D.Modeling.Structs.VectorUtilities +static readonly SA3D.Modeling.Mesh.Basic.BasicMaterial.DefaultValues -> SA3D.Modeling.Mesh.Basic.BasicMaterial +static readonly SA3D.Modeling.Mesh.Buffer.BufferMaterial.DefaultValues -> SA3D.Modeling.Mesh.Buffer.BufferMaterial +static readonly SA3D.Modeling.Mesh.Buffer.BufferMesh.DefaultColor -> SA3D.Modeling.Structs.Color +static readonly SA3D.Modeling.Mesh.Buffer.BufferMesh.DefaultNormal -> System.Numerics.Vector3 +static readonly SA3D.Modeling.Mesh.Gamecube.GCVertexSet.EndVertexSet -> SA3D.Modeling.Mesh.Gamecube.GCVertexSet! +static readonly SA3D.Modeling.Mesh.Gamecube.Parameters.GCAmbientColorParameter.White -> SA3D.Modeling.Mesh.Gamecube.Parameters.GCAmbientColorParameter +static readonly SA3D.Modeling.Mesh.Gamecube.Parameters.GCBlendAlphaParameter.DefaultBlendParameter -> SA3D.Modeling.Mesh.Gamecube.Parameters.GCBlendAlphaParameter +static readonly SA3D.Modeling.Mesh.Gamecube.Parameters.GCDiffuseColorParameter.White -> SA3D.Modeling.Mesh.Gamecube.Parameters.GCDiffuseColorParameter +static readonly SA3D.Modeling.Mesh.Gamecube.Parameters.GCSpecularColorParameter.White -> SA3D.Modeling.Mesh.Gamecube.Parameters.GCSpecularColorParameter +static readonly SA3D.Modeling.Mesh.Gamecube.Parameters.GCTexCoordParameter.DefaultValues -> SA3D.Modeling.Mesh.Gamecube.Parameters.GCTexCoordParameter +static readonly SA3D.Modeling.Mesh.Gamecube.Parameters.GCTexCoordParameter.EnvironmentMapValues -> SA3D.Modeling.Mesh.Gamecube.Parameters.GCTexCoordParameter +static readonly SA3D.Modeling.Mesh.Gamecube.Parameters.GCUnknownParameter.DefaultValues -> SA3D.Modeling.Mesh.Gamecube.Parameters.GCUnknownParameter +static readonly SA3D.Modeling.Strippify.TriangleStrippifier.Global -> SA3D.Modeling.Strippify.TriangleStrippifier! +static readonly SA3D.Modeling.Structs.Color.ColorBlack -> SA3D.Modeling.Structs.Color +static readonly SA3D.Modeling.Structs.Color.ColorBlue -> SA3D.Modeling.Structs.Color +static readonly SA3D.Modeling.Structs.Color.ColorGreen -> SA3D.Modeling.Structs.Color +static readonly SA3D.Modeling.Structs.Color.ColorRed -> SA3D.Modeling.Structs.Color +static readonly SA3D.Modeling.Structs.Color.ColorTransparent -> SA3D.Modeling.Structs.Color +static readonly SA3D.Modeling.Structs.Color.ColorWhite -> SA3D.Modeling.Structs.Color +static SA3D.Modeling.Animation.EnumExtensions.ChannelCount(this SA3D.Modeling.Animation.KeyframeAttributes attributes) -> int +static SA3D.Modeling.Animation.Keyframes.Read(SA3D.Common.IO.EndianStackReader! reader, ref uint address, SA3D.Modeling.Animation.KeyframeAttributes type, SA3D.Modeling.Structs.PointerLUT! lut, bool shortRot = false) -> SA3D.Modeling.Animation.Keyframes! +static SA3D.Modeling.Animation.LandEntryMotion.Read(SA3D.Common.IO.EndianStackReader! data, uint address, SA3D.Modeling.ObjectData.Enums.ModelFormat format, SA3D.Modeling.Structs.PointerLUT! lut) -> SA3D.Modeling.Animation.LandEntryMotion! +static SA3D.Modeling.Animation.LandEntryMotion.StructSize.get -> uint +static SA3D.Modeling.Animation.Motion.Read(SA3D.Common.IO.EndianStackReader! reader, uint address, uint modelCount, SA3D.Modeling.Structs.PointerLUT! lut, bool shortRot = false) -> SA3D.Modeling.Animation.Motion! +static SA3D.Modeling.Animation.NodeMotion.Read(SA3D.Common.IO.EndianStackReader! reader, uint address, SA3D.Modeling.ObjectData.Enums.ModelFormat format, SA3D.Modeling.Structs.PointerLUT! lut) -> SA3D.Modeling.Animation.NodeMotion! +static SA3D.Modeling.Animation.Spotlight.Distance(SA3D.Modeling.Animation.Spotlight from, SA3D.Modeling.Animation.Spotlight to) -> float +static SA3D.Modeling.Animation.Spotlight.Lerp(SA3D.Modeling.Animation.Spotlight from, SA3D.Modeling.Animation.Spotlight to, float time) -> SA3D.Modeling.Animation.Spotlight +static SA3D.Modeling.Animation.Spotlight.Read(SA3D.Common.IO.EndianStackReader! reader, uint address) -> SA3D.Modeling.Animation.Spotlight +static SA3D.Modeling.Animation.Spotlight.StructSize.get -> uint +static SA3D.Modeling.Animation.Utilities.KeyframeRotationUtils.EulerToQuaternion(System.Collections.Generic.SortedDictionary! source, float deviationThreshold, bool rotateZYX) -> System.Collections.Generic.SortedDictionary! +static SA3D.Modeling.Animation.Utilities.KeyframeRotationUtils.EulerToQuaternion(System.Collections.Generic.SortedDictionary! source, float deviationThreshold, bool rotateZYX, System.Collections.Generic.SortedDictionary! result) -> void +static SA3D.Modeling.Animation.Utilities.KeyframeRotationUtils.EulerToQuaternion(this SA3D.Modeling.Animation.Keyframes! keyframes, float deviationThreshold, bool rotateZYX, bool clearEuler) -> void +static SA3D.Modeling.Animation.Utilities.KeyframeRotationUtils.GetRotationMatrices(this SA3D.Modeling.Animation.Keyframes! keyframes, bool targetQuaternion, float deviationThreshold, bool rotateZYX, out bool converted, out System.Collections.Generic.Dictionary? complementary) -> System.Collections.Generic.SortedDictionary! +static SA3D.Modeling.Animation.Utilities.KeyframeRotationUtils.MatrixToEuler(System.Collections.Generic.SortedDictionary! source, bool wasQuaternion, float deviationThreshold, bool rotateZYX, System.Collections.Generic.Dictionary? complementary) -> System.Collections.Generic.SortedDictionary! +static SA3D.Modeling.Animation.Utilities.KeyframeRotationUtils.MatrixToEuler(System.Collections.Generic.SortedDictionary! source, bool wasQuaternion, float deviationThreshold, bool rotateZYX, System.Collections.Generic.Dictionary? complementary, System.Collections.Generic.SortedDictionary! result) -> void +static SA3D.Modeling.Animation.Utilities.KeyframeRotationUtils.MatrixToEuler(this SA3D.Modeling.Animation.Keyframes! keyframes, System.Collections.Generic.SortedDictionary! source, bool wasQuaternion, float deviationThreshold, bool rotateZYX, System.Collections.Generic.Dictionary? complementary) -> void +static SA3D.Modeling.Animation.Utilities.KeyframeRotationUtils.MatrixToQuaternion(System.Collections.Generic.SortedDictionary! source, bool wasQuaternion, float deviationThreshold, bool rotateZYX) -> System.Collections.Generic.SortedDictionary! +static SA3D.Modeling.Animation.Utilities.KeyframeRotationUtils.MatrixToQuaternion(System.Collections.Generic.SortedDictionary! source, bool wasQuaternion, float deviationThreshold, bool rotateZYX, System.Collections.Generic.SortedDictionary! result) -> void +static SA3D.Modeling.Animation.Utilities.KeyframeRotationUtils.MatrixToQuaternion(this SA3D.Modeling.Animation.Keyframes! keyframes, System.Collections.Generic.SortedDictionary! source, bool wasQuaternion, float deviationThreshold, bool rotateZYX) -> void +static SA3D.Modeling.Animation.Utilities.KeyframeRotationUtils.QuaternionToEuler(System.Collections.Generic.SortedDictionary! source, float deviationThreshold, bool rotateZYX) -> System.Collections.Generic.SortedDictionary! +static SA3D.Modeling.Animation.Utilities.KeyframeRotationUtils.QuaternionToEuler(System.Collections.Generic.SortedDictionary! source, float deviationThreshold, bool rotateZYX, System.Collections.Generic.SortedDictionary! result) -> void +static SA3D.Modeling.Animation.Utilities.KeyframeRotationUtils.QuaternionToEuler(this SA3D.Modeling.Animation.Keyframes! keyframes, float deviationThreshold, bool rotateZYX, bool clearQuaternion) -> void +static SA3D.Modeling.File.AnimationFile.CheckIsAnimationFile(byte[]! data) -> bool +static SA3D.Modeling.File.AnimationFile.CheckIsAnimationFile(byte[]! data, uint address) -> bool +static SA3D.Modeling.File.AnimationFile.CheckIsAnimationFile(SA3D.Common.IO.EndianStackReader! reader) -> bool +static SA3D.Modeling.File.AnimationFile.CheckIsAnimationFile(SA3D.Common.IO.EndianStackReader! reader, uint address) -> bool +static SA3D.Modeling.File.AnimationFile.Read(SA3D.Common.IO.EndianStackReader! reader) -> SA3D.Modeling.File.AnimationFile! +static SA3D.Modeling.File.AnimationFile.Read(SA3D.Common.IO.EndianStackReader! reader, uint address) -> SA3D.Modeling.File.AnimationFile! +static SA3D.Modeling.File.AnimationFile.Read(SA3D.Common.IO.EndianStackReader! reader, uint address, uint? nodeCount, bool shortRot) -> SA3D.Modeling.File.AnimationFile! +static SA3D.Modeling.File.AnimationFile.ReadFromData(byte[]! data) -> SA3D.Modeling.File.AnimationFile! +static SA3D.Modeling.File.AnimationFile.ReadFromData(byte[]! data, uint address) -> SA3D.Modeling.File.AnimationFile! +static SA3D.Modeling.File.AnimationFile.ReadFromData(byte[]! data, uint address, uint? nodeCount, bool shortRot) -> SA3D.Modeling.File.AnimationFile! +static SA3D.Modeling.File.AnimationFile.ReadFromFile(string! filepath) -> SA3D.Modeling.File.AnimationFile! +static SA3D.Modeling.File.AnimationFile.ReadFromFile(string! filepath, uint? nodeCount, bool shortRot) -> SA3D.Modeling.File.AnimationFile! +static SA3D.Modeling.File.AnimationFile.Write(SA3D.Common.IO.EndianStackWriter! writer, SA3D.Modeling.Animation.Motion! animation, SA3D.Modeling.File.MetaData? metaData = null) -> void +static SA3D.Modeling.File.AnimationFile.WriteToData(SA3D.Modeling.Animation.Motion! animation, SA3D.Modeling.File.MetaData? metaData = null) -> byte[]! +static SA3D.Modeling.File.AnimationFile.WriteToFile(string! filepath, SA3D.Modeling.Animation.Motion! animation, SA3D.Modeling.File.MetaData? metaData = null) -> void +static SA3D.Modeling.File.LevelFile.CheckIsLevelFile(byte[]! data) -> bool +static SA3D.Modeling.File.LevelFile.CheckIsLevelFile(byte[]! data, uint address) -> bool +static SA3D.Modeling.File.LevelFile.CheckIsLevelFile(SA3D.Common.IO.EndianStackReader! reader) -> bool +static SA3D.Modeling.File.LevelFile.CheckIsLevelFile(SA3D.Common.IO.EndianStackReader! reader, uint address) -> bool +static SA3D.Modeling.File.LevelFile.Read(SA3D.Common.IO.EndianStackReader! reader) -> SA3D.Modeling.File.LevelFile! +static SA3D.Modeling.File.LevelFile.Read(SA3D.Common.IO.EndianStackReader! reader, uint address) -> SA3D.Modeling.File.LevelFile! +static SA3D.Modeling.File.LevelFile.ReadFromData(byte[]! data) -> SA3D.Modeling.File.LevelFile! +static SA3D.Modeling.File.LevelFile.ReadFromData(byte[]! data, uint address) -> SA3D.Modeling.File.LevelFile! +static SA3D.Modeling.File.LevelFile.ReadFromFile(string! filepath) -> SA3D.Modeling.File.LevelFile! +static SA3D.Modeling.File.LevelFile.Write(SA3D.Common.IO.EndianStackWriter! writer, SA3D.Modeling.ObjectData.LandTable! level, SA3D.Modeling.File.MetaData? metaData = null) -> void +static SA3D.Modeling.File.LevelFile.WriteToData(SA3D.Modeling.ObjectData.LandTable! level, SA3D.Modeling.File.MetaData? metaData = null) -> byte[]! +static SA3D.Modeling.File.LevelFile.WriteToFile(string! filepath, SA3D.Modeling.ObjectData.LandTable! level, SA3D.Modeling.File.MetaData? metaData = null) -> void +static SA3D.Modeling.File.MetaData.Read(SA3D.Common.IO.EndianStackReader! reader, uint address, int version, bool hasAnimMorphFiles) -> SA3D.Modeling.File.MetaData! +static SA3D.Modeling.File.ModelFile.CheckIsModelFile(byte[]! data) -> bool +static SA3D.Modeling.File.ModelFile.CheckIsModelFile(byte[]! data, uint address) -> bool +static SA3D.Modeling.File.ModelFile.CheckIsModelFile(SA3D.Common.IO.EndianStackReader! reader) -> bool +static SA3D.Modeling.File.ModelFile.CheckIsModelFile(SA3D.Common.IO.EndianStackReader! reader, uint address) -> bool +static SA3D.Modeling.File.ModelFile.Read(SA3D.Common.IO.EndianStackReader! reader) -> SA3D.Modeling.File.ModelFile! +static SA3D.Modeling.File.ModelFile.Read(SA3D.Common.IO.EndianStackReader! reader, uint address) -> SA3D.Modeling.File.ModelFile! +static SA3D.Modeling.File.ModelFile.ReadFromData(byte[]! data) -> SA3D.Modeling.File.ModelFile! +static SA3D.Modeling.File.ModelFile.ReadFromData(byte[]! data, uint address) -> SA3D.Modeling.File.ModelFile! +static SA3D.Modeling.File.ModelFile.ReadFromFile(string! filepath) -> SA3D.Modeling.File.ModelFile! +static SA3D.Modeling.File.ModelFile.Write(SA3D.Common.IO.EndianStackWriter! writer, SA3D.Modeling.ObjectData.Node! model, bool nj = false, SA3D.Modeling.File.MetaData? metaData = null, SA3D.Modeling.ObjectData.Enums.ModelFormat? format = null) -> void +static SA3D.Modeling.File.ModelFile.WriteToData(SA3D.Modeling.ObjectData.Node! model, bool nj = false, SA3D.Modeling.File.MetaData? metaData = null, SA3D.Modeling.ObjectData.Enums.ModelFormat? format = null) -> byte[]! +static SA3D.Modeling.File.ModelFile.WriteToFile(string! filepath, SA3D.Modeling.ObjectData.Node! model, bool nj = false, SA3D.Modeling.File.MetaData? metaData = null, SA3D.Modeling.ObjectData.Enums.ModelFormat? format = null) -> void +static SA3D.Modeling.Mesh.Attach.Read(SA3D.Common.IO.EndianStackReader! reader, uint address, SA3D.Modeling.ObjectData.Enums.ModelFormat format, SA3D.Modeling.Structs.PointerLUT! lut) -> SA3D.Modeling.Mesh.Attach! +static SA3D.Modeling.Mesh.Attach.ReadBuffer(SA3D.Common.IO.EndianStackReader! reader, uint address, SA3D.Modeling.Structs.PointerLUT! lut) -> SA3D.Modeling.Mesh.Attach! +static SA3D.Modeling.Mesh.Basic.BasicAttach.Read(SA3D.Common.IO.EndianStackReader! reader, uint address, bool DX, SA3D.Modeling.Structs.PointerLUT! lut) -> SA3D.Modeling.Mesh.Basic.BasicAttach! +static SA3D.Modeling.Mesh.Basic.BasicMaterial.operator !=(SA3D.Modeling.Mesh.Basic.BasicMaterial left, SA3D.Modeling.Mesh.Basic.BasicMaterial right) -> bool +static SA3D.Modeling.Mesh.Basic.BasicMaterial.operator ==(SA3D.Modeling.Mesh.Basic.BasicMaterial left, SA3D.Modeling.Mesh.Basic.BasicMaterial right) -> bool +static SA3D.Modeling.Mesh.Basic.BasicMaterial.Read(SA3D.Common.IO.EndianStackReader! reader, uint address) -> SA3D.Modeling.Mesh.Basic.BasicMaterial +static SA3D.Modeling.Mesh.Basic.BasicMesh.Read(SA3D.Common.IO.EndianStackReader! reader, uint address, SA3D.Modeling.Structs.PointerLUT! lut) -> SA3D.Modeling.Mesh.Basic.BasicMesh! +static SA3D.Modeling.Mesh.Basic.Polygon.BasicMultiPolygon.Read(SA3D.Common.IO.EndianStackReader! data, ref uint address) -> SA3D.Modeling.Mesh.Basic.Polygon.BasicMultiPolygon +static SA3D.Modeling.Mesh.Basic.Polygon.BasicQuad.Read(SA3D.Common.IO.EndianStackReader! reader, ref uint address) -> SA3D.Modeling.Mesh.Basic.Polygon.BasicQuad +static SA3D.Modeling.Mesh.Basic.Polygon.BasicTriangle.Read(SA3D.Common.IO.EndianStackReader! reader, ref uint address) -> SA3D.Modeling.Mesh.Basic.Polygon.BasicTriangle +static SA3D.Modeling.Mesh.Buffer.BufferCorner.operator !=(SA3D.Modeling.Mesh.Buffer.BufferCorner l, SA3D.Modeling.Mesh.Buffer.BufferCorner r) -> bool +static SA3D.Modeling.Mesh.Buffer.BufferCorner.operator ==(SA3D.Modeling.Mesh.Buffer.BufferCorner l, SA3D.Modeling.Mesh.Buffer.BufferCorner r) -> bool +static SA3D.Modeling.Mesh.Buffer.BufferCorner.Read(SA3D.Common.IO.EndianStackReader! reader, ref uint address, bool hasColor) -> SA3D.Modeling.Mesh.Buffer.BufferCorner +static SA3D.Modeling.Mesh.Buffer.BufferMaterial.Read(SA3D.Common.IO.EndianStackReader! reader, uint address) -> SA3D.Modeling.Mesh.Buffer.BufferMaterial +static SA3D.Modeling.Mesh.Buffer.BufferMesh.Optimize(System.Collections.Generic.IList! input) -> SA3D.Modeling.Mesh.Buffer.BufferMesh![]! +static SA3D.Modeling.Mesh.Buffer.BufferMesh.Read(SA3D.Common.IO.EndianStackReader! reader, uint address) -> SA3D.Modeling.Mesh.Buffer.BufferMesh! +static SA3D.Modeling.Mesh.Buffer.BufferVertex.operator !=(SA3D.Modeling.Mesh.Buffer.BufferVertex left, SA3D.Modeling.Mesh.Buffer.BufferVertex right) -> bool +static SA3D.Modeling.Mesh.Buffer.BufferVertex.operator *(float l, SA3D.Modeling.Mesh.Buffer.BufferVertex r) -> SA3D.Modeling.Mesh.Buffer.BufferVertex +static SA3D.Modeling.Mesh.Buffer.BufferVertex.operator *(SA3D.Modeling.Mesh.Buffer.BufferVertex l, float r) -> SA3D.Modeling.Mesh.Buffer.BufferVertex +static SA3D.Modeling.Mesh.Buffer.BufferVertex.operator +(SA3D.Modeling.Mesh.Buffer.BufferVertex l, SA3D.Modeling.Mesh.Buffer.BufferVertex r) -> SA3D.Modeling.Mesh.Buffer.BufferVertex +static SA3D.Modeling.Mesh.Buffer.BufferVertex.operator ==(SA3D.Modeling.Mesh.Buffer.BufferVertex left, SA3D.Modeling.Mesh.Buffer.BufferVertex right) -> bool +static SA3D.Modeling.Mesh.Buffer.BufferVertex.Read(SA3D.Common.IO.EndianStackReader! reader, ref uint address, bool hasNormal) -> SA3D.Modeling.Mesh.Buffer.BufferVertex +static SA3D.Modeling.Mesh.Chunk.ChunkAttach.GetActivePolyChunks(SA3D.Modeling.ObjectData.Node! model) -> System.Collections.Generic.Dictionary! +static SA3D.Modeling.Mesh.Chunk.ChunkAttach.Read(SA3D.Common.IO.EndianStackReader! reader, uint address, SA3D.Modeling.Structs.PointerLUT! lut) -> SA3D.Modeling.Mesh.Chunk.ChunkAttach! +static SA3D.Modeling.Mesh.Chunk.ChunkTypeExtensions.CheckHasAttributes(this SA3D.Modeling.Mesh.Chunk.VertexChunkType type) -> bool +static SA3D.Modeling.Mesh.Chunk.ChunkTypeExtensions.CheckHasDiffuseColor(this SA3D.Modeling.Mesh.Chunk.VertexChunkType type) -> bool +static SA3D.Modeling.Mesh.Chunk.ChunkTypeExtensions.CheckHasNormal(this SA3D.Modeling.Mesh.Chunk.VertexChunkType type) -> bool +static SA3D.Modeling.Mesh.Chunk.ChunkTypeExtensions.CheckHasSpecularColor(this SA3D.Modeling.Mesh.Chunk.VertexChunkType type) -> bool +static SA3D.Modeling.Mesh.Chunk.ChunkTypeExtensions.CheckHasWeights(this SA3D.Modeling.Mesh.Chunk.VertexChunkType type) -> bool +static SA3D.Modeling.Mesh.Chunk.ChunkTypeExtensions.CheckIsNormal32(this SA3D.Modeling.Mesh.Chunk.VertexChunkType type) -> bool +static SA3D.Modeling.Mesh.Chunk.ChunkTypeExtensions.CheckIsVec4(this SA3D.Modeling.Mesh.Chunk.VertexChunkType type) -> bool +static SA3D.Modeling.Mesh.Chunk.ChunkTypeExtensions.CheckStripHasColor(this SA3D.Modeling.Mesh.Chunk.PolyChunkType type) -> bool +static SA3D.Modeling.Mesh.Chunk.ChunkTypeExtensions.GetIntegerSize(this SA3D.Modeling.Mesh.Chunk.VertexChunkType type) -> ushort +static SA3D.Modeling.Mesh.Chunk.PolyChunk.Read(SA3D.Common.IO.EndianStackReader! reader, ref uint address, SA3D.Modeling.Structs.PointerLUT! lut) -> SA3D.Modeling.Mesh.Chunk.PolyChunk! +static SA3D.Modeling.Mesh.Chunk.PolyChunk.ReadArray(SA3D.Common.IO.EndianStackReader! reader, uint address, SA3D.Modeling.Structs.PointerLUT! lut) -> SA3D.Modeling.Mesh.Chunk.PolyChunk?[]! +static SA3D.Modeling.Mesh.Chunk.PolyChunk.WriteArray(SA3D.Common.IO.EndianStackWriter! writer, System.Collections.Generic.IEnumerable! chunks, SA3D.Modeling.Structs.PointerLUT! lut) -> uint +static SA3D.Modeling.Mesh.Chunk.Structs.ChunkCorner.operator !=(SA3D.Modeling.Mesh.Chunk.Structs.ChunkCorner left, SA3D.Modeling.Mesh.Chunk.Structs.ChunkCorner right) -> bool +static SA3D.Modeling.Mesh.Chunk.Structs.ChunkCorner.operator ==(SA3D.Modeling.Mesh.Chunk.Structs.ChunkCorner left, SA3D.Modeling.Mesh.Chunk.Structs.ChunkCorner right) -> bool +static SA3D.Modeling.Mesh.Chunk.Structs.ChunkStrip.Read(SA3D.Common.IO.EndianStackReader! reader, ref uint address, int texcoordCount, bool hdTexcoord, bool hasNormal, bool hasColor, int triangleAttributeCount) -> SA3D.Modeling.Mesh.Chunk.Structs.ChunkStrip +static SA3D.Modeling.Mesh.Chunk.Structs.ChunkVertex.operator !=(SA3D.Modeling.Mesh.Chunk.Structs.ChunkVertex left, SA3D.Modeling.Mesh.Chunk.Structs.ChunkVertex right) -> bool +static SA3D.Modeling.Mesh.Chunk.Structs.ChunkVertex.operator ==(SA3D.Modeling.Mesh.Chunk.Structs.ChunkVertex left, SA3D.Modeling.Mesh.Chunk.Structs.ChunkVertex right) -> bool +static SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeQuad.Read(SA3D.Common.IO.EndianStackReader! reader, ref uint address, int polygonAttributeCount) -> SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeQuad +static SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeStrip.Read(SA3D.Common.IO.EndianStackReader! reader, ref uint address, int polygonAttributeCount) -> SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeStrip +static SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeTriangle.Read(SA3D.Common.IO.EndianStackReader! reader, ref uint address, int polygonAttributeCount) -> SA3D.Modeling.Mesh.Chunk.Structs.ChunkVolumeTriangle +static SA3D.Modeling.Mesh.Chunk.VertexChunk.Read(SA3D.Common.IO.EndianStackReader! reader, ref uint address) -> SA3D.Modeling.Mesh.Chunk.VertexChunk! +static SA3D.Modeling.Mesh.Chunk.VertexChunk.ReadArray(SA3D.Common.IO.EndianStackReader! reader, uint address) -> SA3D.Modeling.Mesh.Chunk.VertexChunk?[]! +static SA3D.Modeling.Mesh.Chunk.VertexChunk.WriteArray(SA3D.Common.IO.EndianStackWriter! writer, System.Collections.Generic.IEnumerable! chunks) -> uint +static SA3D.Modeling.Mesh.Gamecube.Enums.GCEnumExtensions.GetStructSize(SA3D.Modeling.Mesh.Gamecube.Enums.GCStructType structType, SA3D.Modeling.Mesh.Gamecube.Enums.GCDataType dataType) -> uint +static SA3D.Modeling.Mesh.Gamecube.GCAttach.Read(SA3D.Common.IO.EndianStackReader! reader, uint address, SA3D.Modeling.Structs.PointerLUT! lut) -> SA3D.Modeling.Mesh.Gamecube.GCAttach! +static SA3D.Modeling.Mesh.Gamecube.GCCorner.operator !=(SA3D.Modeling.Mesh.Gamecube.GCCorner left, SA3D.Modeling.Mesh.Gamecube.GCCorner right) -> bool +static SA3D.Modeling.Mesh.Gamecube.GCCorner.operator ==(SA3D.Modeling.Mesh.Gamecube.GCCorner left, SA3D.Modeling.Mesh.Gamecube.GCCorner right) -> bool +static SA3D.Modeling.Mesh.Gamecube.GCMesh.Read(SA3D.Common.IO.EndianStackReader! reader, uint address, ref SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat indexFormat) -> SA3D.Modeling.Mesh.Gamecube.GCMesh! +static SA3D.Modeling.Mesh.Gamecube.GCMesh.ReadArray(SA3D.Common.IO.EndianStackReader! reader, uint address, int count) -> SA3D.Modeling.Mesh.Gamecube.GCMesh![]! +static SA3D.Modeling.Mesh.Gamecube.GCMesh.WriteArrayContents(SA3D.Common.IO.EndianStackWriter! writer, SA3D.Modeling.Mesh.Gamecube.GCMesh![]! meshes) -> byte[]! +static SA3D.Modeling.Mesh.Gamecube.GCPolygon.Read(SA3D.Common.IO.EndianStackReader! reader, ref uint address, SA3D.Modeling.Mesh.Gamecube.Enums.GCIndexFormat indexFormat) -> SA3D.Modeling.Mesh.Gamecube.GCPolygon +static SA3D.Modeling.Mesh.Gamecube.GCVertexSet.CreateColor0Set(SA3D.Modeling.Structs.Color[]! colors) -> SA3D.Modeling.Mesh.Gamecube.GCVertexSet! +static SA3D.Modeling.Mesh.Gamecube.GCVertexSet.CreateNormalSet(System.Numerics.Vector3[]! normals) -> SA3D.Modeling.Mesh.Gamecube.GCVertexSet! +static SA3D.Modeling.Mesh.Gamecube.GCVertexSet.CreatePositionSet(System.Numerics.Vector3[]! positions) -> SA3D.Modeling.Mesh.Gamecube.GCVertexSet! +static SA3D.Modeling.Mesh.Gamecube.GCVertexSet.CreateTexcoord0Set(System.Numerics.Vector2[]! texcoords) -> SA3D.Modeling.Mesh.Gamecube.GCVertexSet! +static SA3D.Modeling.Mesh.Gamecube.GCVertexSet.Read(SA3D.Common.IO.EndianStackReader! reader, uint address) -> SA3D.Modeling.Mesh.Gamecube.GCVertexSet! +static SA3D.Modeling.Mesh.Gamecube.GCVertexSet.ReadArray(SA3D.Common.IO.EndianStackReader! reader, uint address) -> SA3D.Modeling.Mesh.Gamecube.GCVertexSet![]! +static SA3D.Modeling.Mesh.Gamecube.GCVertexSet.WriteArray(SA3D.Common.IO.EndianStackWriter! writer, SA3D.Modeling.Mesh.Gamecube.GCVertexSet![]! vertexSets) -> uint +static SA3D.Modeling.Mesh.Gamecube.Parameters.GCLightingParameter.DefaultColorParam -> ushort +static SA3D.Modeling.Mesh.Gamecube.Parameters.GCLightingParameter.DefaultNormalParam -> ushort +static SA3D.Modeling.Mesh.Gamecube.Parameters.GCParameterExtensions.Write(this SA3D.Modeling.Mesh.Gamecube.Parameters.IGCParameter! parameter, SA3D.Common.IO.EndianStackWriter! writer) -> void +static SA3D.Modeling.Mesh.Weighted.WeightedMesh.Create(SA3D.Modeling.Mesh.Weighted.WeightedVertex[]! vertices, SA3D.Modeling.Mesh.Buffer.BufferCorner[]![]! triangleSets, SA3D.Modeling.Mesh.Buffer.BufferMaterial[]! materials, bool hasColors) -> SA3D.Modeling.Mesh.Weighted.WeightedMesh! +static SA3D.Modeling.Mesh.Weighted.WeightedMesh.EnsurePolygonsValid(ref SA3D.Modeling.Mesh.Weighted.WeightedMesh![]! meshes) -> void +static SA3D.Modeling.Mesh.Weighted.WeightedMesh.FromAttach(SA3D.Modeling.Mesh.Attach! attach, SA3D.Modeling.Mesh.Weighted.BufferMode bufferMode) -> SA3D.Modeling.Mesh.Weighted.WeightedMesh! +static SA3D.Modeling.Mesh.Weighted.WeightedMesh.FromModel(SA3D.Modeling.ObjectData.Node! model, SA3D.Modeling.Mesh.Weighted.BufferMode bufferMode) -> SA3D.Modeling.Mesh.Weighted.WeightedMesh![]! +static SA3D.Modeling.Mesh.Weighted.WeightedMesh.MergeAtRoots(SA3D.Modeling.Mesh.Weighted.WeightedMesh![]! meshes) -> SA3D.Modeling.Mesh.Weighted.WeightedMesh![]! +static SA3D.Modeling.Mesh.Weighted.WeightedMesh.MergeWeightedMeshes(System.Collections.Generic.IEnumerable! meshes) -> SA3D.Modeling.Mesh.Weighted.WeightedMesh! +static SA3D.Modeling.Mesh.Weighted.WeightedMesh.ToModel(SA3D.Modeling.ObjectData.Node! model, SA3D.Modeling.Mesh.Weighted.WeightedMesh![]! meshes, SA3D.Modeling.Mesh.AttachFormat format, bool optimize, bool ignoreWeights = false) -> void +static SA3D.Modeling.Mesh.Weighted.WeightedVertex.operator !=(SA3D.Modeling.Mesh.Weighted.WeightedVertex left, SA3D.Modeling.Mesh.Weighted.WeightedVertex right) -> bool +static SA3D.Modeling.Mesh.Weighted.WeightedVertex.operator ==(SA3D.Modeling.Mesh.Weighted.WeightedVertex left, SA3D.Modeling.Mesh.Weighted.WeightedVertex right) -> bool +static SA3D.Modeling.ObjectData.Enums.EnumExtensions.CheckIsCollision(this SA3D.Modeling.ObjectData.Enums.SurfaceAttributes attributes) -> bool +static SA3D.Modeling.ObjectData.Enums.EnumExtensions.CheckIsVisual(this SA3D.Modeling.ObjectData.Enums.SurfaceAttributes attributes) -> bool +static SA3D.Modeling.ObjectData.Enums.EnumExtensions.ToSA1(this SA3D.Modeling.ObjectData.Enums.SurfaceAttributes attributes) -> SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes +static SA3D.Modeling.ObjectData.Enums.EnumExtensions.ToSA2(this SA3D.Modeling.ObjectData.Enums.SurfaceAttributes attributes) -> SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes +static SA3D.Modeling.ObjectData.Enums.EnumExtensions.ToUniversal(this SA3D.Modeling.ObjectData.Enums.SA1SurfaceAttributes attributes) -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +static SA3D.Modeling.ObjectData.Enums.EnumExtensions.ToUniversal(this SA3D.Modeling.ObjectData.Enums.SA2SurfaceAttributes attributes) -> SA3D.Modeling.ObjectData.Enums.SurfaceAttributes +static SA3D.Modeling.ObjectData.Events.TransformSet.FromNode(SA3D.Modeling.ObjectData.Node! node) -> SA3D.Modeling.ObjectData.Events.TransformSet +static SA3D.Modeling.ObjectData.LandEntry.CreateWithAttach(SA3D.Modeling.Mesh.Attach! attach, SA3D.Modeling.ObjectData.Enums.SurfaceAttributes surfaceAttributes) -> SA3D.Modeling.ObjectData.LandEntry! +static SA3D.Modeling.ObjectData.LandEntry.Read(SA3D.Common.IO.EndianStackReader! reader, uint address, SA3D.Modeling.ObjectData.Enums.ModelFormat modelFormat, SA3D.Modeling.ObjectData.Enums.ModelFormat tableFormat, SA3D.Modeling.Structs.PointerLUT! lut) -> SA3D.Modeling.ObjectData.LandEntry! +static SA3D.Modeling.ObjectData.LandTable.Read(SA3D.Common.IO.EndianStackReader! data, uint address, SA3D.Modeling.ObjectData.Enums.ModelFormat format, SA3D.Modeling.Structs.PointerLUT! lut) -> SA3D.Modeling.ObjectData.LandTable! +static SA3D.Modeling.ObjectData.Node.Read(SA3D.Common.IO.EndianStackReader! reader, uint address, SA3D.Modeling.ObjectData.Enums.ModelFormat format, SA3D.Modeling.Structs.PointerLUT! lut) -> SA3D.Modeling.ObjectData.Node! +static SA3D.Modeling.Strippify.TriangleStrippifier.JoinedStripEnumerator(T[]![]! strips, bool[]? reversed) -> System.Collections.Generic.IEnumerable! +static SA3D.Modeling.Strippify.TriangleStrippifier.JoinStrips(T[]![]! strips, bool[]? reversed) -> T[]! +static SA3D.Modeling.Structs.Bounds.FromPoints(System.Collections.Generic.IEnumerable! points) -> SA3D.Modeling.Structs.Bounds +static SA3D.Modeling.Structs.Bounds.Read(SA3D.Common.IO.EndianStackReader! reader, ref uint address) -> SA3D.Modeling.Structs.Bounds +static SA3D.Modeling.Structs.Bounds.Read(SA3D.Common.IO.EndianStackReader! reader, uint address) -> SA3D.Modeling.Structs.Bounds +static SA3D.Modeling.Structs.Color.Distance(SA3D.Modeling.Structs.Color from, SA3D.Modeling.Structs.Color to) -> float +static SA3D.Modeling.Structs.Color.Lerp(SA3D.Modeling.Structs.Color from, SA3D.Modeling.Structs.Color to, float t) -> SA3D.Modeling.Structs.Color +static SA3D.Modeling.Structs.Color.operator !=(SA3D.Modeling.Structs.Color l, SA3D.Modeling.Structs.Color r) -> bool +static SA3D.Modeling.Structs.Color.operator *(float l, SA3D.Modeling.Structs.Color r) -> SA3D.Modeling.Structs.Color +static SA3D.Modeling.Structs.Color.operator *(SA3D.Modeling.Structs.Color l, float r) -> SA3D.Modeling.Structs.Color +static SA3D.Modeling.Structs.Color.operator +(SA3D.Modeling.Structs.Color l, SA3D.Modeling.Structs.Color r) -> SA3D.Modeling.Structs.Color +static SA3D.Modeling.Structs.Color.operator -(SA3D.Modeling.Structs.Color l, SA3D.Modeling.Structs.Color r) -> SA3D.Modeling.Structs.Color +static SA3D.Modeling.Structs.Color.operator /(SA3D.Modeling.Structs.Color l, float r) -> SA3D.Modeling.Structs.Color +static SA3D.Modeling.Structs.Color.operator ==(SA3D.Modeling.Structs.Color l, SA3D.Modeling.Structs.Color r) -> bool +static SA3D.Modeling.Structs.ColorIOTypeExtensions.GetByteSize(this SA3D.Modeling.Structs.ColorIOType type) -> int +static SA3D.Modeling.Structs.DebugStringExtensions.DebugString(this float val) -> string! +static SA3D.Modeling.Structs.DebugStringExtensions.DebugString(this System.Numerics.Quaternion quat) -> string! +static SA3D.Modeling.Structs.DebugStringExtensions.DebugString(this System.Numerics.Vector2 vector) -> string! +static SA3D.Modeling.Structs.DebugStringExtensions.DebugString(this System.Numerics.Vector3 vector) -> string! +static SA3D.Modeling.Structs.EndianIOExtensions.ReadArray(this SA3D.Common.IO.EndianStackReader! reader, uint address, uint count, SA3D.Modeling.Structs.EndianIOExtensions.ReadValueAdvanceDelegate! read) -> T[]! +static SA3D.Modeling.Structs.EndianIOExtensions.ReadArray(this SA3D.Common.IO.EndianStackReader! reader, uint address, uint count, uint elementByteSize, SA3D.Modeling.Structs.EndianIOExtensions.ReadValueDelegate! read) -> T[]! +static SA3D.Modeling.Structs.EndianIOExtensions.ReadArrayWithLUT(this SA3D.Common.IO.EndianStackReader! reader, uint address, uint count, SA3D.Modeling.Structs.EndianIOExtensions.ReadValueAdvanceDelegate! read, SA3D.Common.Lookup.BaseLUT! lut) -> T[]! +static SA3D.Modeling.Structs.EndianIOExtensions.ReadArrayWithLUT(this SA3D.Common.IO.EndianStackReader! reader, uint address, uint count, uint elementByteSize, SA3D.Modeling.Structs.EndianIOExtensions.ReadValueDelegate! read, SA3D.Common.Lookup.BaseLUT! lut) -> T[]! +static SA3D.Modeling.Structs.EndianIOExtensions.ReadColor(this SA3D.Common.IO.EndianStackReader! reader, ref uint address, SA3D.Modeling.Structs.ColorIOType type) -> SA3D.Modeling.Structs.Color +static SA3D.Modeling.Structs.EndianIOExtensions.ReadColor(this SA3D.Common.IO.EndianStackReader! reader, uint address, SA3D.Modeling.Structs.ColorIOType type) -> SA3D.Modeling.Structs.Color +static SA3D.Modeling.Structs.EndianIOExtensions.ReadLabeledArray(this SA3D.Common.IO.EndianStackReader! reader, uint address, uint count, SA3D.Modeling.Structs.EndianIOExtensions.ReadValueAdvanceDelegate! read, string! genPrefix, SA3D.Common.Lookup.BaseLUT! lut) -> SA3D.Common.Lookup.LabeledArray! +static SA3D.Modeling.Structs.EndianIOExtensions.ReadLabeledArray(this SA3D.Common.IO.EndianStackReader! reader, uint address, uint count, uint elementByteSize, SA3D.Modeling.Structs.EndianIOExtensions.ReadValueDelegate! read, string! genPrefix, SA3D.Common.Lookup.BaseLUT! lut) -> SA3D.Common.Lookup.LabeledArray! +static SA3D.Modeling.Structs.EndianIOExtensions.ReadLabeledReadOnlyArray(this SA3D.Common.IO.EndianStackReader! reader, uint address, uint count, SA3D.Modeling.Structs.EndianIOExtensions.ReadValueAdvanceDelegate! read, string! genPrefix, SA3D.Common.Lookup.BaseLUT! lut) -> SA3D.Common.Lookup.LabeledReadOnlyArray! +static SA3D.Modeling.Structs.EndianIOExtensions.ReadLabeledReadOnlyArray(this SA3D.Common.IO.EndianStackReader! reader, uint address, uint count, uint elementByteSize, SA3D.Modeling.Structs.EndianIOExtensions.ReadValueDelegate! read, string! genPrefix, SA3D.Common.Lookup.BaseLUT! lut) -> SA3D.Common.Lookup.LabeledReadOnlyArray! +static SA3D.Modeling.Structs.EndianIOExtensions.ReadQuaternion(this SA3D.Common.IO.EndianStackReader! reader, ref uint address) -> System.Numerics.Quaternion +static SA3D.Modeling.Structs.EndianIOExtensions.ReadQuaternion(this SA3D.Common.IO.EndianStackReader! reader, uint address) -> System.Numerics.Quaternion +static SA3D.Modeling.Structs.EndianIOExtensions.ReadVector2(this SA3D.Common.IO.EndianStackReader! reader, ref uint address, SA3D.Modeling.Structs.FloatIOType type = SA3D.Modeling.Structs.FloatIOType.Float) -> System.Numerics.Vector2 +static SA3D.Modeling.Structs.EndianIOExtensions.ReadVector2(this SA3D.Common.IO.EndianStackReader! reader, uint address, SA3D.Modeling.Structs.FloatIOType type = SA3D.Modeling.Structs.FloatIOType.Float) -> System.Numerics.Vector2 +static SA3D.Modeling.Structs.EndianIOExtensions.ReadVector3(this SA3D.Common.IO.EndianStackReader! reader, ref uint address, SA3D.Modeling.Structs.FloatIOType type = SA3D.Modeling.Structs.FloatIOType.Float) -> System.Numerics.Vector3 +static SA3D.Modeling.Structs.EndianIOExtensions.ReadVector3(this SA3D.Common.IO.EndianStackReader! reader, uint address, SA3D.Modeling.Structs.FloatIOType type = SA3D.Modeling.Structs.FloatIOType.Float) -> System.Numerics.Vector3 +static SA3D.Modeling.Structs.EndianIOExtensions.WriteCollection(this SA3D.Common.IO.EndianStackWriter! writer, System.Collections.Generic.ICollection! values, SA3D.Modeling.Structs.EndianIOExtensions.WriteValueDelegate! write) -> uint +static SA3D.Modeling.Structs.EndianIOExtensions.WriteCollection(this SA3D.Common.IO.EndianStackWriter! writer, System.Collections.Generic.ICollection! values, SA3D.Modeling.Structs.EndianIOExtensions.WriteValueDelegate! write, SA3D.Modeling.Structs.EndianIOExtensions.WriteValueDelegate? preWrite) -> uint +static SA3D.Modeling.Structs.EndianIOExtensions.WriteCollectionWithLUT(this SA3D.Common.IO.EndianStackWriter! writer, System.Collections.Generic.ICollection? values, SA3D.Modeling.Structs.EndianIOExtensions.WriteValueDelegate! write, SA3D.Common.Lookup.BaseLUT! lut) -> uint +static SA3D.Modeling.Structs.EndianIOExtensions.WriteCollectionWithLUT(this SA3D.Common.IO.EndianStackWriter! writer, System.Collections.Generic.ICollection? values, SA3D.Modeling.Structs.EndianIOExtensions.WriteValueDelegate! write, SA3D.Modeling.Structs.EndianIOExtensions.WriteValueDelegate? preWrite, SA3D.Common.Lookup.BaseLUT! lut) -> uint +static SA3D.Modeling.Structs.EndianIOExtensions.WriteColor(this SA3D.Common.IO.EndianStackWriter! writer, SA3D.Modeling.Structs.Color color, SA3D.Modeling.Structs.ColorIOType type) -> void +static SA3D.Modeling.Structs.EndianIOExtensions.WriteQuaternion(this SA3D.Common.IO.EndianStackWriter! writer, System.Numerics.Quaternion quaternion) -> void +static SA3D.Modeling.Structs.EndianIOExtensions.WriteVector2(this SA3D.Common.IO.EndianStackWriter! writer, System.Numerics.Vector2 vector2, SA3D.Modeling.Structs.FloatIOType type = SA3D.Modeling.Structs.FloatIOType.Float) -> void +static SA3D.Modeling.Structs.EndianIOExtensions.WriteVector3(this SA3D.Common.IO.EndianStackWriter! writer, System.Numerics.Vector3 vector3, SA3D.Modeling.Structs.FloatIOType type = SA3D.Modeling.Structs.FloatIOType.Float) -> void +static SA3D.Modeling.Structs.FloatIOTypeExtensions.GetByteSize(this SA3D.Modeling.Structs.FloatIOType type) -> int +static SA3D.Modeling.Structs.FloatIOTypeExtensions.GetPrinter(this SA3D.Modeling.Structs.FloatIOType type) -> System.Func! +static SA3D.Modeling.Structs.FloatIOTypeExtensions.GetReader(this SA3D.Modeling.Structs.FloatIOType type) -> System.Func! +static SA3D.Modeling.Structs.FloatIOTypeExtensions.GetWriter(this SA3D.Modeling.Structs.FloatIOType type) -> System.Action! +static SA3D.Modeling.Structs.MatrixUtilities.CreateRotationMatrix(System.Numerics.Vector3 rotation, bool ZYX) -> System.Numerics.Matrix4x4 +static SA3D.Modeling.Structs.MatrixUtilities.CreateTransformMatrix(System.Numerics.Vector3 position, System.Numerics.Quaternion rotation, System.Numerics.Vector3 scale) -> System.Numerics.Matrix4x4 +static SA3D.Modeling.Structs.MatrixUtilities.CreateTransformMatrix(System.Numerics.Vector3 position, System.Numerics.Vector3 rotation, System.Numerics.Vector3 scale, bool rotateZYX) -> System.Numerics.Matrix4x4 +static SA3D.Modeling.Structs.MatrixUtilities.GetNormalMatrix(this System.Numerics.Matrix4x4 matrix) -> System.Numerics.Matrix4x4 +static SA3D.Modeling.Structs.MatrixUtilities.ToCompatibleEuler(System.Numerics.Matrix4x4 matrix, System.Numerics.Vector3 previous, bool rotateZYX) -> System.Numerics.Vector3 +static SA3D.Modeling.Structs.MatrixUtilities.ToEuler(System.Numerics.Matrix4x4 matrix, bool rotateZYX) -> System.Numerics.Vector3 +static SA3D.Modeling.Structs.QuaternionUtilities.EulerToQuaternion(this System.Numerics.Vector3 rotation, bool rotateZYX) -> System.Numerics.Quaternion +static SA3D.Modeling.Structs.QuaternionUtilities.QuaternionToCompatibleEuler(this System.Numerics.Quaternion rotation, System.Numerics.Vector3 previous, bool rotateZYX) -> System.Numerics.Vector3 +static SA3D.Modeling.Structs.QuaternionUtilities.QuaternionToEuler(this System.Numerics.Quaternion quaternion, bool rotateZYX) -> System.Numerics.Vector3 +static SA3D.Modeling.Structs.QuaternionUtilities.RealLerp(System.Numerics.Quaternion from, System.Numerics.Quaternion to, float t) -> System.Numerics.Quaternion +static SA3D.Modeling.Structs.VectorUtilities.CalculateAverage(System.Numerics.Vector3[]! points) -> System.Numerics.Vector3 +static SA3D.Modeling.Structs.VectorUtilities.CalculateCenter(System.Collections.Generic.IEnumerable! points) -> System.Numerics.Vector3 +static SA3D.Modeling.Structs.VectorUtilities.GreatestValue(this System.Numerics.Vector3 vector) -> float +static SA3D.Modeling.Structs.VectorUtilities.NormalToXZAngles(this System.Numerics.Vector3 normal) -> System.Numerics.Vector3 +static SA3D.Modeling.Structs.VectorUtilities.XZAnglesToNormal(this System.Numerics.Vector3 rotation) -> System.Numerics.Vector3 +virtual SA3D.Modeling.Mesh.Attach.CanWrite(SA3D.Modeling.ObjectData.Enums.ModelFormat format) -> bool +virtual SA3D.Modeling.Mesh.Attach.CheckHasWeights() -> bool +virtual SA3D.Modeling.Mesh.Attach.Clone() -> SA3D.Modeling.Mesh.Attach! +virtual SA3D.Modeling.Mesh.Attach.Format.get -> SA3D.Modeling.Mesh.AttachFormat +virtual SA3D.Modeling.Mesh.Attach.RecalculateBounds() -> void +virtual SA3D.Modeling.Mesh.Attach.WriteInternal(SA3D.Common.IO.EndianStackWriter! writer, SA3D.Modeling.ObjectData.Enums.ModelFormat format, SA3D.Modeling.Structs.PointerLUT! lut) -> uint +virtual SA3D.Modeling.Mesh.Chunk.PolyChunk.Clone() -> SA3D.Modeling.Mesh.Chunk.PolyChunk! \ No newline at end of file diff --git a/src/SA3D.Modeling/SA3D.Modeling.csproj b/src/SA3D.Modeling/SA3D.Modeling.csproj new file mode 100644 index 0000000..0de83e6 --- /dev/null +++ b/src/SA3D.Modeling/SA3D.Modeling.csproj @@ -0,0 +1,30 @@ + + + + + + net7.0 + SA3D.Modeling + + + + SA3D Model and Animation Library + SA3D Model and Animation Library + Reading, writing and processing library for Sonic Adventure models + 1.0.0 + https://github.com/X-Hax/SA3D.Modeling + https://github.com/X-Hax/SA3D.Modeling + SA3D.Modeling + README.md + + + + + + + + + + + + diff --git a/src/SA3D.Modeling/Strippify/Edge.cs b/src/SA3D.Modeling/Strippify/Edge.cs new file mode 100644 index 0000000..206e4bf --- /dev/null +++ b/src/SA3D.Modeling/Strippify/Edge.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; + +namespace SA3D.Modeling.Strippify +{ + /// + /// An edge between two + /// + internal class Edge + { + /// + /// The two vertices that the edge connects + /// + public Vertex[] Vertices { get; } + + /// + /// Triangles that the edge is part of + /// + public List Triangles { get; } + + /// + /// Creates a new edge between two vertices + /// + public Edge(Vertex v1, Vertex v2) + { + Vertices = new Vertex[] { v1, v2 }; + Triangles = new List(); + } + + /// + /// Adds a triangle to the edge + /// + public void AddTriangle(Triangle tri) + { + foreach(Triangle t in Triangles) + { + t.Neighbours.Add(tri); + tri.Neighbours.Add(t); + } + + Triangles.Add(tri); + } + } +} diff --git a/src/SA3D.Modeling/Strippify/Mesh.cs b/src/SA3D.Modeling/Strippify/Mesh.cs new file mode 100644 index 0000000..d93df71 --- /dev/null +++ b/src/SA3D.Modeling/Strippify/Mesh.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using System.Linq; + +namespace SA3D.Modeling.Strippify +{ + /// + /// A mesh made of connections between triangles, egdes and vertices + /// + internal class Mesh + { + /// + /// All triangles in the mesh + /// + public Triangle[] Triangles { get; } + + /// + /// all vertices in the mesh + /// + public Vertex[] Vertices { get; } + + /// + /// Creates a new mesh from a triangle list + /// + /// + /// + public Mesh(int[] triangleList, bool raiseTopoError) + { + int vertCount = triangleList.Max(x => x) + 1; + + Vertices = new Vertex[vertCount]; + + for(int i = 0; i < vertCount; i++) + { + Vertices[i] = new Vertex(i); + } + + List edges = new(); + List triangles = new(); + + for(int i = 0; i < triangleList.Length; i += 3) + { + if(triangleList[i] == triangleList[i + 1] + || triangleList[i + 1] == triangleList[i + 2] + || triangleList[i + 2] == triangleList[i]) + { + continue; + } + + triangles.Add(new( + new Vertex[] { + Vertices[triangleList[i]], + Vertices[triangleList[i+1]], + Vertices[triangleList[i+2]] + }, + edges, raiseTopoError)); + } + + int triEdgeCount = edges.Count(x => x.Triangles.Count > 2); + //if (triEdgeCount > 0) + //{ + // Console.WriteLine("Tripple edges: " + triEdgeCount); + //} + + Triangles = triangles.ToArray(); + } + } +} diff --git a/src/SA3D.Modeling/Strippify/TopologyException.cs b/src/SA3D.Modeling/Strippify/TopologyException.cs new file mode 100644 index 0000000..06cbddd --- /dev/null +++ b/src/SA3D.Modeling/Strippify/TopologyException.cs @@ -0,0 +1,16 @@ +using System; + +namespace SA3D.Modeling.Strippify +{ + /// + /// Gets raised when certain 3D topology rules are ignored and/or not processable + /// + public class TopologyException : Exception + { + /// + /// Creates a new topology exception. + /// + /// The message to pass along. + public TopologyException(string msg) : base(msg) { } + } +} diff --git a/src/SA3D.Modeling/Strippify/Triangle.cs b/src/SA3D.Modeling/Strippify/Triangle.cs new file mode 100644 index 0000000..c41b53e --- /dev/null +++ b/src/SA3D.Modeling/Strippify/Triangle.cs @@ -0,0 +1,262 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; + +namespace SA3D.Modeling.Strippify +{ + /// + /// Triangle between three vertices + /// + internal class Triangle + { + /// + /// The three vertices that the triangle consists of + /// + public Vertex[] Vertices { get; } + + /// + /// The three edges that the triangle consists of + /// + public Edge[] Edges { get; } + + /// + /// The triangles that the triangle borders through the edges + /// + public List Neighbours { get; } + + /// + /// Whether the triangle was already used in the strip + /// + public bool Used { get; set; } + + /// + /// Neighbouring triangles that havent been used yet + /// + public Triangle[] AvailableNeighbours + => Neighbours.Where(x => !x.Used).ToArray(); + + public int AvailableNeighbourCount + => Neighbours.Count(x => !x.Used); + + /// + /// Creates a new triangle from an index and three vertices, and adds newly created edges to an output list + /// + /// Vertices (must be 3) + /// Edge list of the entire mesh, to add new edges to + /// Whether to raise a topo error when encountered. + public Triangle(Vertex[] vertices, List outEdges, bool raiseTopoError) + { + Vertices = vertices; + Edges = new Edge[3]; + Neighbours = new List(); + Used = false; + + Vertex prevVert = vertices[2]; + int i = 0; + foreach(Vertex v in vertices) + { + v.Triangles.Add(this); + Edges[i] = AddEdge(v, prevVert, outEdges, raiseTopoError); + prevVert = v; + + i++; + } + + } + + /// + /// Creates a new edge for the triangle. + /// + /// + /// + /// + /// + /// + private Edge AddEdge(Vertex v1, Vertex v2, List edges, bool raiseTopoError) + { + if(!v1.IsConnectedWith(v2, out Edge? e)) + { + e = v1.Connect(v2); + edges.Add(e); + } + else if(raiseTopoError && e.Triangles.Count > 1) + { + throw new TopologyException("Some edge has more than 2 faces! Can't strippify!"); + } + + e.AddTriangle(this); + + return e; + } + + /// + /// Whether a vertex is contained in the triangle + /// + /// + /// + public bool HasVertex(Vertex? vert) + { + return Vertices.Contains(vert); + } + + /// + /// Gets the third vertex in a triangle.
+ /// If any of the two vertices are not part of the triangle, then the method returns null + ///
+ /// + /// + /// + public Vertex? GetThirdVertex(Vertex v1, Vertex v2) + { + if(Vertices.Contains(v1) && Vertices.Contains(v2)) + { + foreach(Vertex v in Vertices) + { + if(v != v1 && v != v2) + { + return v; + } + } + } + + return null; + } + + /// + /// Reurns a shared edge two triangles + /// + /// + /// + public Edge? GetSharedEdge(Triangle other) + { + foreach(Edge e in Edges) + { + if(other.Edges.Contains(e)) + { + return e; + } + } + + return null; + } + + + /// + /// Gets the next triangle in a strip sequence (with swapping) + /// + /// + /// + public Triangle? NextTriangleS() + { + Triangle[] trisToUse = AvailableNeighbours; + + if(trisToUse.Length == 0) + { + return null; + } + + if(trisToUse.Length == 1) + { + return trisToUse[0]; + } + + int[] weights = new int[trisToUse.Length]; + int[] vConnection = new int[trisToUse.Length]; + int biggestConnection = 0; + + for(int i = 0; i < trisToUse.Length; i++) + { + Triangle t = trisToUse[i]; + + if(t.AvailableNeighbourCount == 0) + { + return t; + } + + weights[i] = t.AvailableNeighbourCount; + Vertex[] eVerts = t.GetSharedEdge(this)?.Vertices ?? throw new NullReferenceException("No shared edge found"); + vConnection[i] = eVerts[0].AvailableTris + eVerts[1].AvailableTris; + + if(vConnection[i] > biggestConnection) + { + biggestConnection = vConnection[i]; + } + } + + for(int i = 0; i < vConnection.Length; i++) + { + weights[i] += vConnection[i] < biggestConnection ? -1 : 1; + } + + int index = 0; + for(int j = 1; j < trisToUse.Length; j++) + { + if(weights[j] < weights[index]) + { + index = j; + } + } + + return trisToUse[index]; + } + + /// + /// Gets the next triangle in a strip sequence (no swapping) + /// + /// + /// + /// + public Triangle? NextTriangle(Vertex v1, Vertex v2) + { + if(!v1.IsConnectedWith(v2, out Edge? e)) + { + throw new InvalidOperationException("Vertices are not connected"); + } + + return e.Triangles.FirstOrDefault(x => x != this && !x.Used); + } + + + /// + /// Whether the culling directions between two triangles differ + /// + /// + /// + public bool HasBrokenCullFlow(Triangle other) + { + int t = 0; + foreach(Vertex v in Vertices) + { + if(other.Vertices.Contains(v)) + { + int tt = Array.IndexOf(other.Vertices, v); + return Vertices[(t + 1) % 3] == other.Vertices[(tt + 1) % 3]; + } + + t++; + } + + return false; + } + + public Vertex? GetNextVertexByFlow(Vertex vert) + { + // get the index + for(int i = 0; i < 3; i++) + { + if(Vertices[i] == vert) + { + return Vertices[(i + 1) % 3]; + } + } + + return null; + } + + public override string ToString() + { + return $"{Vertices[0].Index}-{Vertices[1].Index}-{Vertices[2].Index}"; + } + } +} diff --git a/src/SA3D.Modeling/Strippify/TriangleStrippifier.cs b/src/SA3D.Modeling/Strippify/TriangleStrippifier.cs new file mode 100644 index 0000000..43d7a11 --- /dev/null +++ b/src/SA3D.Modeling/Strippify/TriangleStrippifier.cs @@ -0,0 +1,451 @@ +using SA3D.Common; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace SA3D.Modeling.Strippify +{ + /// + /// Calculates triangle strips from a triangle list. + /// + public class TriangleStrippifier + { + /// + /// Globally available and used triangle strippifier. + ///
Used for every natively used strippifier in this library. + ///
Does not raise Topo Error by default. + ///
+ public static readonly TriangleStrippifier Global = new(false); + + /// + /// Whether to raise an exception when a topology anomaly occurs. + /// + public bool RaiseTopoError { get; set; } + + /// + /// Creates a new triangle strippifier. + /// + /// + public TriangleStrippifier(bool raiseTopoError = false) + { + RaiseTopoError = raiseTopoError; + } + + + /// + /// Strippifies a list of triangle elements and returns a 2D array, where each array is a single strip. + /// + /// Type of triangle elements. + /// The triangle elements to strippify. + /// Triangle strips. + /// + public T[][] Strippify(T[] triangleList) where T : IEquatable + { + int[][] strips = Strippify(triangleList, out DistinctMap distinctMap); + + T[][] result = new T[strips.Length][]; + + for(int i = 0; i < strips.Length; i++) + { + int[] strip = strips[i]; + T[] resultStrip = new T[strip.Length]; + + for(int j = 0; j < strip.Length; j++) + { + resultStrip[j] = distinctMap.Values[strip[j]]; + } + + result[i] = resultStrip; + } + + return result; + } + + /// + /// Strippifies a list of triangle elements and returns a 2D array, where each array is a single strip. + ///
Degenerate elements at the beginning of strips are removed and instead marked in the reversedStrips array. + ///
+ /// Type of triangle elements + /// The triangle elements to strippify. + /// An array indicating which strips have a reversed forward direction. + /// Triangle strips. + public T[][] StrippifyNoDegen(T[] triangleList, out bool[] reversedStrips) where T : IEquatable + { + int[][] strips = Strippify(triangleList, out DistinctMap distinctMap); + + T[][] result = new T[strips.Length][]; + reversedStrips = new bool[strips.Length]; + + for(int i = 0; i < strips.Length; i++) + { + int[] strip = strips[i]; + + bool reversed = strip[0] == strip[1]; + reversedStrips[i] = reversed; + + int src = reversed ? 1 : 0; + + T[] resultStrip = new T[strip.Length - src]; + + for(int dst = 0; src < strip.Length; src++, dst++) + { + resultStrip[dst] = distinctMap.Values[strip[src]]; + } + + result[i] = resultStrip; + } + + return result; + } + + /// + /// Strippifies a list of triangle elements and returns a 2D array of indices to the distinct values, where each array is a single strip. + /// + /// Type of triangle elements + /// The triangle elements to strippify. + /// The index mapping with distinct values. + /// Triangle strips consisting of indices to distinct triangle elements. + /// + public int[][] Strippify(T[] triangleList, out DistinctMap distinctMap) where T : IEquatable + { + if(triangleList.Length % 3 != 0) + { + throw new ArgumentException("Triangle list size needs to be a multiple of 3!", nameof(triangleList)); + } + + int[] triangleIndices; + + if(!DistinctMap.TryCreateDistinctMap(triangleList, out distinctMap)) + { + triangleIndices = new int[triangleList.Length]; + + for(int i = 0; i < triangleIndices.Length; i++) + { + triangleIndices[i] = i; + } + } + else + { + triangleIndices = distinctMap.Map!; + } + + return Strippify(triangleIndices); + } + + /// + /// Strippifies a list of triangle indices and returns a 2D array, where each array is a single strip. + /// + /// Input triangle list + /// Triangle strips. + public int[][] Strippify(int[] triangleList) + { + /* based on the paper written by David Kronmann: + https://pdfs.semanticscholar.org/9749/331d92f865282c3f5a19b73b25c4f0ac02bc.pdf + The code has been written and modified by Justin113D, + and also optimized the strips by handling the priority list different */ + + if(triangleList.Length % 3 != 0) + { + throw new ArgumentException("Triangle list size needs to be a multiple of 3!", nameof(triangleList)); + } + + Mesh mesh = new(triangleList, RaiseTopoError); // reading the index data into a virtual mesh + int written = 0; // amount of written triangles + List strips = new(); // the result list + + int triCount = mesh.Triangles.Length; + + // creates a strip from a triangle with no (free) neighbours + void AddZTriangle(Triangle tri) + { + Vertex[] verts = tri.Vertices; + strips.Add(new int[] { verts[0].Index, verts[1].Index, verts[2].Index }); + written++; + tri.Used = true; + } + + Triangle? getFirstTri() + { + Triangle? resultTri = null; + + foreach(Triangle t in mesh.Triangles) + { + if(t.Used) + { + continue; + } + + int tnCount = t.AvailableNeighbours.Length; + if(tnCount == 0) + { + AddZTriangle(t); + continue; + } + + if(resultTri == null) + { + resultTri = t; + continue; + } + + if(tnCount < resultTri.AvailableNeighbours.Length) + { + if(tnCount == 1) + { + return t; + } + + resultTri = t; + } + } + + return resultTri; + } + + Triangle? firstTri = getFirstTri(); + + // as long as some triangles remain to be written, keep the loop running + while(written != triCount) + { + // when looking for the first triangle, we also filter out some + // single triangles, which means that it will alter the written + // count. thats why we have to call it before the loop starts + // and before the end of the loop, instead of once at the start + + // the first thing we gotta do is determine the + // first (max) 3 triangles to write + Triangle currentTri = firstTri ?? throw new NullReferenceException("First triangle is null!"); + currentTri.Used = true; + + Triangle newTri = currentTri.NextTriangleS() ?? throw new TopologyException("First tri somehow has no usable neighbours"); + + // If the two triangles have a broken cull flow, then dont continue + // the strip (well ok, there is a chance it could continue on + // another tri, but its not worth looking for such a triangle) + if(currentTri.HasBrokenCullFlow(newTri)) + { + AddZTriangle(currentTri); + // since we are wrapping back around, we have + // to set the first tri too + firstTri = getFirstTri(); + continue; + } + + newTri.Used = true; // confirming that we are using it now + + // get the starting vert + // (the one which is not connected with the new tri) + Vertex[] sharedVerts = currentTri.GetSharedEdge(newTri)?.Vertices ?? throw new TopologyException("Triangles are somehow not connected"); + Vertex prevVert = currentTri.GetThirdVertex(sharedVerts[0], sharedVerts[1]) ?? throw new TopologyException("Triangles are somehow not connected"); + + // get the vertex which wouldnt be connected to + // the tri afterwards, to prevent swapping + Triangle? secNewTri = newTri.NextTriangleS(); + Vertex currentVert; + Vertex nextVert; + + // if the third tri isnt valid, just end the strip; + // now you might be thinking: + // "but justin, what if the strip can be reversed and continued on the other end?" + // good point, but! This only occurs if the second triangle has no free neighbours. + // this can only happen if the first triangle also has no other free neighbours, + // otherwise the second triangle would have been chosen as the first one. + // Thus we have two triangles in the same strip + if(secNewTri == null) + { + // gotta get the correct second vertex to keep the vertex flow + currentVert = currentTri.GetNextVertexByFlow(prevVert) ?? throw new TopologyException("Vertex somehow not part of the vertices"); + nextVert = currentVert == sharedVerts[0] ? sharedVerts[1] : sharedVerts[0]; + + int thirdVertex = newTri.GetThirdVertex(currentVert, nextVert)?.Index ?? throw new TopologyException("Triangles are somehow not connected"); + + strips.Add(new int[] { prevVert.Index, currentVert.Index, nextVert.Index, thirdVertex }); + written += 2; + + // since we are wrapping back around, + // we have to set the first tri too + firstTri = getFirstTri(); + continue; + } + else if(secNewTri.HasVertex(sharedVerts[0])) + { + currentVert = sharedVerts[1]; + nextVert = sharedVerts[0]; + } + else + { + currentVert = sharedVerts[0]; + nextVert = sharedVerts[1]; + } + + // initializing the strip base + int[] strip = StripLoop( + firstTri, ref written, + newTri, secNewTri, + prevVert, currentVert, nextVert); + + strips.Add(strip); + firstTri = getFirstTri(); + } + + return strips.ToArray(); + } + + + private int[] StripLoop(Triangle firstTriangle, ref int written, Triangle tri2, Triangle tri3, Vertex vert1, Vertex vert2, Vertex vert3) + { + List strip = new() { vert1.Index, vert2.Index, vert3.Index }; + written++; + + // shift verts two forward + Vertex prevVert = vert3; + Vertex currentVert = tri2.GetThirdVertex(vert2, vert3) ?? throw new TopologyException("Third vertex somehow null"); + + // shift triangles one forward + Triangle currentTri = tri2; + Triangle? newTri = currentTri.HasBrokenCullFlow(tri3) ? null : tri3; + + // creating the strip + bool reachedEnd = false; + bool reversedList = false; + while(!reachedEnd) + { + // writing the next index + strip.Add(currentVert.Index); + written++; + + // ending or reversing the loop when the current + // tri is None (end of the strip) + if(newTri == null) + { + if(!reversedList && firstTriangle.AvailableNeighbours.Length > 0) + { + reversedList = true; + + prevVert = vert2; + currentVert = vert1; + + newTri = firstTriangle.NextTriangle(prevVert, currentVert); + if(newTri == null) + { + reachedEnd = true; + continue; + } + + strip.Reverse(); + + (currentTri, firstTriangle) = (firstTriangle, currentTri); + } + else + { + reachedEnd = true; + continue; + } + } + + // getting the next vertex to write + Vertex? nextVert = newTri.GetThirdVertex(prevVert, currentVert); + + if(nextVert == null) + { + reachedEnd = true; + continue; + } + + prevVert = currentVert; + currentVert = nextVert; + + Triangle oldTri = currentTri; + currentTri = newTri; + currentTri.Used = true; + + newTri = oldTri.HasBrokenCullFlow(currentTri) + ? null + : currentTri.NextTriangle(prevVert, currentVert); + } + + // checking if the triangle is reversed + FlipStrip(strip, firstTriangle); + return strip.ToArray(); + } + + private void FlipStrip(List strip, Triangle firstTriangle) + { + for(int i = 0; i < 3; i++) + { + if(strip[i] == firstTriangle.Vertices[0].Index) + { + if(strip[(i + 1) % 3] != firstTriangle.Vertices[1].Index) + { + if(strip.Count % 2 == 1) + { + strip.Reverse(); + } + else + { + strip.Insert(0, strip[0]); + } + } + + return; + } + } + } + + + /// + /// Returns an enumerable with the same sequence as but without allocating an array. + ///
For concatenating multiple strips together using degenerate triangles. + ///
+ /// Strip corner type. + /// Strips to concatenate. + /// Indicates which strips are reversed. + /// The joined strip. + public static IEnumerable JoinedStripEnumerator(T[][] strips, bool[]? reversed) + { + bool realRev = false; + + for(int i = 0; i < strips.Length; i++) + { + T[] strip = strips[i]; + + if(i > 0) + { + yield return strip[0]; + realRev = !realRev; + } + + if(realRev != (reversed?[i] == true)) + { + yield return strip[0]; + realRev = !realRev; + } + + foreach(T item in strip) + { + yield return item; + realRev = !realRev; + } + + + if(i < strips.Length - 1) + { + yield return strip[^1]; + realRev = !realRev; + } + } + } + + /// + /// Concatenates multiple strips together using degenerate triangles. + /// + /// Strip corner type. + /// Strips to concatenate. + /// Indicates which strips are reversed. + /// The joined strip. + public static T[] JoinStrips(T[][] strips, bool[]? reversed) + { + return JoinedStripEnumerator(strips, reversed).ToArray(); + } + } +} diff --git a/src/SA3D.Modeling/Strippify/Vertex.cs b/src/SA3D.Modeling/Strippify/Vertex.cs new file mode 100644 index 0000000..73d23f2 --- /dev/null +++ b/src/SA3D.Modeling/Strippify/Vertex.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace SA3D.Modeling.Strippify +{ + /// + /// A single point in 3D space, used to create polygons + /// + internal class Vertex + { + /// + /// The index of this vertex + /// + public int Index { get; } + + /// + /// The edges connected to this vertex + /// + public Dictionary Edges { get; } + + /// + /// The triangles of this vertex + /// + public List Triangles { get; } + + /// + /// Returns the amount of triangles connected to this vertex that havent already been used + /// + public int AvailableTris => Triangles.Count(x => !x.Used); + + /// + /// Creates a new Vertex by index + /// + /// + public Vertex(int index) + { + Index = index; + Edges = new Dictionary(); + Triangles = new List(); + } + + /// + /// Returns the edge between two vertices. If the edge doesnt exist, null is returned + /// + /// Vertex to check connection with + /// + /// + public bool IsConnectedWith(Vertex other, [MaybeNullWhen(false)] out Edge edge) + { + return Edges.TryGetValue(other, out edge); + } + + /// + /// Connects a vertex with another and returns the connected edge + /// + /// + /// + public Edge Connect(Vertex other) + { + Edge e = new(this, other); + Edges.Add(other, e); + other.Edges.Add(this, e); + return e; + } + + public override string ToString() + { + return $"{Index} - {Triangles.Count}/{AvailableTris}"; + } + } +} diff --git a/src/SA3D.Modeling/Structs/Bounds.cs b/src/SA3D.Modeling/Structs/Bounds.cs new file mode 100644 index 0000000..d649ef4 --- /dev/null +++ b/src/SA3D.Modeling/Structs/Bounds.cs @@ -0,0 +1,130 @@ +using SA3D.Common.IO; +using System.Collections.Generic; +using System.Numerics; + +namespace SA3D.Modeling.Structs +{ + /// + /// Bounding sphere determining the bounds of an object in 3D space. + /// + public struct Bounds + { + private Vector3 _position; + + private float _radius; + + /// + /// Position of the Bounds. + /// + public Vector3 Position + { + readonly get => _position; + set + { + _position = value; + RecalculateMatrix(); + } + } + + /// + /// Radius of the Bounds. + /// + public float Radius + { + readonly get => _radius; + set + { + _radius = value; + RecalculateMatrix(); + } + } + + /// + /// Matrix to transform a spherical mesh of diameter 1 to represent the bounds. + /// + public Matrix4x4 Matrix { get; private set; } + + /// + /// Creates new bounds from a position and radius + /// + /// + /// + public Bounds(Vector3 position, float radius) + { + _position = position; + _radius = radius; + Matrix = Matrix4x4.CreateScale(_radius) * Matrix4x4.CreateTranslation(_position); + } + + private void RecalculateMatrix() + { + Matrix = Matrix4x4.CreateScale(_radius) * Matrix4x4.CreateTranslation(_position); + } + + /// + /// Creates the tightest possible bounds from a list of points + /// + /// + /// + public static Bounds FromPoints(IEnumerable points) + { + Vector3 position = VectorUtilities.CalculateCenter(points); + float radius = 0; + foreach(Vector3 p in points) + { + float distance = Vector3.Distance(position, p); + if(distance > radius) + { + radius = distance; + } + } + + return new Bounds(position, radius); + } + + #region I/O + + /// + /// Reads bounds from an endian stack reader. Advances the address by the number of bytes read. + /// + /// The reader to read from. + /// Address at which to read. + /// The read bounds. + public static Bounds Read(EndianStackReader reader, ref uint address) + { + Vector3 position = reader.ReadVector3(ref address); + float radius = reader.ReadFloat(address); + address += 4; + return new(position, radius); + } + + /// + /// Reads bounds from an endian stack reader. + /// + /// The reader to read from. + /// Address at which to read. + /// The read bounds. + public static Bounds Read(EndianStackReader reader, uint address) + { + return Read(reader, ref address); + } + + /// + /// Writes the bounds to an endian stack writer. + /// + /// The writer to write to. + public readonly void Write(EndianStackWriter writer) + { + writer.WriteVector3(Position); + writer.WriteFloat(Radius); + } + + #endregion + + /// + public override readonly string ToString() + { + return $"{Position} : {Radius}"; + } + } +} diff --git a/src/SA3D.Modeling/Structs/Color.cs b/src/SA3D.Modeling/Structs/Color.cs new file mode 100644 index 0000000..981a77b --- /dev/null +++ b/src/SA3D.Modeling/Structs/Color.cs @@ -0,0 +1,525 @@ +using System; +using System.Numerics; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; + +namespace SA3D.Modeling.Structs +{ + /// + /// RGBA Color value. + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public partial struct Color : IEquatable + { + #region Constants + + /// + /// White color; #FFFFFFFF + /// + public static readonly Color ColorWhite + = new(0xFF, 0xFF, 0xFF, 0xFF); + + /// + /// Black color; #000000FF + /// + public static readonly Color ColorBlack + = new(0x00, 0x00, 0x00, 0xFF); + + /// + /// Red color; #FF0000FF + /// + public static readonly Color ColorRed + = new(0xFF, 0x00, 0x00, 0xFF); + + /// + /// Green color; 0#0FF00FF + /// + public static readonly Color ColorGreen + = new(0x00, 0xFF, 0x00, 0xFF); + + /// + /// Blue color; #0000FFFF + /// + public static readonly Color ColorBlue + = new(0x00, 0x00, 0xFF, 0xFF); + + /// + /// Transparent color; #00000000 + /// + public static readonly Color ColorTransparent + = default; + + [GeneratedRegex("^[0-9A-F]+$")] + private static partial Regex HexNumRegex(); + + #endregion + + /// + /// Red. + /// + public byte Red { get; set; } + + /// + /// Green. + /// + public byte Green { get; set; } + + /// + /// Blue. + /// + public byte Blue { get; set; } + + /// + /// Alpha/Transparency. + /// + public byte Alpha { get; set; } + + #region Converter Properties + + /// + /// Red as a float. Ranges from 0 - 1. + /// + public float RedF + { + readonly get => Red / 255.0f; + set => Red = FromFloat(value); + } + + /// + /// Green as a float. Ranges from 0 - 1. + /// + public float GreenF + { + readonly get => Green / 255.0f; + set => Green = FromFloat(value); + } + + /// + /// Blue as a float. Ranges from 0 - 1. + /// + public float BlueF + { + readonly get => Blue / 255.0f; + set => Blue = FromFloat(value); + } + + /// + /// Alpha as a float. Ranges from 0 - 1. + /// + public float AlphaF + { + readonly get => Alpha / 255.0f; + set => Alpha = FromFloat(value); + } + + /// + /// The color as a 4 component float vector. + /// + public Vector4 FloatVector + { + readonly get => new(RedF, GreenF, BlueF, AlphaF); + set + { + RedF = value.X; + BlueF = value.Y; + GreenF = value.Z; + AlphaF = value.W; + } + } + + /// + /// RGBA representation of the color (32 bits). + /// + public uint RGBA + { + readonly get => (uint)(Alpha | (Blue << 8) | (Green << 16) | (Red << 24)); + set + { + Alpha = (byte)(value & 0xFF); + Blue = (byte)((value >> 8) & 0xFF); + Green = (byte)((value >> 16) & 0xFF); + Red = (byte)(value >> 24); + } + } + + /// + /// ARGB representation of the color (32 bits). + /// + public uint ARGB + { + readonly get => (uint)(Blue | (Green << 8) | (Red << 16) | (Alpha << 24)); + set + { + Blue = (byte)(value & 0xFF); + Green = (byte)((value >> 8) & 0xFF); + Red = (byte)((value >> 16) & 0xFF); + Alpha = (byte)(value >> 24); + } + } + + /// + /// ARGB representation of the color (16 bits). + /// + public ushort ARGB4 + { + readonly get => (ushort)((Blue >> 4) | (Green & 0xF) | ((Red << 4) & 0xF) | ((Alpha << 8) & 0xF)); + set + { + Blue = (byte)((value << 4) & 0xF0); + Blue |= (byte)(Blue >> 4); + + Green = (byte)(value & 0xF0); + Green |= (byte)(Green >> 4); + + Red = (byte)((value >> 4) & 0xF0); + Red |= (byte)(Red >> 4); + + Alpha = (byte)((value >> 8) & 0xF0); + Alpha |= (byte)(Alpha >> 4); + } + } + + /// + /// RGB565 representation of the color (16 bits). + /// + public ushort RGB565 + { + readonly get => (ushort)((Blue >> 3) | ((Green << 3) & 0x3F) | ((Red << 8) & 0x1F)); + set + { + Blue = (byte)(((value << 3) | (value >> 2)) & 0xFFu); + Green = (byte)(((value >> 3) | (value >> 9)) & 0xFFu); + Red = (byte)(((value >> 8) | (value >> 13)) & 0xFFu); + Alpha = 0xFF; + } + } + + /// + /// System.Drawing representation of the color. + /// + public System.Drawing.Color SystemColor + { + readonly get => System.Drawing.Color.FromArgb(Alpha, Red, Green, Blue); + set + { + Red = value.R; + Green = value.G; + Blue = value.B; + Alpha = value.A; + } + } + + /// + /// RGBA string - #RRGGBBAA
+ /// setter formats:
+ /// #RRGGBBAA
+ /// #RRGGBB
+ /// #RGBA
+ /// #RGB
+ /// (The # is optional for all formats) + ///
+ public string Hex + { + readonly get => $"#{Red:X2}{Green:X2}{Blue:X2}{Alpha:X2}"; + set + { + string hex = value.Replace(" ", ""); + hex = hex.StartsWith("#") ? hex[1..] : hex; + hex = hex.ToUpper(); + + // check if the format is valid + if(!HexNumRegex().IsMatch(hex)) + { + throw new FormatException("Invalid Color format!"); + } + + static byte conv(char character) + { + return (byte)(character >= 'A' ? character - 'A' + 10 : character - '0'); + } + + static byte comp(byte lo, byte hi) + { + return (byte)(lo | (hi << 4)); + } + + switch(hex.Length) + { + case 3: // RGB + Red = conv(hex[0]); + Green = conv(hex[1]); + Blue = conv(hex[2]); + Alpha = byte.MaxValue; + + Red = comp(Red, Red); + Green = comp(Green, Green); + Blue = comp(Blue, Blue); + break; + case 4: // RGBA + Red = conv(hex[0]); + Green = conv(hex[1]); + Blue = conv(hex[2]); + Alpha = conv(hex[3]); + + Red = comp(Red, Red); + Green = comp(Green, Green); + Blue = comp(Blue, Blue); + Alpha = comp(Alpha, Alpha); + break; + case 6: // RRGGBB + Red = comp(conv(hex[1]), conv(hex[0])); + Green = comp(conv(hex[3]), conv(hex[2])); + Blue = comp(conv(hex[5]), conv(hex[4])); + Alpha = byte.MaxValue; + break; + case 8: // RRGGBBAA + Red = comp(conv(hex[1]), conv(hex[0])); + Green = comp(conv(hex[3]), conv(hex[2])); + Blue = comp(conv(hex[5]), conv(hex[4])); + Alpha = comp(conv(hex[7]), conv(hex[6])); + break; + default: + throw new FormatException("Invalid Color format!"); + } + } + } + + #endregion + + #region Constructors + + /// + /// Creates a new color. + /// + /// Red color. + /// Green color. + /// Blue color. + /// alpha color. + public Color(byte red, byte green, byte blue, byte alpha) + { + Red = red; + Green = green; + Blue = blue; + Alpha = alpha; + } + + /// + /// Creates a new opaque color. + /// + /// Red color. + /// Green color. + /// Blue color. + public Color(byte red, byte green, byte blue) : this(red, green, blue, 0xFF) { } + + /// + /// Creates a new color from floating point values. + /// + /// Red color. + /// Green color. + /// Blue color. + /// alpha color. + public Color(float red, float green, float blue, float alpha) : this() + { + RedF = red; + GreenF = green; + BlueF = blue; + AlphaF = alpha; + } + + /// + /// Creates a new opaque color from floating point values. + /// + /// Red color. + /// Green color. + /// Blue color. + public Color(float red, float green, float blue) : this(red, green, blue, 1) { } + + /// + /// Creates a new color from a 4 component floating point vector. + /// + /// Color value vector. + public Color(Vector4 vector) : this(vector.X, vector.Y, vector.Z, vector.W) { } + + #endregion + + #region Arithmetic Operators/Methods + + /// + /// Calculates the colors luminance value. + /// + public readonly float GetLuminance() + { + return (0.2126f * RedF) + (0.7152f * GreenF) + (0.0722f * BlueF); + } + + /// + /// Linearly interpolates two colors. + /// + /// The color from which to interpolate. + /// The color to which to interpolate. + /// Time value to interpolate with. Range 0 - 1. + /// The interpolated color. + public static Color Lerp(Color from, Color to, float t) + { + float inverse = 1 - t; + return new( + (to.RedF * t) + (from.RedF * inverse), + (to.GreenF * t) + (from.GreenF * inverse), + (to.BlueF * t) + (from.BlueF * inverse), + (to.AlphaF * t) + (from.AlphaF * inverse)); + } + + /// + /// Calculates the vector distance between two colors (Calculates with floating point values). + /// + /// First color. + /// Second color. + /// The distance. + public static float Distance(Color from, Color to) + { + return MathF.Sqrt( + MathF.Pow(from.RedF - to.RedF, 2) + + MathF.Pow(from.GreenF - to.GreenF, 2) + + MathF.Pow(from.BlueF - to.BlueF, 2) + + MathF.Pow(from.AlphaF - to.AlphaF, 2) + ); + } + + /// + /// Adds the individual color components of two colors together (calculates with bytes). + /// + /// Lefthand color. + /// Righthand color. + /// The calculated color. + public static Color operator +(Color l, Color r) + { + return new() + { + Red = (byte)Math.Min(l.Red + r.Red, byte.MaxValue), + Green = (byte)Math.Min(l.Green + r.Green, byte.MaxValue), + Blue = (byte)Math.Min(l.Blue + r.Blue, byte.MaxValue), + Alpha = (byte)Math.Min(l.Alpha + r.Alpha, byte.MaxValue) + }; + } + + /// + /// Subtracts the individual color compoenents of two colors. (calculates with bytes). + /// + /// The color to subtract from + /// The color to subtract. + /// The calculated color. + public static Color operator -(Color l, Color r) + { + return new() + { + Red = (byte)Math.Max(l.Red - r.Red, 0), + Green = (byte)Math.Max(l.Green - r.Green, 0), + Blue = (byte)Math.Max(l.Blue - r.Blue, 0), + Alpha = (byte)Math.Max(l.Alpha - r.Alpha, 0) + }; + } + + /// + /// Multiplies the individual color components by a value (calculates with floats). + /// + /// The color to multiply. + /// The value to multiply by + /// The calculated color. + public static Color operator *(Color l, float r) + { + return new() + { + RedF = Math.Clamp(l.RedF * r, 0, 1), + GreenF = Math.Clamp(l.GreenF * r, 0, 1), + BlueF = Math.Clamp(l.BlueF * r, 0, 1), + AlphaF = Math.Clamp(l.AlphaF * r, 0, 1) + }; + } + + /// + /// Multiplies the individual color components by a value (calculates with floats). + /// + /// The value to multiply by + /// The color to multiply. + /// The calculated color. + public static Color operator *(float l, Color r) + { + return r * l; + } + + /// + /// Divides the individual color components by a value (calculates with floats). + /// + /// The color to divide. + /// The value to divide by. + /// The calculated color. + public static Color operator /(Color l, float r) + { + return l * (1f / r); + } + + #endregion + + #region Logical Operators/Methods + + /// + public override readonly bool Equals(object? obj) + { + return obj is Color color && + Red == color.Red && + Green == color.Green && + Blue == color.Blue && + Alpha == color.Alpha; + } + + /// + public override readonly int GetHashCode() + { + return HashCode.Combine(Red, Green, Blue, Alpha); + } + + /// + /// Compares the components of 2 colors for equality. + /// + /// Lefthand color. + /// Righthand color. + /// Whether the colors are equal. + public static bool operator ==(Color l, Color r) + { + return l.Equals(r); + } + + /// + /// Compares the components of 2 colors for inequality. + /// + /// Lefthand color. + /// Righthand color. + /// Whether the colors are inequal. + public static bool operator !=(Color l, Color r) + { + return !l.Equals(r); + } + + readonly bool IEquatable.Equals(Color other) + { + return Equals(other); + } + + #endregion + + + private static byte FromFloat(float value) + { + return (byte)(float.Clamp(value, 0, 1) * 255); + } + + /// + /// Returns a hexadecimal string represntation of the color. + /// + /// The hexadecimal string. + public override readonly string ToString() + { + return Hex; + } + } +} diff --git a/src/SA3D.Modeling/Structs/ColorIOType.cs b/src/SA3D.Modeling/Structs/ColorIOType.cs new file mode 100644 index 0000000..fd7bd9e --- /dev/null +++ b/src/SA3D.Modeling/Structs/ColorIOType.cs @@ -0,0 +1,58 @@ +namespace SA3D.Modeling.Structs +{ + /// + /// Write/Read mode for colors. + /// + public enum ColorIOType + { + /// + /// ARGB Color; Each channel takes a byte. + /// + ARGB8_32, + + /// + /// ARGB Color, but written as two shorts (important for big endian ARGB). + /// + ARGB8_16, + + /// + /// Color; Each channel uses 4 bits. + /// + ARGB4, + + /// + /// Colors; Red and blue use 5 bits, green 6 bits. + /// + RGB565, + + /// + /// BGRA Color; Each channel takes a byte. + /// + RGBA8, + } + + /// + /// Extension methods for + /// + public static class ColorIOTypeExtensions + { + /// + /// Returns how many bytes the given color type takes up. + /// + /// Type to get the size of. + /// The value size. + public static int GetByteSize(this ColorIOType type) + { + return type switch + { + ColorIOType.ARGB8_32 => 4, + ColorIOType.ARGB8_16 => 4, + ColorIOType.ARGB4 => 2, + ColorIOType.RGB565 => 2, + ColorIOType.RGBA8 => 4, + _ => 0, + }; + } + + } +} diff --git a/src/SA3D.Modeling/Structs/DebugStringExtensions.cs b/src/SA3D.Modeling/Structs/DebugStringExtensions.cs new file mode 100644 index 0000000..c78e7a9 --- /dev/null +++ b/src/SA3D.Modeling/Structs/DebugStringExtensions.cs @@ -0,0 +1,50 @@ +using System.Numerics; + +namespace SA3D.Modeling.Structs +{ + /// + /// Extends float and various float structures to allow for printing strings that are always the same length. + /// + public static class DebugStringExtensions + { + /// + /// Creates an easily readable debug string for a float. + /// + /// The float to create the string for. + /// The debug string. + public static string DebugString(this float val) + { + return (val >= 0 ? " " : string.Empty) + val.ToString("F3"); + } + + /// + /// Creates an easily readable debug string for a quaternion. + /// + /// The quaternion to create the string for. + /// The debug string. + public static string DebugString(this Quaternion quat) + { + return $"({quat.W.DebugString()}, {quat.X.DebugString()}, {quat.Y.DebugString()}, {quat.Z.DebugString()})"; + } + + /// + /// Creates an easily readable debug string for a vector. + /// + /// The vector to create the string for. + /// The debug string. + public static string DebugString(this Vector2 vector) + { + return $"({vector.X.DebugString()}, {vector.Y.DebugString()})"; + } + + /// + /// Creates an easily readable debug string for a vector. + /// + /// The vector to create the string for. + /// The debug string. + public static string DebugString(this Vector3 vector) + { + return $"({vector.X.DebugString()}, {vector.Y.DebugString()}, {vector.Z.DebugString()})"; + } + } +} diff --git a/src/SA3D.Modeling/Structs/EndianIOExtensions.cs b/src/SA3D.Modeling/Structs/EndianIOExtensions.cs new file mode 100644 index 0000000..6da8a50 --- /dev/null +++ b/src/SA3D.Modeling/Structs/EndianIOExtensions.cs @@ -0,0 +1,561 @@ +using SA3D.Common.IO; +using SA3D.Common.Lookup; +using System; +using System.Collections.Generic; +using System.Numerics; + +namespace SA3D.Modeling.Structs +{ + /// + /// Extension methods for and . + /// + public static class EndianIOExtensions + { + #region Writing + + /// + /// Writes a color to an endian stack writer. + /// + /// The writer to write to. + /// The color to write. + /// The data type by which to write the color. + /// + public static void WriteColor(this EndianStackWriter writer, Color color, ColorIOType type) + { + switch(type) + { + case ColorIOType.RGBA8: + writer.WriteUInt(color.RGBA); + break; + case ColorIOType.ARGB8_32: + writer.WriteUInt(color.ARGB); + break; + case ColorIOType.ARGB8_16: + uint val = color.ARGB; + writer.WriteUShort((ushort)val); + writer.WriteUShort((ushort)(val >> 16)); + break; + case ColorIOType.ARGB4: + writer.WriteUShort(color.ARGB4); + break; + case ColorIOType.RGB565: + writer.WriteUShort(color.RGB565); + break; + default: + throw new ArgumentException($"Invalid type.", nameof(type)); + } + } + + /// + /// Writes a 2 component vector to an endian stack writer. + /// + /// The writer to write to. + /// The vector to write. + /// The data type by which to write the vector. + public static void WriteVector2(this EndianStackWriter writer, Vector2 vector2, FloatIOType type = FloatIOType.Float) + { + Action floatWriter = type.GetWriter(); + + floatWriter(writer, vector2.X); + floatWriter(writer, vector2.Y); + } + + /// + /// Writes a 3 component vector to an endian stack writer. + /// + /// The writer to write to. + /// The vector to write. + /// The data type by which to write the vector. + public static void WriteVector3(this EndianStackWriter writer, Vector3 vector3, FloatIOType type = FloatIOType.Float) + { + Action floatWriter = type.GetWriter(); + + floatWriter(writer, vector3.X); + floatWriter(writer, vector3.Y); + floatWriter(writer, vector3.Z); + } + + /// + /// Writes a quaternion to an endian stack writer (WXYZ). + /// + /// The writer to write to. + /// The quaternion to write. + public static void WriteQuaternion(this EndianStackWriter writer, Quaternion quaternion) + { + writer.WriteFloat(quaternion.W); + writer.WriteFloat(quaternion.X); + writer.WriteFloat(quaternion.Y); + writer.WriteFloat(quaternion.Z); + } + + + /// + /// Delegate for writing values to an endian stack writer. + /// + /// The type of value to write. + /// The write to write to. + /// The value to write.. + public delegate void WriteValueDelegate(EndianStackWriter writer, T value); + + /// + /// Writes a collection to a writer and returns the pointer position at which the collection has started writing. + /// + /// Type of value the collection stores. + /// The writer to write to. + /// The values to write. + /// Function responsible for writing. + /// If specified, this will be executed for every value before the actual collection gets written. + /// The address at which the collection was written. + public static uint WriteCollection( + this EndianStackWriter writer, + ICollection values, + WriteValueDelegate write, + WriteValueDelegate? preWrite) + { + if(preWrite != null) + { + foreach(T value in values) + { + preWrite(writer, value); + } + } + + uint result = writer.PointerPosition; + + foreach(T value in values) + { + write(writer, value); + } + + return result; + } + + /// + /// Writes a collection to a writer and returns the pointer position at which the collection has started writing. + /// + /// Type of value the collection stores. + /// The writer to write to. + /// The values to write. + /// Function responsible for writing. + /// The address at which the collection was written. + public static uint WriteCollection( + this EndianStackWriter writer, + ICollection values, + WriteValueDelegate write) + { + return writer.WriteCollection(values, write, null); + } + + /// + /// Writes a collection to a writer and returns the pointer position at which the collection has started writing. + ///
Checks if the collection is already in the LUT, and does not write the collection if that is the case. + ///
+ /// Type of value the collection stores. + /// The writer to write to. + /// The values to write. + /// Function responsible for writing. + /// If specified, this will be executed for every value before the actual collection gets written. + /// Pointer references to utilize. + /// The address at which the collection was written. + public static uint WriteCollectionWithLUT( + this EndianStackWriter writer, + ICollection? values, + WriteValueDelegate write, + WriteValueDelegate? preWrite, + BaseLUT lut) + { + return lut.GetAddAddress(values, () => writer.WriteCollection(values!, write, preWrite)); + } + + /// + /// Writes a collection to a writer and returns the pointer position at which the collection has started writing. + ///
Checks if the collection is already in the LUT, and does not write the collection if that is the case. + ///
+ /// Type of value the collection stores. + /// The writer to write to. + /// The values to write. + /// Function responsible for writing. + /// Pointer references to utilize. + /// The address at which the collection was written. + public static uint WriteCollectionWithLUT( + this EndianStackWriter writer, + ICollection? values, + WriteValueDelegate write, + BaseLUT lut) + { + return writer.WriteCollectionWithLUT(values, write, null, lut); + } + + #endregion + + #region Reading + + /// + /// Reads a color off an endian stack reader. Advances the address by the number of bytes read. + /// + /// The reader to read from. + /// The address at which to read. + /// The type by which to read the color. + /// The read color. + /// + public static Color ReadColor(this EndianStackReader reader, ref uint address, ColorIOType type) + { + Color col = default; + switch(type) + { + case ColorIOType.RGBA8: + col.RGBA = reader.ReadUInt(address); + address += 4; + break; + case ColorIOType.ARGB8_32: + col.ARGB = reader.ReadUInt(address); + address += 4; + break; + case ColorIOType.ARGB8_16: + ushort GB = reader.ReadUShort(address); + ushort AR = reader.ReadUShort(address + 2); + col.ARGB = (uint)(GB | (AR << 16)); + address += 4; + break; + case ColorIOType.ARGB4: + col.ARGB4 = reader.ReadUShort(address); + address += 2; + break; + case ColorIOType.RGB565: + col.RGB565 = reader.ReadUShort(address); + address += 2; + break; + default: + throw new ArgumentException($"Invalid type.", nameof(type)); + } + + return col; + } + + /// + /// Reads a color off an endian stack reader. + /// + /// The reader to read from. + /// The address at which to read. + /// The type by which to read the color. + /// The read color. + /// + public static Color ReadColor(this EndianStackReader reader, uint address, ColorIOType type) + { + return reader.ReadColor(ref address, type); + } + + /// + /// Reads a 2 component vector off an endian stack reader. Advances the address by the number of bytes read. + /// + /// The reader to read from. + /// The address at which to read. + /// The type by which to read the vector. + /// The read vector. + public static Vector2 ReadVector2(this EndianStackReader reader, ref uint address, FloatIOType type = FloatIOType.Float) + { + Func readFloat = type.GetReader(); + uint fieldSize = (uint)type.GetByteSize(); + + Vector2 result = new( + readFloat(reader, address), + readFloat(reader, address += fieldSize) + ); + + address += fieldSize; + return result; + } + + /// + /// Reads a 2 component vector off an endian stack reader. + /// + /// The reader to read from. + /// The address at which to read. + /// The type by which to read the vector. + /// The read vector. + public static Vector2 ReadVector2(this EndianStackReader reader, uint address, FloatIOType type = FloatIOType.Float) + { + return reader.ReadVector2(ref address, type); + } + + /// + /// Reads a 3 component vector off an endian stack reader. Advances the address by the number of bytes read. + /// + /// The reader to read from. + /// The address at which to read. + /// The type by which to read the vector. + /// The read vector. + public static Vector3 ReadVector3(this EndianStackReader reader, ref uint address, FloatIOType type = FloatIOType.Float) + { + Func readFloat = type.GetReader(); + uint fieldSize = (uint)type.GetByteSize(); + + Vector3 result = new( + readFloat(reader, address), + readFloat(reader, address += fieldSize), + readFloat(reader, address += fieldSize) + ); + + address += fieldSize; + return result; + } + + /// + /// Reads a 3 component vector off an endian stack reader. + /// + /// The reader to read from. + /// The address at which to read. + /// The type by which to read the vector. + /// The read vector. + public static Vector3 ReadVector3(this EndianStackReader reader, uint address, FloatIOType type = FloatIOType.Float) + { + return reader.ReadVector3(ref address, type); + } + + /// + /// Reads a quaternion off an endian stack reader. Advances the address by the number of bytes read. + /// + /// The reader to read from. + /// The address at which to read. + /// The read quaternion. + public static Quaternion ReadQuaternion(this EndianStackReader reader, ref uint address) + { + Quaternion result = new() + { + W = reader.ReadFloat(address), + X = reader.ReadFloat(address + 4), + Y = reader.ReadFloat(address + 8), + Z = reader.ReadFloat(address + 12) + }; + + address += 16; + return result; + } + + /// + /// Reads a quaternion off an endian stack reader. Advances the address by the number of bytes read. + /// + /// The reader to read from. + /// The address at which to read. + /// The read quaternion. + public static Quaternion ReadQuaternion(this EndianStackReader reader, uint address) + { + return reader.ReadQuaternion(ref address); + } + + + /// + /// Delegate for reading values from an endian stack reader. Used for reading and advancing the address by the number of bytes read. + /// + /// The type of value to read. + /// The reader to read from. + /// The address at which to read. + /// The read value + public delegate T ReadValueAdvanceDelegate(EndianStackReader reader, ref uint address); + + /// + /// Delegate for reading values from an endian stack reader. + /// + /// The type of value to read. + /// The reader to read from. + /// The address at which to read. + /// The read value + public delegate T ReadValueDelegate(EndianStackReader reader, uint address); + + /// + /// Reads an array of values off an endian stack reader. + /// + /// Type of value to read. + /// The reader to read from. + /// The address at which to read. + /// The number of elements to read. + /// The read function. + /// The read array. + public static T[] ReadArray( + this EndianStackReader reader, + uint address, + uint count, + ReadValueAdvanceDelegate read) + { + T[] result = new T[count]; + + for(int i = 0; i < count; i++) + { + result[i] = read(reader, ref address); + } + + return result; + } + + /// + /// Reads an array of values off an endian stack reader. + /// + /// Type of value to read. + /// The reader to read from. + /// The address at which to read. + /// The number of elements to read. + /// Number of bytes to advance the address after reading an element. + /// The read function. + /// The read array. + public static T[] ReadArray( + this EndianStackReader reader, + uint address, + uint count, + uint elementByteSize, + ReadValueDelegate read) + { + T[] result = new T[count]; + + for(int i = 0; i < count; i++) + { + result[i] = read(reader, address); + address += elementByteSize; + } + + return result; + } + + /// + /// Reads an array of values off an endian stack reader. + ///
Checks if the collection is already in the LUT, and returns it if that is the case. + ///
+ /// Type of value to read. + /// The reader to read from. + /// The address at which to read. + /// The number of elements to read. + /// The read function. + /// Pointer references to utilize. + /// The read array. + public static T[] ReadArrayWithLUT( + this EndianStackReader reader, + uint address, + uint count, + ReadValueAdvanceDelegate read, + BaseLUT lut) + { + return lut.GetAddValue(address, () => ReadArray(reader, address, count, read)); + } + + + /// + /// Reads an array of values off an endian stack reader. + ///
Checks if the collection is already in the LUT, and returns it if that is the case. + ///
+ /// Type of value to read. + /// The reader to read from. + /// The address at which to read. + /// The number of elements to read. + /// Number of bytes to advance the address after reading an element. + /// The read function. + /// Pointer references to utilize. + /// The read array. + public static T[] ReadArrayWithLUT( + this EndianStackReader reader, + uint address, + uint count, + uint elementByteSize, + ReadValueDelegate read, + BaseLUT lut) + { + return lut.GetAddValue(address, () => ReadArray(reader, address, count, elementByteSize, read)); + } + + /// + /// Reads a labeled array of values off an endian stack reader. + ///
Checks if the collection is already in the LUT, and returns it if that is the case. + ///
+ /// Type of value to read. + /// The reader to read from. + /// The address at which to read. + /// The number of elements to read. + /// The read function. + /// Label prefix to use when generating the arrays label, when none is found in the label dictionary. + /// Pointer references to utilize. + /// The read array. + public static LabeledArray ReadLabeledArray( + this EndianStackReader reader, + uint address, + uint count, + ReadValueAdvanceDelegate read, + string genPrefix, + BaseLUT lut) + { + return lut.GetAddLabeledValue(address, genPrefix, () => new LabeledArray(ReadArray(reader, address, count, read))); + } + + /// + /// Reads a labeled array of values off an endian stack reader. + ///
Checks if the collection is already in the LUT, and returns it if that is the case. + ///
+ /// Type of value to read. + /// The reader to read from. + /// The address at which to read. + /// The number of elements to read. + /// Number of bytes to advance the address after reading an element. + /// The read function. + /// Label prefix to use when generating the arrays label, when none is found in the label dictionary. + /// Pointer references to utilize. + /// The read array. + public static LabeledArray ReadLabeledArray( + this EndianStackReader reader, + uint address, + uint count, + uint elementByteSize, + ReadValueDelegate read, + string genPrefix, + BaseLUT lut) + { + return lut.GetAddLabeledValue(address, genPrefix, () => new LabeledArray(ReadArray(reader, address, count, elementByteSize, read))); + } + + /// + /// Reads a labeled read only array of values off an endian stack reader. + ///
Checks if the collection is already in the LUT, and returns it if that is the case. + ///
+ /// Type of value to read. + /// The reader to read from. + /// The address at which to read. + /// The number of elements to read. + /// The read function. + /// Label prefix to use when generating the arrays label, when none is found in the label dictionary. + /// Pointer references to utilize. + /// The read array. + public static LabeledReadOnlyArray ReadLabeledReadOnlyArray( + this EndianStackReader reader, + uint address, + uint count, + ReadValueAdvanceDelegate read, + string genPrefix, + BaseLUT lut) + { + return lut.GetAddLabeledValue(address, genPrefix, () => new LabeledReadOnlyArray(ReadArray(reader, address, count, read))); + } + + /// + /// Reads a labeled read only array of values off an endian stack reader. + ///
Checks if the collection is already in the LUT, and returns it if that is the case. + ///
+ /// Type of value to read. + /// The reader to read from. + /// The address at which to read. + /// The number of elements to read. + /// Number of bytes to advance the address after reading an element. + /// The read function. + /// Label prefix to use when generating the arrays label, when none is found in the label dictionary. + /// Pointer references to utilize. + /// The read array. + public static LabeledReadOnlyArray ReadLabeledReadOnlyArray( + this EndianStackReader reader, + uint address, + uint count, + uint elementByteSize, + ReadValueDelegate read, + string genPrefix, + BaseLUT lut) + { + return lut.GetAddLabeledValue(address, genPrefix, () => new LabeledReadOnlyArray(ReadArray(reader, address, count, elementByteSize, read))); + } + + #endregion + } +} diff --git a/src/SA3D.Modeling/Structs/FloatIOType.cs b/src/SA3D.Modeling/Structs/FloatIOType.cs new file mode 100644 index 0000000..f2d0486 --- /dev/null +++ b/src/SA3D.Modeling/Structs/FloatIOType.cs @@ -0,0 +1,194 @@ +using SA3D.Common; +using SA3D.Common.IO; +using System; +using System.Globalization; + +namespace SA3D.Modeling.Structs +{ + /// + /// Write/Read mode for float values. + /// + public enum FloatIOType + { + /// + /// Interpret as is. + /// + Float, + + /// + /// Interpret float as a short. + /// + Short, + + /// + /// Interpret float as an integer. + /// + Integer, + + /// + /// Interpret (Radians) as a 16 Bit BAMS angle. + /// + BAMS16, + + /// + /// Interpret (Radians) as 32 Bit BAMS angle. + /// + BAMS32 + } + + /// + /// Extension methods for + /// + public static class FloatIOTypeExtensions + { + /// + /// Returns the number of bytes the given type takes up. + /// + /// Type to get the size of. + /// The value size. + public static int GetByteSize(this FloatIOType type) + { + return type switch + { + FloatIOType.Float => 4, + FloatIOType.Short => 2, + FloatIOType.Integer => 4, + FloatIOType.BAMS16 => 2, + FloatIOType.BAMS32 => 4, + _ => throw new ArgumentException("Type invalid.", nameof(type)), + }; + } + + /// + /// Returns a function for converting a value to a string in the given IO type. + /// + /// The type to print out as. + /// The function for converting float values. + /// + public static Func GetPrinter(this FloatIOType type) + { + static string GetText(float value) + { + return value.ToString("F5", CultureInfo.InvariantCulture) + "f"; + } + + static string GetShortText(float value) + { + return ((short)MathF.Round(value)).ToString(); + } + + static string GetIntegerText(float value) + { + return ((int)MathF.Round(value)).ToString(); + } + + static string GetBAMS16Text(float value) + { + return ((ushort)MathHelper.RadToBAMS(value)).ToCHex(); + } + + static string GetBAMS32Text(float value) + { + return ((uint)MathHelper.RadToBAMS(value)).ToCHex(); + } + + return type switch + { + FloatIOType.Short => GetShortText, + FloatIOType.Float => GetText, + FloatIOType.Integer => GetIntegerText, + FloatIOType.BAMS16 => GetBAMS16Text, + FloatIOType.BAMS32 => GetBAMS32Text, + _ => throw new ArgumentException("Type invalid", nameof(type)), + }; + } + + /// + /// Returns a function for writing a value to an endian stack writer in the given IO type. + /// + /// The type to write out as. + /// The function for writing a float value to a writer. + /// + public static Action GetWriter(this FloatIOType type) + { + static void WriteFloat(EndianStackWriter writer, float value) + { + writer.WriteFloat(value); + } + + static void WriteShort(EndianStackWriter writer, float value) + { + writer.WriteShort((short)MathF.Round(value)); + } + + static void WriteInteger(EndianStackWriter writer, float value) + { + writer.WriteInt((int)MathF.Round(value)); + } + + static void WriteBAMS16(EndianStackWriter writer, float value) + { + writer.WriteShort((short)MathHelper.RadToBAMS(value)); + } + + static void WriteBAMS32(EndianStackWriter writer, float value) + { + writer.WriteInt(MathHelper.RadToBAMS(value)); + } + + return type switch + { + FloatIOType.Float => WriteFloat, + FloatIOType.Short => WriteShort, + FloatIOType.Integer => WriteInteger, + FloatIOType.BAMS16 => WriteBAMS16, + FloatIOType.BAMS32 => WriteBAMS32, + _ => throw new ArgumentException("Type invalid", nameof(type)), + }; + } + + /// + /// Returns a function for reading a value off an endian stack reader in the given IO type. + /// + /// The type to read as. + /// The function for reading a float value off a reader. + /// + public static Func GetReader(this FloatIOType type) + { + static float ReadFloat(EndianStackReader reader, uint address) + { + return reader.ReadFloat(address); + } + + static float ReadShort(EndianStackReader reader, uint address) + { + return reader.ReadShort(address); + } + + static float ReadInteger(EndianStackReader reader, uint address) + { + return reader.ReadInt(address); + } + + static float ReadBAMS16(EndianStackReader reader, uint address) + { + return MathHelper.BAMSToRad(reader.ReadShort(address)); + } + + static float ReadBAMS32(EndianStackReader reader, uint address) + { + return MathHelper.BAMSToRad(reader.ReadInt(address)); + } + + return type switch + { + FloatIOType.Float => ReadFloat, + FloatIOType.Short => ReadShort, + FloatIOType.Integer => ReadInteger, + FloatIOType.BAMS16 => ReadBAMS16, + FloatIOType.BAMS32 => ReadBAMS32, + _ => throw new ArgumentException("Type invalid", nameof(type)), + }; + } + } +} diff --git a/src/SA3D.Modeling/Structs/MatrixUtilities.cs b/src/SA3D.Modeling/Structs/MatrixUtilities.cs new file mode 100644 index 0000000..fd0bacd --- /dev/null +++ b/src/SA3D.Modeling/Structs/MatrixUtilities.cs @@ -0,0 +1,247 @@ +using System; +using System.Numerics; + +namespace SA3D.Modeling.Structs +{ + /// + /// Matrix related utility methods. + /// + public static class MatrixUtilities + { + /// + /// Creates a rotation matrix from euler angles. + /// + /// The euler angles to use. + /// Whether the euler angles are used in ZYX order. + /// + public static Matrix4x4 CreateRotationMatrix(Vector3 rotation, bool ZYX) + { + float radX = rotation.X; + float radY = rotation.Y; + float radZ = rotation.Z; + + float sX = MathF.Sin(radX); + float cX = MathF.Cos(radX); + + float sY = MathF.Sin(radY); + float cY = MathF.Cos(radY); + + float sZ = MathF.Sin(radZ); + float cZ = MathF.Cos(radZ); + + if(ZYX) + { + // Well, in sa2 it rotates in ZXY order, so thats what we do here. dont ask me... + // equal to matZ * matX * matY + return new() + { + M11 = (sY * sX * sZ) + (cY * cZ), + M12 = cX * sZ, + M13 = (cY * sX * sZ) - (sY * cZ), + + M21 = (sY * sX * cZ) - (cY * sZ), + M22 = cX * cZ, + M23 = (cY * sX * cZ) + (sY * sZ), + + M31 = sY * cX, + M32 = -sX, + M33 = cY * cX, + + M44 = 1 + }; + } + else + { + // equal to matX * matY * matZ + return new() + { + M11 = cZ * cY, + M12 = sZ * cY, + M13 = -sY, + + M21 = (cZ * sY * sX) - (sZ * cX), + M22 = (sZ * sY * sX) + (cZ * cX), + M23 = cY * sX, + + M31 = (cZ * sY * cX) + (sZ * sX), + M32 = (sZ * sY * cX) - (cZ * sX), + M33 = cY * cX, + + M44 = 1 + }; + } + } + + /// + /// Creates a transform matrix from a position, rotation and scale. + /// + /// The position. + /// The quaternion rotation. + /// The scale. + /// The transform matrix. + public static Matrix4x4 CreateTransformMatrix(Vector3 position, Quaternion rotation, Vector3 scale) + { + return Matrix4x4.CreateScale(scale) * Matrix4x4.CreateFromQuaternion(rotation) * Matrix4x4.CreateTranslation(position); + } + + /// + /// Creates a transform matrix from a position, euler angles and scale. + /// + /// The position. + /// The euler angles. + /// The scale. + /// Whether the euler angles are used in ZYX order. + /// The transform matrix. + public static Matrix4x4 CreateTransformMatrix(Vector3 position, Vector3 rotation, Vector3 scale, bool rotateZYX) + { + return Matrix4x4.CreateScale(scale) * CreateRotationMatrix(rotation, rotateZYX) * Matrix4x4.CreateTranslation(position); + } + + /// + /// Returns the normal matrix for a transform matrix. + /// + /// The transform matrix to get the normal matrix for. + /// The normal matrix. + /// + public static Matrix4x4 GetNormalMatrix(this Matrix4x4 matrix) + { + if(!Matrix4x4.Invert(matrix, out Matrix4x4 result)) + { + throw new InvalidOperationException("Matrix failed to invert."); + } + + result = Matrix4x4.Transpose(result); + return result; + } + + + private static (Vector3 a, Vector3 b) MatrixToNormalizedEuler2(Matrix4x4 matrix, bool rotateZYX) + { + Vector3 a, b; + + if(rotateZYX) + { + float cx = float.Hypot(matrix.M33, matrix.M31); + + if(cx > 16f * 1.192092896e-07F) + { + a = new( + MathF.Atan2(-matrix.M32, cx), + MathF.Atan2(matrix.M31, matrix.M33), + MathF.Atan2(matrix.M12, matrix.M22) + ); + + b = new( + MathF.Atan2(-matrix.M32, -cx), + MathF.Atan2(-matrix.M31, -matrix.M33), + MathF.Atan2(-matrix.M12, -matrix.M22) + ); + } + else + { + a = b = new( + MathF.Atan2(-matrix.M32, cx), + MathF.Atan2(-matrix.M21, matrix.M11), + 0f + ); + } + } + else + { + float cy = float.Hypot(matrix.M11, matrix.M12); + + if(cy > 16f * 1.192092896e-07F) + { + a = new( + MathF.Atan2(matrix.M23, matrix.M33), + MathF.Atan2(-matrix.M13, cy), + MathF.Atan2(matrix.M12, matrix.M11) + ); + + b = new( + MathF.Atan2(-matrix.M23, -matrix.M33), + MathF.Atan2(-matrix.M13, -cy), + MathF.Atan2(-matrix.M12, -matrix.M11) + ); + } + else + { + a = new( + MathF.Atan2(-matrix.M32, matrix.M22), + MathF.Atan2(-matrix.M13, cy), + 0f + ); + b = a; + } + } + + return (a, b); + } + + private static Vector3 CompatibleEuler(Vector3 rotation, Vector3 previous) + { + const float piThreshold = 5.1f; + const float pi2 = 2 * MathF.PI; + + Vector3 dif = rotation - previous; + for(int i = 0; i < 3; i++) + { + if(dif[i] > piThreshold) + { + rotation[i] -= MathF.Floor((dif[i] / pi2) + 0.5f) * pi2; + } + else if(dif[i] < piThreshold) + { + rotation[i] += MathF.Floor((-dif[i] / pi2) + 0.5f) * pi2; + } + } + + dif = rotation - previous; + + for(int i = 0; i < 3; i++) + { + if(MathF.Abs(dif[i]) > 3.2f + && MathF.Abs(dif[(i + 1) % 3]) < 1.6f + && MathF.Abs(dif[(i + 2) % 3]) < 1.6f) + { + rotation[i] += dif[i] > 0.0 ? -pi2 : pi2; + } + + } + + return rotation; + } + + /// + /// Converts a matrix to euler angles relative to pre-existing euler angles, which ensures that there is no more than half a rotation between the two eulers. + /// + /// The matrix to convert. + /// The euler to be compatible to. + /// Whether the euler angles should be in ZYX order. + /// + public static Vector3 ToCompatibleEuler(Matrix4x4 matrix, Vector3 previous, bool rotateZYX) + { + (Vector3 a, Vector3 b) = MatrixToNormalizedEuler2(matrix, rotateZYX); + + a = CompatibleEuler(a, previous); + b = CompatibleEuler(b, previous); + + float d1 = MathF.Abs(a.X - previous.X) + MathF.Abs(a.Y - previous.Y) + MathF.Abs(a.Z - previous.Z); + float d2 = MathF.Abs(b.X - previous.X) + MathF.Abs(b.Y - previous.Y) + MathF.Abs(b.Z - previous.Z); + + return d1 > d2 ? b : a; + } + + /// + /// Converts a quaternion to euler angles. + /// + /// The matrix to convert. + /// Whether the euler angles should be in ZYX order. + /// The converted euler. + public static Vector3 ToEuler(Matrix4x4 matrix, bool rotateZYX) + { + (Vector3 a, _) = MatrixToNormalizedEuler2(matrix, rotateZYX); + return a; + } + } +} diff --git a/src/SA3D.Modeling/Structs/PointerLUT.cs b/src/SA3D.Modeling/Structs/PointerLUT.cs new file mode 100644 index 0000000..cc8b2b1 --- /dev/null +++ b/src/SA3D.Modeling/Structs/PointerLUT.cs @@ -0,0 +1,90 @@ +using SA3D.Common.Lookup; +using SA3D.Modeling.Animation; +using SA3D.Modeling.Mesh; +using SA3D.Modeling.Mesh.Chunk; +using SA3D.Modeling.ObjectData; +using System.Collections.Generic; + +namespace SA3D.Modeling.Structs +{ + /// + /// Pointer Lookup Table. + /// + public class PointerLUT : BaseLUT + { + /// + /// Pointer dictionary for nodes. + /// + public PointerDictionary Nodes { get; } + + /// + /// Pointer dictionary for attaches. + /// + public PointerDictionary Attaches { get; } + + /// + /// Pointer dictionary for motions. + /// + public PointerDictionary Motions { get; } + + /// + /// Pointer dictionary for nodemotions. + /// + public PointerDictionary NodeMotions { get; } + + /// + /// Pointer dictionary for polygon chunks. + /// + public PointerDictionary PolyChunks { get; } + + /// + /// Pointer dictionary for other objects. + /// + public PointerDictionary Other { get; } + + /// + /// Creates a new LUT with preexisting labels. + /// + /// The labels to populate the LUT with. + public PointerLUT(Dictionary labels) : base(labels) + { + Nodes = new(); + Attaches = new(); + Motions = new(); + NodeMotions = new(); + PolyChunks = new(); + Other = new(); + } + + /// + /// Creates a new empty LUT. + /// + public PointerLUT() : this(new()) { } + + /// + protected override void AddEntry(uint address, object value) + { + switch(value) + { + case Node node: + Nodes.Add(address, node); + break; + case Attach attach: + Attaches.Add(address, attach); + break; + case Motion motion: + Motions.Add(address, motion); + break; + case NodeMotion action: + NodeMotions.Add(address, action); + break; + case PolyChunk polychunk: + PolyChunks.Add(address, polychunk); + break; + default: + Other.Add(address, value); + break; + } + } + } +} diff --git a/src/SA3D.Modeling/Structs/PositionNormal.cs b/src/SA3D.Modeling/Structs/PositionNormal.cs new file mode 100644 index 0000000..577a202 --- /dev/null +++ b/src/SA3D.Modeling/Structs/PositionNormal.cs @@ -0,0 +1,35 @@ +using System; +using System.Numerics; + +namespace SA3D.Modeling.Structs +{ + internal struct PositionNormal : IEquatable + { + public Vector3 position; + + public Vector3 normal; + + public PositionNormal(Vector3 position, Vector3 normal) + { + this.position = position; + this.normal = normal; + } + + public override readonly bool Equals(object? obj) + { + return obj is PositionNormal normal && + position.Equals(normal.position) && + this.normal.Equals(normal.normal); + } + + public readonly bool Equals(PositionNormal other) + { + return Equals((object?)other); + } + + public override readonly int GetHashCode() + { + return HashCode.Combine(position, normal); + } + } +} diff --git a/src/SA3D.Modeling/Structs/QuaternionUtilities.cs b/src/SA3D.Modeling/Structs/QuaternionUtilities.cs new file mode 100644 index 0000000..79a4cc2 --- /dev/null +++ b/src/SA3D.Modeling/Structs/QuaternionUtilities.cs @@ -0,0 +1,136 @@ +using System; +using System.Numerics; +using static SA3D.Common.MathHelper; + +namespace SA3D.Modeling.Structs +{ + /// + /// Quaternion related utility methods. + /// + public static class QuaternionUtilities + { + /// + /// Converts a quaternion to euler angles. + /// + /// The quaternion to convert. + /// Whether the euler angles should be in ZYX order. + /// The converted euler. + public static Vector3 QuaternionToEuler(this Quaternion quaternion, bool rotateZYX) + { + // normalize the values + Quaternion vN = Quaternion.Normalize(quaternion); + float x = vN.X; + float y = vN.Y; + float z = vN.Z; + float w = vN.W; + + // this will have a magnitude of 0.5 or greater if and only if this is a singularity case + Vector3 v = new(); + if(rotateZYX) + { + float test = (w * x) - (y * z); + + if(test > 0.4995f) // singularity at north pole + { + v.Z = 2f * MathF.Atan2(z, x); + v.X = HalfPi; + } + else if(test < -0.4995f) // singularity at south pole + { + v.Z = -2f * MathF.Atan2(z, x); + v.X = -HalfPi; + } + else + { + v.X = MathF.Asin(2f * test); + v.Y = MathF.Atan2(2f * ((w * y) + (z * x)), 1 - (2f * ((y * y) + (x * x)))); + v.Z = MathF.Atan2(2f * ((w * z) + (y * x)), 1 - (2f * ((z * z) + (x * x)))); + } + + } + else + { + float test = (w * y) - (x * z); + + if(test > 0.4995f) // singularity at north pole + { + v.X = 2f * MathF.Atan2(x, y); + v.Y = HalfPi; + } + else if(test < -0.4995f) // singularity at south pole + { + v.X = -2f * MathF.Atan2(x, y); + v.Y = -HalfPi; + } + else + { + v.X = MathF.Atan2(2f * ((w * x) + (z * y)), 1 - (2f * ((y * y) + (x * x)))); + v.Y = MathF.Asin(2f * test); + v.Z = MathF.Atan2(2f * ((w * z) + (y * x)), 1 - (2f * ((z * z) + (y * y)))); + } + } + + static void normalize(ref float t) + { + t %= float.Tau; + if(t < -float.Pi) + { + t += float.Tau; + } + else if(t > float.Pi) + { + t -= float.Tau; + } + } + + normalize(ref v.X); + normalize(ref v.Y); + normalize(ref v.Z); + return v; + } + + /// + /// Converts a quaternion to euler angles relative to pre-existing euler angles, which ensures that there is no more than half a rotation between the two eulers. + /// + /// The quaternion to convert. + /// The euler to be compatible to. + /// Whether the euler angles should be in ZYX order. + /// + public static Vector3 QuaternionToCompatibleEuler(this Quaternion rotation, Vector3 previous, bool rotateZYX) + { + Matrix4x4 matrix = Matrix4x4.CreateFromQuaternion(Quaternion.Normalize(rotation)); + return MatrixUtilities.ToCompatibleEuler(matrix, previous, rotateZYX); + } + + /// + /// Converts euler angles to a quaternion. + /// + /// The euler angles to convert. + /// Whether the euler angles are applied in ZYX order. + /// The converted Quaternion. + public static Quaternion EulerToQuaternion(this Vector3 rotation, bool rotateZYX) + { + Matrix4x4 mtx = MatrixUtilities.CreateRotationMatrix(rotation, rotateZYX); + return Quaternion.CreateFromRotationMatrix(mtx); + } + + + /// + /// Lerps the individual components of two quaternions without additional checks. + /// + /// The quaternion from which to start interpolating. + /// The quaternion to interpolate to. + /// The time value to interpolate by. + /// The interpolated quaternion. + public static Quaternion RealLerp(Quaternion from, Quaternion to, float t) + { + float t1 = 1 - t; + return new( + (t1 * from.X) + (t * to.X), + (t1 * from.Y) + (t * to.Y), + (t1 * from.Z) + (t * to.Z), + (t1 * from.W) + (t * to.W)); + } + } +} + diff --git a/src/SA3D.Modeling/Structs/VectorUtilities.cs b/src/SA3D.Modeling/Structs/VectorUtilities.cs new file mode 100644 index 0000000..b35b78a --- /dev/null +++ b/src/SA3D.Modeling/Structs/VectorUtilities.cs @@ -0,0 +1,140 @@ +using SA3D.Common; +using System; +using System.Collections.Generic; +using System.Numerics; + +namespace SA3D.Modeling.Structs +{ + /// + /// Vector 3 Extensions + /// + public static class VectorUtilities + { + /// + /// Returns the greatest of the 3 values in a vector. + /// + public static float GreatestValue(this Vector3 vector) + { + float r = vector.X; + if(vector.Y > r) + { + r = vector.Y; + } + + if(vector.Z > r) + { + r = vector.Z; + } + + return r; + } + + /// + /// Calculates the average position of a collection of points. + /// + /// + /// + public static Vector3 CalculateAverage(Vector3[] points) + { + Vector3 center = new(); + + if(points == null || points.Length == 0) + { + return center; + } + + foreach(Vector3 p in points) + { + center += p; + } + + return center / points.Length; + } + + /// + /// Calculates the bounding box center of a collection of points. + /// + /// + /// + public static Vector3 CalculateCenter(IEnumerable points) + { + Vector3? first = null; + foreach(Vector3 point in points) + { + first = point; + break; + } + + if(first == null) + { + return default; + } + + Vector3 Positive = first.Value; + Vector3 Negative = first.Value; + + static void boundsCheck(float i, ref float p, ref float n) + { + if(i > p) + { + p = i; + } + else if(i < n) + { + n = i; + } + } + + foreach(Vector3 p in points) + { + boundsCheck(p.X, ref Positive.X, ref Negative.X); + boundsCheck(p.Y, ref Positive.Y, ref Negative.Y); + boundsCheck(p.Z, ref Positive.Z, ref Negative.Z); + } + + return (Positive + Negative) / 2; + } + + /// + /// Calculates the XZ euler angles necessary to rotate towards the given normal. + /// + /// The normal to get the rotation of. + /// The euler angle. + public static Vector3 NormalToXZAngles(this Vector3 normal) + { + bool close0 = MathF.Abs(normal.X) < 0.002f && MathF.Abs(normal.Y) < 0.002f; + + if(normal.Z > 0.9999f || (close0 && normal.Z > 0)) + { + return new(MathHelper.HalfPi, 0, 0); + } + else if(normal.Z < -0.9999f || (close0 && normal.Z < 0)) + { + return new(-MathHelper.HalfPi, 0, 0); + } + else + { + return new( + MathF.Asin(normal.Z), + 0, + -MathF.Atan2(normal.X, normal.Y) + ); + } + } + + /// + /// Rotates a by the given XZ euler angles. + /// + /// The angles to rotate by. + /// The calculated normal. + public static Vector3 XZAnglesToNormal(this Vector3 rotation) + { + float cos = MathF.Cos(rotation.X); + return new Vector3( + MathF.Sin(-rotation.Z) * cos, + MathF.Cos(-rotation.Z) * cos, + MathF.Sin(rotation.X) + ); + } + } +} diff --git a/src/SA3D.ProjectConfigurations b/src/SA3D.ProjectConfigurations new file mode 160000 index 0000000..72a639b --- /dev/null +++ b/src/SA3D.ProjectConfigurations @@ -0,0 +1 @@ +Subproject commit 72a639b6ee8c45397e0da29ef23e229ac1055e78