diff --git a/NifSkope.pro b/NifSkope.pro index dee05dc68..9dff53152 100644 --- a/NifSkope.pro +++ b/NifSkope.pro @@ -2,8 +2,12 @@ ## BUILD OPTIONS ############################### -TEMPLATE = vcapp -TARGET = NifSkope +*msvc* { + TEMPLATE = vcapp +} else { + TEMPLATE = app +} +TARGET = NifSkope QT += xml opengl network widgets @@ -82,24 +86,6 @@ VISUALSTUDIO = false include(NifSkope_functions.pri) -############################### -## MACROS -############################### - -# NifSkope Version -VER = $$getVersion() -# NifSkope Revision -REVISION = $$getRevision() - -# NIFSKOPE_VERSION macro -DEFINES += NIFSKOPE_VERSION=\\\"$${VER}\\\" - -# NIFSKOPE_REVISION macro -!isEmpty(REVISION) { - DEFINES += NIFSKOPE_REVISION=\\\"$${REVISION}\\\" -} - - ############################### ## OUTPUT DIRECTORIES ############################### @@ -147,6 +133,7 @@ HEADERS += \ src/gl/BSMesh.h \ src/gl/bsshape.h \ src/gl/controllers.h \ + src/gl/glcontrollable.h \ src/gl/glcontroller.h \ src/gl/glmarker.h \ src/gl/glmesh.h \ @@ -158,7 +145,6 @@ HEADERS += \ src/gl/gltex.h \ src/gl/gltexloaders.h \ src/gl/gltools.h \ - src/gl/icontrollable.h \ src/gl/renderer.h \ src/io/material.h \ src/io/MeshFile.h \ @@ -198,6 +184,8 @@ HEADERS += \ src/ui/checkablemessagebox.h \ src/ui/settingsdialog.h \ src/ui/settingspane.h \ + src/ui/ToolDialog.h \ + src/ui/UiUtils.h \ src/xml/nifexpr.h \ src/xml/xmlconfig.h \ src/gamemanager.h \ @@ -212,7 +200,8 @@ HEADERS += \ lib/json.hpp \ lib/stb_image.h \ lib/stb_image_write.h \ - lib/tiny_gltf.h + lib/tiny_gltf.h \ + lib/Miniball.hpp SOURCES += \ src/data/nifitem.cpp \ @@ -221,6 +210,7 @@ SOURCES += \ src/gl/BSMesh.cpp \ src/gl/bsshape.cpp \ src/gl/controllers.cpp \ + src/gl/glcontrollable.cpp \ src/gl/glcontroller.cpp \ src/gl/glmarker.cpp \ src/gl/glmesh.cpp \ @@ -290,6 +280,8 @@ SOURCES += \ src/ui/checkablemessagebox.cpp \ src/ui/settingsdialog.cpp \ src/ui/settingspane.cpp \ + src/ui/ToolDialog.cpp \ + src/ui/UiUtils.cpp \ src/xml/kfmxml.cpp \ src/xml/nifexpr.cpp \ src/xml/nifxml.cpp \ @@ -300,14 +292,12 @@ SOURCES += \ src/nifskope.cpp \ src/nifskope_ui.cpp \ src/spellbook.cpp \ - src/version.cpp \ lib/half.cpp RESOURCES += \ res/nifskope.qrc FORMS += \ - src/ui/about_dialog.ui \ src/ui/checkablemessagebox.ui \ src/ui/nifskope.ui \ src/ui/settingsdialog.ui \ @@ -362,6 +352,7 @@ gli { zlib { macx { DEFINES += Z_HAVE_UNISTD_H + QMAKE_CFLAGS += -fno-define-target-os-macros } !*msvc*:QMAKE_CFLAGS += -isystem ../nifskope/lib/zlib !*msvc*:QMAKE_CXXFLAGS += -isystem ../nifskope/lib/zlib @@ -436,9 +427,9 @@ win32 { } -# MinGW, GCC +# MinGW, GCC, clang # Recommended: GCC 4.8.1+ -*-g++ { +*-g++|*-clang { # COMPILER FLAGS @@ -471,20 +462,6 @@ macx { # Pre/Post Link in build_pass only build_pass|!debug_and_release { -############################### -## QMAKE_PRE_LINK -############################### - - # Find `sed` command - SED = $$getSed() - - !isEmpty(SED) { - # Replace @VERSION@ with number from build/VERSION - # Copy build/README.md.in > README.md - QMAKE_PRE_LINK += $${SED} -e s/@VERSION@/$${VER}/ $${PWD}/build/README.md.in > $${PWD}/README.md $$nt - } - - ############################### ## QMAKE_POST_LINK ############################### @@ -514,9 +491,12 @@ win32:contains(QT_ARCH, i386) { README.md \ README_GLTF.md + CONFIGS += \ + build/qt.conf + copyDirs( $$SHADERS, shaders ) #copyDirs( $$LANG, lang ) - copyFiles( $$XML $$QSS ) + copyFiles( $$XML $$QSS $$CONFIGS ) # Copy Readmes and rename to TXT copyFiles( $$READMES,,,, md:txt ) diff --git a/NifSkope_functions.pri b/NifSkope_functions.pri index f2b4d9268..82127e06c 100644 --- a/NifSkope_functions.pri +++ b/NifSkope_functions.pri @@ -80,52 +80,6 @@ defineReplace(get7z) { } -_VERSION = -_REVISION = - -# Retrieve NifSkope version -defineReplace(getVersion) { - # I turned this into a function because I didn't want - # the Version/Revision macros to have to straddle the - # includes. (VERSION needed to come before, REVISION after) - !isEmpty(_VERSION):return($$_VERSION) - - _VERSION = $$cat(build/VERSION) - export(_VERSION) - return($$_VERSION) -} - -# Retrieve NifSkope revision -defineReplace(getRevision) { - - !isEmpty(_REVISION):return($$_REVISION) - - GIT_HEAD = $$cat(.git/HEAD) - # At this point GIT_HEAD either contains commit hash, or symbolic ref: - # GIT_HEAD = 303c05416ecceb3368997c86676a6e63e968bc9b - # GIT_HEAD = ref: refs/head/feature/blabla - contains(GIT_HEAD, "ref:") { - # Resolve symbolic ref - GIT_HEAD = .git/$$member(GIT_HEAD, 1) - # GIT_HEAD now points to the file containing hash, - # e.g. .git/refs/head/feature/blabla - exists($$GIT_HEAD) { - GIT_HEAD = $$cat($$GIT_HEAD) - } else { - clear(GIT_HEAD) - } - } - count(GIT_HEAD, 1) { - # Single component, hopefully the commit hash - # Fetch first seven characters (abbreviated hash) - GIT_HEAD ~= s/^(.......).*/\\1/ - _REVISION = $$GIT_HEAD - export(_REVISION) - return($$_REVISION) - } - return() -} - # Format Qt Version defineReplace(QtHex) { diff --git a/NifSkope_targets.pri b/NifSkope_targets.pri index 05324c26b..a4b510da5 100644 --- a/NifSkope_targets.pri +++ b/NifSkope_targets.pri @@ -138,9 +138,7 @@ SED = $$getSed() unix:doxygen.commands += $${_7z} -o $${TAGS}$${QMAKE_DIR_SEP}tags.zip -d $${TAGS} $$nt } -doxygen.commands += $${SED} -e \"s%@VERSION@%$$getVersion()%g;\ - s%@REVISION@%$$getRevision()%g;\ - s%@OUTPUT@%$${OUTPUT}%g;\ +doxygen.commands += $${SED} -e \"s%@OUTPUT@%$${OUTPUT}%g;\ s%@INPUT@%$${INPUT}%g;\ s%@PWD@%$${ROOT}%g;\ s%@QT_VER@%$$QtHex()%g;\ diff --git a/README.md b/README.md index 7a30e8429..62402af42 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# NifSkope 2.0.dev9 +# NifSkope 2.0 Dev 9d (Gavrant) NifSkope is a tool for opening and editing the NetImmerse file format (NIF). NIF is used by video games such as Morrowind, Oblivion, Skyrim, Fallout 3/NV/4/76, Starfield, Civilization IV, and more. diff --git a/build/VERSION b/build/VERSION deleted file mode 100644 index 81b9f9a0c..000000000 --- a/build/VERSION +++ /dev/null @@ -1 +0,0 @@ -2.0.dev9 diff --git a/build/doxygen/Doxyfile.in b/build/doxygen/Doxyfile.in index 8ca900d52..4afe73ca5 100644 --- a/build/doxygen/Doxyfile.in +++ b/build/doxygen/Doxyfile.in @@ -1987,8 +1987,6 @@ PREDEFINED = UNICODE \ QT_NO_DEBUG \ Q_OS_WIN \ Q_OS_WIN32 \ - "NIFSKOPE_VERSION=\"@VERSION@\"" \ - "NIFSKOPE_REVISION=\"@REVISION@\"" \ "QT_VERSION=@QT_VER@" # If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this diff --git a/build/nif.xml b/build/nif.xml index 832a539e7..ef58996e1 100644 --- a/build/nif.xml +++ b/build/nif.xml @@ -1,106 +1,6 @@ - - - - Dark Age of Camelot - Star Trek: Bridge Commander - Dark Age of Camelot - Dark Age of Camelot, Star Trek: Bridge Commander - {{Munch's Oddysee}}, Oblivion - Freedom Force - {{Morrowind}}, {{Freedom Force}} - Dark Age of Camelot - Civilization IV - Dark Age of Camelot, Civilization IV - {{Culpa Innata}}, Civilization IV, Dark Age of Camelot, Empire Earth II - {{Zoo Tycoon 2}}, Civilization IV, Oblivion - Oblivion - {{Freedom Force vs. the 3rd Reich}}, {{Axis and Allies}}, {{Empire Earth II}}, {{Kohan 2}}, {{Sid Meier's Pirates!}}, Dark Age of Camelot, Civilization IV, Wildlife Park 2, The Guild 2, NeoSteam - Oblivion - Oblivion - {{Pro Cycling Manager}}, {{Prison Tycoon}}, {{Red Ocean}}, {{Wildlife Park 2}}, Civilization IV, Loki - {{Blood Bowl}} - Oblivion - WorldShift - WorldShift - {{WorldShift}} - {{Civilization IV}}, {{Sid Meier's Railroads}}, Florensia, Ragnarok Online 2, IRIS Online - {{Oblivion KF}} - Fallout 3 - {{Oblivion}} - {{Shin Megami Tensei: Imagine}} - {{Florensia}}, Empire Earth III, Atlantica Online, IRIS Online, Wizard101 - Fallout 3, Fallout NV - Fallout 3 - Fallout 3, Fallout NV - Fallout 3, Fallout NV - Fallout 3, Fallout NV - Fallout 3, Fallout NV - Fallout 3, Fallout NV - Fallout 3, Fallout NV - {{Fallout 3}}, {{Fallout NV}} - {{Skyrim}} - {{Skyrim SE}} - {{Fallout 4}} - Fallout 4 (LS_Mirelurk.nif, Screen.nif) - {{Fallout 76}} - {{Empire Earth III}}, {{FFT Online}}, Atlantica Online, IRIS Online, Wizard101 - Emerge - Emerge - Emerge - Emerge - {{Bully SE}}, Warhammer, Lazeska, Howling Sword, Ragnarok Online 2, Divinity 2 (0x10000) - {{Divinity 2}} - MicroVolts, KrazyRain - {{MicroVolts}}, {{IRIS Online}}, {{Ragnarok Online 2}}, KrazyRain, Atlantica Online, Wizard101 - Epic Mickey - Epic Mickey - Epic Mickey 2 - Emerge - Emerge - Rocksmith, Rocksmith 2014 - Ghost In The Shell: First Assault, MapleStory 2 - - - - - - - - - - - - - - - + Commonly used version expressions. @@ -119,6 +19,8 @@ FO3 and later. Skyrim and later. SSE and later. + Fallout 76 and later. + Starfield and later. SSE only. Fallout 4 strictly, excluding stream 132 and 139 in dev files. Fallout 4/76 including dev files. @@ -127,26 +29,28 @@ Bethesda 132 and later. Bethesda 152 and later. Fallout 76 stream 155 only. - Fallout 76 stream 152 and higher + SSE, FO4, FO76 Bethesda 20.2 only. Divinity 2 - - + + + - The set of versions that any Basic, Compound, NiObject, Enum, or Bitflags is restricted to. - + The set of versions that any Basic, Struct, NiObject, Enum, or Bitflags is restricted to. + - - + + + @@ -167,9 +71,17 @@ + + + + + + + + @@ -199,7 +111,7 @@ - + Global Tokens. NOTE: These must be listed after the above tokens so that they replace last. For example, `verexpr` uses these tokens. @@ -207,7 +119,7 @@ - + All Operators except for unary not (!), parentheses, and member of (\) NOTE: These can be ignored entirely by string substitution and dealt with directly. NOTE: These must be listed after the above tokens so that they replace last. For example, `verexpr` uses these tokens. @@ -229,6 +141,116 @@ + + + + + + + Dark Age of Camelot + Star Trek: Bridge Commander + Dark Age of Camelot + Dark Age of Camelot, Star Trek: Bridge Commander + {{Munch's Oddysee}}, Oblivion + Freedom Force + {{Morrowind}}, {{Freedom Force}} + Dark Age of Camelot + Civilization IV + Dark Age of Camelot, Civilization IV + {{Culpa Innata}}, Civilization IV, Dark Age of Camelot, Empire Earth II + {{Zoo Tycoon 2}}, Civilization IV, Oblivion + Oblivion + {{Freedom Force vs. the 3rd Reich}}, {{Axis and Allies}}, {{Empire Earth II}}, {{Kohan 2}}, {{Sid Meier's Pirates!}}, Dark Age of Camelot, Civilization IV, Wildlife Park 2, The Guild 2, NeoSteam + Oblivion + Oblivion + {{Pro Cycling Manager}}, {{Prison Tycoon}}, {{Red Ocean}}, {{Wildlife Park 2}}, Civilization IV, Loki + {{Blood Bowl}} + Oblivion + WorldShift + WorldShift + {{WorldShift}} + {{Civilization IV}}, {{Sid Meier's Railroads}}, Florensia, Ragnarok Online 2, IRIS Online + {{Oblivion KF}}, Oblivion + Oblivion, Fallout 3 + {{Oblivion}} + {{Shin Megami Tensei: Imagine}} + {{Florensia}}, Empire Earth III, Atlantica Online, IRIS Online, Wizard101 + Fallout 3, Fallout NV + Fallout 3 + Fallout 3, Fallout NV + Fallout 3, Fallout NV + Fallout 3, Fallout NV + Fallout 3, Fallout NV + Fallout 3, Fallout NV + Fallout 3, Fallout NV + {{Fallout 3}}, {{Fallout NV}} + {{Skyrim}} + {{Skyrim SE}} + {{Fallout 4}} + Fallout 4 (LS_Mirelurk.nif, Screen.nif) + {{Fallout 76}} + {{Starfield}} + {{Empire Earth III}}, {{FFT Online}}, Atlantica Online, IRIS Online, Wizard101 + QQSpeed + Emerge + Emerge + Emerge + Emerge + {{Bully SE}}, {{LEGO Universe}}, Warhammer, Lazeska, Howling Sword, Ragnarok Online 2, Divinity 2 (0x10000), Digimon Masters Online + {{Divinity 2}} + Fantasy Frontier, Aura Kingdom + {{Fantasy Frontier}}, {{Aura Kingdom}} + MicroVolts, KrazyRain + {{MicroVolts}}, {{IRIS Online}}, {{Ragnarok Online 2}}, KrazyRain, Atlantica Online, Wizard101, Archlord 2 + {{MXM}} + Epic Mickey + Epic Mickey + Epic Mickey 2 + Emerge + Emerge + Rocksmith, Rocksmith 2014 + Ghost In The Shell: First Assault, MapleStory 2 + + + + + + + + + + + + + + + + @@ -263,12 +285,20 @@ An 8-bit character. + + A signed 8-bit integer. + + An unsigned 8-bit integer. + + A float in the range -1.0:1.0, stored as a byte. + + - A boolean; 32-bit from 4.0.0.2, and 8-bit from 4.1.0.1 on. + A boolean; 32-bit up to and including 4.0.0.2, 8-bit from 4.1.0.1 on. @@ -327,6 +357,10 @@ A 32-bit unsigned integer, used to refer to strings in the header. + + Describes the options for the accum root on NiControllerSequence. @@ -544,9 +578,11 @@ - Bethesda Havok. Material descriptor for a Havok shape in Skyrim. + Bethesda Havok. Material descriptor for a Havok shape in Skyrim. CRC32 of the lowercase of the Creation Kit Material Name. + + @@ -559,6 +595,7 @@ + @@ -588,6 +625,7 @@ + @@ -595,6 +633,7 @@ + @@ -603,24 +642,19 @@ + + + + - - - - - @@ -783,10 +817,14 @@ - - - - + + + + + + + + @@ -1395,6 +1433,8 @@ + @@ -1508,10 +1548,10 @@ Flags for NiTimeController - + - + @@ -1520,7 +1560,7 @@ - + Bethesda-only. Always true for weapon blood after FO3. @@ -1609,6 +1649,10 @@ If Layer is CHARCONTROLLER (CC), true means "CC Trigger Only". + + A string of given length. The string length. @@ -1697,7 +1741,7 @@ The distance range where a specific level of detail applies. - Begining of range. + Beginning of range. End of Range. @@ -1722,11 +1766,18 @@ Third coordinate. - + A vector in 3D space (x,y,z). - First coordinate. - Second coordinate. - Third coordinate. + First coordinate. + Second coordinate. + Third coordinate. + + + + A vector in 3D space (x,y,z). + First coordinate. + Second coordinate. + Third coordinate. @@ -1907,9 +1958,9 @@ - + - + @@ -2029,17 +2080,17 @@ The bits of BSVertexDesc that describe the enabled vertex attributes. - @@ -2058,6 +2109,7 @@ + Byte fields for normal, tangent and bitangent map [0, 255] to [-1, 1]. @@ -2066,36 +2118,36 @@ - - - + + + - - - - - + + + + + - + - - - + + + - - - - - + + + + + Skinning data for a submesh, optimized for hardware skinning. Part of NiSkinPartition. Number of vertices in this submesh. - Number of triangles in this submesh. + Number of triangles in this submesh. Number of bones influencing this submesh. Number of strips in this submesh (zero if not stripped). Number of weight coefficients per vertex. The Gamebryo engine seems to work well only if this number is equal to 4, even if there are less than 4 influences per vertex. @@ -2107,7 +2159,7 @@ The vertex weights. The vertex weights. The strip lengths. - Do we have triangle or strip data? + Do we have triangle or strip data? The strips. The strips. The triangles. @@ -2183,15 +2235,22 @@ Furniture marker orientation. Refers to a furnituremarkerxx.nif file. Always seems to be the same as Position Ref 2. Refers to a furnituremarkerxx.nif file. Always seems to be the same as Position Ref 1. - Similar to Orientation, in float form. + Rotation around z-axis in radians. + + + + + + + Bethesda Havok. A triangle with extra data used for physics. The triangle. - Additional havok information on how triangles are welded. + Additional havok information on how triangles are welded. This is the triangle's normal. @@ -2289,7 +2348,7 @@ Motor damping value A factor of the current error to calculate the recovery velocity A constant velocity which is used to recover from errors - Is Motor enabled + Is Motor enabled @@ -2298,8 +2357,8 @@ Maximum motor force Relative stiffness - - Is Motor enabled + + Is Motor enabled @@ -2309,7 +2368,7 @@ Maximum motor force The spring constant in N/m The spring damping in Nsec/m - Is Motor enabled + Is Motor enabled @@ -2493,7 +2552,7 @@ - + @@ -2555,7 +2614,7 @@ Bethesda extension of hkpCompressedMeshShape::BigTriangle. Triangles that don't fit the maximum size. - + @@ -2563,21 +2622,21 @@ A vector that moves the chunk by the specified amount. W is not used. Rotation. Reference point for rotation is bhkRigidBody translation. - + Bethesda extension of hkpCompressedMeshShape::Chunk. A compressed chunk of hkpCompressedMeshShape geometry. Index of material in bhkCompressedMeshShapeData::Chunk Materials Index of another chunk in the chunks list. Index of transformation in bhkCompressedMeshShapeData::Chunk Transforms - - + Number of vertices, multiplied by 3. + Vertex positions in havok coordinates*1000. - + Vertex indices as used by strips. - - - + Length of strips longer than one triangle. + Generally the same as Num Indices field. + @@ -2608,6 +2667,13 @@ + + Abstract object type. @@ -2618,7 +2684,7 @@ - + @@ -2812,7 +2878,7 @@ A good choice is 5% - 20% of the smallest diameter of the object. Motion system? Overrides Quality when on Keyframed? - + The initial deactivator type of the body. How aggressively the engine will try to zero the velocity for slow objects. This does not save CPU. The type of interaction with other objects. @@ -2845,7 +2911,7 @@ Maximal linear velocity. Maximal angular velocity. Motion system? Overrides Quality when on Keyframed? - + The initial deactivator type of the body. How aggressively the engine will try to zero the velocity for slow objects. This does not save CPU. @@ -2950,7 +3016,7 @@ Bethesda extension of hkpBallSocketChainData. A chain of ball and socket constraints. - Should equal (Num Chained Entities - 1) * 2 + Should equal (Num Chained Entities - 1) * 2 Two pivot points A and B for each constraint. High values are harder and more reactive, lower values are smoother. Defines damping strength for the current velocity. @@ -3078,9 +3144,11 @@ Number of bytes for MOPP data. - XYZ: Origin of the object in mopp coordinates. This is the minimum of all vertices in the packed shape along each axis, minus 0.1. + XYZ: Origin of the object in mopp coordinates. This is the minimum of all vertices in the packed shape along each axis, minus the radius of the child bhkPackedNiTriStripsShape/ + bhkCompressedMeshShape. W: The scaling factor to quantize the MOPP: the quantization factor is equal to 256*256 divided by this number. - In Oblivion files, scale is taken equal to 256*256*254 / (size + 0.2) where size is the largest dimension of the bounding box of the packed shape. + In Oblivion and Skyrim files, scale is taken equal to 256*256*254 / (size + 2 * radius) where size is the largest dimension of the bounding box of the packed shape, + and radius is the radius of the child bhkPackedNiTriStripsShape/bhkCompressedMeshShape. Tells if MOPP Data was organized into smaller chunks (PS3) or not (PC) The tree of bounding volume data. @@ -3249,15 +3317,15 @@ - - - - - - - - - + + + + + + + + + @@ -3271,8 +3339,8 @@ - - + + @@ -3319,7 +3387,7 @@ - + bhkNiCollisionObject flags. 0x100 and 0x200 are only for bhkBlendCollisionObject - The data format of components. + The data format of components. Mask 0x00FF0000 to get the number of subfields. Mask 0x0000FF00 to get the size of each subfield. + It's not a bitfield, because the values are not independent. @@ -7090,6 +7170,7 @@ + @@ -7103,6 +7184,9 @@ + + The data stream as binary (fallback if interpretation with arg2 is not implemented). + @@ -7113,15 +7197,15 @@ The regions in the mesh. Regions can be used to mark off submeshes which are independent draw calls. Number of components of the data (matches corresponding field in MeshData). The format of each component in this data stream. - - + + Type of data (POSITION, POSITION_BP, INDEX, NORMAL, NORMAL_BP, TEXCOORD, BLENDINDICES, BLENDWEIGHT, BONE_PALETTE, COLOR, DISPLAYLIST, - MORPH_POSITION, BINORMAL_BP, TANGENT_BP). + MORPH_POSITION, BINORMAL_BP, TANGENT, TANGENT_BP). An extra index of the data. For example, if there are 3 uv maps, @@ -7132,7 +7216,7 @@ Reference to a data stream object which holds the data used by this reference. - Sets whether this stream data is per-instance data for use in hardware instancing. + Sets whether this stream data is per-instance data for use in hardware instancing. The number of submesh-to-region mappings that this data stream has. A lookup table that maps submeshes to regions. @@ -7195,9 +7279,9 @@ - - - + 1 if it has weights, 0 otherwise. + Total number of floats in the bone transform matrices - divide by 16 to get the number of matrices. + Transform matrices corresponding to the bones. Note: Stored transposed to normally. @@ -7268,7 +7352,7 @@ The number of bones referenced by this mesh modifier. Pointers to the bone nodes that affect this skin. The transforms that go from bind-pose space to bone space. - The bounds of the bones. Only stored if the RECOMPUTE_BOUNDS bit is set. + The bounds of the bones. Only stored if the RECOMPUTE_BOUNDS bit is set. @@ -7337,7 +7421,7 @@ - + @@ -7472,15 +7556,15 @@ The force type is set by each derived class and cannot be changed. - - - - - - - - - + + + + + + + + + @@ -7976,7 +8060,7 @@ The name of the NiAVObject serving as the accumulation root. This is where all accumulated translations, scales, and rotations are applied. - + @@ -8032,6 +8116,10 @@ + + Epic Mickey specific block. + + Compressed collision mesh. Points to root node? @@ -8052,7 +8140,7 @@ - + hkpCompressedMeshShape::MaterialType @@ -8098,8 +8186,8 @@ using the default values. Name should be 'INV' (without the quotes). For rotations, a short of "4712" appears as "4.712" but "959" appears as "0.959" meshes\weapons\daedric\daedricbowskinned.nif - - + + Zoom factor. @@ -8152,7 +8240,7 @@ Fallout 4 Tri Shape - + @@ -8188,7 +8276,7 @@ - > + @@ -8424,4 +8512,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 01 in the example nifs + + + + + Seemingly always zeros. + Always 3 in tested mesh. + Middle entry seemingly always 10^8, other two the same as LOD Distances + Entries corresponding to LODDistance specification in the block's NiStringExtraData, in the order of 1, 3, 2 + + + + + Guess is that this is the number of LOD entries, but unsure given that there was only one example. + + + + + + + + + Object name. + + + + + + + + + + A KFM without header + + + + + + + + + + + + + + + + diff --git a/build/qt.conf b/build/qt.conf new file mode 100644 index 000000000..af467c15b --- /dev/null +++ b/build/qt.conf @@ -0,0 +1,2 @@ +[Platforms] +WindowsArguments = dpiawareness=1 \ No newline at end of file diff --git a/lib/Miniball.hpp b/lib/Miniball.hpp new file mode 100644 index 000000000..71d6e90c4 --- /dev/null +++ b/lib/Miniball.hpp @@ -0,0 +1,525 @@ +// Copright (C) 1999-2021, Bernd Gaertner +// November 12, 2021 +// +// 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 . +// +// Contact: +// -------- +// Bernd Gaertner +// Institute of Theoretical Computer Science +// ETH Zuerich +// CAB G31.1 +// CH-8092 Zuerich, Switzerland +// http://www.inf.ethz.ch/personal/gaertner + +#include +#include +#include +#include +#include + +namespace Miniball { + + // Global Functions + // ================ + template + inline NT mb_sqr (NT r) {return r*r;} + + // Functors + // ======== + + // functor to map a point iterator to the corresponding coordinate iterator; + // generic version for points whose coordinate containers have begin() + template < typename Pit_, typename Cit_ > + struct CoordAccessor { + typedef Pit_ Pit; + typedef Cit_ Cit; + inline Cit operator() (Pit it) const { return (*it).begin(); } + }; + + // partial specialization for points whose coordinate containers are arrays + template < typename Pit_, typename Cit_ > + struct CoordAccessor { + typedef Pit_ Pit; + typedef Cit_* Cit; + inline Cit operator() (Pit it) const { return *it; } + }; + + // Class Declaration + // ================= + + template + class Miniball { + private: + // types + // The iterator type to go through the input points + typedef typename CoordAccessor::Pit Pit; + // The iterator type to go through the coordinates of a single point. + typedef typename CoordAccessor::Cit Cit; + // The coordinate type + typedef typename std::iterator_traits::value_type NT; + // The iterator to go through the support points + typedef typename std::list::iterator Sit; + + // data members... + const int d; // dimension + Pit points_begin; + Pit points_end; + CoordAccessor coord_accessor; + double time; + const NT nt0; // NT(0) + + //...for the algorithms + std::list L; + Sit support_end; + int fsize; // number of forced points + int ssize; // number of support points + + // ...for the ball updates + NT* current_c; + NT current_sqr_r; + NT** c; + NT* sqr_r; + + // helper arrays + NT* q0; + NT* z; + NT* f; + NT** v; + NT** a; + + // by how much do we allow points outside? + NT default_tol; + + public: + // The iterator type to go through the support points + typedef typename std::list::const_iterator SupportPointIterator; + + // PRE: [begin, end) is a nonempty range + // POST: computes the smallest enclosing ball of the points in the range + // [begin, end); the functor a maps a point iterator to an iterator + // through the d coordinates of the point + Miniball (int d_, Pit begin, Pit end, CoordAccessor ca = CoordAccessor()); + + // POST: returns a pointer to the first element of an array that holds + // the d coordinates of the center of the computed ball + const NT* center () const; + + // POST: returns the squared radius of the computed ball + NT squared_radius () const; + + // POST: returns the number of support points of the computed ball; + // the support points form a minimal set with the same smallest + // enclosing ball as the input set; in particular, the support + // points are on the boundary of the computed ball, and their + // number is at most d+1 + int nr_support_points () const; + + // POST: returns an iterator to the first support point + SupportPointIterator support_points_begin () const; + + // POST: returns a past-the-end iterator for the range of support points + SupportPointIterator support_points_end () const; + + // POST: returns the maximum excess of any input point w.r.t. the computed + // ball, divided by the squared radius of the computed ball. The + // excess of a point is the difference between its squared distance + // from the center and the squared radius; Ideally, the return value + // is 0. subopt is set to the absolute value of the most negative + // coefficient in the affine combination of the support points that + // yields the center. Ideally, this is a convex combination, and there + // is no negative coefficient in which case subopt is set to 0. + NT relative_error (NT& subopt) const; + + // POST: return true if the relative error is at most tol, and the + // suboptimality is 0; the default tolerance is 10 times the + // coordinate type's machine epsilon + bool is_valid () const; + + // POST: returns the time in seconds taken by the constructor call for + // computing the smallest enclosing ball + double get_time() const; + + // POST: deletes dynamically allocated arrays + ~Miniball(); + + private: + void mtf_mb (Sit n); + void mtf_move_to_front (Sit j); + void pivot_mb (Pit n); + void pivot_move_to_front (Pit j); + NT excess (Pit pit) const; + void pop (); + bool push (Pit pit); + NT suboptimality () const; + void create_arrays(); + void delete_arrays(); + }; + + // Class Definition + // ================ + template + Miniball::Miniball (int d_, Pit begin, Pit end, + CoordAccessor ca) + : d (d_), + points_begin (begin), + points_end (end), + coord_accessor (ca), + time (clock()), + nt0 (NT(0)), + L(), + support_end (L.begin()), + fsize(0), + ssize(0), + current_c (NULL), + current_sqr_r (NT(-1)), + c (NULL), + sqr_r (NULL), + q0 (NULL), + z (NULL), + f (NULL), + v (NULL), + a (NULL), + default_tol (NT(10) * std::numeric_limits::epsilon()) + { + assert (points_begin != points_end); + create_arrays(); + + // set initial center + for (int j=0; j + Miniball::~Miniball() + { + delete_arrays(); + } + + template + void Miniball::create_arrays() + { + c = new NT*[d+1]; + v = new NT*[d+1]; + a = new NT*[d+1]; + for (int i=0; i + void Miniball::delete_arrays() + { + delete[] f; + delete[] z; + delete[] q0; + delete[] sqr_r; + for (int i=0; i + const typename Miniball::NT* + Miniball::center () const + { + return current_c; + } + + template + typename Miniball::NT + Miniball::squared_radius () const + { + return current_sqr_r; + } + + template + int Miniball::nr_support_points () const + { + assert (ssize < d+2); + return ssize; + } + + template + typename Miniball::SupportPointIterator + Miniball::support_points_begin () const + { + return L.begin(); + } + + template + typename Miniball::SupportPointIterator + Miniball::support_points_end () const + { + return support_end; + } + + template + typename Miniball::NT + Miniball::relative_error (NT& subopt) const + { + NT e, max_e = nt0; + // compute maximum absolute excess of support points + for (SupportPointIterator it = support_points_begin(); + it != support_points_end(); ++it) { + e = excess (*it); + if (e < nt0) e = -e; + if (e > max_e) { + max_e = e; + } + } + // compute maximum excess of any point + for (Pit i = points_begin; i != points_end; ++i) + if ((e = excess (i)) > max_e) + max_e = e; + + subopt = suboptimality(); + assert (current_sqr_r > nt0 || max_e == nt0); + return (current_sqr_r == nt0 ? nt0 : max_e / current_sqr_r); + } + + template + bool Miniball::is_valid () const + { + NT suboptimality; + return ( (relative_error (suboptimality) <= default_tol) && (suboptimality == 0) ); + } + + template + double Miniball::get_time() const + { + return time; + } + + template + void Miniball::mtf_mb (Sit n) + { + // Algorithm 1: mtf_mb (L_{n-1}, B), where L_{n-1} = [L.begin, n) + // B: the set of forced points, defining the current ball + // S: the superset of support points computed by the algorithm + // -------------------------------------------------------------- + // from B. Gaertner, Fast and Robust Smallest Enclosing Balls, ESA 1999, + // http://www.inf.ethz.ch/personal/gaertner/texts/own_work/esa99_final.pdf + + // PRE: B = S + assert (fsize == ssize); + + support_end = L.begin(); + if ((fsize) == d+1) return; + + // incremental construction + for (Sit i = L.begin(); i != n;) + { + // INV: (support_end - L.begin() == |S|-|B|) + assert (std::distance (L.begin(), support_end) == ssize - fsize); + + Sit j = i++; + if (excess(*j) > nt0) + if (push(*j)) { // B := B + p_i + mtf_mb (j); // mtf_mb (L_{i-1}, B + p_i) + pop(); // B := B - p_i + mtf_move_to_front(j); + } + } + // POST: the range [L.begin(), support_end) stores the set S\B + } + + template + void Miniball::mtf_move_to_front (Sit j) + { + if (support_end == j) + support_end++; + L.splice (L.begin(), L, j); + } + + template + void Miniball::pivot_mb (Pit n) + { + // Algorithm 2: pivot_mb (L_{n-1}), where L_{n-1} = [L.begin, n) + // -------------------------------------------------------------- + // from B. Gaertner, Fast and Robust Smallest Enclosing Balls, ESA 1999, + // http://www.inf.ethz.ch/personal/gaertner/texts/own_work/esa99_final.pdf + const NT* c; + Pit pivot, k; + NT e, max_e, sqr_r; + Cit p; + unsigned int loops_without_progress = 0; + NT best_sqr_r = current_sqr_r; + do { + sqr_r = current_sqr_r; + + pivot = points_begin; + max_e = nt0; + for (k = points_begin; k != n; ++k) { + p = coord_accessor(k); + e = -sqr_r; + c = current_c; + for (int j=0; j(*p++-*c++); + if (e > max_e) { + max_e = e; + pivot = k; + } + } + + if (sqr_r < nt0 || max_e > nt0) { + // check if the pivot is already contained in the support set + if (std::find(L.begin(), support_end, pivot) == support_end) { + assert (fsize == 0); + if (push (pivot)) { + mtf_mb(support_end); + pop(); + pivot_move_to_front(pivot); + } + } + } + if (best_sqr_r < current_sqr_r) { + best_sqr_r = current_sqr_r; + loops_without_progress = 0; + } + else + ++loops_without_progress; + } while (loops_without_progress < 2); + } + + template + void Miniball::pivot_move_to_front (Pit j) + { + L.push_front(j); + if (std::distance(L.begin(), support_end) == d+2) + support_end--; + } + + template + inline typename Miniball::NT + Miniball::excess (Pit pit) const + { + Cit p = coord_accessor(pit); + NT e = -current_sqr_r; + NT* c = current_c; + for (int k=0; k(*p++-*c++); + } + return e; + } + + template + void Miniball::pop () + { + --fsize; + } + + template + bool Miniball::push (Pit pit) + { + int i, j; + NT eps = mb_sqr(std::numeric_limits::epsilon()); + + Cit cit = coord_accessor(pit); + Cit p = cit; + + if (fsize==0) { + for (i=0; i(v[fsize][j]); + z[fsize]*=2; + + // reject push if z_fsize too small + if (z[fsize](*p++-c[fsize-1][i]); + f[fsize]=e/z[fsize]; + + for (i=0; i + typename Miniball::NT + Miniball::suboptimality () const + { + NT* l = new NT[d+1]; + NT min_l = nt0; + l[0] = NT(1); + for (int i=ssize-1; i>0; --i) { + l[i] = f[i]; + for (int k=ssize-1; k>i; --k) + l[i]-=a[k][i]*l[k]; + if (l[i] < min_l) min_l = l[i]; + l[0] -= l[i]; + } + if (l[0] < min_l) min_l = l[0]; + delete[] l; + if (min_l < nt0) + return -min_l; + return nt0; + } + +} // end Namespace Miniball diff --git a/lib/gli b/lib/gli index 8e43030b3..779b99ac6 160000 --- a/lib/gli +++ b/lib/gli @@ -1 +1 @@ -Subproject commit 8e43030b3e12bb58a4663d85adc5c752f89099c0 +Subproject commit 779b99ac6656e4d30c3b24e96e0136a59649a869 diff --git a/lib/half.h b/lib/half.h index 75657d34c..a72235d6e 100644 --- a/lib/half.h +++ b/lib/half.h @@ -15,4 +15,23 @@ half_sub( uint16_t ha, uint16_t hb ) return half_add( ha, hb ^ 0x8000 ); } + +// Added for NifSkope - BEGIN + +inline float halfToFloat( uint16_t u ) +{ + union { float f; uint32_t i; } hv; + hv.i = half_to_float( u ); + return hv.f; +} + +inline uint16_t floatToHalf( float f ) +{ + union { float f; uint32_t i; } hv; + hv.f = f; + return half_from_float( hv.i ); +} + +// Added for NifSkope - END + #endif /* HALF_H */ diff --git a/lib/qhull b/lib/qhull index 9f0abe778..c2ef2209c 160000 --- a/lib/qhull +++ b/lib/qhull @@ -1 +1 @@ -Subproject commit 9f0abe778c8ae80951f14066dfc86e4d6d391177 +Subproject commit c2ef2209c28dc61ccfd22514971236587e820121 diff --git a/lib/zlib b/lib/zlib index 508932916..09155eaa2 160000 --- a/lib/zlib +++ b/lib/zlib @@ -1 +1 @@ -Subproject commit 50893291621658f355bc5b4d450a8d06a563053d +Subproject commit 09155eaa2f9270dc4ed1fa13e2b4b2613e6e4851 diff --git a/res/icon.rc b/res/icon.rc index d2afed6e9..ccde2ba39 100644 --- a/res/icon.rc +++ b/res/icon.rc @@ -1 +1,61 @@ -IDI_ICON1 ICON DISCARDABLE "icon.ico" +// Microsoft Visual C++ generated resource script. +// +#include "src/version.h" +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_ICON1 ICON "icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +VS_VERSION_INFO VERSIONINFO + FILEVERSION APP_VER_MAJOR,APP_VER_MINOR,APP_VER_REVISION,APP_VER_BUILD + PRODUCTVERSION APP_VER_MAJOR,APP_VER_MINOR,APP_VER_REVISION,APP_VER_BUILD + FILEFLAGSMASK 0x3fL +#ifdef _DEBUG + FILEFLAGS 0x1L +#else + FILEFLAGS 0x0L +#endif + FILEOS 0x40004L + FILETYPE 0x1L + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" + BEGIN + VALUE "CompanyName", APP_COMPANY + VALUE "FileDescription", APP_NAME + VALUE "FileVersion", APP_VER_FULL + VALUE "InternalName", "NifSkope.exe" + VALUE "LegalCopyright", "Copyright (c) 2005-2014, NIF File Format Library and Tools" + VALUE "OriginalFilename", "NifSkope.exe" + VALUE "ProductName", APP_NAME + VALUE "ProductVersion", APP_VER_FULL + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// diff --git a/res/nifskope.png b/res/nifskope.png index ece4bfaec..e68eadf59 100644 Binary files a/res/nifskope.png and b/res/nifskope.png differ diff --git a/res/shaders/sk_multilayer.frag b/res/shaders/sk_multilayer.frag index 2a4e85944..9ad4b3f74 100644 --- a/res/shaders/sk_multilayer.frag +++ b/res/shaders/sk_multilayer.frag @@ -1,4 +1,4 @@ -#version 120 +#version 130 uniform sampler2D BaseMap; uniform sampler2D NormalMap; @@ -37,16 +37,14 @@ uniform float outerReflection; uniform mat4 worldMatrix; -varying vec3 LightDir; -varying vec3 ViewDir; +in vec3 LightDir; +in vec3 ViewDir; -varying vec4 A; -varying vec4 C; -varying vec4 D; +in vec4 A; +in vec4 C; +in vec4 D; -varying vec3 N; -varying vec3 t; -varying vec3 b; +in mat3 tbnMatrix; vec3 tonemap(vec3 x) @@ -66,8 +64,8 @@ vec3 toGrayscale(vec3 color) return vec3(dot(vec3(0.3, 0.59, 0.11), color)); } -// Compute inner layer’s texture coordinate and transmission depth -// vTexCoord: Outer layer’s texture coordinate +// Compute inner layer’s texture coordinate and transmission depth +// vTexCoord: Outer layer’s texture coordinate // vInnerScale: Tiling of inner texture // vViewTS: View vector in tangent space // vNormalTS: Normal in tangent space (sampled normal map) @@ -87,7 +85,7 @@ vec3 ParallaxOffsetAndDepth( vec2 vTexCoord, vec2 vInnerScale, vec3 vViewTS, vec // introduced the additional parameter. vec2 vTexelSize = vec2( 1.0/(1024.0 * vInnerScale.x), 1.0/(1024.0 * vInnerScale.y) ); - // Inner layer’s texture coordinate due to parallax + // Inner layer’s texture coordinate due to parallax vec2 vOffset = vTexelSize * fTransDist * vTransTS.xy; vec2 vOffsetTexCoord = vTexCoord + vOffset; @@ -102,7 +100,7 @@ void main( void ) vec4 baseMap = texture2D( BaseMap, offset ); vec4 normalMap = texture2D( NormalMap, offset ); - vec3 normal = normalize(normalMap.rgb * 2.0 - 1.0); + vec3 normal = normalize(tbnMatrix * (normalMap.rgb * 2.0 - 1.0)); // Sample the non-parallax offset alpha channel of the inner map // Used to modulate the innerThickness @@ -128,8 +126,7 @@ void main( void ) vec4 innerMap = texture2D( InnerMap, parallax.xy * innerScale ); vec3 reflected = reflect( -E, normal ); - vec3 reflectedVS = b * reflected.x + t * reflected.y + N * reflected.z; - vec3 reflectedWS = vec3( worldMatrix * (gl_ModelViewMatrixInverse * vec4( reflectedVS, 0.0 )) ); + vec3 reflectedWS = vec3( worldMatrix * (gl_ModelViewMatrixInverse * vec4( reflected, 0.0 )) ); vec4 color; diff --git a/src/data/nifitem.h b/src/data/nifitem.h index 4c61d0c6e..6385c2785 100644 --- a/src/data/nifitem.h +++ b/src/data/nifitem.h @@ -676,29 +676,29 @@ class NifItem inline bool hasStrType( const char * testType ) const { return itemData.type() == QLatin1String(testType); } //! Set the name - inline void setName( const QString & name ) { itemData.setName( name ); } + inline void setName( const QString & name ) { itemData.setName( name ); } //! Set the string type inline void setStrType( const QString & type ) { itemData.setType( type ); } //! Set the template type inline void setTempl( const QString & temp ) { itemData.setTempl( temp ); } //! Set the argument attribute - inline void setArg( const QString & arg ) { itemData.setArg( arg ); } + inline void setArg( const QString & arg ) { itemData.setArg( arg ); } //! Set the first array length - inline void setArr1( const QString & arr1 ) { itemData.setArr1( arr1 ); } + inline void setArr1( const QString & arr1 ) { itemData.setArr1( arr1 ); } //! Set the second array length - inline void setArr2( const QString & arr2 ) { itemData.setArr2( arr2 ); } + inline void setArr2( const QString & arr2 ) { itemData.setArr2( arr2 ); } //! Set the condition attribute - inline void setCond( const QString & cond ) { itemData.setCond( cond ); } + inline void setCond( const QString & cond ) { itemData.setCond( cond ); } //! Set the earliest version attribute - inline void setVer1( int v1 ) { itemData.setVer1( v1 ); } + inline void setVer1( int v1 ) { itemData.setVer1( v1 ); } //! Set the latest version attribute - inline void setVer2( int v2 ) { itemData.setVer2( v2 ); } + inline void setVer2( int v2 ) { itemData.setVer2( v2 ); } //! Set the description text - inline void setText( const QString & text ) { itemData.setText( text ); } + inline void setText( const QString & text ) { itemData.setText( text ); } //! Set the version condition attribute - inline void setVerCond( const QString & cond ) { itemData.setVerCond( cond ); } + inline void setVerCond( const QString & cond ) { itemData.setVerCond( cond ); } inline void setIsConditionless( bool flag ) { itemData.setIsConditionless( flag ); } diff --git a/src/data/niftypes.cpp b/src/data/niftypes.cpp index 3c3f2336c..960a61a67 100644 --- a/src/data/niftypes.cpp +++ b/src/data/niftypes.cpp @@ -39,6 +39,24 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. //! @file niftypes.cpp Type functions +float normDegf( float deg ) +{ + while ( deg < 0.0f ) + deg += 360.0f; + while ( deg >= 360.0f ) + deg -= 360.0f; + return deg; +} + +double normDegd( double deg ) +{ + while ( deg < 0.0 ) + deg += 360.0; + while ( deg >= 360.0 ) + deg -= 360.0; + return deg; +} + const float Quat::identity[4] = { 1.0, 0.0, 0.0, 0.0 }; @@ -689,6 +707,7 @@ bool Transform::canConstruct( const NifModel * nif, const QModelIndex & parent ) Transform::Transform( const NifModel * nif, const QModelIndex & transform ) { + // TODO: Replace with Transform( NifFieldConst transformRoot ) everywhere. QModelIndex t = nif->getIndex( transform, "Transform" ); if ( !t.isValid() ) { t = nif->getIndex( transform, "Skin Transform" ); @@ -701,6 +720,20 @@ Transform::Transform( const NifModel * nif, const QModelIndex & transform ) scale = nif->get( t, "Scale" ); } +Transform::Transform( NifFieldConst dataRoot ) +{ + NifFieldConst transformRoot = dataRoot.child("Transform"); + if ( !transformRoot ) { + transformRoot = dataRoot.child("Skin Transform"); + if ( !transformRoot ) + transformRoot = dataRoot; + } + + rotation = transformRoot["Rotation"].value(); + translation = transformRoot["Translation"].value(); + scale = transformRoot["Scale"].value(); +} + void Transform::writeBack( NifModel * nif, const QModelIndex & transform ) const { QModelIndex t = nif->getIndex( transform, "Transform" ); diff --git a/src/data/niftypes.h b/src/data/niftypes.h index 0e2e7a870..8615ebed3 100644 --- a/src/data/niftypes.h +++ b/src/data/niftypes.h @@ -46,6 +46,8 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. //! @file niftypes.h Matrix, Matrix4, Triangle, Vector2, Vector3, Vector4, Color3, Color4, Quat +using NifSkopeFlagsType = unsigned int; + #ifndef PI #define PI M_PI #endif @@ -71,10 +73,24 @@ constexpr inline float deg2rad( float deg ) { return deg2radf( deg ); } //! Convert degrees to radians (double). constexpr inline double deg2rad( double deg ) { return deg2radd( deg ); } +//! Normalize angle value in degrees to 0 (inclusive)..360 (exclusive) range. +float normDegf( float deg ); +//! Normalize angle value in degrees to 0 (inclusive)..360 (exclusive) range. +double normDegd( double deg ); +//! Normalize angle value in degrees to 0 (inclusive)..360 (exclusive) range. +inline float normDeg( float deg ) { return normDegf( deg ); } +//! Normalize angle value in degrees to 0 (inclusive)..360 (exclusive) range. +inline double normDeg( double deg ) { return normDegd( deg ); } + +class NifItem; class NifModel; class QModelIndex; +template class NifFieldTemplate; +using NifField = NifFieldTemplate; +using NifFieldConst = NifFieldTemplate; + //! Format a float with out of range values QString NumOrMinMax( float val, char f = 'g', int prec = 6 ); @@ -236,6 +252,11 @@ inline QDataStream & operator>>( QDataStream & ds, Vector2 & v ) return ds; } +//! Texture coordinates (UV map) +using TexCoords = QVector; +Q_DECLARE_TYPEINFO(TexCoords, Q_MOVABLE_TYPE); + + //! A vector of 3 floats class Vector3 { @@ -1085,6 +1106,7 @@ class Transform * @param transform The index to create the transform from. */ Transform( const NifModel * nif, const QModelIndex & transform ); + Transform( NifFieldConst dataRoot ); //! Default constructor Transform() { scale = 1.0; } @@ -1123,54 +1145,56 @@ class Transform QString toString() const; }; +typedef quint16 TriVertexIndex; + //! A triangle class Triangle { public: + TriVertexIndex v[3]; + + static constexpr TriVertexIndex MAX_VERTEX_INDEX = 0xffff; + //! Default constructor Triangle() { v[0] = v[1] = v[2] = 0; } //! Constructor - Triangle( quint16 a, quint16 b, quint16 c ) { set( a, b, c ); } + Triangle( TriVertexIndex a, TriVertexIndex b, TriVertexIndex c ) { set( a, b, c ); } //! Array operator - quint16 & operator[]( unsigned int i ) + TriVertexIndex & operator[]( unsigned int i ) { Q_ASSERT( i < 3 ); return v[i]; } //! Const array operator - const quint16 & operator[]( unsigned int i ) const + const TriVertexIndex & operator[]( unsigned int i ) const { Q_ASSERT( i < 3 ); return v[i]; } //! Sets the vertices of the triangle - void set( quint16 a, quint16 b, quint16 c ) + void set( TriVertexIndex a, TriVertexIndex b, TriVertexIndex c ) { v[0] = a; v[1] = b; v[2] = c; } //! Gets the first vertex - inline quint16 v1() const { return v[0]; } + inline TriVertexIndex v1() const { return v[0]; } //! Gets the second vertex - inline quint16 v2() const { return v[1]; } + inline TriVertexIndex v2() const { return v[1]; } //! Gets the third vertex - inline quint16 v3() const { return v[2]; } + inline TriVertexIndex v3() const { return v[2]; } /*! Flips the triangle face * * Triangles are usually drawn anticlockwise(?); by changing the order of * the vertices the triangle is flipped. */ - void flip() { quint16 x = v[0]; v[0] = v[1]; v[1] = x; } + void flip() { auto x = v[0]; v[0] = v[1]; v[1] = x; } //! Add operator - Triangle operator+( quint16 d ) + Triangle operator+( TriVertexIndex d ) { - Triangle t( *this ); - t.v[0] += d; - t.v[1] += d; - t.v[2] += d; - return t; + return Triangle( v1() + d, v2() + d, v3() + d ); } //! Equality operator @@ -1178,13 +1202,6 @@ class Triangle { return (v[0] == other.v[0]) && (v[1] == other.v[1]) && (v[2] == other.v[2]); } - -protected: - quint16 v[3]; - friend class NifIStream; - friend class NifOStream; - - friend QDataStream & operator>>( QDataStream & ds, Triangle & t ); }; //! QDebug stream operator for Triangle @@ -1213,6 +1230,12 @@ inline float clamp01( float a ) return a; } + +//! Triangle strip +using TriStrip = QVector; +Q_DECLARE_TYPEINFO(TriStrip, Q_MOVABLE_TYPE); + + //! A 3 value color (RGB) class Color3 { @@ -1361,6 +1384,8 @@ class Color4 //! Constructor explicit Color4( const Color3 & c, float alpha = 1.0 ) { rgba[0] = c[0]; rgba[1] = c[1]; rgba[2] = c[2]; rgba[3] = alpha; } //! Constructor + explicit Color4( const Vector3 & c, float alpha = 1.0 ) { rgba[0] = c[0]; rgba[1] = c[1]; rgba[2] = c[2]; rgba[3] = alpha; } + //! Constructor explicit Color4( const QColor & c ) { fromQColor( c ); } //! Constructor Color4( float r, float g, float b, float a ) { setRGBA( r, g, b, a ); } diff --git a/src/gamemanager.cpp b/src/gamemanager.cpp index ded9f8654..ee8ff51a2 100644 --- a/src/gamemanager.cpp +++ b/src/gamemanager.cpp @@ -151,7 +151,7 @@ GameMode GameManager::get_game( uint32_t version, uint32_t user, uint32_t bsver case BSSTREAM_9: return OBLIVION; case BSSTREAM_11: - if ( user == 10 || version == 0x14000005 ) // TODO: Enumeration + if ( user == 10 || version <= 0x14000005 ) // TODO: Enumeration return OBLIVION; else if ( user == 11 ) return FALLOUT_3NV; diff --git a/src/gl/BSMesh.cpp b/src/gl/BSMesh.cpp index 276852187..7b4519a0b 100644 --- a/src/gl/BSMesh.cpp +++ b/src/gl/BSMesh.cpp @@ -5,13 +5,23 @@ #include "gl/renderer.h" #include "io/material.h" #include "io/nifstream.h" +#include "io/MeshFile.h" #include "model/nifmodel.h" #include #include -BSMesh::BSMesh(Scene* s, const QModelIndex& iBlock) : Shape(s, iBlock) +BoneWeightsUNorm::BoneWeightsUNorm(QVector> weights) +{ + weightsUNORM.resize(weights.size()); + for ( int i = 0; i < weights.size(); i++ ) { + weightsUNORM[i] = BoneWeightUNORM16(weights[i].first, weights[i].second / 65535.0); + } +} + +BSMesh::BSMesh( Scene * _scene, NifFieldConst _block ) + : Shape( _scene, _block ) { } @@ -33,7 +43,7 @@ void BSMesh::drawShapes(NodeList* secondPass, bool presort) auto nif = NifModel::fromIndex(iBlock); if ( lodLevel != scene->lodLevel ) { lodLevel = scene->lodLevel; - updateData(nif); + updateData(); } glPushMatrix(); @@ -51,10 +61,9 @@ void BSMesh::drawShapes(NodeList* secondPass, bool presort) if ( Node::SELECTING ) { if ( scene->isSelModeObject() ) { - int s_nodeId = ID2COLORKEY(nodeId); - glColor4ubv((GLubyte*)&s_nodeId); + glSelectionBufferColor( nodeId ); } else { - glColor4f(0, 0, 0, 1); + glColor4f( 0, 0, 0, 1 ); } } @@ -221,28 +230,6 @@ int BSMesh::meshCount() return meshes.size(); } -void BSMesh::drawVerts() const -{ - return; - glDisable(GL_LIGHTING); - glNormalColor(); - - glBegin(GL_POINTS); - for ( int i = 0; i < transVerts.count(); i++ ) { - if ( Node::SELECTING ) { - int id = ID2COLORKEY((shapeNumber << 16) + i); - glColor4ubv((GLubyte*)&id); - } - glVertex(transVerts.value(i)); - } - glEnd(); -} - -QModelIndex BSMesh::vertexAt(int) const -{ - return QModelIndex(); -} - void BSMesh::updateImpl(const NifModel* nif, const QModelIndex& index) { qDebug() << "updateImpl"; @@ -263,12 +250,9 @@ void BSMesh::updateImpl(const NifModel* nif, const QModelIndex& index) forMeshIndex(nif, createMeshFile); } -void BSMesh::updateData(const NifModel* nif) +void BSMesh::updateDataImpl() { qDebug() << "updateData"; - resetSkinning(); - resetVertexData(); - resetSkeletonData(); gpuLODs.clear(); boneNames.clear(); boneTransforms.clear(); @@ -283,7 +267,7 @@ void BSMesh::updateData(const NifModel* nif) qWarning() << "Both static and skeletal mesh LODs exist"; } - lodLevel = std::min(scene->lodLevel, Scene::LodLevel(lodCount - 1)); + lodLevel = std::min( scene->lodLevel, Scene::MAX_LOD_LEVEL_STARFIELD ); auto meshIndex = (hasMeshLODs) ? 0 : lodLevel; if ( lodCount > lodLevel ) { @@ -308,50 +292,50 @@ void BSMesh::updateData(const NifModel* nif) boundSphere.applyInv(viewTrans()); } - auto links = nif->getChildLinks(nif->getBlockNumber(iBlock)); + auto links = model->getChildLinks(model->getBlockNumber(iBlock)); for ( const auto link : links ) { - auto idx = nif->getBlockIndex(link); - if ( nif->blockInherits(idx, "BSShaderProperty") ) { - materialPath = nif->get(idx, "Name"); - } else if ( nif->blockInherits(idx, "NiIntegerExtraData") ) { - materialID = nif->get(idx, "Integer Data"); - } else if ( nif->blockInherits(idx, "BSSkin::Instance") ) { + auto idx = model->getBlockIndex(link); + if ( model->blockInherits(idx, "BSShaderProperty") ) { + materialPath = model->get(idx, "Name"); + } else if ( model->blockInherits(idx, "NiIntegerExtraData") ) { + materialID = model->get(idx, "Integer Data"); + } else if ( model->blockInherits(idx, "BSSkin::Instance") ) { iSkin = idx; - iSkinData = nif->getBlockIndex(nif->getLink(nif->getIndex(idx, "Data"))); - skinID = nif->getBlockNumber(iSkin); + iSkinData = model->getBlockIndex(model->getLink(model->getIndex(idx, "Data"))); + skinID = model->getBlockNumber(iSkin); - auto iBones = nif->getLinkArray(iSkin, "Bones"); + auto iBones = model->getLinkArray(iSkin, "Bones"); for ( const auto b : iBones ) { if ( b == -1 ) continue; - auto iBone = nif->getBlockIndex(b); - boneNames.append(nif->resolveString(iBone, "Name")); + auto iBone = model->getBlockIndex(b); + boneNames.append(model->resolveString(iBone, "Name")); } - auto numBones = nif->get(iSkinData, "Num Bones"); + auto numBones = model->get(iSkinData, "Num Bones"); boneTransforms.resize(numBones); - auto iBoneList = nif->getIndex(iSkinData, "Bone List"); + auto iBoneList = model->getIndex(iSkinData, "Bone List"); for ( int i = 0; i < numBones; i++ ) { auto iBone = iBoneList.child(i, 0); Transform trans; - trans.rotation = nif->get(iBone, "Rotation"); - trans.translation = nif->get(iBone, "Translation"); - trans.scale = nif->get(iBone, "Scale"); + trans.rotation = model->get(iBone, "Rotation"); + trans.translation = model->get(iBone, "Translation"); + trans.scale = model->get(iBone, "Scale"); boneTransforms[i] = trans; } } } // Do after dependent blocks above for ( const auto link : links ) { - auto idx = nif->getBlockIndex(link); - if ( nif->blockInherits(idx, "SkinAttach") ) { - boneNames = nif->getArray(idx, "Bones"); + auto idx = model->getBlockIndex(link); + if ( model->blockInherits(idx, "SkinAttach") ) { + boneNames = model->getArray(idx, "Bones"); if ( std::all_of(boneNames.begin(), boneNames.end(), [](const QString& name) { return name.isEmpty(); }) ) { boneNames.clear(); - auto iBones = nif->getLinkArray(nif->getIndex(iSkin, "Bones")); + auto iBones = model->getLinkArray(model->getIndex(iSkin, "Bones")); for ( const auto& b : iBones ) { - auto iBone = nif->getBlockIndex(b); - boneNames.append(nif->resolveString(iBone, "Name")); + auto iBone = model->getBlockIndex(b); + boneNames.append(model->resolveString(iBone, "Name")); } } } diff --git a/src/gl/BSMesh.h b/src/gl/BSMesh.h index 2875512ab..fcb86fc74 100644 --- a/src/gl/BSMesh.h +++ b/src/gl/BSMesh.h @@ -1,6 +1,6 @@ #pragma once #include "glshape.h" -#include "io/MeshFile.h" +// #include "io/MeshFile.h" #include #include @@ -9,16 +9,38 @@ class QByteArray; class NifModel; +class MeshFile; namespace tinygltf { class Model; } -class BSMesh : public Shape +//! A bone, weight pair +class BoneWeightUNORM16 final { +public: + BoneWeightUNORM16() + : bone( 0 ), weight( 0.0f ) {} + BoneWeightUNORM16(quint16 b, float w) + : bone( b ), weight( w ) {} + + quint16 bone; + float weight; +}; +class BoneWeightsUNorm : public SkinBone +{ public: - BSMesh(Scene* s, const QModelIndex& iBlock); + BoneWeightsUNorm() {} + BoneWeightsUNorm(QVector> weights); + + QVector weightsUNORM; +}; + +class BSMesh : public Shape +{ +public: + BSMesh( Scene * _scene, NifFieldConst _block ); // Node @@ -36,11 +58,6 @@ class BSMesh : public Shape // end Node - // Shape - - void drawVerts() const override; - QModelIndex vertexAt(int) const override; - QVector> meshes; int materialID = 0; @@ -54,7 +71,7 @@ class BSMesh : public Shape protected: void updateImpl(const NifModel* nif, const QModelIndex& index) override; - void updateData(const NifModel* nif) override; + void updateDataImpl() override; QModelIndex iMeshes; diff --git a/src/gl/bsshape.cpp b/src/gl/bsshape.cpp index 14a1500fb..6c0dc6f8c 100644 --- a/src/gl/bsshape.cpp +++ b/src/gl/bsshape.cpp @@ -1,40 +1,24 @@ #include "bsshape.h" - -#include "gl/glnode.h" #include "gl/glscene.h" -#include "gl/renderer.h" -#include "io/material.h" -#include "model/nifmodel.h" - -void BSShape::updateImpl( const NifModel * nif, const QModelIndex & index ) +BSShape::BSShape( Scene * _scene, NifFieldConst _block ) + : Shape( _scene, _block ) { - Shape::updateImpl( nif, index ); - - if ( index == iBlock ) { - isLOD = nif->isNiBlock( iBlock, "BSMeshLODTriShape" ); - if ( isLOD ) - emit nif->lodSliderChanged(true); - } } -void BSShape::updateData( const NifModel * nif ) +void BSShape::updateDataImpl() { - auto vertexFlags = nif->get(iBlock, "Vertex Desc"); + isSkinned = block.child("Vertex Desc").value().HasFlag(VertexAttribute::VA_SKINNING); + isDynamic = block.inherits("BSDynamicTriShape"); + sRGB = ( modelBSVersion() >= 151 ); - isDynamic = nif->blockInherits(iBlock, "BSDynamicTriShape"); - - hasVertexColors = vertexFlags.HasFlag(VertexAttribute::VA_COLOR); - - dataBound = BoundSphere(nif, iBlock); + dataBound = BoundSphere(block); // Is the shape skinned? - resetSkinning(); - if ( vertexFlags.HasFlag(VertexAttribute::VA_SKINNING) ) { - isSkinned = true; - + NifFieldConst skinBlock, skinDataBlock, skinPartBlock; + if ( isSkinned ) { QString skinInstName, skinDataName; - if ( nif->getBSVersion() >= 130 ) { + if ( modelBSVersion() >= 130 ) { skinInstName = "BSSkin::Instance"; skinDataName = "BSSkin::BoneData"; } else { @@ -42,762 +26,243 @@ void BSShape::updateData( const NifModel * nif ) skinDataName = "NiSkinData"; } - iSkin = nif->getBlockIndex( nif->getLink( nif->getIndex( iBlock, "Skin" ) ), skinInstName ); - if ( iSkin.isValid() ) { - iSkinData = nif->getBlockIndex( nif->getLink( iSkin, "Data" ), skinDataName ); - if ( nif->getBSVersion() == 100 ) - iSkinPart = nif->getBlockIndex( nif->getLink( iSkin, "Skin Partition" ), "NiSkinPartition" ); + skinBlock = block.child("Skin").linkBlock(skinInstName); + if ( skinBlock ) { + iSkin = skinBlock.toIndex(); // ??? + skinDataBlock = skinBlock.child("Data").linkBlock(skinDataName); + iSkinData = skinDataBlock.toIndex(); // ??? + if ( modelBSVersion() == 100 ) { + skinPartBlock = skinBlock.child("Skin Partition").linkBlock("NiSkinPartition"); + iSkinPart = skinPartBlock.toIndex(); // ??? + } } } // Fill vertex data - resetVertexData(); - numVerts = 0; - if ( isSkinned && iSkinPart.isValid() ) { + NifFieldConst vertexData; + if ( skinPartBlock ) { // For skinned geometry, the vertex data is stored in the NiSkinPartition // The triangles are split up among the partitions - iData = nif->getIndex( iSkinPart, "Vertex Data" ); - int dataSize = nif->get( iSkinPart, "Data Size" ); - int vertexSize = nif->get( iSkinPart, "Vertex Size" ); - if ( iData.isValid() && dataSize > 0 && vertexSize > 0 ) + vertexData = skinPartBlock.child("Vertex Data"); + int dataSize = skinPartBlock.child("Data Size").value(); + int vertexSize = skinPartBlock.child("Vertex Size").value(); + if ( vertexData && dataSize > 0 && vertexSize > 0 ) numVerts = dataSize / vertexSize; } else { - iData = nif->getIndex( iBlock, "Vertex Data" ); - if ( iData.isValid() ) - numVerts = nif->rowCount( iData ); + vertexData = block.child("Vertex Data"); + numVerts = vertexData.childCount(); } + iData = vertexData.toIndex(); // ??? + addVertexSelection( vertexData, VertexSelectionType::BS_VERTEX_DATA ); + mainVertexRoot = vertexData; TexCoords coordset; // For compatibility with coords list QVector dynVerts; if ( isDynamic ) { - dynVerts = nif->getArray( iBlock, "Vertices" ); - int nDynVerts = dynVerts.count(); - if ( nDynVerts < numVerts ) - numVerts = nDynVerts; - } + auto dynVertsRoot = block["Vertices"]; + addVertexSelection( dynVertsRoot, VertexSelectionType::VERTICES ); + reportFieldCountMismatch( dynVertsRoot, dynVertsRoot.childCount(), vertexData, numVerts, block ); + dynVerts = dynVertsRoot.array(); + if ( dynVerts.count() < numVerts ) + dynVerts.resize( numVerts ); + mainVertexRoot = dynVertsRoot; + } + + if ( numVerts > 0 ) { + // Pre-cache num. indices of all needed vertex fields to avoid looking them by string name for every vertex in the shape again and again. + auto firstVertex = vertexData[0]; + + int iVertexField = firstVertex.child("Vertex").row(); + int iNormalField = firstVertex.child("Normal").row(); + int iTangentField = firstVertex.child("Tangent").row(); + int iBitangentXField = firstVertex.child("Bitangent X").row(); + int iBitangentYField = firstVertex.child("Bitangent Y").row(); + int iBitangentZField = firstVertex.child("Bitangent Z").row(); + int iUVField = firstVertex.child("UV").row(); + int iColorField = firstVertex.child("Vertex Colors").row(); + + hasVertexNormals = ( iNormalField >= 0 ); + hasVertexTangents = ( iTangentField >= 0 ); + hasVertexBitangents = ( ( iBitangentXField >= 0 || isDynamic ) && ( iBitangentYField >= 0 ) && ( iBitangentZField >= 0 ) ); + hasVertexUVs = ( iUVField >= 0 ); + hasVertexColors = ( iColorField >= 0 ); + + verts.reserve(numVerts); + if ( hasVertexNormals ) + norms.reserve(numVerts); + if ( hasVertexTangents ) + tangents.reserve(numVerts); + if ( hasVertexBitangents ) + bitangents.reserve(numVerts); + if ( hasVertexUVs ) + coordset.reserve(numVerts); + if ( hasVertexColors ) + colors.reserve(numVerts); - for ( int i = 0; i < numVerts; i++ ) { - auto idx = nif->index( i, 0, iData ); - float bitX; + for ( int i = 0; i < numVerts; i++ ) { + float bitX; + auto vdata = vertexData[i]; + + if ( isDynamic ) { + auto & dynv = dynVerts.at(i); + verts << Vector3( dynv ); + bitX = dynv[3]; + } else { + verts << ( iVertexField >= 0 ? vdata[iVertexField].value() : Vector3() ); + bitX = ( iBitangentXField >= 0 ) ? vdata[iBitangentXField].value() : 0.0f; + } - if ( isDynamic ) { - auto& dynv = dynVerts.at(i); - verts << Vector3( dynv ); - bitX = dynv[3]; - } else { - verts << nif->get( idx, "Vertex" ); - bitX = nif->get( idx, "Bitangent X" ); + if ( hasVertexNormals ) + norms << vdata[iNormalField].value(); + if ( hasVertexTangents ) + tangents << vdata[iTangentField].value(); + if ( hasVertexBitangents ) + bitangents << Vector3( bitX, vdata[iBitangentYField].value(), vdata[iBitangentZField].value() ); + if ( hasVertexUVs ) + coordset << vdata[iUVField].value(); + if ( hasVertexColors ) + colors << vdata[iColorField].value(); } - - // Bitangent Y/Z - auto bitY = nif->get( idx, "Bitangent Y" ); - auto bitZ = nif->get( idx, "Bitangent Z" ); - - coordset << nif->get( idx, "UV" ); - norms += nif->get( idx, "Normal" ); - tangents += nif->get( idx, "Tangent" ); - bitangents += Vector3( bitX, bitY, bitZ ); - - auto vcIdx = nif->getIndex( idx, "Vertex Colors" ); - colors += vcIdx.isValid() ? nif->get( vcIdx ) : Color4(0, 0, 0, 1); } // Add coords as the first set of QList coords.append( coordset ); - numVerts = verts.count(); - - // Fill triangle data - if ( isSkinned && iSkinPart.isValid() ) { - auto iPartitions = nif->getIndex( iSkinPart, "Partitions" ); - if ( iPartitions.isValid() ) { - int n = nif->rowCount( iPartitions ); - for ( int i = 0; i < n; i++ ) - triangles << nif->getArray( nif->index( i, 0, iPartitions ), "Triangles" ); + // Fill triangle (and partition) data + if ( skinPartBlock ) { + for ( auto partEntry : skinPartBlock.child("Partitions").iter() ) { + TriangleRange * partTriRange = addTriangles( partEntry.child("Triangles") ); + addTriangleRange( partEntry, TriangleRange::FLAG_HIGHLIGHT, partTriRange->start, partTriRange->length ); + + auto vertexMapRoot = partEntry.child("Vertex Map"); + if ( vertexMapRoot.childCount() == 0 ) + vertexMapRoot = NifFieldConst(); + addVertexSelection( vertexMapRoot, VertexSelectionType::VERTICES, vertexMapRoot ); + addVertexSelection( partEntry.child("Vertex Weights"), VertexSelectionType::VERTICES, vertexMapRoot ); + addVertexSelection( partEntry.child("Bone Indices"), VertexSelectionType::VERTICES, vertexMapRoot ); + addPartitionBoneSelection( partEntry.child("Bones"), partTriRange ); } } else { - auto iTriData = nif->getIndex( iBlock, "Triangles" ); - if ( iTriData.isValid() ) - triangles = nif->getArray( iTriData ); + addTriangles( block.child("Triangles") ); } - // TODO (Gavrant): validate triangles' vertex indices, throw out triangles with the wrong ones // Fill skeleton data - resetSkeletonData(); - if ( isSkinned && iSkin.isValid() ) { - skeletonRoot = nif->getLink( iSkin, "Skeleton Root" ); - if ( nif->getBSVersion() < 130 ) - skeletonTrans = Transform( nif, iSkinData ); - - bones = nif->getLinkArray( iSkin, "Bones" ); - auto nTotalBones = bones.count(); - - weights.fill( BoneWeights(), nTotalBones ); - for ( int i = 0; i < nTotalBones; i++ ) - weights[i].bone = bones[i]; - auto nTotalWeights = weights.count(); - - for ( int i = 0; i < numVerts; i++ ) { - auto idx = nif->index( i, 0, iData ); - auto wts = nif->getArray( idx, "Bone Weights" ); - auto bns = nif->getArray( idx, "Bone Indices" ); - if ( wts.count() < 4 || bns.count() < 4 ) - continue; - - for ( int j = 0; j < 4; j++ ) { - if ( bns[j] >= nTotalWeights ) - continue; - - if ( wts[j] > 0.0 ) - weights[bns[j]].weights << VertexWeight( i, wts[j] ); - } - } - - auto b = nif->getIndex( iSkinData, "Bone List" ); - for ( int i = 0; i < nTotalWeights; i++ ) - weights[i].setTransform( nif, b.child( i, 0 ) ); - } -} - -QModelIndex BSShape::vertexAt( int idx ) const -{ - auto nif = NifModel::fromIndex( iBlock ); - if ( !nif ) - return QModelIndex(); - - // Vertices are on NiSkinPartition in version 100 - auto blk = iBlock; - if ( iSkinPart.isValid() ) { - if ( isDynamic ) - return nif->getIndex( blk, "Vertices" ).child( idx, 0 ); - - blk = iSkinPart; - } - - return nif->getIndex( nif->getIndex( blk, "Vertex Data" ).child( idx, 0 ), "Vertex" ); -} - -void BSShape::transformShapes() -{ - if ( isHidden() ) - return; - - auto nif = NifModel::fromValidIndex( iBlock ); - if ( !nif ) { - clear(); - return; - } - - Node::transformShapes(); - - transformRigid = true; - - if ( isSkinned && weights.count() && scene->hasOption(Scene::DoSkinning) ) { - transformRigid = false; - - transVerts.resize( numVerts ); - transVerts.fill( Vector3() ); - transNorms.resize( numVerts ); - transNorms.fill( Vector3() ); - transTangents.resize( numVerts ); - transTangents.fill( Vector3() ); - transBitangents.resize( numVerts ); - transBitangents.fill( Vector3() ); - - Node * root = findParent( 0 ); - for ( const BoneWeights & bw : weights ) { - Node * bone = root ? root->findChild( bw.bone ) : nullptr; - if ( bone ) { - Transform t = scene->view * bone->localTrans( 0 ) * bw.trans; - for ( const VertexWeight & w : bw.weights ) { - if ( w.vertex >= numVerts ) + if ( skinBlock ) { + // skeletonRoot = skinBlock.child("Skeleton Root").link(); + skeletonRoot = 0; // Always 0 + + if ( modelBSVersion() < 130 ) + skeletonTrans = Transform( skinDataBlock ); + + initSkinBones( skinBlock.child("Bones"), skinDataBlock.child("Bone List"), block ); + + // Read vertex weights from vertex data + int nBones = bones.count(); + if ( nBones > 0 && numVerts > 0 ) { + auto firstVertex = vertexData[0]; + int iIndicesField = firstVertex["Bone Indices"].row(); + int iWeightsField = firstVertex["Bone Weights"].row(); + + if ( iIndicesField >= 0 && iWeightsField >= 0 ) { + const int WEIGHTS_PER_VERTEX = 4; + for ( int vind = 0; vind < numVerts; vind++ ) { + auto vdata = vertexData[vind]; + auto vbones = vdata[iIndicesField]; + auto vweights = vdata[iWeightsField]; + if ( vbones.childCount() < WEIGHTS_PER_VERTEX || vweights.childCount() < WEIGHTS_PER_VERTEX ) continue; - transVerts[w.vertex] += t * verts[w.vertex] * w.weight; - transNorms[w.vertex] += t.rotation * norms[w.vertex] * w.weight; - transTangents[w.vertex] += t.rotation * tangents[w.vertex] * w.weight; - transBitangents[w.vertex] += t.rotation * bitangents[w.vertex] * w.weight; + for ( int wind = 0; wind < WEIGHTS_PER_VERTEX; wind++ ) { + float w = vweights[wind].value(); + if ( w <= 0.0f ) + continue; + int bind = vbones[wind].value(); + if ( bind >= nBones || bind < 0 ) { + vbones[wind].reportError( tr("Invalid bone index %1.").arg(bind) ); + continue; + } + bones[bind].vertexWeights << VertexWeight( vind, w ); + } } } } - - for ( int n = 0; n < numVerts; n++ ) { - transNorms[n].normalize(); - transTangents[n].normalize(); - transBitangents[n].normalize(); - } - - boundSphere = BoundSphere( transVerts ); - boundSphere.applyInv( viewTrans() ); - needUpdateBounds = false; - } else { - transVerts = verts; - transNorms = norms; - transTangents = tangents; - transBitangents = bitangents; } - transColors = colors; - // TODO (Gavrant): suspicious code. Should the check be replaced with !bssp.hasVertexAlpha ? - if ( nif->getBSVersion() < 130 && bslsp && !bslsp->hasSF1(ShaderFlags::SLSF1_Vertex_Alpha) ) { - for ( int c = 0; c < colors.count(); c++ ) - transColors[c] = Color4( colors[c].red(), colors[c].green(), colors[c].blue(), 1.0f ); + // LODs + if ( block.hasName("BSMeshLODTriShape") ) { + initLodData(); } -} -void BSShape::drawShapes( NodeList * secondPass, bool presort ) -{ - if ( isHidden() ) - return; + // Bounding sphere + addBoundSphereSelection( block.child("Bounding Sphere") ); - glPointSize( 8.5 ); + // Triangle segments (BSSegmentedTriShape, BSSubIndexTriShape) + for ( auto segEntry: block.child("Segment").iter() ) { + // TODO: validate ranges, with reportError - // TODO: Only run this if BSXFlags has "EditorMarkers present" flag - if ( !scene->hasOption(Scene::ShowMarkers) && name.contains( "EditorMarker" ) ) - return; + const TriangleRange * segRange = addTriangleRange( + segEntry, + TriangleRange::FLAG_HIGHLIGHT | TriangleRange::FLAG_DEEP, + segEntry["Start Index"].value() / 3, + segEntry["Num Primitives"].value() + ); - // Draw translucent meshes in second pass - if ( secondPass && drawInSecondPass ) { - secondPass->add( this ); - return; - } - - auto nif = NifModel::fromIndex( iBlock ); - - if ( Node::SELECTING ) { - if ( scene->isSelModeObject() ) { - int s_nodeId = ID2COLORKEY( nodeId ); - glColor4ubv( (GLubyte *)&s_nodeId ); - } else { - glColor4f( 0, 0, 0, 1 ); + for ( auto subSegEntry : segEntry.child("Sub Segment").iter() ) { + TriangleRange * subSegRange = addTriangleRange( + subSegEntry, + TriangleRange::FLAG_HIGHLIGHT | TriangleRange::FLAG_DEEP, + subSegEntry["Start Index"].value() / 3, + subSegEntry["Num Primitives"].value() + ); + if ( subSegRange ) + subSegRange->parentRange = segRange; } } - if ( transformRigid ) { - glPushMatrix(); - glMultMatrix( viewTrans() ); - } - - // Render polygon fill slightly behind alpha transparency and wireframe - glEnable( GL_POLYGON_OFFSET_FILL ); - if ( drawInSecondPass ) - glPolygonOffset( 0.5f, 1.0f ); - else - glPolygonOffset( 1.0f, 2.0f ); - - glEnableClientState( GL_VERTEX_ARRAY ); - glVertexPointer( 3, GL_FLOAT, 0, transVerts.constData() ); - - if ( !Node::SELECTING ) { - glEnableClientState( GL_NORMAL_ARRAY ); - glNormalPointer( GL_FLOAT, 0, transNorms.constData() ); - - bool doVCs = ( bssp && bssp->hasSF2(ShaderFlags::SLSF2_Vertex_Colors) ); - // Always do vertex colors for FO4 if colors present - if ( nif->getBSVersion() >= 130 && hasVertexColors && colors.count() ) - doVCs = true; - - if ( transColors.count() && scene->hasOption(Scene::DoVertexColors) && doVCs ) { - glEnableClientState( GL_COLOR_ARRAY ); - glColorPointer( 4, GL_FLOAT, 0, transColors.constData() ); - } else if ( nif->getBSVersion() < 130 && !hasVertexColors && (bslsp && bslsp->hasVertexColors) ) { - // Correctly blacken the mesh if SLSF2_Vertex_Colors is still on - // yet "Has Vertex Colors" is not. - glColor( Color3( 0.0f, 0.0f, 0.0f ) ); - } else { - glColor( Color3( 1.0f, 1.0f, 1.0f ) ); - } - } - - if ( !Node::SELECTING ) { - if ( nif->getBSVersion() >= 151 ) - glEnable( GL_FRAMEBUFFER_SRGB ); - else - glDisable( GL_FRAMEBUFFER_SRGB ); - shader = scene->renderer->setupProgram( this, shader ); - - } else { - if ( nif->getBSVersion() >= 151 ) - glDisable( GL_FRAMEBUFFER_SRGB ); - } - - if ( isDoubleSided ) { - glCullFace( GL_FRONT ); - glDrawElements( GL_TRIANGLES, triangles.count() * 3, GL_UNSIGNED_SHORT, triangles.constData() ); - glCullFace( GL_BACK ); - } - - if ( !isLOD ) { - glDrawElements( GL_TRIANGLES, triangles.count() * 3, GL_UNSIGNED_SHORT, triangles.constData() ); - } else if ( triangles.count() ) { - auto lod0 = nif->get( iBlock, "LOD0 Size" ); - auto lod1 = nif->get( iBlock, "LOD1 Size" ); - auto lod2 = nif->get( iBlock, "LOD2 Size" ); - - auto lod0tris = triangles.mid( 0, lod0 ); - auto lod1tris = triangles.mid( lod0, lod1 ); - auto lod2tris = triangles.mid( lod0 + lod1, lod2 ); - - // If Level2, render all - // If Level1, also render Level0 - switch ( scene->lodLevel ) { - case Scene::Level0: - if ( lod2tris.count() ) - glDrawElements( GL_TRIANGLES, lod2tris.count() * 3, GL_UNSIGNED_SHORT, lod2tris.constData() ); - case Scene::Level1: - if ( lod1tris.count() ) - glDrawElements( GL_TRIANGLES, lod1tris.count() * 3, GL_UNSIGNED_SHORT, lod1tris.constData() ); - case Scene::Level2: - default: - if ( lod0tris.count() ) - glDrawElements( GL_TRIANGLES, lod0tris.count() * 3, GL_UNSIGNED_SHORT, lod0tris.constData() ); + // BSPackedCombined... blocks + for ( auto extraEntry : block.child("Extra Data List").iter() ) { + auto extraBlock = extraEntry.linkBlock(); + if ( extraBlock.hasName("BSPackedCombinedGeomDataExtra", "BSPackedCombinedSharedGeomDataExtra") ) { + for ( auto dataEntry : extraBlock.child("Object Data").iter() ) { + for ( auto combinedEntry : dataEntry.child("Combined").iter() ) { + auto pSphere = addBoundSphereSelection( combinedEntry.child("Bounding Sphere") ); + // TODO: copied the code from the previous version of bsshape.cpp, rewrite + Transform t( combinedEntry.child("Transform") ); + pSphere->absoluteTransform = true; + pSphere->transform.rotation = t.rotation.inverted(); + pSphere->transform.translation = pSphere->sphere.center; + pSphere->transform.scale = t.scale; + pSphere->sphere.center = Vector3(); + } + } + iExtraData = extraBlock.toIndex(); // ??? break; } } - - if ( !Node::SELECTING ) - scene->renderer->stopProgram(); - - glDisableClientState( GL_VERTEX_ARRAY ); - glDisableClientState( GL_NORMAL_ARRAY ); - glDisableClientState( GL_COLOR_ARRAY ); - - glDisable( GL_POLYGON_OFFSET_FILL ); - - if ( scene->isSelModeVertex() ) { - drawVerts(); - } - - if ( transformRigid ) - glPopMatrix(); -} - -void BSShape::drawVerts() const -{ - glDisable( GL_LIGHTING ); - glNormalColor(); - - glBegin( GL_POINTS ); - - for ( int i = 0; i < numVerts; i++ ) { - if ( Node::SELECTING ) { - int id = ID2COLORKEY( ( shapeNumber << 16 ) + i ); - glColor4ubv( (GLubyte *)&id ); - } - glVertex( transVerts.value( i ) ); - } - - auto nif = NifModel::fromIndex( iBlock ); - if ( !nif ) - return; - - // Vertices are on NiSkinPartition in version 100 - bool selected = iBlock == scene->currentBlock; - if ( iSkinPart.isValid() ) { - selected |= iSkinPart == scene->currentBlock; - selected |= isDynamic; - } - - - // Highlight selected vertex - if ( !Node::SELECTING && selected ) { - auto idx = scene->currentIndex; - auto n = idx.data( Qt::DisplayRole ).toString(); - if ( n == "Vertex" || n == "Vertices" ) { - glHighlightColor(); - glVertex( transVerts.value( idx.parent().row() ) ); - } - } - - glEnd(); } -void BSShape::drawSelection() const +void BSShape::transformShapes() { - glDisable(GL_FRAMEBUFFER_SRGB); - if ( scene->hasOption(Scene::ShowNodes) ) - Node::drawSelection(); - - if ( isHidden() || !scene->isSelModeObject() ) - return; - - auto idx = scene->currentIndex; - auto blk = scene->currentBlock; - - // Is the current block extra data - bool extraData = false; - - auto nif = NifModel::fromValidIndex(blk); - if ( !nif ) - return; - - // Set current block name and detect if extra data - auto blockName = nif->itemName( blk ); - if ( blockName.startsWith( "BSPackedCombined" ) ) - extraData = true; - - // Don't do anything if this block is not the current block - // or if there is not extra data - if ( blk != iBlock && blk != iSkin && blk != iSkinData && blk != iSkinPart && !extraData ) - return; - - // Name of this index - auto n = idx.data( NifSkopeDisplayRole ).toString(); - // Name of this index's parent - auto p = idx.parent().data( NifSkopeDisplayRole ).toString(); - // Parent index - auto pBlock = nif->getBlockIndex( nif->getParent( blk ) ); - - auto push = [this] ( const Transform & t ) { - if ( transformRigid ) { - glPushMatrix(); - glMultMatrix( t ); - } - }; - - auto pop = [this] () { - if ( transformRigid ) - glPopMatrix(); - }; - - push( viewTrans() ); - - glDepthFunc( GL_LEQUAL ); - - glDisable( GL_LIGHTING ); - glDisable( GL_COLOR_MATERIAL ); - glDisable( GL_TEXTURE_2D ); - glDisable( GL_NORMALIZE ); - glEnable( GL_DEPTH_TEST ); - glDepthMask( GL_FALSE ); - glEnable( GL_BLEND ); - glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA ); - glDisable( GL_ALPHA_TEST ); - - glDisable( GL_CULL_FACE ); - - // TODO: User Settings - GLfloat lineWidth = 1.5; - GLfloat pointSize = 5.0; - - glLineWidth( lineWidth ); - glPointSize( pointSize ); - - glNormalColor(); - - glEnable( GL_POLYGON_OFFSET_FILL ); - glPolygonOffset( -1.0f, -2.0f ); - - float normalScale = bounds().radius / 20; - normalScale /= 2.0f; - - if ( normalScale < 0.1f ) - normalScale = 0.1f; - - - - // Draw All Verts lambda - auto allv = [this]( float size ) { - glPointSize( size ); - glBegin( GL_POINTS ); - - for ( int j = 0; j < transVerts.count(); j++ ) - glVertex( transVerts.value( j ) ); - - glEnd(); - }; - - if ( n == "Bounding Sphere" && !extraData ) { - auto sph = BoundSphere( nif, idx ); - if ( sph.radius > 0.0 ) { - glColor4f( 1, 1, 1, 0.33f ); - drawSphereSimple( sph.center, sph.radius, 72 ); - } - } - - if ( blockName.startsWith( "BSPackedCombined" ) && pBlock == iBlock ) { - QVector idxs; - if ( n == "Bounding Sphere" ) { - idxs += idx; - } else if ( n.startsWith( "BSPackedCombined" ) ) { - auto data = nif->getIndex( idx, "Object Data" ); - int dataCt = nif->rowCount( data ); - - for ( int i = 0; i < dataCt; i++ ) { - auto d = data.child( i, 0 ); - - auto c = nif->getIndex( d, "Combined" ); - int cCt = nif->rowCount( c ); - - for ( int j = 0; j < cCt; j++ ) { - idxs += nif->getIndex( c.child( j, 0 ), "Bounding Sphere" ); - } - } - } - - if ( !idxs.count() ) { - glPopMatrix(); - return; - } - - Vector3 pTrans = nif->get( pBlock.child( 1, 0 ), "Translation" ); - auto iBSphere = nif->getIndex( pBlock, "Bounding Sphere" ); - Vector3 pbvC = nif->get( iBSphere.child( 0, 2 ) ); - float pbvR = nif->get( iBSphere.child( 1, 2 ) ); - - if ( pbvR > 0.0 ) { - glColor4f( 0, 1, 0, 0.33f ); - drawSphereSimple( pbvC, pbvR, 72 ); - } - - glPopMatrix(); - - for ( auto i : idxs ) { - // Transform compound - auto iTrans = i.parent().child( 1, 0 ); - Matrix mat = nif->get( iTrans, "Rotation" ); - //auto trans = nif->get( iTrans, "Translation" ); - float scale = nif->get( iTrans, "Scale" ); - - Vector3 bvC = nif->get( i, "Center" ); - float bvR = nif->get( i, "Radius" ); - - Transform t; - t.rotation = mat.inverted(); - t.translation = bvC; - t.scale = scale; - - glPushMatrix(); - glMultMatrix( scene->view * t ); - - if ( bvR > 0.0 ) { - glColor4f( 1, 1, 1, 0.33f ); - drawSphereSimple( Vector3( 0, 0, 0 ), bvR, 72 ); - } - - glPopMatrix(); - } - - glPushMatrix(); - glMultMatrix( viewTrans() ); - } - - if ( n == "Vertex Data" || n == "Vertex" || n == "Vertices" ) { - allv( 5.0 ); - - int s = -1; - if ( (n == "Vertex Data" && p == "Vertex Data") - || (n == "Vertices" && p == "Vertices") ) { - s = idx.row(); - } else if ( n == "Vertex" ) { - s = idx.parent().row(); - } - - if ( s >= 0 ) { - glPointSize( 10 ); - glDepthFunc( GL_ALWAYS ); - glHighlightColor(); - glBegin( GL_POINTS ); - glVertex( transVerts.value( s ) ); - glEnd(); - } - } - - glPolygonMode( GL_FRONT_AND_BACK, GL_LINE ); - - // Draw Lines lambda - auto lines = [this, &normalScale, &allv, &lineWidth]( const QVector & v ) { - allv( 7.0 ); - - int s = scene->currentIndex.parent().row(); - glBegin( GL_LINES ); - - for ( int j = 0; j < transVerts.count() && j < v.count(); j++ ) { - glVertex( transVerts.value( j ) ); - glVertex( transVerts.value( j ) + v.value( j ) * normalScale * 2 ); - glVertex( transVerts.value( j ) ); - glVertex( transVerts.value( j ) - v.value( j ) * normalScale / 2 ); - } - - glEnd(); - - if ( s >= 0 ) { - glDepthFunc( GL_ALWAYS ); - glHighlightColor(); - glLineWidth( 3.0 ); - glBegin( GL_LINES ); - glVertex( transVerts.value( s ) ); - glVertex( transVerts.value( s ) + v.value( s ) * normalScale * 2 ); - glVertex( transVerts.value( s ) ); - glVertex( transVerts.value( s ) - v.value( s ) * normalScale / 2 ); - glEnd(); - glLineWidth( lineWidth ); - } - }; - - // Draw Normals - if ( n.contains( "Normal" ) ) { - lines( transNorms ); - } - - // Draw Tangents - if ( n.contains( "Tangent" ) ) { - lines( transTangents ); - } - - // Draw Triangles - if ( n == "Triangles" ) { - int s = scene->currentIndex.row(); - if ( s >= 0 ) { - glPolygonMode( GL_FRONT_AND_BACK, GL_FILL ); - glHighlightColor(); - - Triangle tri = triangles.value( s ); - glBegin( GL_TRIANGLES ); - glVertex( transVerts.value( tri.v1() ) ); - glVertex( transVerts.value( tri.v2() ) ); - glVertex( transVerts.value( tri.v3() ) ); - glEnd(); - glPolygonMode( GL_FRONT_AND_BACK, GL_LINE ); - } - } - - // Draw Segments/Subsegments - if ( n == "Segment" || n == "Sub Segment" || n == "Num Primitives" ) { - auto sidx = idx; - int s; - - QVector cols = { { 255, 0, 0, 128 }, { 0, 255, 0, 128 }, { 0, 0, 255, 128 }, { 255, 255, 0, 128 }, - { 0, 255, 255, 128 }, { 255, 0, 255, 128 }, { 255, 255, 255, 128 } - }; - - glPolygonMode( GL_FRONT_AND_BACK, GL_FILL ); - - auto type = idx.sibling( idx.row(), 1 ).data( Qt::DisplayRole ).toString(); - - bool isSegmentArray = (n == "Segment" && type == "BSGeometrySegmentData" && nif->isArray( idx )); - bool isSegmentItem = (n == "Segment" && type == "BSGeometrySegmentData" && !nif->isArray( idx )); - bool isSubSegArray = (n == "Sub Segment" && nif->isArray( idx )); - - int off = 0; - int cnt = 0; - int numRec = 0; - - int o = 0; - if ( isSegmentItem || isSegmentArray ) { - o = 3; // Offset 3 rows for < 130 BSGeometrySegmentData - } else if ( isSubSegArray ) { - o = -3; // Look 3 rows above for Sub Seg Array info - } - - int maxTris = triangles.count(); - - int loopNum = 1; - if ( isSegmentArray ) - loopNum = nif->rowCount( idx ); - - for ( int l = 0; l < loopNum; l++ ) { - - if ( n != "Num Primitives" && !isSubSegArray && !isSegmentArray ) { - sidx = idx.child( 1, 0 ); - } else if ( isSegmentArray ) { - sidx = idx.child( l, 0 ).child( 1, 0 ); - } - s = sidx.row() + o; - - off = sidx.sibling( s - 1, 2 ).data().toInt() / 3; - cnt = sidx.sibling( s, 2 ).data().toInt(); - numRec = sidx.sibling( s + 2, 2 ).data().toInt(); - - auto recs = sidx.sibling( s + 3, 0 ); - for ( int i = 0; i < numRec; i++ ) { - auto subrec = recs.child( i, 0 ); - int o = 0; - if ( subrec.data( Qt::DisplayRole ).toString() != "Sub Segment" ) - o = 3; // Offset 3 rows for < 130 BSGeometrySegmentData - - auto suboff = subrec.child( o, 2 ).data().toInt() / 3; - auto subcnt = subrec.child( o + 1, 2 ).data().toInt(); - - for ( int j = suboff; j < subcnt + suboff; j++ ) { - if ( j >= maxTris ) - continue; - - glColor( Color4( cols.value( i % 7 ) ) ); - Triangle tri = triangles[j]; - glBegin( GL_TRIANGLES ); - glVertex( transVerts.value( tri.v1() ) ); - glVertex( transVerts.value( tri.v2() ) ); - glVertex( transVerts.value( tri.v3() ) ); - glEnd(); - } - } - - // Sub-segmentless Segments - if ( numRec == 0 && cnt > 0 ) { - glColor( Color4( cols.value( (idx.row() + l) % 7 ) ) ); - - for ( int i = off; i < cnt + off; i++ ) { - if ( i >= maxTris ) - continue; - - Triangle tri = triangles[i]; - glBegin( GL_TRIANGLES ); - glVertex( transVerts.value( tri.v1() ) ); - glVertex( transVerts.value( tri.v2() ) ); - glVertex( transVerts.value( tri.v3() ) ); - glEnd(); - } - } - } - - pop(); + if ( isHidden() ) return; - } - - // Draw all bones' bounding spheres - if ( n == "NiSkinData" || n == "BSSkin::BoneData" ) { - // Get shape block - if ( nif->getBlockIndex( nif->getParent( nif->getParent( blk ) ) ) == iBlock ) { - auto iBones = nif->getIndex( blk, "Bone List" ); - int ct = nif->rowCount( iBones ); - for ( int i = 0; i < ct; i++ ) { - auto b = iBones.child( i, 0 ); - boneSphere( nif, b ); - } - } - pop(); + auto nif = NifModel::fromValidIndex( iBlock ); + if ( !nif ) { + clear(); return; } - // Draw bone bounding sphere - if ( n == "Bone List" ) { - if ( nif->isArray( idx ) ) { - for ( int i = 0; i < nif->rowCount( idx ); i++ ) - boneSphere( nif, idx.child( i, 0 ) ); - } else { - boneSphere( nif, idx ); - } - } + Node::transformShapes(); - // General wireframe - if ( blk == iBlock && idx != iData && p != "Vertex Data" && p != "Vertices" ) { - glLineWidth( 1.6f ); - glNormalColor(); - for ( const Triangle& tri : triangles ) { - glBegin( GL_TRIANGLES ); - glVertex( transVerts.value( tri.v1() ) ); - glVertex( transVerts.value( tri.v2() ) ); - glVertex( transVerts.value( tri.v3() ) ); - glEnd(); - } + if ( doSkinning() ) { + applySkinningTransforms( scene->view ); + } else { + applyRigidTransforms(); } - glDisable( GL_POLYGON_OFFSET_FILL ); - - pop(); + // Colors + applyColorTransforms(); } BoundSphere BSShape::bounds() const diff --git a/src/gl/bsshape.h b/src/gl/bsshape.h index 514a88f49..b3ac01e21 100644 --- a/src/gl/bsshape.h +++ b/src/gl/bsshape.h @@ -4,38 +4,26 @@ #include "gl/glshape.h" -class NifModel; -class NodeList; - +// Nodes of type BSTriShape (FO4/SKSE+) class BSShape : public Shape { - public: - BSShape( Scene * s, const QModelIndex & b ) : Shape( s, b ) { } + BSShape( Scene * _scene, NifFieldConst _block ); // Node void transformShapes() override; - void drawShapes( NodeList * secondPass = nullptr, bool presort = false ) override; - void drawSelection() const override; - BoundSphere bounds() const override; // end Node - // Shape - - void drawVerts() const override; - QModelIndex vertexAt( int ) const override; - protected: - BoundSphere dataBound; + BoundSphere dataBound; // TODO: move to Shape, replace with a pointer to BoundSphereSelection? bool isDynamic = false; - void updateImpl( const NifModel * nif, const QModelIndex & index ) override; - void updateData( const NifModel * nif ) override; + void updateDataImpl() override; }; #endif // BSSHAPE_H diff --git a/src/gl/controllers.cpp b/src/gl/controllers.cpp index 7d01d0f44..53ba4de83 100644 --- a/src/gl/controllers.cpp +++ b/src/gl/controllers.cpp @@ -32,732 +32,921 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "controllers.h" -#include "gl/glshape.h" -#include "gl/glparticles.h" -#include "gl/glproperty.h" #include "gl/glscene.h" -#include "model/nifmodel.h" -// `NiControllerManager` blocks +// ControllerManager class -ControllerManager::ControllerManager( Node * node, const QModelIndex & index ) - : Controller( index ), target( node ) +ControllerManager::ControllerManager( Node * _parent, NifFieldConst ctrlBlock ) + : Controller( ctrlBlock ), parent( _parent ) { + Q_ASSERT( hasParent() ); } -bool ControllerManager::update( const NifModel * nif, const QModelIndex & index ) +void ControllerManager::updateImpl( NifFieldConst changedBlock ) { - if ( Controller::update( nif, index ) ) { - if ( target ) { - Scene * scene = target->scene; - QVector lSequences = nif->getLinkArray( index, "Controller Sequences" ); - for ( const auto l : lSequences ) { - QModelIndex iSeq = nif->getBlockIndex( l, "NiControllerSequence" ); + Controller::updateImpl( changedBlock ); + + if ( changedBlock == block && hasParent() ) { + Scene * scene = parent->scene; + for ( auto seqEntry : block.child("Controller Sequences").iter() ) { + auto seqBlock = seqEntry.linkBlock("NiControllerSequence"); + if ( !seqBlock ) + continue; + + QString seqName = seqBlock.child("Name").value(); + if ( !scene->animGroups.contains( seqName ) ) { + scene->animGroups.append( seqName ); + + QMap tags = scene->animTags[seqName]; + auto keyBlock = seqBlock.child("Text Keys").linkBlock("NiTextKeyExtraData"); + for ( auto keyEntry: keyBlock.child("Text Keys").iter() ) + tags.insert( keyEntry["Value"].value(), keyEntry["Time"].value() ); + scene->animTags[seqName] = tags; + } + } + } +} - if ( iSeq.isValid() ) { - QString name = nif->get( iSeq, "Name" ); +void ControllerManager::setSequence( const QString & seqName ) +{ + if ( !hasParent() ) + return; - if ( !scene->animGroups.contains( name ) ) { - scene->animGroups.append( name ); + MultiTargetTransformController * multiTargetTransformer = nullptr; + for ( Controller * c : parent->controllers ) { + if ( c->typeId() == QStringLiteral("NiMultiTargetTransformController") ) { + multiTargetTransformer = static_cast(c); + break; + } + } - QMap tags = scene->animTags[name]; + MorphController * curMorphController = nullptr; + int nextMorphIndex = 0; + + // TODO: All of the below does not work well with block updates + for ( auto seqEntry : block.child("Controller Sequences").iter() ) { + auto seqBlock = seqEntry.linkBlock("NiControllerSequence"); + if ( !seqBlock || seqBlock["Name"].value() != seqName ) + continue; + + start = seqBlock.child("Start Time").value(); + stop = seqBlock.child("Stop Time").value(); + phase = seqBlock.child("Phase").value(); + frequency = seqBlock.child("Frequency").value(); + + for ( auto ctrlBlockEntry : seqBlock.child("Controlled Blocks").iter() ) { + auto resolveStrField = [ctrlBlockEntry]( const QString & strName, const QString & offsetName ) -> QString { + auto strField = ctrlBlockEntry.child(strName); + if ( strField ) + return strField.value(); + + auto offsetField = ctrlBlockEntry.child(offsetName); + if ( offsetField ) { + QModelIndex iOffset = offsetField.toIndex(); + return iOffset.sibling( iOffset.row(), NifModel::ValueCol ).data( NifSkopeDisplayRole ).toString(); + } - QModelIndex iKeys = nif->getBlockIndex( nif->getLink( iSeq, "Text Keys" ), "NiTextKeyExtraData" ); - QModelIndex iTags = nif->getIndex( iKeys, "Text Keys" ); + return QString(); + }; - for ( int r = 0; r < nif->rowCount( iTags ); r++ ) { - tags.insert( nif->get( iTags.child( r, 0 ), "Value" ), nif->get( iTags.child( r, 0 ), "Time" ) ); - } + QString nodeName; + auto targetNameField = ctrlBlockEntry.child("Target Name"); + if ( targetNameField ) { + nodeName = targetNameField.value(); + } else { + nodeName = resolveStrField( QStringLiteral("Node Name"), QStringLiteral("Node Name Offset") ); + } + if ( nodeName.isEmpty() ) + continue; + Node * node = parent->findChild( nodeName ); + if ( !node ) + continue; + + auto interpBlock = ctrlBlockEntry.child("Interpolator").linkBlock("NiInterpolator"); + auto controllerBlock = ctrlBlockEntry.child("Controller").linkBlock("NiTimeController"); + + QString ctrlType = resolveStrField( QStringLiteral("Controller Type"), QStringLiteral("Controller Type Offset") ); + if ( ctrlType.isEmpty() && controllerBlock ) + ctrlType = controllerBlock.name(); + + if ( multiTargetTransformer && ctrlType == QStringLiteral("NiTransformController") ) { + if ( multiTargetTransformer->setNodeInterpolator( node, interpBlock ) ) { + multiTargetTransformer->start = start; + multiTargetTransformer->stop = stop; + multiTargetTransformer->phase = phase; + multiTargetTransformer->frequency = frequency; + continue; + } + } - scene->animTags[name] = tags; + if ( ctrlType == QStringLiteral("NiGeomMorpherController") ) { + auto ctrl = node->findController( controllerBlock ); + if ( ctrl && ctrl->typeId() == ctrlType ) { + if ( ctrl != curMorphController ) { + curMorphController = static_cast( ctrl ); + nextMorphIndex = 0; } + curMorphController->setMorphInterpolator( nextMorphIndex, interpBlock ); + nextMorphIndex++; } + continue; } - } - return true; - } + QString propType = resolveStrField( QStringLiteral("Property Type"), QStringLiteral("Property Type Offset") ); + + if ( ctrlType == QStringLiteral("BSLightingShaderPropertyFloatController") + || ctrlType == QStringLiteral("BSLightingShaderPropertyColorController") + || ctrlType == QStringLiteral("BSEffectShaderPropertyFloatController") + || ctrlType == QStringLiteral("BSEffectShaderPropertyColorController") + || ctrlType == QStringLiteral("BSNiAlphaPropertyTestRefController") ) + { + auto ctrl = node->findPropertyController( propType, controllerBlock ); + if ( ctrl ) + ctrl->setInterpolator( interpBlock ); + continue; + } - return false; -} + QString var1 = resolveStrField( QStringLiteral("Controller ID"), QStringLiteral("Controller ID Offset") ); + QString var2 = resolveStrField( QStringLiteral("Interpolator ID"), QStringLiteral("Interpolator ID Offset") ); + Controller * ctrl = node->findPropertyController( propType, ctrlType, var1, var2 ); + if ( ctrl ) { + ctrl->start = start; + ctrl->stop = stop; + ctrl->phase = phase; + ctrl->frequency = frequency; -void ControllerManager::setSequence( const QString & seqname ) -{ - auto nif = NifModel::fromValidIndex(iBlock); - if ( nif && target ) { - MultiTargetTransformController * multiTargetTransformer = 0; - for ( Controller * c : target->controllers ) { - if ( c->typeId() == "NiMultiTargetTransformController" ) { - multiTargetTransformer = static_cast(c); - break; + ctrl->setInterpolator( interpBlock ); } } + } +} - QVector lSequences = nif->getLinkArray( iBlock, "Controller Sequences" ); - for ( const auto l : lSequences ) { - QModelIndex iSeq = nif->getBlockIndex( l, "NiControllerSequence" ); - if ( iSeq.isValid() && nif->get( iSeq, "Name" ) == seqname ) { - start = nif->get( iSeq, "Start Time" ); - stop = nif->get( iSeq, "Stop Time" ); - phase = nif->get( iSeq, "Phase" ); - frequency = nif->get( iSeq, "Frequency" ); +// TransformInterpolator class - QModelIndex iCtrlBlcks = nif->getIndex( iSeq, "Controlled Blocks" ); +void TransformInterpolator::updateDataImpl() +{ + auto dataBlock = interpolatorBlock.child("Data").linkBlock("NiKeyframeData"); + registerUpdateBlock( dataBlock ); - for ( int r = 0; r < nif->rowCount( iCtrlBlcks ); r++ ) { - QModelIndex iCB = iCtrlBlcks.child( r, 0 ); + rotation.updateData( dataBlock ); + translation.updateData( dataBlock["Translations"] ); + scale.updateData( dataBlock["Scales"] ); +} - QModelIndex iInterp = nif->getBlockIndex( nif->getLink( iCB, "Interpolator" ), "NiInterpolator" ); +void TransformInterpolator::applyTransformImpl( float time ) +{ + Transform & transform = target()->local; - QModelIndex iController = nif->getBlockIndex( nif->getLink( iCB, "Controller" ), "NiTimeController" ); + rotation.interpolate( transform.rotation, time ); + translation.interpolate( transform.translation, time ); + scale.interpolate( transform.scale, time ); +} - QString nodename = nif->get( iCB, "Node Name" ); - if ( nodename.isEmpty() ) { - QModelIndex idx = nif->getIndex( iCB, "Node Name Offset" ); - nodename = idx.sibling( idx.row(), NifModel::ValueCol ).data( NifSkopeDisplayRole ).toString(); +// BSplineInterpolator class - if ( nodename.isEmpty() ) - nodename = nif->get( iCB, "Target Name" ); - } +/********************************************************************* +Simple b-spline curve algorithm - QString proptype = nif->get( iCB, "Property Type" ); +Copyright 1994 by Keith Vertanen (vertankd@cda.mrs.umn.edu) - if ( proptype.isEmpty() ) { - QModelIndex idx = nif->getIndex( iCB, "Property Type Offset" ); - proptype = idx.sibling( idx.row(), NifModel::ValueCol ).data( NifSkopeDisplayRole ).toString(); - } +Released to the public domain (your mileage may vary) - QString ctrltype = nif->get( iCB, "Controller Type" ); +Found at: Programmers Heaven (www.programmersheaven.com/zone3/cat415/6660.htm) +Modified by: Theo +- reformat and convert doubles to floats +- removed point structure in favor of arbitrary sized float array +**********************************************************************/ - if ( ctrltype.isEmpty() ) { - QModelIndex idx = nif->getIndex( iCB, "Controller Type Offset" ); - ctrltype = idx.sibling( idx.row(), NifModel::ValueCol ).data( NifSkopeDisplayRole ).toString(); +// Used to enable static arrays to be members of vectors +struct SplineArraySlice +{ + SplineArraySlice( NifFieldConst _arrayRoot, uint _off = 0 ) + : arrayRoot( _arrayRoot ), off( _off ) + { + } + SplineArraySlice( const SplineArraySlice & other, uint _off = 0 ) + : arrayRoot( other.arrayRoot ), off( other.off + _off ) + { + } - if ( ctrltype.isEmpty() && iController.isValid() ) - ctrltype = nif->itemName( iController ); - } + short operator[]( uint index ) const + { + return arrayRoot[index + off].value(); + } - QString var1 = nif->get( iCB, "Controller ID" ); + NifFieldConst arrayRoot; + uint off; +}; - if ( var1.isEmpty() ) { - QModelIndex idx = nif->getIndex( iCB, "Controller ID Offset" ); - var1 = idx.sibling( idx.row(), NifModel::ValueCol ).data( NifSkopeDisplayRole ).toString(); - } +template +struct SplineTraits +{ + // Zero data + static T & Init( T & v ) + { + v = T(); + return v; + } - QString var2 = nif->get( iCB, "Interpolator ID" ); + // Number of control points used + static int CountOf() + { + return ( sizeof(T) / sizeof(float) ); + } - if ( var2.isEmpty() ) { - QModelIndex idx = nif->getIndex( iCB, "Interpolator ID Offset" ); - var2 = idx.sibling( idx.row(), NifModel::ValueCol ).data( NifSkopeDisplayRole ).toString(); - } + // Compute point from short array and mult/bias + static T & Compute( T & v, SplineArraySlice & c, float mult ) + { + float * vf = (float *)&v; // assume default data is a vector of floats. specialize if necessary. - Node * node = target->findChild( nodename ); + for ( int i = 0; i < CountOf(); ++i ) + vf[i] = vf[i] + ( float(c[i]) / float(SHRT_MAX) ) * mult; - if ( !node ) - continue; + return v; + } + static T & Adjust( T & v, float mult, float bias ) + { + float * vf = (float *)&v; // assume default data is a vector of floats. specialize if necessary. - if ( ctrltype == "NiTransformController" && multiTargetTransformer ) { - if ( multiTargetTransformer->setInterpolatorNode( node, iInterp ) ) { - multiTargetTransformer->start = start; - multiTargetTransformer->stop = stop; - multiTargetTransformer->phase = phase; - multiTargetTransformer->frequency = frequency; - continue; - } - } + for ( int i = 0; i < CountOf(); ++i ) + vf[i] = vf[i] * mult + bias; - if ( ctrltype == "BSLightingShaderPropertyFloatController" - || ctrltype == "BSLightingShaderPropertyColorController" - || ctrltype == "BSEffectShaderPropertyFloatController" - || ctrltype == "BSEffectShaderPropertyColorController" - || ctrltype == "BSNiAlphaPropertyTestRefController" ) - { - //qDebug() << node->name; - - auto ctrl = node->findController( proptype, iController ); - if ( ctrl ) { - ctrl->setInterpolator( iInterp ); - } - continue; - } + return v; + } +}; - Controller * ctrl = node->findController( proptype, ctrltype, var1, var2 ); +template <> struct SplineTraits +{ + static Quat & Init( Quat & v ) + { + v = Quat(); v[0] = 0.0f; return v; + } + static int CountOf() { return 4; } + static Quat & Compute( Quat & v, SplineArraySlice & c, float mult ) + { + for ( int i = 0; i < CountOf(); ++i ) + v[i] = v[i] + ( float(c[i]) / float(SHRT_MAX) ) * mult; - if ( ctrl ) { - ctrl->start = start; - ctrl->stop = stop; - ctrl->phase = phase; - ctrl->frequency = frequency; + return v; + } + static Quat & Adjust( Quat & v, float mult, float bias ) + { + for ( int i = 0; i < CountOf(); ++i ) + v[i] = v[i] * mult + bias; - ctrl->setInterpolator( iInterp ); - } - } - } - } + return v; } -} +}; +// calculate the blending value +static float blend( int k, int t, int * u, float v ) +{ + float value; + + if ( t == 1 ) { + // base case for the recursion + value = ( ( u[k] <= v ) && ( v < u[k + 1] ) ) ? 1.0f : 0.0f; + } else { + if ( ( u[k + t - 1] == u[k] ) && ( u[k + t] == u[k + 1] ) ) // check for divide by zero + value = 0; + else if ( u[k + t - 1] == u[k] ) // if a term's denominator is zero,use just the other + value = ( u[k + t] - v) / ( u[k + t] - u[k + 1] ) * blend( k + 1, t - 1, u, v ); + else if ( u[k + t] == u[k + 1] ) + value = (v - u[k]) / (u[k + t - 1] - u[k]) * blend( k, t - 1, u, v ); + else + value = ( v - u[k] ) / ( u[k + t - 1] - u[k] ) * blend( k, t - 1, u, v ) + + ( u[k + t] - v ) / ( u[k + t] - u[k + 1] ) * blend( k + 1, t - 1, u, v ); + } -// `NiKeyframeController` blocks + return value; +} -KeyframeController::KeyframeController( Node * node, const QModelIndex & index ) - : Controller( index ), target( node ), lTrans( 0 ), lRotate( 0 ), lScale( 0 ) +// figure out the knots +static void compute_intervals( int * u, int n, int t ) { + for ( int j = 0; j <= n + t; j++ ) { + if ( j < t ) + u[j] = 0; + else if ( ( t <= j ) && ( j <= n ) ) + u[j] = j - t + 1; + else if ( j > n ) + u[j] = n - t + 2; // if n-t=-2 then we're screwed, everything goes to 0 + } } -void KeyframeController::updateTime( float time ) +template +static void compute_point( int * u, int n, int t, float v, SplineArraySlice & control, T & output, float mult, float bias ) { - if ( !(active && target) ) - return; + // initialize the variables that will hold our output + int l = SplineTraits::CountOf(); + SplineTraits::Init( output ); - time = ctrlTime( time ); + for ( int k = 0; k <= n; k++ ) { + SplineArraySlice qa( control, k * l ); + SplineTraits::Compute( output, qa, blend( k, t, u, v ) ); + } - interpolate( target->local.rotation, iRotations, time, lRotate ); - interpolate( target->local.translation, iTranslations, time, lTrans ); - interpolate( target->local.scale, iScales, time, lScale ); + SplineTraits::Adjust( output, mult, bias ); } -bool KeyframeController::update( const NifModel * nif, const QModelIndex & index ) +void BSplineInterpolator::updateDataImpl() { - if ( Controller::update( nif, index ) ) { - iTranslations = nif->getIndex( iData, "Translations" ); - iRotations = nif->getIndex( iData, "Rotations" ); + startTime = interpolatorBlock["Start Time"].value(); + stopTime = interpolatorBlock["Stop Time"].value(); - if ( !iRotations.isValid() ) - iRotations = iData; + rotateVars.off = interpolatorBlock["Rotation Handle"].value(); + rotateVars.mult = interpolatorBlock["Rotation Half Range"].value(); + rotateVars.bias = interpolatorBlock["Rotation Offset"].value(); - iScales = nif->getIndex( iData, "Scales" ); - return true; - } + translationVars.off = interpolatorBlock["Translation Handle"].value(); + translationVars.mult = interpolatorBlock["Translation Half Range"].value(); + translationVars.bias = interpolatorBlock["Translation Offset"].value(); - return false; -} + scaleVars.off = interpolatorBlock["Scale Handle"].value(); + scaleVars.mult = interpolatorBlock["Scale Half Range"].value(); + scaleVars.bias = interpolatorBlock["Scale Offset"].value(); + auto splineBlock = interpolatorBlock["Spline Data"].linkBlock("NiBSplineData"); + registerUpdateBlock( splineBlock ); + controlPointsRoot = splineBlock.child("Compact Control Points"); -// `NiTransformController` blocks + auto basisBlock = interpolatorBlock["Basis Data"].linkBlock("NiBSplineBasisData"); + registerUpdateBlock( basisBlock ); + nControlPoints = basisBlock["Num Control Points"].value(); +} -TransformController::TransformController( Node * node, const QModelIndex & index ) - : Controller( index ), target( node ) +void BSplineInterpolator::applyTransformImpl( float time ) { + Transform & transform = target()->local; + + float interval = ( ( time - startTime ) / ( stopTime - startTime ) ) * float(nControlPoints - degree); + + Quat q = transform.rotation.toQuat(); + if ( interpolateValue( q, interval, rotateVars ) ) + transform.rotation.fromQuat( q ); + + interpolateValue( transform.translation, interval, translationVars ); + interpolateValue( transform.scale, interval, scaleVars ); } -void TransformController::updateTime( float time ) +template +bool BSplineInterpolator::interpolateValue( T & value, float interval, const SplineVars & vars ) const { - if ( !(active && target) ) - return; - - time = ctrlTime( time ); + if ( !vars.isActive() ) + return false; - if ( interpolator ) { - interpolator->updateTransform( target->local, time ); + SplineArraySlice subArray( controlPointsRoot, vars.off ); + int t = degree + 1; + int n = nControlPoints - 1; + int l = SplineTraits::CountOf(); + + if ( interval >= float(nControlPoints - degree) ) { + SplineTraits::Init( value ); + SplineArraySlice sa( subArray, n * l ); + SplineTraits::Compute( value, sa, 1.0f ); + SplineTraits::Adjust( value, vars.mult, vars.bias ); + } else { + int * u = new int[ n + t + 1 ]; + compute_intervals( u, n, t ); + compute_point( u, n, t, interval, subArray, value, vars.mult, vars.bias ); + delete [] u; } + + return true; } -void TransformController::setInterpolator( const QModelIndex & idx ) + +// ITransformInterpolator class + +static inline ITransformInterpolator * createTransformInterpolator( NifFieldConst interpolatorBlock, Node * target, Controller * parentController ) { - auto nif = NifModel::fromValidIndex(idx); - if ( nif ) { - if ( interpolator ) { - delete interpolator; - interpolator = 0; - } + if ( interpolatorBlock.hasName("NiBSplineCompTransformInterpolator") ) + return new BSplineInterpolator( interpolatorBlock, target, parentController ); - if ( nif->isNiBlock( idx, "NiBSplineCompTransformInterpolator" ) ) { - iInterpolator = idx; - interpolator = new BSplineTransformInterpolator( this ); - } else if ( nif->isNiBlock( idx, "NiTransformInterpolator" ) ) { - iInterpolator = idx; - interpolator = new TransformInterpolator( this ); - } + if ( interpolatorBlock.hasName("NiTransformInterpolator", "NiKeyframeController") ) + return new TransformInterpolator( interpolatorBlock, target, parentController ); - if ( interpolator ) { - interpolator->update( nif, iInterpolator ); - } - } + // TODO: Report invalid type + return nullptr; } +ITransformInterpolator * TransformController::createInterpolator( NifFieldConst interpolatorBlock ) +{ + return createTransformInterpolator( interpolatorBlock, target, this ); +} -// `NiMultiTargetTransformController` blocks -MultiTargetTransformController::MultiTargetTransformController( Node * node, const QModelIndex & index ) - : Controller( index ), target( node ) +// MultiTargetTransformController class + +MultiTargetTransformController::MultiTargetTransformController( Node * node, NifFieldConst ctrlBlock ) + : Controller( ctrlBlock ), parent( node ) { + Q_ASSERT( !parent.isNull() ); } void MultiTargetTransformController::updateTime( float time ) { - if ( !(active && target) ) - return; - - time = ctrlTime( time ); - - for ( const TransformTarget& tt : extraTargets ) { - if ( tt.first && tt.second ) { - tt.second->updateTransform( tt.first->local, time ); - } + if ( active && transforms.count() > 0 ) { + time = ctrlTime( time ); + for ( auto t : transforms ) + t->applyTransform( time ); } } -bool MultiTargetTransformController::update( const NifModel * nif, const QModelIndex & index ) +void MultiTargetTransformController::updateImpl( NifFieldConst changedBlock ) { - if ( Controller::update( nif, index ) ) { - if ( target ) { - Scene * scene = target->scene; - extraTargets.clear(); + Controller::updateImpl( changedBlock ); + + if ( changedBlock == block && hasParent() ) { + targetNodes.clear(); + Scene * scene = parent->scene; + for ( auto extraEntry : block.child("Extra Targets").iter() ) + targetNodes.add( scene->getNode( extraEntry.linkBlock() ) ); + + // Remove transforms of obsolete nodes + for ( int i = transforms.count() - 1; i >= 0; i-- ) { + if ( !targetNodes.has( transforms[i]->target() ) ) + removeTransformAt( i ); + } + } - QVector lTargets = nif->getLinkArray( index, "Extra Targets" ); - for ( const auto l : lTargets ) { - Node * node = scene->getNode( nif, nif->getBlockIndex( l ) ); + for ( auto t : transforms ) + t->updateData( changedBlock ); +} - if ( node ) { - extraTargets.append( TransformTarget( node, 0 ) ); - } - } +bool MultiTargetTransformController::setNodeInterpolator( Node * node, NifFieldConst interpolatorBlock ) +{ + if ( interpolatorBlock && targetNodes.has( node ) ) { + for ( int i = transforms.count() - 1; i >= 0; i-- ) { + if ( transforms[i]->target() == node ) + removeTransformAt( i ); } - return true; - } + auto t = createTransformInterpolator( interpolatorBlock, node, nullptr ); + t->updateData( interpolatorBlock ); + transforms.append( t ); - for ( const TransformTarget& tt : extraTargets ) { - // TODO: update the interpolators + return true; } return false; } -bool MultiTargetTransformController::setInterpolatorNode( Node * node, const QModelIndex & idx ) +void MultiTargetTransformController::clearTransforms() { - auto nif = NifModel::fromValidIndex(idx); - if ( !nif ) - return false; - - QMutableListIterator it( extraTargets ); + qDeleteAll( transforms ); + transforms.clear(); +} - while ( it.hasNext() ) { - it.next(); +void MultiTargetTransformController::removeTransformAt( int i ) +{ + delete transforms[i]; + transforms.removeAt( i ); +} - auto& val = it.value(); - if ( val.first == node ) { - if ( val.second ) { - delete val.second; - val.second = 0; - } - if ( nif->isNiBlock( idx, "NiBSplineCompTransformInterpolator" ) ) { - val.second = new BSplineTransformInterpolator( this ); - } else if ( nif->isNiBlock( idx, "NiTransformInterpolator" ) ) { - val.second = new TransformInterpolator( this ); - } +// VisibilityInterpolator and VisibilityController classes - if ( val.second ) { - val.second->update( nif, idx ); - } +void VisibilityInterpolator::updateDataImpl() +{ + auto dataBlock = getDataBlock(); + registerUpdateBlock( dataBlock ); + interpolator.updateData( dataBlock["Data"] ); +} - return true; - } - } +void VisibilityInterpolator::applyTransformImpl( float time ) +{ + bool isVisible; + if ( interpolator.interpolate( isVisible, time ) ) + target()->flags.node.hidden = !isVisible; +} - return false; +VisibilityInterpolator * VisibilityController::createInterpolator( NifFieldConst interpolatorBlock ) +{ + return new VisibilityInterpolator( interpolatorBlock, target, this ); } -// `NiVisController` blocks +// MorphInterpolator and MorphController classes -VisibilityController::VisibilityController( Node * node, const QModelIndex & index ) - : Controller( index ), target( node ), visLast( 0 ) +MorphInterpolator::MorphInterpolator( int _verticesIndex, NifFieldConst _interpolatorBlock, Shape * shape, MorphController * _parentController, NifFieldConst _morphDataEntry ) + : IControllerInterpolatorTyped( _interpolatorBlock, shape, _parentController ), + verticesIndex( _verticesIndex ), + morphDataEntry( _morphDataEntry ) { } -void VisibilityController::updateTime( float time ) +void MorphInterpolator::updateDataImpl() { - if ( !(active && target) ) - return; - - time = ctrlTime( time ); - - bool isVisible; - - if ( interpolate( isVisible, iData, "Data", time, visLast ) ) { - target->flags.node.hidden = !isVisible; + if ( interpolatorBlock.inherits("NiMorphData") ) { + interpolator.updateData( morphDataEntry ); + } else { + auto dataBlock = interpolatorBlock.child("Data").linkBlock("NiFloatData"); + registerUpdateBlock( dataBlock ); + interpolator.updateData( dataBlock["Data"] ); } } -bool VisibilityController::update( const NifModel * nif, const QModelIndex & index ) +void MorphInterpolator::applyTransformImpl( float time ) { - if ( Controller::update( nif, index ) ) { - return true; + float x; + if ( interpolator.interpolate( x, time ) ) { + if ( x > 0.0f ) { + if ( x > 1.0f ) + x = 1.0f; + + const auto & inVerts = static_cast( controller )->morphVertices[verticesIndex]; + auto & outVerts = target()->verts; + for ( int i = 0, nVerts = std::min( inVerts.count(), outVerts.count() ); i < nVerts; i++ ) + outVerts[i] += inVerts[i] * x; + } } - - return false; } - -// `NiGeomMorpherController` blocks - -MorphController::MorphController( Shape * mesh, const QModelIndex & index ) - : Controller( index ), target( mesh ) +MorphController::MorphController( Shape * shape, NifFieldConst ctrlBlock ) + : Controller( ctrlBlock ), target( shape ) { + Q_ASSERT( hasTarget() ); } -MorphController::~MorphController() +bool MorphController::isActive() const { - qDeleteAll( morph ); + if ( active && hasTarget() ) { + for ( auto m : morphInterpolators ) { + if ( m && m->isActive() ) + return true; + } + } + + return false; } void MorphController::updateTime( float time ) { - if ( !(target && iData.isValid() && active && morph.count() > 1) ) - return; - - time = ctrlTime( time ); - - if ( target->verts.count() != morph[0]->verts.count() ) - return; - - target->verts = morph[0]->verts; - - float x; - - for ( int i = 1; i < morph.count(); i++ ) { - MorphKey * key = morph[i]; + if ( isActive() ) { + const auto & firstVerts = morphVertices[0]; + if ( target->verts.count() != firstVerts.count() ) + return; // TODO: report error + + time = ctrlTime( time ); + target->verts = firstVerts; + for ( auto m : morphInterpolators ) { + if ( m ) + m->applyTransform( time ); + } + target->needUpdateBounds = true; + } +} - if ( interpolate( x, key->iFrames, time, key->index ) ) { - if ( x < 0 ) - x = 0; - if ( x > 1 ) - x = 1; +void MorphController::setMorphInterpolator( int morphIndex, NifFieldConst interpolatorBlock ) +{ + morphIndex -= 1; + if ( morphIndex >= 0 && morphIndex < morphInterpolators.count() ) { + MorphInterpolator * interpolator; + + // Delete previous interpolator + interpolator = morphInterpolators[morphIndex]; + if ( interpolator ) + delete interpolator; - if ( x != 0 && target->verts.count() == key->verts.count() ) { - for ( int v = 0; v < target->verts.count(); v++ ) - target->verts[v] += key->verts[v] * x; - } + // Init new interpolator + if ( interpolatorBlock ) { + interpolator = new MorphInterpolator( morphIndex + 1, interpolatorBlock, target, this, NifFieldConst() ); + interpolator->updateData( interpolatorBlock ); + } else { + interpolator = nullptr; } + morphInterpolators[morphIndex] = interpolator; } - - target->needUpdateBounds = true; } -bool MorphController::update( const NifModel * nif, const QModelIndex & index ) +void MorphController::updateImpl( NifFieldConst changedBlock ) { - if ( Controller::update( nif, index ) ) { - qDeleteAll( morph ); - morph.clear(); + bool oldActive = isActive(); - QModelIndex midx = nif->getIndex( iData, "Morphs" ); + Controller::updateImpl( changedBlock ); - for ( int r = 0; r < nif->rowCount( midx ); r++ ) { - QModelIndex iInterpolators, iInterpolatorWeights; + if ( ( changedBlock == block || changedBlock == dataBlock ) && hasTarget() ) { + morphVertices.clear(); + clearMorphInterpolators(); - if ( nif->checkVersion( 0, 0x14000005 ) ) { - iInterpolators = nif->getIndex( iBlock, "Interpolators" ); - } else if ( nif->checkVersion( 0x14010003, 0 ) ) { - iInterpolatorWeights = nif->getIndex( iBlock, "Interpolator Weights" ); - } - - QModelIndex iKey = midx.child( r, 0 ); + NifFieldConst interpolatorsRoot; + auto interpolatorWeightsRoot = block.child("Interpolator Weights"); + int iInterpolatorField = -1; + if ( interpolatorWeightsRoot ) { + if ( interpolatorWeightsRoot.childCount() > 0 ) + iInterpolatorField = interpolatorWeightsRoot[0]["Interpolator"].row(); + } else { + interpolatorsRoot = block.child("Interpolators"); + } - MorphKey * key = new MorphKey; - key->index = 0; + dataBlock = block["Data"].linkBlock("NiMorphData"); + auto morphDataRoot = dataBlock.child("Morphs"); + int nMorphs = morphDataRoot.childCount(); + if ( nMorphs > 1 ) { + morphVertices.reserve( nMorphs ); + auto firstMorphVertsRoot = morphDataRoot[0].child("Vectors"); + morphVertices.append( firstMorphVertsRoot.array() ); + + morphInterpolators.reserve( nMorphs - 1 ); + for ( int i = 1; i < nMorphs; i++ ) { + auto morphEntry = morphDataRoot[i]; + auto morphVertsRoot = morphEntry.child( firstMorphVertsRoot.row() ); + IControllable::reportFieldCountMismatch( morphVertsRoot, firstMorphVertsRoot, dataBlock ); + morphVertices.append( morphVertsRoot.array() ); + + NifFieldConst interpolatorBlock; + if ( interpolatorWeightsRoot ) { + interpolatorBlock = interpolatorWeightsRoot[i].child(iInterpolatorField).linkBlock("NiFloatInterpolator"); + } else if ( interpolatorsRoot ) { + interpolatorBlock = interpolatorsRoot[i].linkBlock("NiFloatInterpolator"); + } else { + interpolatorBlock = dataBlock; + } - // this is ugly... - if ( iInterpolators.isValid() ) { - key->iFrames = nif->getIndex( nif->getBlockIndex( nif->getLink( nif->getBlockIndex( nif->getLink( iInterpolators.child( r, 0 ) ), "NiFloatInterpolator" ), "Data" ), "NiFloatData" ), "Data" ); - } else if ( iInterpolatorWeights.isValid() ) { - key->iFrames = nif->getIndex( nif->getBlockIndex( nif->getLink( nif->getBlockIndex( nif->getLink( iInterpolatorWeights.child( r, 0 ), "Interpolator" ), "NiFloatInterpolator" ), "Data" ), "NiFloatData" ), "Data" ); - } else { - key->iFrames = iKey; + if ( interpolatorBlock ) { + morphInterpolators.append( new MorphInterpolator( i, interpolatorBlock, target, this, morphEntry ) ); + } else { + morphInterpolators.append( nullptr ); + } } - - key->verts = nif->getArray( nif->getIndex( iKey, "Vectors" ) ); - - morph.append( key ); } + } - return true; + for ( auto m : morphInterpolators ) { + if ( m ) + m->updateData( changedBlock ); } - return false; + // Force data update for the target if the controller goes from active to inactive. + // This reverts all the changes that have been made by the controller. + if ( oldActive && !isActive() && hasTarget() ) + target->update(); } - -// `NiUVController` blocks - -UVController::UVController( Shape * mesh, const QModelIndex & index ) - : Controller( index ), target( mesh ) +void MorphController::clearMorphInterpolators() { + for ( auto m : morphInterpolators ) { + if ( m ) + delete m; + } + morphInterpolators.clear(); } -UVController::~UVController() + +// UVInterpolator and UVController classes + +void UVInterpolator::updateDataImpl() { + auto dataBlock = interpolatorBlock["Data"].linkBlock("NiUVData"); + registerUpdateBlock( dataBlock ); + + auto groupRoot = dataBlock["UV Groups"]; + for ( int i = 0; i < UV_GROUPS_COUNT; i++ ) + interpolators[i].updateData( groupRoot[i] ); } -void UVController::updateTime( float time ) +void UVInterpolator::applyTransformImpl( float time ) { - auto nif = NifModel::fromIndex( iData ); - QModelIndex uvGroups = nif->getIndex( iData, "UV Groups" ); - // U trans, V trans, U scale, V scale // see NiUVData compound in nif.xml - float val[4] = { 0.0, 0.0, 1.0, 1.0 }; - - if ( uvGroups.isValid() ) { - for ( int i = 0; i < 4 && i < nif->rowCount( uvGroups ); i++ ) { - interpolate( val[i], uvGroups.child( i, 0 ), ctrlTime( time ), luv ); - } - - // adjust coords; verified in SceneImmerse - for ( int i = 0; i < target->coords[0].size(); i++ ) { - // operating on pointers makes this too complicated, so we don't - Vector2 current = target->coords[0][i]; - // scaling/tiling applied before translation - // Note that scaling is relative to center! - current += Vector2( -0.5, -0.5 ); - current = Vector2( current[0] * val[2], current[1] * val[3] ); - current += Vector2( -val[0], val[1] ); - current += Vector2( 0.5, 0.5 ); - target->coords[0][i] = current; - } + float val[UV_GROUPS_COUNT] = { 0.0, 0.0, 1.0, 1.0 }; + for ( int i = 0; i < UV_GROUPS_COUNT; i++ ) + interpolators[i].interpolate( val[i], time ); + + for ( auto & uv : target()->coords[0] ) { + // scaling/tiling applied before translation + // Note that scaling is relative to center! + // Gavrant: -val[0] for uv[0] and +val[1] for uv[1] were in the prev. version of the code + uv[0] = ( uv[0] - 0.5f ) * val[2] + 0.5f - val[0]; + uv[1] = ( uv[1] - 0.5f ) * val[3] + 0.5f + val[1]; } - target->needUpdateData = true; // TODO (Gavrant): it's probably wrong (because the target shape would reset its UV map then) + target()->needUpdateData = true; // TODO (Gavrant): it's probably wrong (because the target shape would reset its UV map then) } -bool UVController::update( const NifModel * nif, const QModelIndex & index ) +UVInterpolator * UVController::createInterpolator( NifFieldConst interpolatorBlock ) { - if ( Controller::update( nif, index ) ) { - // do stuff here - return true; - } - - return false; + return new UVInterpolator( interpolatorBlock, target, this ); } -float random( float r ) -{ - return r * rand() / RAND_MAX; -} +// ParticleInterpolator and ParticleController classes -Vector3 random( Vector3 v ) +ParticleInterpolator::Gravity::Gravity( NifFieldConst block ) + : force( block["Force"].value() ), + type( block["Type"].value() ), + position( block["Position"].value() ), + direction( block["Direction"].value() ) { - v[0] *= random( 1.0 ); - v[1] *= random( 1.0 ); - v[2] *= random( 1.0 ); - return v; } - -// `NiParticleSystemController` and other blocks - -ParticleController::ParticleController( Particles * particles, const QModelIndex & index ) - : Controller( index ), target( particles ) +void ParticleInterpolator::updateDataImpl() { -} + auto ctrlBlock = controllerBlock(); + registerUpdateBlock( ctrlBlock ); + + emitNode = target()->scene->getNode( ctrlBlock["Emitter"].linkBlock() ); + emitStart = ctrlBlock["Emit Start Time"].value(); + emitStop = ctrlBlock["Emit Stop Time"].value(); + emitRate = ctrlBlock.child("Birth Rate").value(); + emitRadius = ctrlBlock["Emitter Dimensions"].value(); + emitAccu = 0; + emitLast = emitStart; + + spd = ctrlBlock.child("Speed").value(); + spdRnd = ctrlBlock["Speed Variation"].value(); + + ttl = ctrlBlock["Lifetime"].value(); + ttlRnd = ctrlBlock["Lifetime Variation"].value(); + + inc = ctrlBlock["Declination"].value(); + incRnd = ctrlBlock["Declination Variation"].value(); + + dec = ctrlBlock["Planar Angle"].value(); + decRnd = ctrlBlock["Planar Angle Variation"].value(); + + size = ctrlBlock["Initial Size"].value(); + + // Particles + float emitMax = ctrlBlock.child("Num Particles").value(); + + particles.clear(); + auto particlesRoot = ctrlBlock.child("Particles"); + int nParticles = std::min( particlesRoot.childCount(), ctrlBlock.child("Num Valid").value() ); + if ( nParticles > 0 ) { + auto firstParticle = particlesRoot[0]; + int iVelocityField = firstParticle["Velocity"].row(); + int iAgeField = firstParticle["Age"].row(); + int iLifeSpanField = firstParticle["Life Span"].row(); + int iLastUpdateField = firstParticle["Last Update"].row(); + int iCodeField = firstParticle["Code"].row(); + + particles.reserve( nParticles ); + for ( int i = 0; i < nParticles; i++ ) { + Particle particle; + auto entry = particlesRoot[i]; + particle.velocity = entry.child(iVelocityField).value(); + particle.lifetime = entry.child(iAgeField).value(); + particle.lifespan = entry.child(iLifeSpanField).value(); + particle.lasttime = entry.child(iLastUpdateField).value(); + particle.vertex = entry.child(iCodeField).value(); + // Display saved particle start on initial load + particles.append( particle ); + } + } -bool ParticleController::update( const NifModel * nif, const QModelIndex & index ) -{ - if ( !target ) - return false; + if ( ctrlBlock.child("Use Birth Rate").value() == false ) { + emitRate = emitMax / (ttl + ttlRnd * 0.5f); + } - if ( Controller::update( nif, index ) || (index.isValid() && iExtras.contains( index )) ) { - emitNode = target->scene->getNode( nif, nif->getBlockIndex( nif->getLink( iBlock, "Emitter" ) ) ); - emitStart = nif->get( iBlock, "Emit Start Time" ); - emitStop = nif->get( iBlock, "Emit Stop Time" ); - emitRate = nif->get( iBlock, "Birth Rate" ); - emitRadius = nif->get( iBlock, "Emitter Dimensions" ); - emitAccu = 0; - emitLast = emitStart; - - spd = nif->get( iBlock, "Speed" ); - spdRnd = nif->get( iBlock, "Speed Variation" ); - - ttl = nif->get( iBlock, "Lifetime" ); - ttlRnd = nif->get( iBlock, "Lifetime Variation" ); - - inc = nif->get( iBlock, "Declination" ); - incRnd = nif->get( iBlock, "Declination Variation" ); - - dec = nif->get( iBlock, "Planar Angle" ); - decRnd = nif->get( iBlock, "Planar Angle Variation" ); - - size = nif->get( iBlock, "Initial Size" ); - grow = 0.0; - fade = 0.0; - - list.clear(); - - QModelIndex iParticles = nif->getIndex( iBlock, "Particles" ); - - if ( iParticles.isValid() ) { - emitMax = nif->get( iBlock, "Num Particles" ); - int numValid = nif->get( iBlock, "Num Valid" ); - - //iParticles = nif->getIndex( iParticles, "Particles" ); - //if ( iParticles.isValid() ) - //{ - for ( int p = 0; p < numValid && p < nif->rowCount( iParticles ); p++ ) { - Particle particle; - particle.velocity = nif->get( iParticles.child( p, 0 ), "Velocity" ); - particle.lifetime = nif->get( iParticles.child( p, 0 ), "Age" ); - particle.lifespan = nif->get( iParticles.child( p, 0 ), "Life Span" ); - particle.lasttime = nif->get( iParticles.child( p, 0 ), "Last Update" ); - particle.vertex = nif->get( iParticles.child( p, 0 ), "Code" ); - // Display saved particle start on initial load - list.append( particle ); - } + // Modfiers + grow = 0.0; + fade = 0.0; + colorInterpolator.clear(); + gravities.clear(); - //} - } + auto modifierBlock = ctrlBlock["Particle Modifier"].linkBlock("NiParticleModifier"); + while ( modifierBlock ) { + registerUpdateBlock( modifierBlock ); - if ( nif->get( iBlock, "Use Birth Rate" ) == 0 ) { - emitRate = emitMax / (ttl + ttlRnd / 2); - } + if ( modifierBlock.hasName("NiParticleGrowFade") ) { + grow = modifierBlock["Grow"].value(); + fade = modifierBlock["Fade"].value(); - iExtras.clear(); - grav.clear(); - iColorKeys = QModelIndex(); - QModelIndex iExtra = nif->getBlockIndex( nif->getLink( iBlock, "Particle Modifier" ) ); - - while ( iExtra.isValid() ) { - iExtras.append( iExtra ); - - QString name = nif->itemName( iExtra ); - - if ( name == "NiParticleGrowFade" ) { - grow = nif->get( iExtra, "Grow" ); - fade = nif->get( iExtra, "Fade" ); - } else if ( name == "NiParticleColorModifier" ) { - iColorKeys = nif->getIndex( nif->getBlockIndex( nif->getLink( iExtra, "Color Data" ), "NiColorData" ), "Data" ); - } else if ( name == "NiGravity" ) { - Gravity g; - g.force = nif->get( iExtra, "Force" ); - g.type = nif->get( iExtra, "Type" ); - g.position = nif->get( iExtra, "Position" ); - g.direction = nif->get( iExtra, "Direction" ); - grav.append( g ); - } + } else if ( modifierBlock.hasName("NiParticleColorModifier") ) { + auto colorDataBlock = modifierBlock["Color Data"].linkBlock("NiColorData"); + registerUpdateBlock( colorDataBlock ); + colorInterpolator.updateData( colorDataBlock["Data"] ); - iExtra = nif->getBlockIndex( nif->getLink( iExtra, "Next Modifier" ) ); + } else if ( modifierBlock.hasName("NiGravity") ) { + gravities.append( Gravity( modifierBlock ) ); } - return true; + modifierBlock = modifierBlock["Next Modifier"].linkBlock("NiParticleModifier"); } - - return false; } -void ParticleController::updateTime( float time ) +void ParticleInterpolator::applyTransformImpl( float time ) { - if ( !(target && active) ) - return; - - localtime = ctrlTime( time ); - - int n = 0; + auto & targetVerts = target()->verts; - while ( n < list.count() ) { - Particle & p = list[n]; + // TODO: rework the code below for the particles list to survive more than one loop of animations - float deltaTime = (localtime > p.lasttime ? localtime - p.lasttime : 0); //( stop - start ) - p.lasttime + localtime ); + for ( int i = 0; i < particles.count(); ) { + Particle & p = particles[i]; + float deltaTime = (time > p.lasttime ? time - p.lasttime : 0); //( stop - start ) - p.lasttime + localtime ); p.lifetime += deltaTime; - if ( p.lifetime < p.lifespan && p.vertex < target->verts.count() ) { - p.position = target->verts[p.vertex]; + if ( p.lifetime < p.lifespan && p.vertex < targetVerts.count() ) { + p.position = targetVerts[p.vertex]; - for ( int i = 0; i < 4; i++ ) - moveParticle( p, deltaTime / 4 ); + for ( int j = 0; j < 4; j++ ) + moveParticle( p, deltaTime / 4.0f ); - p.lasttime = localtime; - n++; + p.lasttime = time; + i++; } else { - list.remove( n ); + particles.removeAt( i ); } } - if ( emitNode && emitNode->isVisible() && localtime >= emitStart && localtime <= emitStop ) { - float emitDelta = (localtime > emitLast ? localtime - emitLast : 0); - emitLast = localtime; + if ( emitNode && emitNode->isVisible() && time >= emitStart && time <= emitStop ) { + float emitDelta = (time > emitLast ? time - emitLast : 0); + emitLast = time; emitAccu += emitDelta * emitRate; int num = int( emitAccu ); - if ( num > 0 ) { emitAccu -= num; - while ( num-- > 0 && list.count() < target->verts.count() ) { + while ( num-- > 0 && particles.count() < targetVerts.count() ) { Particle p; - startParticle( p ); - list.append( p ); + startParticle( p, time ); + particles.append( p ); } } } - n = 0; + auto & targetSizes = target()->sizes; + auto & targetColors = target()->colors; + for ( int i = 0; i < particles.count(); i++ ) { + Particle & p = particles[i]; + p.vertex = i; + targetVerts[i] = p.position; - while ( n < list.count() ) { - Particle & p = list[n]; - p.vertex = n; - target->verts[n] = p.position; + if ( i < targetSizes.count() ) + sizeParticle( p, targetSizes[i] ); - if ( n < target->sizes.count() ) - sizeParticle( p, target->sizes[n] ); + if ( i < targetColors.count() ) + colorParticle( p, targetColors[i] ); + } - if ( n < target->colors.count() ) - colorParticle( p, target->colors[n] ); + target()->active = particles.count(); + target()->size = size; +} - n++; - } +static inline float randomFloat( float r ) +{ + return ( float( rand() ) / float( RAND_MAX ) ) * r; +} - target->active = list.count(); - target->size = size; +static inline Vector3 randomVector( const Vector3 & v ) +{ + return Vector3( v[0] * randomFloat( 1.0 ), v[1] * randomFloat( 1.0 ), v[2] * randomFloat( 1.0 ) ); } -void ParticleController::startParticle( Particle & p ) +void ParticleInterpolator::startParticle( Particle & p, float localTime ) { - p.position = random( emitRadius * 2 ) - emitRadius; - p.position += target->worldTrans().rotation.inverted() * (emitNode->worldTrans().translation - target->worldTrans().translation); + const auto & targetWorldTrans = target()->worldTrans(); + const auto & emitWorldTrans = emitNode->worldTrans(); + + p.position = randomVector( emitRadius * 2 ) - emitRadius; + p.position += targetWorldTrans.rotation.inverted() * ( emitWorldTrans.translation - targetWorldTrans.translation ); - float i = inc + random( incRnd ); - float d = dec + random( decRnd ); + float i = inc + randomFloat( incRnd ); + float d = dec + randomFloat( decRnd ); p.velocity = Vector3( rand() & 1 ? sin( i ) : -sin( i ), 0, cos( i ) ); Matrix m; m.fromEuler( 0, 0, rand() & 1 ? d : -d ); p.velocity = m * p.velocity; - p.velocity = p.velocity * (spd + random( spdRnd )); - p.velocity = target->worldTrans().rotation.inverted() * emitNode->worldTrans().rotation * p.velocity; + p.velocity = p.velocity * (spd + randomFloat( spdRnd )); + p.velocity = targetWorldTrans.rotation.inverted() * emitWorldTrans.rotation * p.velocity; p.lifetime = 0; - p.lifespan = ttl + random( ttlRnd ); - p.lasttime = localtime; + p.lifespan = ttl + randomFloat( ttlRnd ); + p.lasttime = localTime; } -void ParticleController::moveParticle( Particle & p, float deltaTime ) +void ParticleInterpolator::moveParticle( Particle & p, float deltaTime ) { - for ( Gravity g : grav ) { + for ( const Gravity & g : gravities ) { switch ( g.type ) { case 0: - p.velocity += g.direction * (g.force * deltaTime); + p.velocity += g.direction * ( g.force * deltaTime ); break; case 1: { - Vector3 dir = (g.position - p.position); + Vector3 dir = ( g.position - p.position ); dir.normalize(); - p.velocity += dir * (g.force * deltaTime); - } - break; + p.velocity += dir * ( g.force * deltaTime ); + } break; + default: + // TODO: report unsupported value of g.type? + break; } } p.position += p.velocity * deltaTime; } -void ParticleController::sizeParticle( Particle & p, float & sz ) +void ParticleInterpolator::sizeParticle( Particle & p, float & sz ) { sz = 1.0; @@ -768,388 +957,405 @@ void ParticleController::sizeParticle( Particle & p, float & sz ) sz *= (p.lifespan - p.lifetime) / fade; } -void ParticleController::colorParticle( Particle & p, Color4 & color ) +void ParticleInterpolator::colorParticle( Particle & p, Color4 & color ) { - if ( iColorKeys.isValid() ) { - int i = 0; - interpolate( color, iColorKeys, p.lifetime / p.lifespan, i ); - } + colorInterpolator.interpolate( color, p.lifetime / p.lifespan ); } - -// `BSNiAlphaPropertyTestRefController` - -AlphaController::AlphaController( AlphaProperty * prop, const QModelIndex & index ) - : Controller( index ), alphaProp( prop ) +ParticleInterpolator * ParticleController::createInterpolator( NifFieldConst interpolatorBlock ) { + return new ParticleInterpolator( interpolatorBlock, target, this ); } -// `NiAlphaController` -AlphaController::AlphaController( MaterialProperty * prop, const QModelIndex & index ) - : Controller( index ), materialProp( prop ) +// AlphaInterpolator_Material and AlphaController_Material classes + +void AlphaInterpolator_Material::updateDataImpl() { + auto dataBlock = getDataBlock(); + registerUpdateBlock( dataBlock ); + interpolator.updateData( dataBlock["Data"] ); } -void AlphaController::updateTime( float time ) +void AlphaInterpolator_Material::applyTransformImpl( float time ) { - if ( !(active) ) - return; + float val; + if ( interpolator.interpolate( val, time ) ) + target()->alpha = std::clamp( val, 0.0f, 1.0f ); +} - if ( materialProp ) { - interpolate( materialProp->alpha, iData, "Data", ctrlTime( time ), lAlpha ); +AlphaInterpolator_Material * AlphaController_Material::createInterpolator( NifFieldConst interpolatorBlock ) +{ + return new AlphaInterpolator_Material( interpolatorBlock, target, this ); +} - if ( materialProp->alpha < 0 ) - materialProp->alpha = 0; - if ( materialProp->alpha > 1 ) - materialProp->alpha = 1; - } else if ( alphaProp ) { - float threshold; +// AlphaInterpolator_Alpha and AlphaController_Alpha classes - if ( interpolate( threshold, iData, "Data", ctrlTime( time ), lAlpha ) ) - alphaProp->alphaThreshold = threshold / 255.0f; - } +void AlphaInterpolator_Alpha::updateDataImpl() +{ + auto dataBlock = getDataBlock(); + registerUpdateBlock( dataBlock ); + interpolator.updateData( dataBlock["Data"] ); } - -// `NiMaterialColorController` - -MaterialColorController::MaterialColorController( MaterialProperty * prop, const QModelIndex & index ) - : Controller( index ), target( prop ) +void AlphaInterpolator_Alpha::applyTransformImpl( float time ) { + float val; + if ( interpolator.interpolate( val, time ) ) + target()->alphaThreshold = std::clamp( val / 255.0f, 0.0f, 1.0f ); } -void MaterialColorController::updateTime( float time ) +AlphaInterpolator_Alpha * AlphaController_Alpha::createInterpolator( NifFieldConst interpolatorBlock ) { - if ( !(active && target) ) - return; + return new AlphaInterpolator_Alpha( interpolatorBlock, target, this ); +} + - Vector3 v3; - interpolate( v3, iData, "Data", ctrlTime( time ), lColor ); - - Color4 color( Color3( v3 ), 1.0 ); - - switch ( tColor ) { - case tAmbient: - target->ambient = color; - break; - case tDiffuse: - target->diffuse = color; - break; - case tSpecular: - target->specular = color; - break; - case tSelfIllum: - target->emissive = color; - break; +// MaterialColorInterpolator and MaterialColorController classes + +void MaterialColorInterpolator::updateDataImpl() +{ + auto ctrlBlock = controllerBlock(); + registerUpdateBlock( ctrlBlock ); + + auto fieldColor = ctrlBlock.child("Target Color"); + if ( fieldColor ) { + colorType = ColorType( fieldColor.value() ); + } else { + colorType = ColorType( ( ctrlBlock["Flags"].value() >> 4 ) & 7 ); } + // TODO: validate colorType value? + + auto dataBlock = getDataBlock(); + registerUpdateBlock( dataBlock ); + interpolator.updateData( dataBlock["Data"] ); } -bool MaterialColorController::update( const NifModel * nif, const QModelIndex & index ) +void MaterialColorInterpolator::applyTransformImpl( float time ) { - if ( Controller::update( nif, index ) ) { - if ( nif->checkVersion( 0x0A010000, 0 ) ) { - tColor = nif->get( iBlock, "Target Color" ); - } else { - tColor = ((nif->get( iBlock, "Flags" ) >> 4) & 7); + Vector3 val; + if ( interpolator.interpolate( val, time ) ) { + Color4 color( val, 1.0 ); + switch ( colorType ) { + case ColorType::Ambient: + target()->ambient = color; + break; + case ColorType::Diffuse: + target()->diffuse = color; + break; + case ColorType::Specular: + target()->specular = color; + break; + case ColorType::SelfIllum: + target()->emissive = color; + break; } - - return true; } +} - return false; +MaterialColorInterpolator * MaterialColorController::createInterpolator( NifFieldConst interpolatorBlock ) +{ + return new MaterialColorInterpolator( interpolatorBlock, target, this ); } -// `NiFlipController` +// TextureFlipData struct -TexFlipController::TexFlipController( TexturingProperty * prop, const QModelIndex & index ) - : Controller( index ), target( prop ) +void TextureFlipData::updateData( IControllerInterpolator * ctrlInterpolator, const QString & sourcesName, const QString & sourceBlockType ) { + auto ctrlBlock = ctrlInterpolator->controllerBlock(); + ctrlInterpolator->registerUpdateBlock( ctrlBlock ); + slot = ctrlBlock["Texture Slot"].value(); + + auto deltaField = ctrlBlock.child("Delta"); + hasDelta = deltaField.isValid(); + delta = deltaField.value(); + + sources.clear(); + auto sourcesRoot = ctrlBlock[sourcesName]; + sources.reserve( sourcesRoot.childCount() ); + for ( auto entry : sourcesRoot.iter() ) + sources.append( entry.linkBlock(sourceBlockType) ); + + auto dataBlock = ctrlInterpolator->getDataBlock(); + ctrlInterpolator->registerUpdateBlock( dataBlock ); + interpolator.updateData( dataBlock["Data"] ); } -TexFlipController::TexFlipController( TextureProperty * prop, const QModelIndex & index ) - : Controller( index ), oldTarget( prop ) +void TextureFlipData::interpolate( NifFieldConst & sourceBlock, float time ) { + if ( hasDelta ) { + if ( delta > 0.0f ) + sourceBlock = sources[time / delta]; + } else { + float r = 0; + if ( interpolator.interpolate( r, time ) ) + sourceBlock = sources[r]; + } } -void TexFlipController::updateTime( float time ) -{ - auto nif = NifModel::fromValidIndex(iSources); - if ( !((target || oldTarget) && active && nif) ) - return; - float r = 0; +// TextureFlipInterpolator_Texturing and TextureFlipController_Texturing classes - if ( iData.isValid() ) - interpolate( r, iData, "Data", ctrlTime( time ), flipLast ); - else if ( flipDelta > 0 ) - r = ctrlTime( time ) / flipDelta; +void TextureFlipInterpolator_Texturing::updateDataImpl() +{ + data.updateData( this, QStringLiteral("Sources"), QStringLiteral("NiSourceTexture") ); +} - // TexturingProperty - if ( target ) { - target->textures[flipSlot & 7].iSource = nif->getBlockIndex( nif->getLink( iSources.child( (int)r, 0 ) ), "NiSourceTexture" ); - } else if ( oldTarget ) { - oldTarget->iImage = nif->getBlockIndex( nif->getLink( iSources.child( (int)r, 0 ) ), "NiImage" ); - } +void TextureFlipInterpolator_Texturing::applyTransformImpl( float time ) +{ + data.interpolate( target()->textures[data.slot & 7].sourceBlock, time ); } -bool TexFlipController::update( const NifModel * nif, const QModelIndex & index ) +TextureFlipInterpolator_Texturing * TextureFlipController_Texturing::createInterpolator( NifFieldConst interpolatorBlock ) { - if ( Controller::update( nif, index ) ) { - flipDelta = nif->get( iBlock, "Delta" ); - flipSlot = nif->get( iBlock, "Texture Slot" ); + return new TextureFlipInterpolator_Texturing( interpolatorBlock, target, this ); +} - if ( nif->checkVersion( 0x04000000, 0 ) ) { - iSources = nif->getIndex( iBlock, "Sources" ); - } else { - iSources = nif->getIndex( iBlock, "Images" ); - } - return true; - } +// TextureFlipInterpolator_Texture and TextureFlipController_Texture classes - return false; +void TextureFlipInterpolator_Texture::updateDataImpl() +{ + data.updateData( this, QStringLiteral("Images"), QStringLiteral("NiImage") ); } - -// `NiTextureTransformController` - -TexTransController::TexTransController( TexturingProperty * prop, const QModelIndex & index ) - : Controller( index ), target( prop ) +void TextureFlipInterpolator_Texture::applyTransformImpl( float time ) { + data.interpolate( target()->imageBlock, time ); } -void TexTransController::updateTime( float time ) +TextureFlipInterpolator_Texture * TextureFlipController_Texture::createInterpolator( NifFieldConst interpolatorBlock ) { - if ( !(target && active) ) - return; + return new TextureFlipInterpolator_Texture( interpolatorBlock, target, this ); +} + - TexturingProperty::TexDesc * tex = &target->textures[texSlot & 7]; +// TextureTransformInterpolator and TextureTransformController classes +void TextureTransformInterpolator::updateDataImpl() +{ + auto ctrlBlock = controllerBlock(); + registerUpdateBlock( ctrlBlock ); + operationType = OperationType( ctrlBlock["Operation"].value() ); + // TODO: validate operationType value? + textureSlot = ctrlBlock["Texture Slot"].value(); + + auto dataBlock = getDataBlock(); + registerUpdateBlock( dataBlock ); + interpolator.updateData( dataBlock["Data"] ); +} + +void TextureTransformInterpolator::applyTransformImpl( float time ) +{ float val; + if ( interpolator.interpolate( val, time ) ) { + TexturingProperty::TexDesc * tex = target()->textures + (textureSlot & 7); - if ( interpolate( val, iData, "Data", ctrlTime( time ), lX ) ) { // If desired, we could force display even if texture transform was disabled: // tex->hasTransform = true; // however "Has Texture Transform" doesn't exist until 10.1.0.0, and neither does // NiTextureTransformController - so we won't bother - switch ( texOP ) { - case 0: + switch ( operationType ) { + case OperationType::TranslateU: tex->translation[0] = val; break; - case 1: + case OperationType::TranslateV: tex->translation[1] = val; break; - case 2: + case OperationType::Rotate: tex->rotation = val; break; - case 3: + case OperationType::ScaleU: tex->tiling[0] = val; break; - case 4: + case OperationType::ScaleV: tex->tiling[1] = val; break; } } } -bool TexTransController::update( const NifModel * nif, const QModelIndex & index ) +TextureTransformInterpolator * TextureTransformController::createInterpolator( NifFieldConst interpolatorBlock ) { - if ( Controller::update( nif, index ) ) { - texSlot = nif->get( iBlock, "Texture Slot" ); - texOP = nif->get( iBlock, "Operation" ); - return true; - } - - return false; + return new TextureTransformInterpolator( interpolatorBlock, target, this ); } +// EffectFloatInterpolator and EffectFloatController classes -EffectFloatController::EffectFloatController( BSEffectShaderProperty * prop, const QModelIndex & index ) - : Controller( index ), target( prop ) +void EffectFloatInterpolator::updateDataImpl() { + auto ctrlBlock = controllerBlock(); + registerUpdateBlock( ctrlBlock ); + valueType = ValueType( ctrlBlock["Controlled Variable"].value() ); + // TODO: validate valueType value? + + auto dataBlock = getDataBlock(); + registerUpdateBlock( dataBlock ); + interpolator.updateData( dataBlock["Data"] ); } -void EffectFloatController::updateTime( float time ) +void EffectFloatInterpolator::applyTransformImpl( float time ) { - if ( !(target && active) ) - return; - float val; - - int lIdx; - - if ( interpolate( val, iData, "Data", ctrlTime( time ), lIdx ) ) { - switch ( variable ) { - case EffectFloat::Emissive_Multiple: - target->emissiveMult = val; + if ( interpolator.interpolate( val, time ) ) { + switch ( valueType ) { + case ValueType::Emissive_Multiple: + target()->emissiveMult = val; break; - case EffectFloat::Falloff_Start_Angle: - target->falloff.startAngle = val; + case ValueType::Falloff_Start_Angle: + target()->falloff.startAngle = val; break; - case EffectFloat::Falloff_Stop_Angle: - target->falloff.stopAngle = val; + case ValueType::Falloff_Stop_Angle: + target()->falloff.stopAngle = val; break; - case EffectFloat::Falloff_Start_Opacity: - target->falloff.startOpacity = val; + case ValueType::Falloff_Start_Opacity: + target()->falloff.startOpacity = val; break; - case EffectFloat::Falloff_Stop_Opacity: - target->falloff.stopOpacity = val; + case ValueType::Falloff_Stop_Opacity: + target()->falloff.stopOpacity = val; break; - case EffectFloat::Alpha: - target->emissiveColor.setAlpha( val ); + case ValueType::Alpha: + target()->emissiveColor.setAlpha( val ); break; - case EffectFloat::U_Offset: - target->uvOffset.x = val; + case ValueType::U_Offset: + target()->uvOffset.x = val; break; - case EffectFloat::U_Scale: - target->uvScale.x = val; + case ValueType::U_Scale: + target()->uvScale.x = val; break; - case EffectFloat::V_Offset: - target->uvOffset.y = val; + case ValueType::V_Offset: + target()->uvOffset.y = val; break; - case EffectFloat::V_Scale: - target->uvScale.y = val; - break; - default: + case ValueType::V_Scale: + target()->uvScale.y = val; break; } } } -bool EffectFloatController::update( const NifModel * nif, const QModelIndex & index ) +EffectFloatInterpolator * EffectFloatController::createInterpolator( NifFieldConst interpolatorBlock ) { - if ( Controller::update( nif, index ) ) { - variable = EffectFloat::Variable( nif->get( iBlock, "Controlled Variable" ) ); - return true; - } - - return false; + return new EffectFloatInterpolator( interpolatorBlock, target, this ); } -EffectColorController::EffectColorController( BSEffectShaderProperty * prop, const QModelIndex & index ) - : Controller( index ), target( prop ) +// EffectColorInterpolator and EffectColorController classes + +void EffectColorInterpolator::updateDataImpl() { + auto ctrlBlock = controllerBlock(); + registerUpdateBlock( ctrlBlock ); + colorType = ctrlBlock["Controlled Color"].value(); + // TODO: validate variable value? + + auto dataBlock = getDataBlock(); + registerUpdateBlock( dataBlock ); + interpolator.updateData( dataBlock["Data"] ); } -void EffectColorController::updateTime( float time ) +void EffectColorInterpolator::applyTransformImpl( float time ) { - if ( !(target && active) ) - return; - Vector3 val; - - int lIdx; - - if ( interpolate( val, iData, "Data", ctrlTime( time ), lIdx ) ) { - switch ( variable ) { + if ( interpolator.interpolate( val, time ) ) { + switch ( colorType ) { case 0: - target->emissiveColor = Color4( val[0], val[1], val[2], target->emissiveColor.alpha() ); - break; - default: + target()->emissiveColor = Color4( val, target()->emissiveColor.alpha() ); break; } } } -bool EffectColorController::update( const NifModel * nif, const QModelIndex & index ) +EffectColorInterpolator * EffectColorController::createInterpolator( NifFieldConst interpolatorBlock ) { - if ( Controller::update( nif, index ) ) { - variable = nif->get( iBlock, "Controlled Color" ); - return true; - } - - return false; + return new EffectColorInterpolator( interpolatorBlock, target, this ); } -LightingFloatController::LightingFloatController( BSLightingShaderProperty * prop, const QModelIndex & index ) - : Controller( index ), target( prop ) +// LightingFloatInterpolator and LightingFloat LightingFloatController classes + +void LightingFloatInterpolator::updateDataImpl() { + auto ctrlBlock = controllerBlock(); + registerUpdateBlock( ctrlBlock ); + valueType = ValueType( ctrlBlock["Controlled Variable"].value() ); + // TODO: validate valueType value? + + auto dataBlock = getDataBlock(); + registerUpdateBlock( dataBlock ); + interpolator.updateData( dataBlock["Data"] ); } -void LightingFloatController::updateTime( float time ) +void LightingFloatInterpolator::applyTransformImpl( float time ) { - if ( !(target && active) ) - return; - float val; - - int lIdx; - - if ( interpolate( val, iData, "Data", ctrlTime( time ), lIdx ) ) { - switch ( variable ) { - case LightingFloat::Refraction_Strength: + if ( interpolator.interpolate( val, time ) ) { + switch ( valueType ) { + case ValueType::Refraction_Strength: break; - case LightingFloat::Reflection_Strength: - target->environmentReflection = val; + case ValueType::Reflection_Strength: + target()->environmentReflection = val; break; - case LightingFloat::Glossiness: - target->specularGloss = val; + case ValueType::Glossiness: + target()->specularGloss = val; break; - case LightingFloat::Specular_Strength: - target->specularStrength = val; + case ValueType::Specular_Strength: + target()->specularStrength = val; break; - case LightingFloat::Emissive_Multiple: - target->emissiveMult = val; + case ValueType::Emissive_Multiple: + target()->emissiveMult = val; break; - case LightingFloat::Alpha: - target->alpha = val; + case ValueType::Alpha: + target()->alpha = val; break; - case LightingFloat::U_Offset: - target->uvOffset.x = val; + case ValueType::U_Offset: + target()->uvOffset.x = val; break; - case LightingFloat::U_Scale: - target->uvScale.x = val; + case ValueType::U_Scale: + target()->uvScale.x = val; break; - case LightingFloat::V_Offset: - target->uvOffset.y = val; + case ValueType::V_Offset: + target()->uvOffset.y = val; break; - case LightingFloat::V_Scale: - target->uvScale.y = val; - break; - default: + case ValueType::V_Scale: + target()->uvScale.y = val; break; } } } -bool LightingFloatController::update( const NifModel * nif, const QModelIndex & index ) +LightingFloatInterpolator * LightingFloatController::createInterpolator( NifFieldConst interpolatorBlock ) { - if ( Controller::update( nif, index ) ) { - variable = LightingFloat::Variable(nif->get( iBlock, "Controlled Variable" )); - return true; - } - - return false; + return new LightingFloatInterpolator( interpolatorBlock, target, this ); } -LightingColorController::LightingColorController( BSLightingShaderProperty * prop, const QModelIndex & index ) - : Controller( index ), target( prop ) +// LightingColorInterpolator and LightingColorController classes + +void LightingColorInterpolator::updateDataImpl() { + auto ctrlBlock = controllerBlock(); + registerUpdateBlock( ctrlBlock ); + colorType = ctrlBlock["Controlled Color"].value(); + // TODO: validate colorType value? + + auto dataBlock = getDataBlock(); + registerUpdateBlock( dataBlock ); + interpolator.updateData( dataBlock["Data"] ); } -void LightingColorController::updateTime( float time ) +void LightingColorInterpolator::applyTransformImpl( float time ) { - if ( !(target && active) ) - return; - Vector3 val; - - int lIdx; - - if ( interpolate( val, iData, "Data", ctrlTime( time ), lIdx ) ) { - switch ( variable ) { + if ( interpolator.interpolate( val, time ) ) { + switch ( colorType ) { case 0: - target->specularColor = { val[0], val[1], val[2] }; + target()->specularColor.fromVector3( val ); break; case 1: - target->emissiveColor = { val[0], val[1], val[2] }; + target()->emissiveColor.fromVector3( val ); break; default: break; @@ -1157,12 +1363,7 @@ void LightingColorController::updateTime( float time ) } } -bool LightingColorController::update( const NifModel * nif, const QModelIndex & index ) +LightingColorInterpolator * LightingColorController::createInterpolator( NifFieldConst interpolatorBlock ) { - if ( Controller::update( nif, index ) ) { - variable = nif->get( iBlock, "Controlled Color" ); - return true; - } - - return false; + return new LightingColorInterpolator( interpolatorBlock, target, this ); } diff --git a/src/gl/controllers.h b/src/gl/controllers.h index 110221bde..dccffbed7 100644 --- a/src/gl/controllers.h +++ b/src/gl/controllers.h @@ -34,153 +34,210 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #define CONTROLLERS_H #include "gl/glcontroller.h" // Inherited -#include "data/niftypes.h" -#include +#include "gl/glnode.h" +#include "gl/glshape.h" +#include "gl/glparticles.h" +#include "gl/glproperty.h" -//! @file controllers.h Controller subclasses +//! @file controllers.h Implementations of specific controllers -class Shape; -class Node; - -//! Controller for `NiControllerManager` blocks +// Controller for `NiControllerManager` blocks class ControllerManager final : public Controller { + QPointer parent; + public: - ControllerManager( Node * node, const QModelIndex & index ); + ControllerManager( Node * _parent, NifFieldConst ctrlBlock ); - void updateTime( float ) override final {} + bool hasParent() const { return !parent.isNull(); } - bool update( const NifModel * nif, const QModelIndex & index ) override final; + void updateTime( float ) override final {} - void setSequence( const QString & seqname ) override final; + void setSequence( const QString & seqName ) override final; protected: - QPointer target; + void updateImpl( NifFieldConst changedBlock ) override final; }; -//! Controller for `NiKeyframeController` blocks -class KeyframeController final : public Controller +using ITransformInterpolator = IControllerInterpolatorTyped; + +// Interpolator for 'NiTransformInterpolator' and 'NiKeyframeController' +class TransformInterpolator final : public ITransformInterpolator { -public: - KeyframeController( Node * node, const QModelIndex & index ); + ValueInterpolatorVector3 translation; + ValueInterpolatorMatrix rotation; + ValueInterpolatorFloat scale; - void updateTime( float time ) override final; +public: + TransformInterpolator( NifFieldConst _interpolatorBlock, Node * node, Controller * _parentController ) + : ITransformInterpolator( _interpolatorBlock, node, _parentController ) {} - bool update( const NifModel * nif, const QModelIndex & index ) override final; + bool isActive() const override final { return translation.isActive() || rotation.isActive() || scale.isActive(); } protected: - QPointer target; + void updateDataImpl() override final; + void applyTransformImpl( float time ) override final; +}; - QPersistentModelIndex iTranslations, iRotations, iScales; +// Interpolator for 'NiBSplineCompTransformInterpolator' +class BSplineInterpolator final : public ITransformInterpolator +{ + float startTime = 0, stopTime = 0; - int lTrans, lRotate, lScale; -}; + struct SplineVars + { + uint off = USHRT_MAX; + float mult = 0.0f; + float bias = 0.0f; + bool isActive() const { return off != USHRT_MAX; } + }; + SplineVars rotateVars, translationVars, scaleVars; -//! Controller for `NiTransformController` blocks -class TransformController final : public Controller -{ -public: - TransformController( Node * node, const QModelIndex & index ); + NifFieldConst controlPointsRoot; + uint nControlPoints = 0; + int degree = 3; - void updateTime( float time ) override final; +public: + BSplineInterpolator( NifFieldConst _interpolatorBlock, Node * node, Controller * _parentController ) + : ITransformInterpolator( _interpolatorBlock, node, _parentController ) {} - void setInterpolator( const QModelIndex & idx ) override final; + bool isActive() const override final { return rotateVars.isActive() || translationVars.isActive() || scaleVars.isActive(); } protected: - QPointer target; - QPointer interpolator; + void updateDataImpl() override final; + void applyTransformImpl( float time ) override final; + +private: + template bool interpolateValue( T & value, float interval, const SplineVars & vars ) const; }; +// Controller for `NiTransformController` and 'NiKeyframeController' blocks +DECLARE_INTERPOLATED_CONTROLLER( TransformController, Node, ITransformInterpolator ) -//! Controller for `NiMultiTargetTransformController` blocks + +// Controller for `NiMultiTargetTransformController` blocks class MultiTargetTransformController final : public Controller { - typedef QPair, QPointer > TransformTarget; + QPointer parent; + NodeList targetNodes; + QVector transforms; public: - MultiTargetTransformController( Node * node, const QModelIndex & index ); + MultiTargetTransformController( Node * node, NifFieldConst ctrlBlock ); + virtual ~MultiTargetTransformController() { clearTransforms(); } - void updateTime( float time ) override final; + bool hasParent() const { return !parent.isNull(); } - bool update( const NifModel * nif, const QModelIndex & index ) override final; + void updateTime( float time ) override final; - bool setInterpolatorNode( Node * node, const QModelIndex & idx ); + bool setNodeInterpolator( Node * node, NifFieldConst interpolarorBlock ); protected: - QPointer target; - QList extraTargets; + void updateImpl( NifFieldConst changedBlock ) override final; + +private: + void clearTransforms(); + void removeTransformAt( int i ); }; -//! Controller for `NiVisController` blocks -class VisibilityController final : public Controller +// Interpolator for `NiVisController` blocks +class VisibilityInterpolator final : public IControllerInterpolatorTyped { -public: - VisibilityController( Node * node, const QModelIndex & index ); + ValueInterpolatorBool interpolator; - void updateTime( float time ) override final; +public: + VisibilityInterpolator( NifFieldConst _interpolatorBlock, Node * node, Controller * _parentController ) + : IControllerInterpolatorTyped( _interpolatorBlock, node, _parentController ) {} - bool update( const NifModel * nif, const QModelIndex & index ) override final; + bool isActive() const override final { return interpolator.isActive(); } protected: - QPointer target; - - int visLast; + void updateDataImpl() override final; + void applyTransformImpl( float time ) override final; }; +// Controller for `NiVisController` blocks +DECLARE_INTERPOLATED_CONTROLLER( VisibilityController, Node, VisibilityInterpolator ) + + +class MorphController; + +// Interpolator for `NiGeomMorpherController` blocks +class MorphInterpolator final : public IControllerInterpolatorTyped +{ + int verticesIndex; + const NifFieldConst morphDataEntry; + ValueInterpolatorFloat interpolator; + +public: + MorphInterpolator( int _verticesIndex, NifFieldConst _interpolatorBlock, Shape * shape, MorphController * _parentController, NifFieldConst _morphDataEntry ); + + bool isActive() const override final { return interpolator.isActive(); } + +protected: + void updateDataImpl() override final; + void applyTransformImpl( float time ) override final; +}; -//! Controller for `NiGeomMorpherController` blocks +// Controller for `NiGeomMorpherController` blocks class MorphController final : public Controller { - //! A representation of Mesh geometry morphs - struct MorphKey - { - QPersistentModelIndex iFrames; - QVector verts; - int index; - }; + friend class MorphInterpolator; + + QPointer target; + NifFieldConst dataBlock; + QVector< QVector > morphVertices; + QVector morphInterpolators; public: - MorphController( Shape * mesh, const QModelIndex & index ); - ~MorphController(); + MorphController( Shape * shape, NifFieldConst ctrlBlock ); + virtual ~MorphController() { clearMorphInterpolators(); } + + bool hasTarget() const { return !target.isNull(); } + + bool isActive() const; void updateTime( float time ) override final; - bool update( const NifModel * nif, const QModelIndex & index ) override final; + void setMorphInterpolator( int morphIndex, NifFieldConst interpolarorBlock ); protected: - QPointer target; - QVector morph; + void updateImpl( NifFieldConst changedBlock ) override final; + +private: + void clearMorphInterpolators(); }; -//! Controller for `NiUVController` blocks -class UVController final : public Controller +// Interpolator for `NiUVController` blocks +class UVInterpolator final : public IControllerInterpolatorTyped { -public: - UVController( Shape * mesh, const QModelIndex & index ); + static constexpr int UV_GROUPS_COUNT = 4; - ~UVController(); + QVector interpolators; - void updateTime( float time ) override final; +public: + UVInterpolator( NifFieldConst _interpolatorBlock, Shape * shape, Controller * _parentController ) + : IControllerInterpolatorTyped( _interpolatorBlock, shape, _parentController ), interpolators( UV_GROUPS_COUNT ) {} - bool update( const NifModel * nif, const QModelIndex & index ) override final; + bool isActive() const override final { return true; } protected: - QPointer target; - - int luv = 0; + void updateDataImpl() override final; + void applyTransformImpl( float time ) override final; }; +// Controller for `NiUVController` blocks +DECLARE_INTERPOLATED_CONTROLLER( UVController, Shape, UVInterpolator ) -class Particles; -//! Controller for `NiParticleSystemController` and other blocks -class ParticleController final : public Controller +// Interpolator for `NiParticleSystemController` and other blocks +class ParticleInterpolator final : public IControllerInterpolatorTyped { struct Particle { @@ -191,26 +248,23 @@ class ParticleController final : public Controller float lifespan = 0; float lasttime = 0; short y = 0; - short vertex = 0; - - Particle() - { - } + ushort vertex = 0; }; - QVector list; + QVector particles; + struct Gravity { float force; int type; Vector3 position; Vector3 direction; - }; - QVector grav; - QPointer target; + Gravity( NifFieldConst block ); + }; + QVector gravities; - float emitStart = 0, emitStop = 0, emitRate = 0, emitLast = 0, emitAccu = 0, emitMax = 0; QPointer emitNode; + float emitStart = 0, emitStop = 0, emitRate = 0, emitLast = 0, emitAccu = 0; Vector3 emitRadius; float spd = 0, spdRnd = 0; @@ -223,127 +277,190 @@ class ParticleController final : public Controller float grow = 0; float fade = 0; - float localtime = 0; - - QList iExtras; - QPersistentModelIndex iColorKeys; + ValueInterpolatorColor4 colorInterpolator; public: - ParticleController( Particles * particles, const QModelIndex & index ); + ParticleInterpolator( NifFieldConst _interpolatorBlock, Particles * particlesControllable, Controller * _parentController ) + : IControllerInterpolatorTyped( _interpolatorBlock, particlesControllable, _parentController ) {} - bool update( const NifModel * nif, const QModelIndex & index ) override final; + bool isActive() const override final { return true; } - void updateTime( float time ) override final; - - void startParticle( Particle & p ); +protected: + void updateDataImpl() override final; + void applyTransformImpl( float time ) override final; +private: + void startParticle( Particle & p, float localTime ); void moveParticle( Particle & p, float deltaTime ); - void sizeParticle( Particle & p, float & size ); - void colorParticle( Particle & p, Color4 & color ); }; +// Controller for `NiParticleSystemController` and other blocks +DECLARE_INTERPOLATED_CONTROLLER( ParticleController, Particles, ParticleInterpolator ) -class AlphaProperty; -class MaterialProperty; -class TexturingProperty; -class TextureProperty; -class BSEffectShaderProperty; -class BSLightingShaderProperty; -//! Controller for alpha values in a MaterialProperty -class AlphaController final : public Controller +// Interpolator for 'NiAlphaController' blocks (MaterialProperty) +class AlphaInterpolator_Material final : public IControllerInterpolatorTyped { -public: - AlphaController( MaterialProperty * prop, const QModelIndex & index ); + ValueInterpolatorFloat interpolator; - AlphaController( AlphaProperty * prop, const QModelIndex & index ); +public: + AlphaInterpolator_Material( NifFieldConst _interpolatorBlock, MaterialProperty * prop, Controller * _parentController ) + : IControllerInterpolatorTyped( _interpolatorBlock, prop, _parentController ) {} - void updateTime( float time ) override final; + bool isActive() const override final { return interpolator.isActive(); } protected: - QPointer materialProp; - QPointer alphaProp; - - int lAlpha = 0; + void updateDataImpl() override final; + void applyTransformImpl( float time ) override final; }; +// Controller for 'NiAlphaController' blocks (MaterialProperty) +DECLARE_INTERPOLATED_CONTROLLER( AlphaController_Material, MaterialProperty, AlphaInterpolator_Material ) + -//! Controller for color values in a MaterialProperty -class MaterialColorController final : public Controller +// Interpolator for 'BSNiAlphaPropertyTestRefController' blocks (AlphaProperty) +class AlphaInterpolator_Alpha final : public IControllerInterpolatorTyped { -public: - MaterialColorController( MaterialProperty * prop, const QModelIndex & index ); + ValueInterpolatorFloat interpolator; - void updateTime( float time ) override final; +public: + AlphaInterpolator_Alpha( NifFieldConst _interpolatorBlock, AlphaProperty * prop, Controller * _parentController ) + : IControllerInterpolatorTyped( _interpolatorBlock, prop, _parentController ) {} - bool update( const NifModel * nif, const QModelIndex & index ) override final; + bool isActive() const override final { return interpolator.isActive(); } protected: - QPointer target; //!< The MaterialProperty being controlled + void updateDataImpl() override final; + void applyTransformImpl( float time ) override final; +}; - int lColor = 0; //!< Last interpolation time - int tColor = tAmbient; //!< The color slot being controlled +// Controller for 'BSNiAlphaPropertyTestRefController' blocks (AlphaProperty) +DECLARE_INTERPOLATED_CONTROLLER( AlphaController_Alpha, AlphaProperty, AlphaInterpolator_Alpha ) - //! Color slots that can be controlled - enum + +// Interpolator for 'NiMaterialColorController' blocks +class MaterialColorInterpolator final : public IControllerInterpolatorTyped +{ + enum class ColorType { - tAmbient = 0, - tDiffuse = 1, - tSpecular = 2, - tSelfIllum = 3 + Ambient = 0, + Diffuse = 1, + Specular = 2, + SelfIllum = 3, }; + ColorType colorType = ColorType::Ambient; //!< The color slot being controlled + + ValueInterpolatorVector3 interpolator; + +public: + MaterialColorInterpolator( NifFieldConst _interpolatorBlock, MaterialProperty * prop, Controller * _parentController ) + : IControllerInterpolatorTyped( _interpolatorBlock, prop, _parentController ) {} + + bool isActive() const override final { return interpolator.isActive(); } + +protected: + void updateDataImpl() override final; + void applyTransformImpl( float time ) override final; }; +// Controller for 'NiMaterialColorController' blocks +DECLARE_INTERPOLATED_CONTROLLER( MaterialColorController, MaterialProperty, MaterialColorInterpolator ) -//! Controller for source textures in a TexturingProperty -class TexFlipController final : public Controller + +// Common data for texture flip interpolators +struct TextureFlipData { -public: - TexFlipController( TexturingProperty * prop, const QModelIndex & index ); + bool hasDelta = false; + float delta = 0; + int slot = 0; - TexFlipController( TextureProperty * prop, const QModelIndex & index ); + QVector sources; + ValueInterpolatorFloat interpolator; - void updateTime( float time ) override final; + bool isActive() const { return hasDelta ? ( delta > 0.0f ) : interpolator.isActive(); } - bool update( const NifModel * nif, const QModelIndex & index ) override final; + void updateData( IControllerInterpolator * ctrlInterpolator, const QString & sourcesName, const QString & sourceBlockType ); + void interpolate( NifFieldConst & sourceBlock, float time ); +}; -protected: - QPointer target; - QPointer oldTarget; +// Interpolator for 'NiFlipController' blocks (TexturingProperty) +class TextureFlipInterpolator_Texturing final : public IControllerInterpolatorTyped +{ + TextureFlipData data; - float flipDelta = 0; - int flipSlot = 0; +public: + TextureFlipInterpolator_Texturing( NifFieldConst _interpolatorBlock, TexturingProperty * prop, Controller * _parentController ) + : IControllerInterpolatorTyped( _interpolatorBlock, prop, _parentController ) {} - int flipLast = 0; + bool isActive() const override final { return data.isActive(); } - QPersistentModelIndex iSources; +protected: + void updateDataImpl() override final; + void applyTransformImpl( float time ) override final; }; +// Controller for 'NiFlipController' blocks (TexturingProperty) +DECLARE_INTERPOLATED_CONTROLLER( TextureFlipController_Texturing, TexturingProperty, TextureFlipInterpolator_Texturing ) -//! Controller for transformations in a TexturingProperty -class TexTransController final : public Controller +// Interpolator for 'NiFlipController' blocks (TextureProperty) +class TextureFlipInterpolator_Texture final : public IControllerInterpolatorTyped { -public: - TexTransController( TexturingProperty * prop, const QModelIndex & index ); + TextureFlipData data; - void updateTime( float time ) override final; +public: + TextureFlipInterpolator_Texture( NifFieldConst _interpolatorBlock, TextureProperty * prop, Controller * _parentController ) + : IControllerInterpolatorTyped( _interpolatorBlock, prop, _parentController ) {} - bool update( const NifModel * nif, const QModelIndex & index ) override final; + bool isActive() const override final { return data.isActive(); } protected: - QPointer target; + void updateDataImpl() override final; + void applyTransformImpl( float time ) override final; +}; + +// Controller for 'NiFlipController' blocks (TextureProperty) +DECLARE_INTERPOLATED_CONTROLLER( TextureFlipController_Texture, TextureProperty, TextureFlipInterpolator_Texture ) + + +// Interpolator for 'NiTextureTransformController' blocks +class TextureTransformInterpolator final : public IControllerInterpolatorTyped +{ + ValueInterpolatorFloat interpolator; + + enum class OperationType + { + TranslateU = 0, + TranslateV = 1, + Rotate = 2, + ScaleU = 3, + ScaleV = 4, + }; + OperationType operationType = OperationType::TranslateU; + int textureSlot = 0; + +public: + TextureTransformInterpolator( NifFieldConst _interpolatorBlock, TexturingProperty * prop, Controller * _parentController ) + : IControllerInterpolatorTyped( _interpolatorBlock, prop, _parentController ) {} - int texSlot = 0; - int texOP = 0; + bool isActive() const override final { return interpolator.isActive(); } - int lX = 0; +protected: + void updateDataImpl() override final; + void applyTransformImpl( float time ) override final; }; -namespace EffectFloat +// Controller for 'NiTextureTransformController' blocks +DECLARE_INTERPOLATED_CONTROLLER( TextureTransformController, TexturingProperty, TextureTransformInterpolator ) + + +// Interpolator for 'BSEffectShaderPropertyFloatController' blocks +class EffectFloatInterpolator final : public IControllerInterpolatorTyped { - enum Variable + ValueInterpolatorFloat interpolator; + + enum class ValueType { Emissive_Multiple = 0, Falloff_Start_Angle = 1, @@ -356,45 +473,50 @@ namespace EffectFloat V_Offset = 8, V_Scale = 9 }; -} + ValueType valueType = ValueType::Emissive_Multiple; - -//! Controller for float values in a BSEffectShaderProperty -class EffectFloatController final : public Controller -{ public: - EffectFloatController( BSEffectShaderProperty * prop, const QModelIndex & index ); - - void updateTime( float time ) override final; + EffectFloatInterpolator( NifFieldConst _interpolatorBlock, BSEffectShaderProperty * prop, Controller * _parentController ) + : IControllerInterpolatorTyped( _interpolatorBlock, prop, _parentController ) {} - bool update( const NifModel * nif, const QModelIndex & index ) override final; + bool isActive() const override final { return interpolator.isActive(); } protected: - QPointer target; - - EffectFloat::Variable variable = EffectFloat::Emissive_Multiple; + void updateDataImpl() override final; + void applyTransformImpl( float time ) override final; }; +// Controller for 'BSEffectShaderPropertyFloatController' blocks +DECLARE_INTERPOLATED_CONTROLLER( EffectFloatController, BSEffectShaderProperty, EffectFloatInterpolator ) + -//! Controller for color values in a BSEffectShaderProperty -class EffectColorController final : public Controller +// Interpolator for 'BSEffectShaderPropertyColorController' blocks +class EffectColorInterpolator final : public IControllerInterpolatorTyped { -public: - EffectColorController( BSEffectShaderProperty * prop, const QModelIndex & index ); + ValueInterpolatorVector3 interpolator; + int colorType = 0; - void updateTime( float time ) override final; +public: + EffectColorInterpolator( NifFieldConst _interpolatorBlock, BSEffectShaderProperty * prop, Controller * _parentController ) + : IControllerInterpolatorTyped( _interpolatorBlock, prop, _parentController ) {} - bool update( const NifModel * nif, const QModelIndex & index ) override final; + bool isActive() const override final { return interpolator.isActive(); } protected: - QPointer target; - - int variable = 0; + void updateDataImpl() override final; + void applyTransformImpl( float time ) override final; }; -namespace LightingFloat +// Controller for 'BSEffectShaderPropertyColorController' blocks +DECLARE_INTERPOLATED_CONTROLLER( EffectColorController, BSEffectShaderProperty, EffectColorInterpolator ) + + +// Interpolator for 'BSLightingShaderPropertyFloatController' blocks +class LightingFloatInterpolator final : public IControllerInterpolatorTyped { - enum Variable + ValueInterpolatorFloat interpolator; + + enum class ValueType { Refraction_Strength = 0, Reflection_Strength = 8, @@ -407,41 +529,41 @@ namespace LightingFloat V_Offset = 22, V_Scale = 23 }; -} - + ValueType valueType = ValueType::Refraction_Strength; -//! Controller for float values in a BSEffectShaderProperty -class LightingFloatController final : public Controller -{ public: - LightingFloatController( BSLightingShaderProperty * prop, const QModelIndex & index ); + LightingFloatInterpolator( NifFieldConst _interpolatorBlock, BSLightingShaderProperty * prop, Controller * _parentController ) + : IControllerInterpolatorTyped( _interpolatorBlock, prop, _parentController ) {} - void updateTime( float time ) override final; - - bool update( const NifModel * nif, const QModelIndex & index ) override final; + bool isActive() const override final { return interpolator.isActive(); } protected: - QPointer target; - - LightingFloat::Variable variable = LightingFloat::Refraction_Strength; + void updateDataImpl() override final; + void applyTransformImpl( float time ) override final; }; +// Controller for 'BSLightingShaderPropertyFloatController' blocks +DECLARE_INTERPOLATED_CONTROLLER( LightingFloatController, BSLightingShaderProperty, LightingFloatInterpolator ) -//! Controller for color values in a BSEffectShaderProperty -class LightingColorController final : public Controller + +// Interpolator for 'BSLightingShaderPropertyColorController' blocks +class LightingColorInterpolator final : public IControllerInterpolatorTyped { -public: - LightingColorController( BSLightingShaderProperty * prop, const QModelIndex & index ); + ValueInterpolatorVector3 interpolator; + int colorType = 0; - void updateTime( float time ) override final; +public: + LightingColorInterpolator( NifFieldConst _interpolatorBlock, BSLightingShaderProperty * prop, Controller * _parentController ) + : IControllerInterpolatorTyped( _interpolatorBlock, prop, _parentController ) {} - bool update( const NifModel * nif, const QModelIndex & index ) override final; + bool isActive() const override final { return interpolator.isActive(); } protected: - QPointer target; - - int variable = 0; + void updateDataImpl() override final; + void applyTransformImpl( float time ) override final; }; +// Controller for 'BSLightingShaderPropertyColorController' blocks +DECLARE_INTERPOLATED_CONTROLLER( LightingColorController, BSLightingShaderProperty, LightingColorInterpolator ) #endif // CONTROLLERS_H diff --git a/src/gl/glcontrollable.cpp b/src/gl/glcontrollable.cpp new file mode 100644 index 000000000..a15ea9c81 --- /dev/null +++ b/src/gl/glcontrollable.cpp @@ -0,0 +1,196 @@ +/***** BEGIN LICENSE BLOCK ***** + +BSD License + +Copyright (c) 2005-2015, NIF File Format Library and Tools +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. The name of the NIF File Format Library and Tools project may not be + used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +***** END LICENCE BLOCK *****/ + +#include "glcontrollable.h" + +#include "gl/glcontroller.h" +#include "gl/glscene.h" + + +//! @file glcontrollable.cpp IControllable interface + +IControllable::IControllable( Scene * _scene, NifFieldConst _block ) + : scene( _scene ), block( _block ), iBlock( _block.toIndex() ), model( _block.model() ) +{ + Q_ASSERT( scene != nullptr ); + Q_ASSERT( block.isBlock() ); + Q_ASSERT( model != nullptr ); +} + +IControllable::~IControllable() +{ + qDeleteAll( controllers ); +} + +void IControllable::clear() +{ + name = QString(); + + qDeleteAll( controllers ); + controllers.clear(); +} + +Controller * IControllable::createController( [[maybe_unused]] NifFieldConst controllerBlock ) +{ + return nullptr; +} + +Controller * IControllable::findController( const QString & ctrlType, [[maybe_unused]] const QString & var1, [[maybe_unused]] const QString & var2 ) const +{ + Controller * ctrl = nullptr; + + for ( Controller * c : controllers ) { + if ( c->typeId() == ctrlType ) { + if ( !ctrl ) { + ctrl = c; + } else { + ctrl = nullptr; + // TODO: eval var1 + var2 offset to determine which controller is targeted + break; + } + } + } + + return ctrl; +} + +Controller * IControllable::findController( NifFieldConst ctrlBlock ) const +{ + for ( Controller * c : controllers ) { + if ( c->block == ctrlBlock ) + return c; + } + + return nullptr; +} + +void IControllable::update( const NifModel * nif, const QModelIndex & index ) +{ + if ( isValid() ) { + updateImpl( nif, index ); + } else { + clear(); + } +} + +void IControllable::updateImpl( const NifModel * nif, const QModelIndex & index ) +{ + NifFieldConst changedBlock = nif->field(index); + bool syncControllers = false; + + if ( changedBlock == block ) { + name = block.child("Name").value(); + syncControllers = true; + } + + for ( Controller * ctrl : controllers ) { + ctrl->update( changedBlock ); + if ( changedBlock == ctrl->block ) + syncControllers = true; + } + + // Sync the list of attached controllers + if ( syncControllers ) { + QList obsolete( controllers ); + + // TODO: check if we're not stuck in an infinite controller loop + auto ctrlField = block.child("Controller"); + while ( true ) { + auto ctrlBlock = ctrlField.linkBlock("NiTimeController"); + if ( !ctrlBlock ) + break; + + Controller * ctrl = findController( ctrlBlock ); + if ( ctrl ) { + obsolete.removeAll( ctrl ); + } else { + ctrl = createController( ctrlBlock ); + if ( ctrl ) { + controllers.append( ctrl ); + ctrl->update(); + } + } + + ctrlField = ctrlBlock.child("Next Controller"); + } + + for ( Controller * ctrl : obsolete ) { + controllers.removeAll( ctrl ); + delete ctrl; + } + } +} + +void IControllable::transform() +{ + if ( scene->animate ) { + for ( Controller * controller : controllers ) { + controller->updateTime( scene->time ); + } + } +} + +void IControllable::timeBounds( float & tmin, float & tmax ) +{ + if ( controllers.isEmpty() ) + return; + + float mn = controllers.first()->start; + float mx = controllers.first()->stop; + + for ( Controller * c : controllers ) { + mn = qMin( mn, c->start ); + mx = qMax( mx, c->stop ); + } + tmin = qMin( tmin, mn ); + tmax = qMax( tmax, mx ); +} + +void IControllable::setSequence( const QString & seqName ) +{ + for ( Controller * ctrl : controllers ) { + ctrl->setSequence( seqName ); + } +} + +void IControllable::reportFieldCountMismatch( NifFieldConst rootEntry1, int entryCount1, NifFieldConst rootEntry2, int entryCount2, NifFieldConst reportEntry ) +{ + if ( rootEntry1 && rootEntry2 && entryCount1 != entryCount2 ) { + reportEntry.reportError( + tr("The number of entries in %1 (%2) does not match that in %3 (%4).") + .arg( rootEntry1.repr( reportEntry ) ) + .arg( entryCount1 ) + .arg( rootEntry2.repr( reportEntry ) ) + .arg( entryCount2 ) + ); + } +} diff --git a/src/gl/icontrollable.h b/src/gl/glcontrollable.h similarity index 57% rename from src/gl/icontrollable.h rename to src/gl/glcontrollable.h index 466935947..e6ce55651 100644 --- a/src/gl/icontrollable.h +++ b/src/gl/glcontrollable.h @@ -33,63 +33,72 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #ifndef ICONTROLLABLE_H #define ICONTROLLABLE_H +#include "model/nifmodel.h" + #include // Inherited #include #include -#include -//! @file icontrollable.h IControllable interface +//! @file glcontrollable.h IControllable interface class Controller; class Scene; -class NifModel; -//! Anything capable of having a Controller +// A block capable of having a Controller class IControllable : public QObject { Q_OBJECT - friend class ControllerManager; +public: + Scene * const scene; + const NifFieldConst block; + const NifModel * const model; + +protected: + QPersistentModelIndex iBlock; + QList controllers; + QString name; public: - IControllable( Scene * Scene, const QModelIndex & index ); + IControllable( Scene * _scene, NifFieldConst _block ); virtual ~IControllable(); - QModelIndex index() const { return iBlock; } - virtual bool isValid() const { return iBlock.isValid(); } + QModelIndex index() const { return iBlock; } // TODO: Get rid of it + bool isValid() const { return iBlock.isValid(); } + + auto modelVersion() const { return model->getVersionNumber(); } + bool modelVersionInRange( quint32 since, quint32 until ) const { return model->checkVersion( since, until ); } + auto modelBSVersion() const { return model->getBSVersion(); } + + const QString & blockName() const { return name; } virtual void clear(); void update( const NifModel * nif, const QModelIndex & index ); + void update() { update( model, iBlock ); } virtual void transform(); virtual void timeBounds( float & start, float & stop ); - void setSequence( const QString & seqname ); - Controller * findController( const QString & ctrltype, const QString & var1, const QString & var2 ); + void setSequence( const QString & seqName ); - Controller * findController( const QModelIndex & index ); + Controller * findController( const QString & ctrlType, const QString & var1, const QString & var2 ) const; + Controller * findController( NifFieldConst ctrlBlock ) const; - QString getName() const; + static void reportFieldCountMismatch( NifFieldConst rootEntry1, int entryCount1, NifFieldConst rootEntry2, int entryCount2, NifFieldConst reportEntry ); + static void reportFieldCountMismatch( NifFieldConst rootEntry1, NifFieldConst rootEntry2, NifFieldConst reportEntry ) + { + reportFieldCountMismatch( rootEntry1, rootEntry1.childCount(), rootEntry2, rootEntry2.childCount(), reportEntry ); + } protected: - //! Sets the Controller - virtual void setController( const NifModel * nif, const QModelIndex & iController ); + // Create a Controller from a block if applicable + virtual Controller * createController( NifFieldConst controllerBlock ); - //! Actual implementation of update, with the validation check taken care of by update(...) + // Actual implementation of update, with the validation check taken care of by update(...) virtual void updateImpl( const NifModel * nif, const QModelIndex & index ); - - Scene * scene; - - QPersistentModelIndex iBlock; - - QList controllers; - - void registerController( const NifModel* nif, Controller *ctrl ); - - QString name; }; #endif diff --git a/src/gl/glcontroller.cpp b/src/gl/glcontroller.cpp index 9a80c771c..ae63ef08f 100644 --- a/src/gl/glcontroller.cpp +++ b/src/gl/glcontroller.cpp @@ -33,234 +33,59 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "glcontroller.h" #include "gl/glscene.h" -#include "model/nifmodel.h" //! @file glcontroller.cpp Controllable management, Interpolation management -/* - * IControllable - */ - -IControllable::IControllable( Scene * s, const QModelIndex & iBlock) : scene( s ), iBlock( iBlock ) -{ -} - -IControllable::~IControllable() -{ - qDeleteAll( controllers ); -} - -QString IControllable::getName() const -{ - return name; -} - -void IControllable::setController( const NifModel * nif, const QModelIndex & iController ) -{ - Q_UNUSED( nif ); Q_UNUSED( iController ); -} - -void IControllable::clear() -{ - name = QString(); - - qDeleteAll( controllers ); - controllers.clear(); -} - -Controller * IControllable::findController( const QString & ctrltype, const QString & var1, const QString & var2 ) -{ - Q_UNUSED( var2 ); Q_UNUSED( var1 ); - Controller * ctrl = nullptr; - - for ( Controller * c : controllers ) { - if ( c->typeId() == ctrltype ) { - if ( !ctrl ) { - ctrl = c; - } else { - ctrl = nullptr; - // TODO: eval var1 + var2 offset to determine which controller is targeted - break; - } - } - } - - return ctrl; -} - -Controller * IControllable::findController( const QModelIndex & index ) -{ - for ( Controller * c : controllers ) { - if ( c->index() == index ) - return c; - } - - return nullptr; -} - -void IControllable::update( const NifModel * nif, const QModelIndex & index ) +Controller::Controller( NifFieldConst ctrlBlock ) + : block( ctrlBlock ), iBlock( ctrlBlock.toIndex() ) { - if ( !iBlock.isValid() ) - clear(); - else { - updateImpl(nif, index); - } -} - -void IControllable::updateImpl(const NifModel * nif, const QModelIndex & index) -{ - bool doUpdate = ( iBlock == index ); - for ( Controller * ctrl : controllers ) { - ctrl->update( nif, index ); - if ( !doUpdate && ctrl->index() == index ) - doUpdate = true; - } - - if ( doUpdate ) { - name = nif->get( iBlock, "Name" ); - // sync the list of attached controllers - QList rem( controllers ); - QModelIndex iCtrl = nif->getBlockIndex( nif->getLink( iBlock, "Controller" ) ); - - while ( iCtrl.isValid() && nif->blockInherits( iCtrl, "NiTimeController" ) ) { - bool add = true; - - for ( Controller * ctrl : controllers ) { - if ( ctrl->index() == iCtrl ) { - add = false; - rem.removeAll( ctrl ); - } - } - - if ( add ) - setController( nif, iCtrl ); - - iCtrl = nif->getBlockIndex( nif->getLink( iCtrl, "Next Controller" ) ); - } - - for ( Controller * ctrl : rem ) { - controllers.removeAll( ctrl ); - delete ctrl; - } - } + Q_ASSERT( block.isBlock() ); } -void IControllable::transform() +void Controller::setSequence( [[maybe_unused]] const QString & seqName ) { - if ( scene->animate ) { - for ( Controller * controller : controllers ) { - controller->updateTime( scene->time ); - } - } } -void IControllable::timeBounds( float & tmin, float & tmax ) +void Controller::setInterpolator( [[maybe_unused]] NifFieldConst newInterpolatorBlock ) { - if ( controllers.isEmpty() ) - return; - - float mn = controllers.first()->start; - float mx = controllers.first()->stop; - - for ( Controller * c : controllers ) { - mn = qMin( mn, c->start ); - mx = qMax( mx, c->stop ); - } - tmin = qMin( tmin, mn ); - tmax = qMax( tmax, mx ); } -void IControllable::setSequence( const QString & seqname ) +void Controller::update( NifFieldConst changedBlock ) { - for ( Controller * ctrl : controllers ) { - ctrl->setSequence( seqname ); + if ( isValid() ) { + updateImpl( changedBlock ); } } -void IControllable::registerController( const NifModel* nif, Controller* ctrl ) -{ - ctrl->update(nif, ctrl->index()); - controllers.append(ctrl); -} - - -/* - * Controller - */ - -Controller::Controller( const QModelIndex & index ) : iBlock( index ) -{ -} - -QString Controller::typeId() const -{ - if ( iBlock.isValid() ) - return iBlock.data( NifSkopeDisplayRole ).toString(); - - return QString(); -} - -void Controller::setSequence( const QString & seqname ) -{ - Q_UNUSED( seqname ); -} - -void Controller::setInterpolator( const QModelIndex & index ) -{ - iInterpolator = index; - - auto nif = NifModel::fromIndex( index ); - if ( nif ) - iData = nif->getBlockIndex( nif->getLink( iInterpolator, "Data" ) ); -} - -bool Controller::update( const NifModel * nif, const QModelIndex & index ) +void Controller::updateImpl( NifFieldConst changedBlock ) { - if ( index == iBlock && iBlock.isValid() ) { - start = nif->get( index, "Start Time" ); - stop = nif->get( index, "Stop Time" ); - phase = nif->get( index, "Phase" ); - frequency = nif->get( index, "Frequency" ); + if ( changedBlock == block ) { + start = block.child("Start Time").value(); + stop = block.child("Stop Time").value(); + phase = block.child("Phase").value(); + frequency = block.child("Frequency").value(); - int flags = nif->get( index, "Flags" ); + int flags = block.child("Flags").value(); active = flags & 0x08; - extrapolation = (Extrapolation)( ( flags & 0x06 ) >> 1 ); + extrapolation = ExtrapolationType( ( flags & 0x06 ) >> 1 ); // TODO: Bit 4 (16) - Plays entire animation backwards. // TODO: Bit 5 (32) - Generally only set when sequences are present. // TODO: Bit 6 (64) - Always seems to be set on Skyrim NIFs, unknown function. - - QModelIndex idx = nif->getBlockIndex( nif->getLink( iBlock, "Interpolator" ) ); - - if ( idx.isValid() ) { - setInterpolator( idx ); - } else { - idx = nif->getBlockIndex( nif->getLink( iBlock, "Data" ) ); - - if ( idx.isValid() ) - iData = idx; - } } - - if ( index == iInterpolator && iInterpolator.isValid() ) - iData = nif->getBlockIndex( nif->getLink( iInterpolator, "Data" ) ); - - return ( index.isValid() && (index == iBlock || index == iInterpolator || index == iData) ); } float Controller::ctrlTime( float time ) const { time = frequency * time + phase; - if ( time >= start && time <= stop ) return time; switch ( extrapolation ) { - case Cyclic: + case ExtrapolationType::Cyclic: { float delta = stop - start; - if ( delta <= 0 ) return start; @@ -269,10 +94,10 @@ float Controller::ctrlTime( float time ) const return start + y; } - case Reverse: + + case ExtrapolationType::Reverse: { float delta = stop - start; - if ( delta <= 0 ) return start; @@ -284,7 +109,8 @@ float Controller::ctrlTime( float time ) const return stop - y; } - case Constant: + + case ExtrapolationType::Constant: default: if ( time < start ) @@ -297,505 +123,292 @@ float Controller::ctrlTime( float time ) const } } -bool Controller::timeIndex( float time, const NifModel * nif, const QModelIndex & array, int & i, int & j, float & x ) +NifFieldConst Controller::getInterpolatorBlock( NifFieldConst controllerBlock ) { - int count; - - if ( array.isValid() && ( count = nif->rowCount( array ) ) > 0 ) { - if ( time <= nif->get( array.child( 0, 0 ), "Time" ) ) { - i = j = 0; - x = 0.0; - - return true; - } - - if ( time >= nif->get( array.child( count - 1, 0 ), "Time" ) ) { - i = j = count - 1; - x = 0.0; - - return true; - } - - if ( i < 0 || i >= count ) - i = 0; - - float tI = nif->get( array.child( i, 0 ), "Time" ); - - if ( time > tI ) { - j = i + 1; - float tJ; - - while ( time >= ( tJ = nif->get( array.child( j, 0 ), "Time" ) ) ) { - i = j++; - tI = tJ; - } - - x = ( time - tI ) / ( tJ - tI ); - - return true; - } else if ( time < tI ) { - j = i - 1; - float tJ; - - while ( time <= ( tJ = nif->get( array.child( j, 0 ), "Time" ) ) ) { - i = j--; - tI = tJ; - } - - x = ( time - tI ) / ( tJ - tI ); - - // Quadratic Bug Fix - - // Invert x - // Previously, this branch was causing x to decrement from 1.0. - // (This works fine for linear interpolation apparently) - x = 1.0 - x; - - // Swap I and J - // With x inverted, we must swap I and J or the animation will reverse. - auto tmpI = i; - i = j; - j = tmpI; - - // End Bug Fix - - return true; - } + auto interpolatorField = controllerBlock.child("Interpolator"); + if ( interpolatorField ) + return interpolatorField.linkBlock(); - j = i; - x = 0.0; - - return true; - } + if ( controllerBlock.child("Data") ) // Support for old controllers + return controllerBlock; - return false; + return NifFieldConst(); } -template bool interpolate( T & value, const QModelIndex & array, float time, int & last ) -{ - auto nif = NifModel::fromValidIndex(array); - if ( nif ) { - QModelIndex frames = nif->getIndex( array, "Keys" ); - int next; - float x; - - if ( Controller::timeIndex( time, nif, frames, last, next, x ) ) { - T v1 = nif->get( frames.child( last, 0 ), "Value" ); - T v2 = nif->get( frames.child( next, 0 ), "Value" ); - - switch ( nif->get( array, "Interpolation" ) ) { - - case 2: - { - // Quadratic - /* - In general, for keyframe values v1 = 0, v2 = 1 it appears that - setting v1's corresponding "Backward" value to 1 and v2's - corresponding "Forward" to 1 results in a linear interpolation. - */ - - // Tangent 1 - T t1 = nif->get( frames.child( last, 0 ), "Backward" ); - // Tangent 2 - T t2 = nif->get( frames.child( next, 0 ), "Forward" ); - - float x2 = x * x; - float x3 = x2 * x; - - // Cubic Hermite spline - // x(t) = (2t^3 - 3t^2 + 1)P1 + (-2t^3 + 3t^2)P2 + (t^3 - 2t^2 + t)T1 + (t^3 - t^2)T2 - - value = v1 * (2.0f * x3 - 3.0f * x2 + 1.0f) + v2 * (-2.0f * x3 + 3.0f * x2) + t1 * (x3 - 2.0f * x2 + x) + t2 * (x3 - x2); - - } return true; - - case 5: - // Constant - if ( x < 0.5 ) - value = v1; - else - value = v2; - - return true; - default: - value = v1 + ( v2 - v1 ) * x; - return true; - } - } - } - return false; -} +// ValueInterpolator class -template <> bool Controller::interpolate( float & value, const QModelIndex & array, float time, int & last ) +template +ValueInterpolator::Key::Key( NifFieldConst keyRoot, int iTimeField, int iValueField, int iBackwardField, int iForwardField ) + : time( keyRoot[iTimeField].value() ), + value( keyRoot[iValueField].value() ), + backward( keyRoot.child(iBackwardField).value() ), + forward( keyRoot.child(iForwardField).value() ) { - return ::interpolate( value, array, time, last ); } -template <> bool Controller::interpolate( Vector3 & value, const QModelIndex & array, float time, int & last ) +template +void ValueInterpolator::clear() { - return ::interpolate( value, array, time, last ); + keys.clear(); } -template <> bool Controller::interpolate( Color4 & value, const QModelIndex & array, float time, int & last ) +template +void ValueInterpolator::updateData( NifFieldConst keyGroup ) { - return ::interpolate( value, array, time, last ); -} + interpolationMode = InterpolationMode::Unknown; + keys.clear(); -template <> bool Controller::interpolate( Color3 & value, const QModelIndex & array, float time, int & last ) -{ - return ::interpolate( value, array, time, last ); -} + NifFieldConst keyArrayRoot; + if ( keyGroup.hasStrType("KeyGroup", "Morph") ) { + keyArrayRoot = keyGroup.child("Keys"); + auto modeField = keyGroup.child("Interpolation"); + if ( modeField ) + interpolationMode = InterpolationMode( modeField.value() ); -template <> bool Controller::interpolate( bool & value, const QModelIndex & array, float time, int & last ) -{ - int next; - float x; + } else if ( keyGroup.hasStrType("QuatKey") ) { + keyArrayRoot = keyGroup; - auto nif = NifModel::fromValidIndex(array); - if ( nif ) { - QModelIndex frames = nif->getIndex( array, "Keys" ); - - if ( timeIndex( time, nif, frames, last, next, x ) ) { - value = nif->get( frames.child( last, 0 ), "Value" ); - - return true; - } + } else { + if ( keyGroup ) + keyGroup.reportError( QString("Invalid or unsupported interpolator key group type '%1'.").arg( keyGroup.strType() ) ); } - return false; -} + int nKeys = keyArrayRoot.childCount(); + if ( nKeys > 0 ) { + auto firstKey = keyArrayRoot[0]; -template <> bool Controller::interpolate( Matrix & value, const QModelIndex & array, float time, int & last ) -{ - int next; - float x; - - auto nif = NifModel::fromValidIndex(array); - if ( nif ) { - switch ( nif->get( array, "Rotation Type" ) ) { - case 4: - { - QModelIndex subkeys = nif->getIndex( array, "XYZ Rotations" ); - - if ( subkeys.isValid() ) { - float r[3] = {}; - - for ( int s = 0; s < 3 && s < nif->rowCount( subkeys ); s++ ) { - r[s] = 0; - interpolate( r[s], subkeys.child( s, 0 ), time, last ); - } - - value = Matrix::euler( 0, 0, r[2] ) * Matrix::euler( 0, r[1], 0 ) * Matrix::euler( r[0], 0, 0 ); - - return true; - } - } - break; - default: - { - QModelIndex frames = nif->getIndex( array, "Quaternion Keys" ); - - if ( timeIndex( time, nif, frames, last, next, x ) ) { - Quat v1 = nif->get( frames.child( last, 0 ), "Value" ); - Quat v2 = nif->get( frames.child( next, 0 ), "Value" ); - - if ( Quat::dotproduct( v1, v2 ) < 0 ) - v1.negate(); // don't take the long path - - Quat v3 = Quat::slerp( x, v1, v2 ); - /* - Quat v4; - float a = acos( Quat::dotproduct( v1, v2 ) ); - if ( fabs( a ) >= 0.00005 ) - { - float i = 1.0 / sin( a ); - v4 = v1 * sin( ( 1.0 - x ) * a ) * i + v2 * sin( x * a ) * i; - } - */ - value.fromQuat( v3 ); - - return true; - } - } - break; + int iTimeField = firstKey["Time"].row(); + int iValueField = firstKey["Value"].row(); + if ( iTimeField >= 0 && iValueField >= 0 ) { + int iBackwardField = firstKey.child("Backward").row(); + int iForwardField = firstKey.child("Forward").row(); + + keys.reserve( nKeys ); + for ( auto keyEntry : keyArrayRoot.iter() ) + keys.append( Key( keyEntry, iTimeField, iValueField, iBackwardField, iForwardField ) ); } } - - return false; } -/********************************************************************* -Simple b-spline curve algorithm - -Copyright 1994 by Keith Vertanen (vertankd@cda.mrs.umn.edu) +template +bool ValueInterpolator::getFrame( float inTime, ConstKeyPtr & pKey1, ConstKeyPtr & pKey2, float & fraction ) +{ + int lastKey = keys.count() - 1; + if ( lastKey < 0 ) + return false; -Released to the public domain (your mileage may vary) + ConstKeyPtr keyData = keys.constData(); -Found at: Programmers Heaven (www.programmersheaven.com/zone3/cat415/6660.htm) -Modified by: Theo -- reformat and convert doubles to floats -- removed point structure in favor of arbitrary sized float array -**********************************************************************/ + if ( inTime <= keyData[0].time ) { + pKey1 = pKey2 = keyData; + + } else if ( inTime >= keyData[lastKey].time ) { + pKey1 = pKey2 = keyData + lastKey; -/*! Used to enable static arrays to be members of vectors */ -template -struct qarray -{ - qarray( const QModelIndex & array, uint off = 0 ) - : array_( array ), off_( off ) - { - nif_ = NifModel::fromIndex( array_ ); - } - qarray( const qarray & other, uint off = 0 ) - : nif_( other.nif_ ), array_( other.array_ ), off_( other.off_ + off ) - { + } else { + int iKey = ( keyIndexCache >= 0 && keyIndexCache <= lastKey ) ? keyIndexCache : 0; + ConstKeyPtr pKey = keyData + iKey; + if ( pKey->time < inTime ) { + do { + pKey++; + } while( pKey->time < inTime ); + + pKey1 = ( pKey->time == inTime ) ? pKey : ( pKey - 1 ); + pKey2 = pKey; + + } else if ( pKey->time > inTime ) { + do { + pKey--; + } while( pKey->time > inTime ); + + pKey1 = pKey; + pKey2 = ( pKey->time == inTime ) ? pKey : ( pKey + 1 ); + + } else { // pKey->time == inTime + pKey1 = pKey2 = pKey; + } } - T operator[]( uint index ) const - { - return nif_->get( array_.child( index + off_, 0 ) ); + if ( pKey1 != pKey2 ) { + fraction = ( inTime - pKey1->time ) / ( pKey2->time - pKey1->time ); + } else { + fraction = 0.0; } - const NifModel * nif_; - const QModelIndex & array_; - uint off_; -}; + keyIndexCache = pKey1 - keyData; + + return true; +} -template -struct SplineTraits +template +bool ValueInterpolator::interpolate( T & value, float time ) { - // Zero data - static T & Init( T & v ) - { - v = T(); - return v; - } + ConstKeyPtr pKey1, pKey2; + float x; + if ( !getFrame( time, pKey1, pKey2, x ) ) + return false; - // Number of control points used - static int CountOf() - { - return ( sizeof(T) / sizeof(float) ); - } + const T & v1 = pKey1->value; + const T & v2 = pKey2->value; - // Compute point from short array and mult/bias - static T & Compute( T & v, qarray & c, float mult ) + switch( interpolationMode ) { - float * vf = (float *)&v; // assume default data is a vector of floats. specialize if necessary. - - for ( int i = 0; i < CountOf(); ++i ) - vf[i] = vf[i] + ( float(c[i]) / float(SHRT_MAX) ) * mult; - - return v; - } - static T & Adjust( T & v, float mult, float bias ) + case InterpolationMode::Quadratic: { - float * vf = (float *)&v; // assume default data is a vector of floats. specialize if necessary. - - for ( int i = 0; i < CountOf(); ++i ) - vf[i] = vf[i] * mult + bias; + // Quadratic + /* + In general, for keyframe values v1 = 0, v2 = 1 it appears that + setting v1's corresponding "Backward" value to 1 and v2's + corresponding "Forward" to 1 results in a linear interpolation. + */ - return v; - } -}; + // Tangent 1 + const T & t1 = pKey1->backward; + // Tangent 2 + const T & t2 = pKey2->forward; -template <> struct SplineTraits -{ - static Quat & Init( Quat & v ) - { - v = Quat(); v[0] = 0.0f; return v; - } - static int CountOf() { return 4; } - static Quat & Compute( Quat & v, qarray & c, float mult ) - { - for ( int i = 0; i < CountOf(); ++i ) - v[i] = v[i] + ( float(c[i]) / float(SHRT_MAX) ) * mult; - - return v; - } - static Quat & Adjust( Quat & v, float mult, float bias ) - { - for ( int i = 0; i < CountOf(); ++i ) - v[i] = v[i] * mult + bias; + float x2 = x * x; + float x3 = x2 * x; - return v; - } -}; + // Cubic Hermite spline + // x(t) = (2t^3 - 3t^2 + 1)P1 + (-2t^3 + 3t^2)P2 + (t^3 - 2t^2 + t)T1 + (t^3 - t^2)T2 -// calculate the blending value -static float blend( int k, int t, int * u, float v ) -{ - float value; + value = v1 * (2.0f * x3 - 3.0f * x2 + 1.0f) + v2 * (-2.0f * x3 + 3.0f * x2) + t1 * (x3 - 2.0f * x2 + x) + t2 * (x3 - x2); - if ( t == 1 ) { - // base case for the recursion - value = ( ( u[k] <= v ) && ( v < u[k + 1] ) ) ? 1.0f : 0.0f; - } else { - if ( ( u[k + t - 1] == u[k] ) && ( u[k + t] == u[k + 1] ) ) // check for divide by zero - value = 0; - else if ( u[k + t - 1] == u[k] ) // if a term's denominator is zero,use just the other - value = ( u[k + t] - v) / ( u[k + t] - u[k + 1] ) * blend( k + 1, t - 1, u, v ); - else if ( u[k + t] == u[k + 1] ) - value = (v - u[k]) / (u[k + t - 1] - u[k]) * blend( k, t - 1, u, v ); + } break; + + case InterpolationMode::Const: + // Constant + if ( x < 0.5 ) + value = v1; else - value = ( v - u[k] ) / ( u[k + t - 1] - u[k] ) * blend( k, t - 1, u, v ) - + ( u[k + t] - v ) / ( u[k + t] - u[k + 1] ) * blend( k + 1, t - 1, u, v ); - } + value = v2; + break; - return value; -} - -// figure out the knots -static void compute_intervals( int * u, int n, int t ) -{ - for ( int j = 0; j <= n + t; j++ ) { - if ( j < t ) - u[j] = 0; - else if ( ( t <= j ) && ( j <= n ) ) - u[j] = j - t + 1; - else if ( j > n ) - u[j] = n - t + 2; // if n-t=-2 then we're screwed, everything goes to 0 + default: + value = v1 + ( v2 - v1 ) * x; + break; } + + return true; } -template -static void compute_point( int * u, int n, int t, float v, qarray & control, T & output, float mult, float bias ) +template <> +bool ValueInterpolator::interpolate( bool & value, float time ) { - // initialize the variables that will hold our output - int l = SplineTraits::CountOf(); - SplineTraits::Init( output ); - - for ( int k = 0; k <= n; k++ ) { - qarray qa( control, k * l ); - SplineTraits::Compute( output, qa, blend( k, t, u, v ) ); - } + ConstKeyPtr pKey1, pKey2; + float x; + if ( !getFrame( time, pKey1, pKey2, x ) ) + return false; - SplineTraits::Adjust( output, mult, bias ); + value = pKey1->value; + return true; } -template -bool bsplineinterpolate( T & value, int degree, float interval, uint nctrl, const QModelIndex & array, uint off, float mult, float bias ) +template <> +bool ValueInterpolator::interpolate( Quat & value, float time ) { - if ( off == USHRT_MAX ) + ConstKeyPtr pKey1, pKey2; + float x; + if ( !getFrame( time, pKey1, pKey2, x ) ) return false; - qarray subArray( array, off ); - int t = degree + 1; - int n = nctrl - 1; - int l = SplineTraits::CountOf(); + Quat v1 = pKey1->value; + const Quat & v2 = pKey2->value; - if ( interval >= float(nctrl - degree) ) { - SplineTraits::Init( value ); - qarray sa( subArray, n * l ); - SplineTraits::Compute( value, sa, 1.0f ); - SplineTraits::Adjust( value, mult, bias ); - } else { - int * u = new int[ n + t + 1 ]; - compute_intervals( u, n, t ); - compute_point( u, n, t, interval, subArray, value, mult, bias ); - delete [] u; - } + if ( Quat::dotproduct( v1, v2 ) < 0 ) + v1.negate(); // don't take the long path + value = Quat::slerp( x, v1, v2 ); return true; } -Interpolator::Interpolator( Controller * owner ) : parent( owner ) -{ -} +// A hack to avoid "unresolved external symbol" errors for ValueInterpolator members +// https://stackoverflow.com/questions/495021/why-can-templates-only-be-implemented-in-the-header-file?noredirect=1&lq=1 +template ValueInterpolator; +template ValueInterpolator; +template ValueInterpolator; +template ValueInterpolator; +template ValueInterpolator; -bool Interpolator::update( const NifModel * nif, const QModelIndex & index ) -{ - Q_UNUSED( nif ); Q_UNUSED( index ); - return true; -} -QPersistentModelIndex Interpolator::GetControllerData() -{ - return parent->iData; -} -TransformInterpolator::TransformInterpolator( Controller * owner ) - : Interpolator( owner ), lTrans( 0 ), lRotate( 0 ), lScale( 0 ) +// ValueInterpolatorMatrix class + +void ValueInterpolatorMatrix::clear() { + eulers.clear(); + quat.clear(); } -bool TransformInterpolator::update( const NifModel * nif, const QModelIndex & index ) +bool ValueInterpolatorMatrix::isActive() const { - if ( Interpolator::update( nif, index ) ) { - QModelIndex iData = nif->getBlockIndex( nif->getLink( index, "Data" ), "NiKeyframeData" ); - iTranslations = nif->getIndex( iData, "Translations" ); - iRotations = nif->getIndex( iData, "Rotations" ); - - if ( !iRotations.isValid() ) - iRotations = iData; - - iScales = nif->getIndex( iData, "Scales" ); - - return true; + if ( eulers.count() > 0 ) { + for ( auto e : eulers ) { + if ( e.isActive() ) + return true; + } + return false; + } else { + return quat.isActive(); } - - return false; } -bool TransformInterpolator::updateTransform( Transform & tm, float time ) +void ValueInterpolatorMatrix::updateData( NifFieldConst keyGroup ) { - Controller::interpolate( tm.rotation, iRotations, time, lRotate ); - Controller::interpolate( tm.translation, iTranslations, time, lTrans ); - Controller::interpolate( tm.scale, iScales, time, lScale ); + clear(); - return true; -} - - -BSplineTransformInterpolator::BSplineTransformInterpolator( Controller * owner ) : TransformInterpolator( owner ) -{ + auto eulerRoot = keyGroup.child("XYZ Rotations"); + if ( eulerRoot ) { + eulers.resize( EULER_COUNT ); + for ( int i = 0; i < EULER_COUNT; i++ ) + eulers[i].updateData( eulerRoot[i] ); + } else { + quat.updateData( keyGroup["Quaternion Keys"] ); + } } -bool BSplineTransformInterpolator::update( const NifModel * nif, const QModelIndex & index ) +bool ValueInterpolatorMatrix::interpolate( Matrix & value, float time ) { - if ( Interpolator::update( nif, index ) ) { - start = nif->get( index, "Start Time" ); - stop = nif->get( index, "Stop Time" ); - - iSpline = nif->getBlockIndex( nif->getLink( index, "Spline Data" ) ); - iBasis = nif->getBlockIndex( nif->getLink( index, "Basis Data" ) ); - - if ( iSpline.isValid() ) - iControl = nif->getIndex( iSpline, "Compact Control Points" ); - - if ( iBasis.isValid() ) - nCtrl = nif->get( iBasis, "Num Control Points" ); - - auto trans = nif->getIndex( index, "Transform" ); - - lTrans = nif->getIndex( trans, "Translation" ); - lRotate = nif->getIndex( trans, "Rotation" ); - lScale = nif->getIndex( trans, "Scale" ); - - lTransOff = nif->get( index, "Translation Handle" ); - lRotateOff = nif->get( index, "Rotation Handle" ); - lScaleOff = nif->get( index, "Scale Handle" ); - lTransMult = nif->get( index, "Translation Half Range" ); - lRotateMult = nif->get( index, "Rotation Half Range" ); - lScaleMult = nif->get( index, "Scale Half Range" ); - lTransBias = nif->get( index, "Translation Offset" ); - lRotateBias = nif->get( index, "Rotation Offset" ); - lScaleBias = nif->get( index, "Scale Offset" ); + if ( eulers.count() > 0 ) { + bool success = false; + float r[EULER_COUNT]; + for ( int i = 0; i < EULER_COUNT; i++ ) { + r[i] = 0.0f; + if ( eulers[i].interpolate( r[i], time ) ) + success = true; + } - return true; + if ( success ) { + value = Matrix::euler( 0, 0, r[2] ) * Matrix::euler( 0, r[1], 0 ) * Matrix::euler( r[0], 0, 0 ); + return true; + } + } else { + Quat outv; + if ( quat.interpolate( outv, time ) ) { + value.fromQuat( outv ); + return true; + } } return false; } -bool BSplineTransformInterpolator::updateTransform( Transform & transform, float time ) -{ - float interval = ( ( time - start ) / ( stop - start ) ) * float(nCtrl - degree); - Quat q = transform.rotation.toQuat(); - if ( ::bsplineinterpolate( q, degree, interval, nCtrl, iControl, lRotateOff, lRotateMult, lRotateBias ) ) - transform.rotation.fromQuat( q ); +// IControllerInterpolator class - ::bsplineinterpolate( transform.translation, degree, interval, nCtrl, iControl, lTransOff, lTransMult, lTransBias ); - ::bsplineinterpolate( transform.scale, degree, interval, nCtrl, iControl, lScaleOff, lScaleMult, lScaleBias ); +IControllerInterpolator::IControllerInterpolator( NifFieldConst _interpolatorBlock, IControllable * _targetControllable, Controller * _parentController ) + : interpolatorBlock( _interpolatorBlock ), targetControllable( _targetControllable ), controller( _parentController ) +{ + Q_ASSERT( interpolatorBlock.isBlock() ); + Q_ASSERT( hasTarget() ); +} - return true; +void IControllerInterpolator::updateData( NifFieldConst changedBlock ) +{ + if ( changedBlock == interpolatorBlock || needDataUpdate || updateBlocks.contains( changedBlock ) ) { + needDataUpdate = false; + updateBlocks.clear(); + if ( hasTarget() ) + updateDataImpl(); + } } diff --git a/src/gl/glcontroller.h b/src/gl/glcontroller.h index 42be7816d..59ac5079f 100644 --- a/src/gl/glcontroller.h +++ b/src/gl/glcontroller.h @@ -35,150 +35,282 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "model/nifmodel.h" -#include // Inherited #include -#include +#include -//! @file glcontroller.h Controller, Interpolator, TransformInterpolator, BSplineTransformInterpolator +class IControllable; -class Transform; - -//! Something which can be attached to anything Controllable +//! A block which can be attached to anything Controllable class Controller { - friend class Interpolator; - public: - Controller( const QModelIndex & index ); - virtual ~Controller() {} + const NifFieldConst block; float start = 0; float stop = 0; float phase = 0; float frequency = 0; - //! Extrapolation type - enum Extrapolation + enum class ExtrapolationType { - Cyclic = 0, Reverse = 1, Constant = 2 - } extrapolation = Cyclic; + Cyclic = 0, + Reverse = 1, + Constant = 2 + }; + ExtrapolationType extrapolation = ExtrapolationType::Cyclic; bool active = false; - //! Find the model index of the controller - QModelIndex index() const { return iBlock; } +protected: + QPersistentModelIndex iBlock; + +public: + Controller( NifFieldConst ctrlBlock ); + virtual ~Controller() {} + + // Get the block type of the controller + const QString & typeId() const { return block.name(); } + + // Get the model index of the controller + QModelIndex index() const { return iBlock; } // TODO: Get rid of it - //! Set the interpolator - virtual void setInterpolator( const QModelIndex & iInterpolator ); + bool isValid() const { return iBlock.isValid(); } - //! Set sequence name for animation groups - virtual void setSequence( const QString & seqname ); + // Set the interpolator from its block + virtual void setInterpolator( NifFieldConst newInterpolatorBlock ); - //! Find the type of the controller - virtual QString typeId() const; + // Set sequence name for animation groups + virtual void setSequence( const QString & seqName ); - //! Update for model and index - virtual bool update( const NifModel * nif, const QModelIndex & index ); + // Update for model and index + void update( NifFieldConst changedBlock ); + void update() { update( block ); } //! Update for specified time virtual void updateTime( float time ) = 0; - //! Determine the controller time based on the specified time +protected: + virtual void updateImpl( NifFieldConst changedBlock ); + + // Determine the controller time based on the specified time float ctrlTime( float time ) const; - /*! Interpolate given the index of an array - * - * @param[out] value The value being interpolated - * @param[in] array The array index - * @param[in] time The scene time - * @param[out] lastIndex The last index - */ - template static bool interpolate( T & value, const QModelIndex & array, float time, int & lastIndex ); - - /*! Interpolate given an index and the array name - * - * @param[out] value The value being interpolated - * @param[in] data The index which houses the array - * @param[in] arrayid The name of the array - * @param[in] time The scene time - * @param[out] lastIndex The last index - */ - template static bool interpolate( T & value, const QModelIndex & data, const QString & arrayid, float time, int & lastindex ); - - /*! Returns the fraction of the way between two keyframes based on the scene time - * - * @param[in] inTime The scene time - * @param[in] nif The NIF - * @param[in] keysArray The Keys array in the interpolator - * @param[out] prevFrame The previous row in the Keys array - * @param[out] nextFrame The next row in the Keys array - * @param[out] fraction The current distance between the prev and next frame, as a fraction - */ - static bool timeIndex( float inTime, const NifModel * nif, const QModelIndex & keysArray, int & prevFrame, int & nextFrame, float & fraction ); + static NifFieldConst getInterpolatorBlock( NifFieldConst controllerBlock ); +}; -protected: - QPersistentModelIndex iBlock; - QPersistentModelIndex iInterpolator; - QPersistentModelIndex iData; +// Value interpolator template +template +class ValueInterpolator final +{ + enum class InterpolationMode + { + Unknown = -1, + Linear = 1, + Quadratic = 2, + TBC = 3, // Tension Bias Continuity + XyzRotation = 4, + Const = 5, + }; + InterpolationMode interpolationMode = InterpolationMode::Unknown; + + struct Key + { + float time; + T value; + T backward; + T forward; + + Key( NifFieldConst keyRoot, int iTimeField, int iValueField, int iBackwardField, int iForwardField ); + }; + using ConstKeyPtr = const Key *; + + QVector keys; + int keyIndexCache = 0; + +public: + void clear(); + + bool isActive() const { return keys.count() > 0; } + + void updateData( NifFieldConst keyGroup ); + bool interpolate( T & value, float time ); + +private: + bool getFrame( float inTime, ConstKeyPtr & prevKey, ConstKeyPtr & nextKey, float & fraction ); }; -template bool Controller::interpolate( T & value, const QModelIndex & data, const QString & arrayid, float time, int & lastindex ) +using ValueInterpolatorBool = ValueInterpolator; +using ValueInterpolatorFloat = ValueInterpolator; +using ValueInterpolatorVector3 = ValueInterpolator; +using ValueInterpolatorColor3 = ValueInterpolator; +using ValueInterpolatorColor4 = ValueInterpolator; + + +// Matrix value interpolator +class ValueInterpolatorMatrix final { - auto nif = NifModel::fromValidIndex(data); - if ( nif ) { - QModelIndex array = nif->getIndex( data, arrayid ); - return interpolate( value, array, time, lastindex ); - } + static constexpr int EULER_COUNT = 3; - return false; -} + QVector eulers; + ValueInterpolator quat; -class Interpolator : public QObject +public: + void clear(); + + bool isActive() const; + + void updateData( NifFieldConst keyGroup ); + bool interpolate( Matrix & value, float time ); +}; + + +// Base interface for controller interpolators +class IControllerInterpolator { public: - Interpolator( Controller * owner ); + const NifFieldConst interpolatorBlock; + Controller * const controller; + +protected: + QPointer targetControllable; + +private: + bool needDataUpdate = true; + QVector updateBlocks; // (Extra) blocks that trigger updateDataImpl() + +public: + IControllerInterpolator( NifFieldConst _interpolatorBlock, IControllable * _targetControllable, Controller * _parentController ); + IControllerInterpolator() = delete; + IControllerInterpolator( const IControllerInterpolator & ) = delete; + + bool hasTarget() const { return !targetControllable.isNull(); } + + NifFieldConst controllerBlock() const { return controller ? controller->block : NifFieldConst(); } + + NifFieldConst getDataBlock() const { return interpolatorBlock.child("Data").linkBlock(); } + + void registerUpdateBlock( NifFieldConst updateBlock ) + { + if ( updateBlock ) + updateBlocks.append( updateBlock ); + } + + void updateData( NifFieldConst changedBlock ); - virtual bool update( const NifModel * nif, const QModelIndex & index ); + void applyTransform( float time ) + { + if ( hasTarget() ) + applyTransformImpl( time ); + } protected: - QPersistentModelIndex GetControllerData(); - Controller * parent; + virtual void updateDataImpl() = 0; + virtual void applyTransformImpl( float time ) = 0; }; -class TransformInterpolator : public Interpolator + +// Template for a controller interpolator targetting IControllables of certain type +template +class IControllerInterpolatorTyped : public IControllerInterpolator { public: - TransformInterpolator( Controller * owner ); + IControllerInterpolatorTyped( NifFieldConst _interpolatorBlock, ControllableType * _targetControllable, Controller * _parentController ) + : IControllerInterpolator( _interpolatorBlock, _targetControllable, _parentController ) {} - bool update( const NifModel * nif, const QModelIndex & index ) override; - virtual bool updateTransform( Transform & tm, float time ); + ControllableType * target() const { return static_cast( targetControllable.data() ); } -protected: - QPersistentModelIndex iTranslations, iRotations, iScales; - int lTrans, lRotate, lScale; + virtual bool isActive() const = 0; }; -class BSplineTransformInterpolator : public TransformInterpolator + +// Template for a simple Controller with a target IControllable and a controller interpolator +template +class InterpolatedController : public Controller { +protected: + InterpolatorType * interpolator = nullptr; + QPointer target; + public: - BSplineTransformInterpolator( Controller * owner ); + InterpolatedController( ControllableType * _targetControllable, NifFieldConst ctrlBlock ) + : Controller( ctrlBlock ), target( _targetControllable ) + { + Q_ASSERT( hasTarget() ); + } + virtual ~InterpolatedController() { clearInterpolator(); } + + bool hasValidInterpolator() const { return interpolator && interpolator->hasTarget(); } + + bool hasTarget() const { return !target.isNull(); } - bool update( const NifModel * nif, const QModelIndex & index ) override; - bool updateTransform( Transform & tm, float time ) override; + bool isActive() const { return active && hasTarget() && hasValidInterpolator() && interpolator->isActive(); } + + void setInterpolator( NifFieldConst newInterpolatorBlock ) override final + { + setInterpolatorImpl( newInterpolatorBlock, true ); + } + + void updateTime( float time ) override final + { + if ( isActive() ) + interpolator->applyTransform( ctrlTime( time ) ); + } protected: - float start = 0, stop = 0; - QPersistentModelIndex iControl, iSpline, iBasis; - QPersistentModelIndex lTrans, lRotate, lScale; - uint lTransOff = USHRT_MAX, lRotateOff = USHRT_MAX, lScaleOff = USHRT_MAX; - float lTransMult = 0, lRotateMult = 0, lScaleMult = 0; - float lTransBias = 0, lRotateBias = 0, lScaleBias = 0; - uint nCtrl = 0; - int degree = 3; + void updateImpl( NifFieldConst changedBlock ) override final + { + bool oldActive = isActive(); + + Controller::updateImpl( changedBlock ); + + if ( changedBlock == block && hasTarget() ) + setInterpolatorImpl( getInterpolatorBlock( block ), false ); + + if ( hasValidInterpolator() ) + interpolator->updateData( changedBlock ); + + // Force data update for the target if the controller goes from active to inactive. + // This reverts all the changes that have been made by the controller. + if ( oldActive && !isActive() && hasTarget() ) + target->update(); + } + + void clearInterpolator() + { + if ( interpolator ) { + delete interpolator; + interpolator = nullptr; + } + } + + void setInterpolatorImpl( NifFieldConst newInterpolatorBlock, bool instantDataUpdate ) + { + if ( !hasTarget() || !newInterpolatorBlock ) { + clearInterpolator(); + } else if ( interpolator && interpolator->interpolatorBlock == newInterpolatorBlock ) { + // No change, do nothing + } else { + clearInterpolator(); + interpolator = createInterpolator( newInterpolatorBlock ); + if ( interpolator && instantDataUpdate ) + interpolator->updateData( newInterpolatorBlock ); + } + } + + virtual InterpolatorType * createInterpolator( NifFieldConst interpolatorBlock ) = 0; }; +#define DECLARE_INTERPOLATED_CONTROLLER( ControllerType, ControllableType, InterpolatorType ) \ + class ControllerType final : public InterpolatedController \ + { \ + public: \ + ControllerType( ControllableType * _targetControllable, NifFieldConst ctrlBlock ) \ + : InterpolatedController( _targetControllable, ctrlBlock ) {} \ + protected: \ + InterpolatorType * createInterpolator( NifFieldConst interpolatorBlock ) override final; \ + }; #endif diff --git a/src/gl/glmesh.cpp b/src/gl/glmesh.cpp index ff0a14968..97c91412a 100644 --- a/src/gl/glmesh.cpp +++ b/src/gl/glmesh.cpp @@ -32,108 +32,44 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "glmesh.h" -#include "message.h" -#include "gl/controllers.h" #include "gl/glscene.h" -#include "gl/renderer.h" -#include "io/material.h" #include "io/nifstream.h" -#include "model/nifmodel.h" +#include "lib/nvtristripwrapper.h" #include -#include -#include - -#include //! @file glmesh.cpp Scene management for visible meshes such as NiTriShapes. -const char * NIMESH_ABORT = QT_TR_NOOP( "NiMesh rendering encountered unsupported types. Rendering may be broken." ); +Mesh::Mesh( Scene * _scene, NifFieldConst _block ) + : Shape( _scene, _block ) +{ +} void Mesh::updateImpl( const NifModel * nif, const QModelIndex & index ) { Shape::updateImpl(nif, index); - if ( index == iBlock ) { - isLOD = nif->isNiBlock( iBlock, "BSLODTriShape" ); - if ( isLOD ) - emit nif->lodSliderChanged( true ); - - } else if ( index == iData || index == iTangentData ) { + if ( index == iData || index == iExtraData ) needUpdateData = true; - - } } -void Mesh::updateData( const NifModel * nif ) +void Mesh::updateDataImpl() { - resetSkinning(); - resetVertexData(); - if ( nif->checkVersion( 0x14050000, 0 ) && nif->blockInherits( iBlock, "NiMesh" ) ) - updateData_NiMesh( nif ); - else - updateData_NiTriShape( nif ); - - // Fill skinning and skeleton data - resetSkeletonData(); - if ( iSkin.isValid() ) { - isSkinned = true; - - iSkinData = nif->getBlockIndex( nif->getLink( iSkin, "Data" ), "NiSkinData" ); - - iSkinPart = nif->getBlockIndex( nif->getLink( iSkin, "Skin Partition" ), "NiSkinPartition" ); - if ( !iSkinPart.isValid() && iSkinData.isValid() ) { - // nif versions < 10.2.0.0 have skin partition linked in the skin data block - iSkinPart = nif->getBlockIndex( nif->getLink( iSkinData, "Skin Partition" ), "NiSkinPartition" ); - } - - skeletonRoot = nif->getLink( iSkin, "Skeleton Root" ); - skeletonTrans = Transform( nif, iSkinData ); - - bones = nif->getLinkArray( iSkin, "Bones" ); - - QModelIndex idxBones = nif->getIndex( iSkinData, "Bone List" ); - if ( idxBones.isValid() ) { - int nTotalBones = bones.count(); - int nBoneList = nif->rowCount( idxBones ); - // Ignore weights listed in NiSkinData if NiSkinPartition exists - int vcnt = ( nif->get( iSkinData, "Has Vertex Weights" ) && !iSkinPart.isValid() ) ? numVerts : 0; - for ( int b = 0; b < nBoneList && b < nTotalBones; b++ ) - weights.append( BoneWeights( nif, idxBones.child( b, 0 ), bones[b], vcnt ) ); - } - - if ( iSkinPart.isValid() ) { - QModelIndex idx = nif->getIndex( iSkinPart, "Partitions" ); - - uint numTris = 0; - uint numStrips = 0; - for ( int i = 0; i < nif->rowCount( idx ) && idx.isValid(); i++ ) { - partitions.append( SkinPartition( nif, idx.child( i, 0 ) ) ); - numTris += partitions[i].triangles.size(); - numStrips += partitions[i].tristrips.size(); - } - - triangles.clear(); - tristrips.clear(); - - triangles.reserve( numTris ); - tristrips.reserve( numStrips ); - - for ( const SkinPartition& part : partitions ) { - triangles << part.getRemappedTriangles(); - tristrips << part.getRemappedTristrips(); - } - } + if ( block.inherits("NiMesh") && modelVersion() >= 0x14050000 ) { + updateData_NiMesh(); + } else { + updateData_NiTriShape(); } } -void Mesh::updateData_NiMesh( const NifModel * nif ) +void Mesh::updateData_NiMesh() { - iData = nif->getIndex( iBlock, "Datastreams" ); - if ( !iData.isValid() ) + auto datastreams = block.child("Datastreams"); + if ( !datastreams ) return; - int nTotalStreams = nif->rowCount( iData ); + iData = datastreams.toIndex(); // ??? + int nTotalStreams = datastreams.childCount(); // All the semantics used by this mesh NiMesh::SemanticFlags semFlags = NiMesh::HAS_NONE; @@ -143,49 +79,41 @@ void Mesh::updateData_NiMesh( const NifModel * nif ) using CompSemIdxMap = QVector>; QVector compSemanticIndexMaps; for ( int i = 0; i < nTotalStreams; i++ ) { - auto iStreamEntry = iData.child( i, 0 ); - - auto stream = nif->getLink( iStreamEntry, "Stream" ); - auto iDataStream = nif->getBlockIndex( stream ); - - auto usage = NiMesh::DataStreamUsage( nif->get( iDataStream, "Usage" ) ); - auto access = nif->get( iDataStream, "Access" ); + auto streamEntry = datastreams[i]; + auto streamBlock = streamEntry.child("Stream").linkBlock(); + auto usage = NiMesh::DataStreamUsage( streamBlock.child("Usage").value() ); + auto access = streamBlock.child("Access").value(); // Invalid Usage and Access, abort if ( usage == access && access == 0 ) return; // For each datastream, store the semantic and the index (used for E_TEXCOORD) - auto iComponentSemantics = nif->getIndex( iStreamEntry, "Component Semantics" ); - uint numComponents = nif->get( iStreamEntry, "Num Components" ); + auto componentSemantics = streamEntry["Component Semantics"]; + auto numComponents = streamEntry["Num Components"].value(); CompSemIdxMap compSemanticIndexMap; for ( uint j = 0; j < numComponents; j++ ) { - auto iComponentEntry = iComponentSemantics.child( j, 0 ); - - auto name = nif->get( iComponentEntry, "Name" ); - auto sem = NiMesh::semanticStrings.value( name ); - uint idx = nif->get( iComponentEntry, "Index" ); - compSemanticIndexMap.insert( j, {sem, idx} ); + auto componentEntry = componentSemantics[j]; + auto entrySemantic = NiMesh::semanticStrings.value( componentEntry["Name"].value() ); + auto entryIndex = componentEntry["Index"].value(); + compSemanticIndexMap.insert( j, {entrySemantic, entryIndex} ); // Create UV stubs for multi-coord systems - if ( sem == NiMesh::E_TEXCOORD ) + if ( entrySemantic == NiMesh::E_TEXCOORD ) coords.append( TexCoords() ); // Assure Index datastream is first and Usage is correct bool invalidIndex = false; - if ( (sem == NiMesh::E_INDEX && (i != 0 || usage != NiMesh::USAGE_VERTEX_INDEX)) - || (usage == NiMesh::USAGE_VERTEX_INDEX && (i != 0 || sem != NiMesh::E_INDEX)) ) + if ( (entrySemantic == NiMesh::E_INDEX && (i != 0 || usage != NiMesh::USAGE_VERTEX_INDEX)) + || (usage == NiMesh::USAGE_VERTEX_INDEX && (i != 0 || entrySemantic != NiMesh::E_INDEX)) ) invalidIndex = true; if ( invalidIndex ) { - Message::append( tr( NIMESH_ABORT ), - tr( "[%1] NifSkope requires 'INDEX' datastream be first, with Usage type 'USAGE_VERTEX_INDEX'." ) - .arg( stream ), - QMessageBox::Warning ); + streamEntry.reportError( tr("NifSkope requires 'INDEX' datastream be first, with Usage type 'USAGE_VERTEX_INDEX'.") ); return; } - semFlags = NiMesh::SemanticFlags(semFlags | (1 << sem)); + semFlags = NiMesh::SemanticFlags(semFlags | (1 << entrySemantic)); } compSemanticIndexMaps << compSemanticIndexMap; @@ -206,19 +134,18 @@ void Mesh::updateData_NiMesh( const NifModel * nif ) // TODO: For now, submeshes are not actually used and the regions are // filled in order for each data stream. // Submeshes may be required if total index values exceed USHRT_MAX - auto iStreamEntry = iData.child( i, 0 ); + auto streamEntry = datastreams[i]; QMap submeshMap; - ushort numSubmeshes = nif->get( iStreamEntry, "Num Submeshes" ); - auto iSubmeshMap = nif->getIndex( iStreamEntry, "Submesh To Region Map" ); + auto numSubmeshes = streamEntry.child("Num Submeshes").value(); + auto submeshMapEntries = streamEntry.child("Submesh To Region Map"); for ( ushort j = 0; j < numSubmeshes; j++ ) - submeshMap.insert( j, nif->get( iSubmeshMap.child( j, 0 ) ) ); + submeshMap.insert( j, submeshMapEntries[j].value() ); // Get the datastream - quint32 stream = nif->getLink( iStreamEntry, "Stream" ); - auto iDataStream = nif->getBlockIndex( stream ); + auto streamBlock = streamEntry.child("Stream").linkBlock(); - auto usage = NiMesh::DataStreamUsage(nif->get( iDataStream, "Usage" )); + auto usage = NiMesh::DataStreamUsage( streamBlock.child("Usage").value() ); // Only process USAGE_VERTEX and USAGE_VERTEX_INDEX if ( usage > NiMesh::USAGE_VERTEX ) continue; @@ -227,12 +154,12 @@ void Mesh::updateData_NiMesh( const NifModel * nif ) // Each region has a Start Index which is added as an offset to the index read from the stream QVector> regions; quint32 numIndices = 0; - auto iRegions = nif->getIndex( iDataStream, "Regions" ); - if ( iRegions.isValid() ) { - quint32 numRegions = nif->get( iDataStream, "Num Regions" ); + auto regionEntries = streamBlock.child("Regions"); + if ( regionEntries ) { + quint32 numRegions = streamBlock["Num Regions"].value(); for ( quint32 j = 0; j < numRegions; j++ ) { - auto iRegionEntry = iRegions.child( j, 0 ); - regions.append( { nif->get( iRegionEntry, "Start Index" ), nif->get( iRegionEntry, "Num Indices" ) } ); + auto entry = regionEntries[j]; + regions.append( { entry["Start Index"].value(), entry["Num Indices"].value() } ); numIndices += regions[j].second; } @@ -254,7 +181,6 @@ void Mesh::updateData_NiMesh( const NifModel * nif ) tangents.resize( maxSize ); bitangents.resize( maxSize ); colors.resize( maxSize ); - weights.resize( maxSize ); if ( coords.size() == 0 ) coords.resize( 1 ); @@ -264,18 +190,18 @@ void Mesh::updateData_NiMesh( const NifModel * nif ) // Get the format of each component QVector datastreamFormats; - uint numStreamComponents = nif->get( iDataStream, "Num Components" ); - auto iComponentFormats = nif->getIndex( iDataStream, "Component Formats" ); - for ( uint j = 0; j < numStreamComponents; j++ ) { - auto format = nif->get( iComponentFormats.child( j, 0 ) ); + auto numComponents = streamBlock["Num Components"].value(); + auto componentFormats = streamBlock["Component Formats"]; + for ( uint j = 0; j < numComponents; j++ ) { + auto format = componentFormats[j].value(); datastreamFormats.append( NiMesh::DataStreamFormat(format) ); } - Q_ASSERT( compSemanticIndexMaps[i].size() == numStreamComponents ); + Q_ASSERT( compSemanticIndexMaps[i].size() == numComponents ); auto tempMdl = std::make_unique( this ); - QByteArray streamData = nif->get( nif->getIndex( iDataStream, "Data" ).child( 0, 0 ) ); + QByteArray streamData = streamBlock["Data"][0].value(); QBuffer streamBuffer( &streamData ); streamBuffer.open( QIODevice::ReadOnly ); @@ -286,7 +212,7 @@ void Mesh::updateData_NiMesh( const NifModel * nif ) for ( const auto & r : regions ) for ( uint j = 0; j < r.second; j++ ) { auto off = r.first; Q_ASSERT( totalIndices >= off + j ); - for ( uint k = 0; k < numStreamComponents; k++ ) { + for ( uint k = 0; k < numComponents; k++ ) { auto typeK = datastreamFormats[k]; int typeLength = ( (typeK & 0x000F0000) >> 0x10 ); @@ -349,19 +275,19 @@ void Mesh::updateData_NiMesh( const NifModel * nif ) switch ( compType ) { case NiMesh::E_POSITION: case NiMesh::E_POSITION_BP: - verts[j + off] = tempValue.get( nif, nullptr ); + verts[j + off] = tempValue.get( model, nullptr ); break; case NiMesh::E_NORMAL: case NiMesh::E_NORMAL_BP: - norms[j + off] = tempValue.get( nif, nullptr ); + norms[j + off] = tempValue.get( model, nullptr ); break; case NiMesh::E_TANGENT: case NiMesh::E_TANGENT_BP: - tangents[j + off] = tempValue.get( nif, nullptr ); + tangents[j + off] = tempValue.get( model, nullptr ); break; case NiMesh::E_BINORMAL: case NiMesh::E_BINORMAL_BP: - bitangents[j + off] = tempValue.get( nif, nullptr ); + bitangents[j + off] = tempValue.get( model, nullptr ); break; default: break; @@ -373,9 +299,9 @@ void Mesh::updateData_NiMesh( const NifModel * nif ) // TODO: The total index value across all submeshes // is likely allowed to exceed USHRT_MAX. // For now limit the index. - quint32 ind = tempValue.get( nif, nullptr ) + off; + quint32 ind = tempValue.get( model, nullptr ) + off; if ( ind > 0xFFFF ) - qDebug() << QString( "[%1] %2" ).arg( stream ).arg( ind ); + qDebug() << QString( "[%1] %2" ).arg( streamBlock.repr() ).arg( ind ); ind = std::min( ind, (quint32)0xFFFF ); @@ -392,7 +318,7 @@ void Mesh::updateData_NiMesh( const NifModel * nif ) if ( compType == NiMesh::E_TEXCOORD ) { quint32 coordSet = compSemanticIndexMaps[i].value( k ).second; Q_ASSERT( coords.size() > coordSet ); - coords[coordSet][j + off] = tempValue.get( nif, nullptr ); + coords[coordSet][j + off] = tempValue.get( model, nullptr ); } break; case NiMesh::F_UINT8_4: @@ -401,20 +327,18 @@ void Mesh::updateData_NiMesh( const NifModel * nif ) case NiMesh::F_NORMUINT8_4: Q_ASSERT( usage == NiMesh::USAGE_VERTEX ); if ( compType == NiMesh::E_COLOR ) - colors[j + off] = tempValue.get( nif, nullptr ); + colors[j + off] = tempValue.get( model, nullptr ); break; case NiMesh::F_NORMUINT8_4_BGRA: Q_ASSERT( usage == NiMesh::USAGE_VERTEX ); if ( compType == NiMesh::E_COLOR ) { // Swizzle BGRA -> RGBA - auto c = tempValue.get( nif, nullptr ).data(); + auto c = tempValue.get( model, nullptr ).data(); colors[j + off] = {c[2], c[1], c[0], c[3]}; } break; default: - Message::append( tr( NIMESH_ABORT ), tr( "[%1] Unsupported Component: %2" ).arg( stream ) - .arg( NifValue::enumOptionName( "ComponentFormat", typeK ) ), - QMessageBox::Warning ); + streamBlock.reportError( tr("Unsupported Component: %2.").arg( NifValue::enumOptionName( "ComponentFormat", typeK ) ) ); abort = true; break; } @@ -430,28 +354,30 @@ void Mesh::updateData_NiMesh( const NifModel * nif ) compIdx++; } - // Clear unused vertex attributes - // Note: Do not clear normals as this breaks fixed function for some reason - // TODO (Gavrant): figure out why clearing normals "breaks fixed function for some reason" - if ( !(semFlags & NiMesh::HAS_BINORMAL) ) - bitangents.clear(); - if ( !(semFlags & NiMesh::HAS_TANGENT) ) - tangents.clear(); - if ( !(semFlags & NiMesh::HAS_COLOR) ) - colors.clear(); - if ( !(semFlags & NiMesh::HAS_BLENDINDICES) || !(semFlags & NiMesh::HAS_BLENDWEIGHT) ) - weights.clear(); + // Set vertex attributes flags + if ( semFlags & NiMesh::HAS_NORMAL ) + hasVertexNormals = true; + if ( semFlags & NiMesh::HAS_TANGENT ) + hasVertexTangents = true; + if ( semFlags & NiMesh::HAS_BINORMAL ) + hasVertexBitangents = true; + if ( semFlags & NiMesh::HAS_TEXCOORD ) + hasVertexUVs = true; + if ( semFlags & NiMesh::HAS_COLOR ) + hasVertexColors = true; Q_ASSERT( verts.size() == maxIndex + 1 ); Q_ASSERT( indices.size() == totalIndices ); numVerts = verts.count(); // Make geometry - triangles.resize( indices.size() / 3 ); - auto meshPrimitiveType = nif->get( iBlock, "Primitive Type" ); + int nTotalTris = indices.size() / 3; + triangles.resize( nTotalTris ); + auto typeField = block["Primitive Type"]; + auto meshPrimitiveType = typeField.value(); switch ( meshPrimitiveType ) { case NiMesh::PRIMITIVE_TRIANGLES: - for ( int k = 0, t = 0; k < indices.size(); k += 3, t++ ) + for ( int t = 0, k = 0; t < nTotalTris; t++, k += 3 ) triangles[t] = { indices[k], indices[k + 1], indices[k + 2] }; break; case NiMesh::PRIMITIVE_TRISTRIPS: @@ -459,825 +385,404 @@ void Mesh::updateData_NiMesh( const NifModel * nif ) case NiMesh::PRIMITIVE_LINESTRIPS: case NiMesh::PRIMITIVE_QUADS: case NiMesh::PRIMITIVE_POINTS: - Message::append( tr( NIMESH_ABORT ), tr( "[%1] Unsupported Primitive: %2" ) - .arg( nif->getBlockNumber( iBlock ) ) - .arg( NifValue::enumOptionName( "MeshPrimitiveType", meshPrimitiveType ) ), - QMessageBox::Warning - ); + typeField.reportError( tr("Unsupported primitive type value: %1.").arg( NifValue::enumOptionName("MeshPrimitiveType", meshPrimitiveType) ) ); break; } } -void Mesh::updateData_NiTriShape( const NifModel * nif ) -{ - // Find iData and iSkin blocks among the children - for ( auto childLink : nif->getChildLinks( id() ) ) { - QModelIndex iChild = nif->getBlockIndex( childLink ); - if ( !iChild.isValid() ) - continue; - - if ( nif->blockInherits( iChild, "NiTriShapeData" ) || nif->blockInherits( iChild, "NiTriStripsData" ) ) { - if ( !iData.isValid() ) { - iData = iChild; - } else if ( iData != iChild ) { - Message::append( tr( "Warnings were generated while updating meshes." ), - tr( "Block %1 has multiple data blocks" ).arg( id() ) - ); - } - } else if ( nif->blockInherits( iChild, "NiSkinInstance" ) ) { - if ( !iSkin.isValid() ) { - iSkin = iChild; - } else if ( iSkin != iChild ) { - Message::append( tr( "Warnings were generated while updating meshes." ), - tr( "Block %1 has multiple skin instances" ).arg( id() ) - ); - } - } - } - if ( !iData.isValid() ) - return; - - // Fill vertex data - verts = nif->getArray( iData, "Vertices" ); - numVerts = verts.count(); - - norms = nif->getArray( iData, "Normals" ); - if ( norms.count() < numVerts ) - norms.clear(); - - colors = nif->getArray( iData, "Vertex Colors" ); - if ( colors.count() < numVerts ) - colors.clear(); - // Detect if "Has Vertex Colors" is set to Yes in NiTriShape - // Used to compare against SLSF2_Vertex_Colors - hasVertexColors = (colors.count() > 0); - - tangents = nif->getArray( iData, "Tangents" ); - bitangents = nif->getArray( iData, "Bitangents" ); - - QModelIndex iExtraData = nif->getIndex( iBlock, "Extra Data List" ); - if ( iExtraData.isValid() ) { - int nExtra = nif->rowCount( iExtraData ); - for ( int e = 0; e < nExtra; e++ ) { - QModelIndex iExtra = nif->getBlockIndex( nif->getLink( iExtraData.child( e, 0 ) ), "NiBinaryExtraData" ); - if ( nif->get( iExtra, "Name" ) == "Tangent space (binormal & tangent vectors)" ) { - iTangentData = iExtra; - QByteArray data = nif->get( iExtra, "Binary Data" ); - if ( data.count() == numVerts * 4 * 3 * 2 ) { - tangents.resize( numVerts ); - bitangents.resize( numVerts ); - Vector3 * t = (Vector3 *)data.data(); - - for ( int c = 0; c < numVerts; c++ ) - tangents[c] = *t++; - - for ( int c = 0; c < numVerts; c++ ) - bitangents[c] = *t++; - } - } - } - } - - coords.clear(); - QModelIndex iUVSets = nif->getIndex( iData, "UV Sets" ); - if ( iUVSets.isValid() ) { - int nSets = nif->rowCount( iUVSets ); - for ( int r = 0; r < nSets; r++ ) { - TexCoords tc = nif->getArray( iUVSets.child( r, 0 ) ); - if ( tc.count() < numVerts ) - tc.clear(); - coords.append( tc ); - } - } - - // Fill triangle/strips data - auto dataName = nif->itemName( iData ); - if ( dataName == "NiTriShapeData" ) { - // check indexes - // TODO: check other indexes as well - // TODO (Gavrant): test this! - QVector dataTris = nif->getArray( iData, "Triangles" ); - int nDataTris = dataTris.count(); - - for ( int i = 0; i < nDataTris; i++ ) { - Triangle t = dataTris[i]; - if ( t[0] < numVerts && t[1] < numVerts && t[2] < numVerts ) - triangles.append(t); - } - - int diff = nDataTris - triangles.count(); - if ( diff > 0 ) { - int block_idx = nif->getBlockNumber( nif->getIndex( iData, "Triangles" ) ); - Message::append( tr( "Warnings were generated while rendering mesh." ), - tr( "Block %1: %2 invalid indices in NiTriShapeData.Triangles" ).arg( block_idx ).arg( diff ) - ); - } - } else if ( dataName == "NiTriStripsData" ) { - QModelIndex points = nif->getIndex( iData, "Points" ); - if ( points.isValid() ) { - int nStrips = nif->rowCount( points ); - for ( int r = 0; r < nStrips; r++ ) - tristrips.append( nif->getArray( points.child( r, 0 ) ) ); - } else { - Message::append( tr( "Warnings were generated while rendering mesh." ), - tr( "Block %1: Invalid 'Points' array in %2" ) - .arg( nif->getBlockNumber( iData ) ) - .arg( dataName ) - ); - } - } -} - -QModelIndex Mesh::vertexAt( int idx ) const +static inline void remapTriangleVertices( Triangle & t, const QVector & vertexMap ) { - auto nif = NifModel::fromIndex( iBlock ); - if ( !nif ) - return QModelIndex(); - - auto iVertexData = nif->getIndex( iData, "Vertices" ); - auto iVertex = iVertexData.child( idx, 0 ); - - return iVertex; -} - -bool compareTriangles( const QPair & tri1, const QPair & tri2 ) -{ - return ( tri1.second < tri2.second ); -} - -void Mesh::transformShapes() -{ - if ( isHidden() ) - return; - - Node::transformShapes(); - - transformRigid = true; - - if ( isSkinned && ( weights.count() || partitions.count() ) && scene->hasOption(Scene::DoSkinning) ) { - transformRigid = false; - - int vcnt = verts.count(); - int ncnt = norms.count(); - int tcnt = tangents.count(); - int bcnt = bitangents.count(); - - transVerts.resize( vcnt ); - transVerts.fill( Vector3() ); - transNorms.resize( vcnt ); - transNorms.fill( Vector3() ); - transTangents.resize( vcnt ); - transTangents.fill( Vector3() ); - transBitangents.resize( vcnt ); - transBitangents.fill( Vector3() ); - - Node * root = findParent( skeletonRoot ); - - if ( partitions.count() ) { - for ( const SkinPartition& part : partitions ) { - QVector boneTrans( part.boneMap.count() ); - - for ( int t = 0; t < boneTrans.count(); t++ ) { - Node * bone = root ? root->findChild( bones.value( part.boneMap[t] ) ) : 0; - boneTrans[ t ] = scene->view; - - if ( bone ) - boneTrans[ t ] = boneTrans[ t ] * bone->localTrans( skeletonRoot ) * weights.value( part.boneMap[t] ).trans; - - //if ( bone ) boneTrans[ t ] = bone->viewTrans() * weights.value( part.boneMap[t] ).trans; - } - - for ( int v = 0; v < part.vertexMap.count(); v++ ) { - int vindex = part.vertexMap[ v ]; - if ( vindex < 0 || vindex >= vcnt ) - break; - - if ( transVerts[vindex] == Vector3() ) { - for ( int w = 0; w < part.numWeightsPerVertex; w++ ) { - QPair weight = part.weights[ v * part.numWeightsPerVertex + w ]; - - - Transform trans = boneTrans.value( weight.first ); - - if ( vcnt > vindex ) - transVerts[vindex] += trans * verts[vindex] * weight.second; - if ( ncnt > vindex ) - transNorms[vindex] += trans.rotation * norms[vindex] * weight.second; - if ( tcnt > vindex ) - transTangents[vindex] += trans.rotation * tangents[vindex] * weight.second; - if ( bcnt > vindex ) - transBitangents[vindex] += trans.rotation * bitangents[vindex] * weight.second; - } - } - } - } - } else { - int x = 0; - for ( const BoneWeights& bw : weights ) { - Transform trans = viewTrans() * skeletonTrans; - Node * bone = root ? root->findChild( bw.bone ) : 0; - - if ( bone ) - trans = trans * bone->localTrans( skeletonRoot ) * bw.trans; - - if ( bone ) - weights[x++].tcenter = bone->viewTrans() * bw.center; - else - x++; - - for ( const VertexWeight& vw : bw.weights ) { - int vindex = vw.vertex; - if ( vindex < 0 || vindex >= vcnt ) - break; - - if ( vcnt > vindex ) - transVerts[vindex] += trans * verts[vindex] * vw.weight; - if ( ncnt > vindex ) - transNorms[vindex] += trans.rotation * norms[vindex] * vw.weight; - if ( tcnt > vindex ) - transTangents[vindex] += trans.rotation * tangents[vindex] * vw.weight; - if ( bcnt > vindex ) - transBitangents[vindex] += trans.rotation * bitangents[vindex] * vw.weight; - } - } - } - - for ( int n = 0; n < transNorms.count(); n++ ) - transNorms[n].normalize(); - - for ( int t = 0; t < transTangents.count(); t++ ) - transTangents[t].normalize(); - - for ( int t = 0; t < transBitangents.count(); t++ ) - transBitangents[t].normalize(); - - boundSphere = BoundSphere( transVerts ); - boundSphere.applyInv( viewTrans() ); - needUpdateBounds = false; + int nMappedVertices = vertexMap.count(); + if ( t.v1() >= nMappedVertices || t.v2() >= nMappedVertices || t.v3() >= nMappedVertices ) { + // Intentionally break all vertices of the triangle if any of them failed mapping. + // Then the triangle will be discarded on post-update triangles cleanup. + // And in the worst case (the total number of vertices in the shape is (Triangle::MAX_VERTEX_INDEX + 1) or greater) the triangle still won't be rendered. + t.set( Triangle::MAX_VERTEX_INDEX, Triangle::MAX_VERTEX_INDEX, Triangle::MAX_VERTEX_INDEX ); } else { - transVerts = verts; - transNorms = norms; - transTangents = tangents; - transBitangents = bitangents; - transColors = colors; - } - - sortedTriangles = triangles; - - MaterialProperty * matprop = findProperty(); - if ( matprop && matprop->alphaValue() != 1.0 ) { - float a = matprop->alphaValue(); - transColors.resize( colors.count() ); - - for ( int c = 0; c < colors.count(); c++ ) - transColors[c] = colors[c].blend( a ); - } else { - transColors = colors; - // TODO (Gavrant): suspicious code. Should the check be replaced with !bssp.hasVertexAlpha ? - if ( bslsp && !bslsp->hasSF1(ShaderFlags::SLSF1_Vertex_Alpha) ) { - for ( int c = 0; c < colors.count(); c++ ) - transColors[c] = Color4( colors[c].red(), colors[c].green(), colors[c].blue(), 1.0f ); - } + t.set( vertexMap[t.v1()], vertexMap[t.v2()], vertexMap[t.v3()] ); } } -BoundSphere Mesh::bounds() const +void Mesh::updateData_NiTriShape() { - if ( needUpdateBounds ) { - needUpdateBounds = false; - boundSphere = BoundSphere( verts ); - } - - return worldTrans() * boundSphere; -} - -void Mesh::drawShapes( NodeList * secondPass, bool presort ) -{ - if ( isHidden() ) - return; - - // TODO: Only run this if BSXFlags has "EditorMarkers present" flag - if ( !scene->hasOption(Scene::ShowMarkers) && name.startsWith( "EditorMarker" ) ) - return; - - // BSOrderedNode - presorted |= presort; - - // Draw translucent meshes in second pass - if ( secondPass && drawInSecondPass ) { - secondPass->add( this ); - return; - } + // Find iData and iSkin blocks among the children + NifFieldConst dataBlock, skinBlock; + for ( auto childLink : model->getChildLinks( id() ) ) { + auto childBlock = model->block( childLink ); + if ( !childBlock ) + continue; - auto nif = NifModel::fromIndex( iBlock ); - - if ( Node::SELECTING ) { - if ( scene->isSelModeObject() ) { - int s_nodeId = ID2COLORKEY( nodeId ); - glColor4ubv( (GLubyte *)&s_nodeId ); - } else { - glColor4f( 0, 0, 0, 1 ); + if ( childBlock.inherits("NiTriShapeData", "NiTriStripsData") ) { + if ( !dataBlock ) + dataBlock = childBlock; + else if ( dataBlock != childBlock ) + block.reportError( tr("Block has multiple data blocks.") ); + } else if ( childBlock.inherits("NiSkinInstance") ) { + if ( !skinBlock ) + skinBlock = childBlock; + else if ( skinBlock != childBlock ) + block.reportError( tr("Block has multiple skin instances.") ); } } + if ( !dataBlock ) + return; + iData = dataBlock.toIndex(); // ??? - // TODO: Option to hide Refraction and other post effects - - // rigid mesh? then pass the transformation on to the gl layer - - if ( transformRigid ) { - glPushMatrix(); - glMultMatrix( viewTrans() ); - } - - //if ( !Node::SELECTING ) { - // qDebug() << viewTrans().translation; - //qDebug() << Vector3( nif->get( iBlock, "Translation" ) ); - //} - - // Debug axes - //drawAxes(Vector3(), 35.0); - - // setup array pointers - - // Render polygon fill slightly behind alpha transparency and wireframe - glEnable( GL_POLYGON_OFFSET_FILL ); - if ( drawInSecondPass ) - glPolygonOffset( 0.5f, 1.0f ); - else - glPolygonOffset( 1.0f, 2.0f ); - - glEnableClientState( GL_VERTEX_ARRAY ); - glVertexPointer( 3, GL_FLOAT, 0, transVerts.constData() ); - - if ( !Node::SELECTING ) { - if ( transNorms.count() ) { - glEnableClientState( GL_NORMAL_ARRAY ); - glNormalPointer( GL_FLOAT, 0, transNorms.constData() ); - } + NifFieldConst skinDataBlock, skinPartBlock; + if ( skinBlock ) { + isSkinned = true; + iSkin = skinBlock.toIndex(); // ??? - // Do VCs if legacy or if either bslsp or bsesp is set - bool doVCs = ( !bssp || bssp->hasSF2(ShaderFlags::SLSF2_Vertex_Colors) ); + skinDataBlock = skinBlock.child("Data").linkBlock("NiSkinData"); + iSkinData = skinDataBlock.toIndex(); // ??? - if ( transColors.count() && scene->hasOption(Scene::DoVertexColors) && doVCs ) { - glEnableClientState( GL_COLOR_ARRAY ); - glColorPointer( 4, GL_FLOAT, 0, transColors.constData() ); - } else { - if ( !hasVertexColors && (bslsp && bslsp->hasVertexColors) ) { - // Correctly blacken the mesh if SLSF2_Vertex_Colors is still on - // yet "Has Vertex Colors" is not. - glColor( Color3( 0.0f, 0.0f, 0.0f ) ); - } else { - glColor( Color3( 1.0f, 1.0f, 1.0f ) ); - } + skinPartBlock = skinBlock.child("Skin Partition").linkBlock("NiSkinPartition"); + if ( !skinPartBlock && skinDataBlock ) { + // nif versions < 10.2.0.0 have skin partition linked in the skin data block + skinPartBlock = skinDataBlock.child("Skin Partition").linkBlock("NiSkinPartition"); } + iSkinPart = skinPartBlock.toIndex(); // ??? } - // TODO: Hotspot. See about optimizing this. - if ( !Node::SELECTING ) - shader = scene->renderer->setupProgram( this, shader ); + // Fill vertex data + mainVertexRoot = dataBlock.child("Vertices"); + verts = mainVertexRoot.array(); + numVerts = verts.count(); + addVertexSelection( mainVertexRoot, VertexSelectionType::VERTICES ); - if ( isDoubleSided ) { - glDisable( GL_CULL_FACE ); + auto normalsField = dataBlock.child("Normals"); + if ( normalsField ) { + reportFieldCountMismatch( normalsField, mainVertexRoot, dataBlock ); + hasVertexNormals = true; + norms = normalsField.array(); + addVertexSelection( normalsField, VertexSelectionType::NORMALS ); } - if ( !isLOD ) { - // render the triangles - if ( sortedTriangles.count() ) - glDrawElements( GL_TRIANGLES, sortedTriangles.count() * 3, GL_UNSIGNED_SHORT, sortedTriangles.constData() ); - - } else if ( sortedTriangles.count() ) { - auto lod0 = nif->get( iBlock, "LOD0 Size" ); - auto lod1 = nif->get( iBlock, "LOD1 Size" ); - auto lod2 = nif->get( iBlock, "LOD2 Size" ); - - auto lod0tris = sortedTriangles.mid( 0, lod0 ); - auto lod1tris = sortedTriangles.mid( lod0, lod1 ); - auto lod2tris = sortedTriangles.mid( lod0 + lod1, lod2 ); - - // If Level2, render all - // If Level1, also render Level0 - switch ( scene->lodLevel ) { - case Scene::Level0: - if ( lod2tris.count() ) - glDrawElements( GL_TRIANGLES, lod2tris.count() * 3, GL_UNSIGNED_SHORT, lod2tris.constData() ); - case Scene::Level1: - if ( lod1tris.count() ) - glDrawElements( GL_TRIANGLES, lod1tris.count() * 3, GL_UNSIGNED_SHORT, lod1tris.constData() ); - case Scene::Level2: - default: - if ( lod0tris.count() ) - glDrawElements( GL_TRIANGLES, lod0tris.count() * 3, GL_UNSIGNED_SHORT, lod0tris.constData() ); + NifFieldConst extraTangents; + for ( auto extraEntry : block.child("Extra Data List").iter() ) { + auto extraBlock = extraEntry.linkBlock("NiBinaryExtraData"); + if ( extraBlock && extraBlock.child("Name").value() == QStringLiteral("Tangent space (binormal & tangent vectors)") ) { + extraTangents = extraBlock; break; } } - - // render the tristrips - for ( auto & s : tristrips ) - glDrawElements( GL_TRIANGLE_STRIP, s.count(), GL_UNSIGNED_SHORT, s.constData() ); - - if ( isDoubleSided ) { - glEnable( GL_CULL_FACE ); - } - - if ( !Node::SELECTING ) - scene->renderer->stopProgram(); - - glDisableClientState( GL_VERTEX_ARRAY ); - glDisableClientState( GL_NORMAL_ARRAY ); - glDisableClientState( GL_COLOR_ARRAY ); - - glDisable( GL_POLYGON_OFFSET_FILL ); - - glPointSize( 8.5 ); - if ( scene->isSelModeVertex() ) { - drawVerts(); - } - - if ( transformRigid ) - glPopMatrix(); -} - -void Mesh::drawVerts() const -{ - glDisable( GL_LIGHTING ); - glNormalColor(); - - glBegin( GL_POINTS ); - - for ( int i = 0; i < transVerts.count(); i++ ) { - if ( Node::SELECTING ) { - int id = ID2COLORKEY( (shapeNumber << 16) + i ); - glColor4ubv( (GLubyte *)&id ); + if ( extraTangents ) { + hasVertexTangents = true; + hasVertexBitangents = true; + iExtraData = extraTangents.toIndex(); // ??? + auto extraDataRoot = extraTangents["Binary Data"]; + QByteArray extraData = extraDataRoot.value(); + int nExtraCount = extraData.count() / ( sizeof(Vector3) * 2 ); + reportFieldCountMismatch(extraDataRoot, nExtraCount, mainVertexRoot, numVerts, block ); + tangents.resize(nExtraCount); + bitangents.resize(nExtraCount); + Vector3 * t = (Vector3 *) extraData.data(); + for ( int i = 0; i < nExtraCount; i++ ) + tangents[i] = *t++; + for ( int i = 0; i < nExtraCount; i++ ) + bitangents[i] = *t++; + addVertexSelection( extraDataRoot, VertexSelectionType::EXTRA_TANGENTS ); + } else { + auto tangentsField = dataBlock.child("Tangents"); + if ( tangentsField ) { + reportFieldCountMismatch( tangentsField, mainVertexRoot, dataBlock ); + hasVertexTangents = true; + tangents = tangentsField.array(); + addVertexSelection( tangentsField, VertexSelectionType::TANGENTS ); } - glVertex( transVerts.value( i ) ); - } - // Highlight selected vertex - if ( !Node::SELECTING && iData == scene->currentBlock ) { - auto idx = scene->currentIndex; - if ( idx.data( Qt::DisplayRole ).toString() == "Vertices" ) { - glHighlightColor(); - glVertex( transVerts.value( idx.row() ) ); + auto bitangentsField = dataBlock.child("Bitangents"); + if ( bitangentsField ) { + reportFieldCountMismatch( tangentsField, mainVertexRoot, dataBlock ); + hasVertexBitangents = true; + bitangents = bitangentsField.array(); + addVertexSelection( bitangentsField, VertexSelectionType::BITANGENTS ); } } - glEnd(); -} - -void Mesh::drawSelection() const -{ - if ( scene->hasOption(Scene::ShowNodes) ) - Node::drawSelection(); - - if ( isHidden() || !scene->isSelModeObject() ) - return; - - auto idx = scene->currentIndex; - auto blk = scene->currentBlock; - - auto nif = NifModel::fromIndex( idx ); - if ( !nif ) - return; - - if ( blk != iBlock && blk != iData && blk != iSkinPart && blk != iSkinData - && ( !iTangentData.isValid() || blk != iTangentData ) ) - { - return; - } - - if ( transformRigid ) { - glPushMatrix(); - glMultMatrix( viewTrans() ); - } - - glDisable( GL_LIGHTING ); - glDisable( GL_COLOR_MATERIAL ); - glDisable( GL_TEXTURE_2D ); - glDisable( GL_NORMALIZE ); - glEnable( GL_DEPTH_TEST ); - glDepthMask( GL_FALSE ); - glEnable( GL_BLEND ); - glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA ); - glDisable( GL_ALPHA_TEST ); - - glDisable( GL_CULL_FACE ); - - glEnable( GL_POLYGON_OFFSET_FILL ); - glPolygonOffset( -1.0f, -2.0f ); - - glLineWidth( 1.0 ); - glPointSize( 3.5 ); - - QString n; - int i = -1; - - if ( blk == iBlock || idx == iData ) { - n = "Faces"; - } else if ( blk == iData || blk == iSkinPart ) { - n = idx.data( NifSkopeDisplayRole ).toString(); - - QModelIndex iParent = idx.parent(); - if ( iParent.isValid() && iParent != iData ) { - n = iParent.data( NifSkopeDisplayRole ).toString(); - i = idx.row(); + auto uvSetsRoot = dataBlock.child("UV Sets"); + if ( uvSetsRoot ) { + hasVertexUVs = true; + for ( auto uvSetField : uvSetsRoot.iter() ) { + reportFieldCountMismatch( uvSetField, mainVertexRoot, dataBlock ); + coords.append( uvSetField.array() ); + addVertexSelection( uvSetField, VertexSelectionType::VERTICES ); } - } else if ( blk == iTangentData ) { - n = "TSpace"; - } else { - n = idx.data( NifSkopeDisplayRole ).toString(); + addVertexSelection( uvSetsRoot, VertexSelectionType::VERTEX_ROOT ); } - glDepthFunc( GL_LEQUAL ); - glNormalColor(); - - glPolygonMode( GL_FRONT_AND_BACK, GL_POINT ); - - if ( n == "Vertices" || n == "Normals" || n == "Vertex Colors" - || n == "UV Sets" || n == "Tangents" || n == "Bitangents" ) - { - glBegin( GL_POINTS ); - - for ( int j = 0; j < transVerts.count(); j++ ) - glVertex( transVerts.value( j ) ); - - glEnd(); + auto colorsField = dataBlock.child("Vertex Colors"); + if ( colorsField ) { + reportFieldCountMismatch( colorsField, mainVertexRoot, dataBlock ); + hasVertexColors = true; + colors = colorsField.array(); + addVertexSelection( colorsField, VertexSelectionType::VERTICES ); + } - if ( i >= 0 ) { - glDepthFunc( GL_ALWAYS ); - glHighlightColor(); - glBegin( GL_POINTS ); - glVertex( transVerts.value( i ) ); - glEnd(); + // Fill triangle/strips data + if ( !skinPartBlock ) { + if ( dataBlock.isBlockType("NiTriShapeData") ) { + addTriangles( dataBlock.child("Triangles") ); + } else if ( dataBlock.isBlockType("NiTriStripsData") ) { + addStrips( dataBlock.child("Points"), 0 ); + } else { + dataBlock.reportError( tr("Could not find triangles or strips in data block of type '%1'.").arg( dataBlock.name() ) ); } } - if ( n == "Points" ) { - glBegin( GL_POINTS ); - auto nif = NifModel::fromIndex( iData ); - QModelIndex points = nif->getIndex( iData, "Points" ); - - if ( points.isValid() ) { - for ( int j = 0; j < nif->rowCount( points ); j++ ) { - QModelIndex iPoints = points.child( j, 0 ); - - for ( int k = 0; k < nif->rowCount( iPoints ); k++ ) { - glVertex( transVerts.value( nif->get( iPoints.child( k, 0 ) ) ) ); + // Fill skinning and skeleton data + if ( skinBlock ) { + skeletonRoot = skinBlock.child("Skeleton Root").link(); + + skeletonTrans = Transform( skinDataBlock ); + + // Fill bones + auto nodeListRoot = skinDataBlock.child("Bone List"); + initSkinBones( skinBlock.child("Bones"), nodeListRoot, block ); + int nTotalBones = bones.count(); + + // Fill vertex weights, triangles, strips + if ( skinPartBlock ) { + auto partRoot = skinPartBlock.child("Partitions"); + int nPartitions = partRoot.childCount(); + + QVector blockTriRanges( nPartitions ); + QVector blockStripRanges( nPartitions ); + + QVector weightedVertices( numVerts ); + + for ( int iPart = 0; iPart < nPartitions; iPart++ ) { + auto partEntry = partRoot[iPart]; + + // Vertex map + auto vertexMapRoot = partEntry.child("Vertex Map"); + QVector partVertexMap; + int nPartMappedVertices = vertexMapRoot.childCount(); + if ( nPartMappedVertices > 0 ) { + partVertexMap.reserve( nPartMappedVertices ); + for ( auto mapEntry : vertexMapRoot.iter() ) { + int v = mapEntry.value(); + if ( v < 0 || v >= numVerts ) + mapEntry.reportError( tr("Invalid vertex index %1.").arg(v) ); + partVertexMap << v; + } + addVertexSelection( vertexMapRoot, VertexSelectionType::VERTICES, vertexMapRoot ); } - } - } - - glEnd(); - - if ( i >= 0 ) { - glDepthFunc( GL_ALWAYS ); - glHighlightColor(); - glBegin( GL_POINTS ); - QModelIndex iPoints = points.child( i, 0 ); - if ( nif->isArray( idx ) ) { - for ( int j = 0; j < nif->rowCount( iPoints ); j++ ) { - glVertex( transVerts.value( nif->get( iPoints.child( j, 0 ) ) ) ); + // Bone map + auto boneMapRoot = partEntry.child("Bones"); + int nPartBones = boneMapRoot.childCount(); + QVector partBoneMap; + partBoneMap.reserve( nPartBones ); + for ( auto mapEntry : boneMapRoot.iter() ) { + int b = mapEntry.value(); + if ( b < 0 || b >= nTotalBones ) + mapEntry.reportError( tr("Invalid bone index %1.").arg(b) ); + partBoneMap << b; } - } else { - iPoints = idx.parent(); - glVertex( transVerts.value( nif->get( iPoints.child( i, 0 ) ) ) ); - } - - glEnd(); - } - } - glPolygonMode( GL_FRONT_AND_BACK, GL_LINE ); + // Vertex weights + int weightsPerVertex = partEntry.child("Num Weights Per Vertex").value(); + auto boneIndicesRoot = partEntry.child("Bone Indices"); + auto weightsRoot = partEntry.child("Vertex Weights"); + reportFieldCountMismatch( boneIndicesRoot, weightsRoot, partEntry ); + int nDataVerts = std::min( boneIndicesRoot.childCount(), weightsRoot.childCount() ); + if ( nPartMappedVertices > 0 ) { + reportFieldCountMismatch( boneIndicesRoot, vertexMapRoot, partEntry ); + if ( nPartMappedVertices < nDataVerts ) + nDataVerts = nPartMappedVertices; + addVertexSelection( boneIndicesRoot, VertexSelectionType::VERTICES, vertexMapRoot ); + addVertexSelection( weightsRoot, VertexSelectionType::VERTICES, vertexMapRoot ); + } else { + if ( nDataVerts > numVerts ) + nDataVerts = numVerts; + addVertexSelection( boneIndicesRoot, VertexSelectionType::VERTICES ); + addVertexSelection( weightsRoot, VertexSelectionType::VERTICES ); + } + for ( int v = 0; v < nDataVerts; v++ ) { + int vind; + if ( nPartMappedVertices > 0 ) { + vind = partVertexMap[v]; + if ( vind < 0 || vind >= numVerts ) + continue; + } else { + vind = v; + } - // TODO: Reenable as an alternative to MSAA when MSAA is not supported - //glEnable( GL_LINE_SMOOTH ); - //glHint( GL_LINE_SMOOTH_HINT, GL_NICEST ); + if ( weightedVertices[vind] ) + continue; + weightedVertices[vind] = true; + + auto bentry = boneIndicesRoot[v]; + auto wentry = weightsRoot[v]; + for ( int wind = 0; wind < weightsPerVertex; wind++ ) { + float w = wentry[wind].value(); + if ( w == 0.0f ) + continue; + int b = bentry[wind].value(); + if ( b < 0 || b >= nPartBones ) { + bentry[wind].reportError( tr("Invalid bone index %1.").arg(b) ); + continue; + } + int bind = partBoneMap[b]; + if ( bind < 0 || bind >= nTotalBones ) + continue; + bones[bind].vertexWeights << VertexWeight( vind, w ); + } + } - if ( n == "Normals" || n == "TSpace" ) { - float normalScale = bounds().radius / 20; + // Triangles + auto partTrisRoot = partEntry.child("Triangles"); + if ( partTrisRoot ) { + int iPartStart = triangles.count(); + + if ( nPartMappedVertices > 0 ) { + QVector tris; + tris.reserve( partTrisRoot.childCount() ); + for ( auto triEntry : partTrisRoot.iter() ) { + Triangle t = triEntry.value(); + for ( TriVertexIndex & tv : t.v ) { + if ( tv >= nPartMappedVertices ) { + triEntry.reportError( tr("Invalid vertex map index %1.").arg(tv) ); + } + } + remapTriangleVertices( t, partVertexMap ); + tris << t; + } + addTriangles( partTrisRoot, tris ); + } else { + addTriangles( partTrisRoot); + } - if ( normalScale < 0.1f ) - normalScale = 0.1f; + blockTriRanges[iPart] = addTriangleRange( partEntry, TriangleRange::FLAG_HIGHLIGHT, iPartStart ); + } - glBegin( GL_LINES ); + // Strips + auto partStripsRoot = partEntry.child("Strips"); + if ( partStripsRoot ) { + int iPartStart = stripTriangles.count(); + + if ( nPartMappedVertices > 0 ) { + for ( auto stripEntry: partStripsRoot.iter() ) { + TriStrip stripPoints; + stripPoints.reserve( stripEntry.childCount() ); + for ( auto pointEntry : stripEntry.iter() ) { + TriVertexIndex p = pointEntry.value(); + if ( p >= nPartMappedVertices ) { + pointEntry.reportError( tr("Invalid vertex map index %1.").arg(p) ); + } + stripPoints << p; + } + + QVector stripTris = triangulateStrip( stripPoints ); + for ( auto & t : stripTris ) { + remapTriangleVertices( t, partVertexMap ); + } + addStrip( stripEntry, stripTris, vertexMapRoot ); + } + addStripRange( partStripsRoot, TriangleRange::FLAG_HIGHLIGHT, iPartStart ); + } else { + addStrips( partStripsRoot, TriangleRange::FLAG_HIGHLIGHT ); + } - for ( int j = 0; j < transVerts.count() && j < transNorms.count(); j++ ) { - glVertex( transVerts.value( j ) ); - glVertex( transVerts.value( j ) + transNorms.value( j ) * normalScale ); - } + blockStripRanges[iPart] = addStripRange( partEntry, TriangleRange::FLAG_HIGHLIGHT, iPartStart ); + } - if ( n == "TSpace" ) { - for ( int j = 0; j < transVerts.count() && j < transTangents.count() && j < transBitangents.count(); j++ ) { - glVertex( transVerts.value( j ) ); - glVertex( transVerts.value( j ) + transTangents.value( j ) * normalScale ); - glVertex( transVerts.value( j ) ); - glVertex( transVerts.value( j ) + transBitangents.value( j ) * normalScale ); + // Add an empty selection range for partEntry if the partition has no triangles and strips at all. + TriangleRange * r = blockTriRanges[iPart]; + if ( !r ) { + r = blockStripRanges[iPart]; + if ( !r ) + r = addTriangleRange( partEntry, TriangleRange::FLAG_HIGHLIGHT, 0, 0 ); + } + addPartitionBoneSelection( boneMapRoot, r ); } - } - - glEnd(); - - if ( i >= 0 ) { - glDepthFunc( GL_ALWAYS ); - glHighlightColor(); - glBegin( GL_LINES ); - glVertex( transVerts.value( i ) ); - glVertex( transVerts.value( i ) + transNorms.value( i ) * normalScale ); - glEnd(); - } - } - if ( n == "Tangents" ) { - float normalScale = bounds().radius / 20; - normalScale /= 2.0f; - - if ( normalScale < 0.1f ) - normalScale = 0.1f; - - glBegin( GL_LINES ); - - for ( int j = 0; j < transVerts.count() && j < transTangents.count(); j++ ) { - glVertex( transVerts.value( j ) ); - glVertex( transVerts.value( j ) + transTangents.value( j ) * normalScale * 2 ); - glVertex( transVerts.value( j ) ); - glVertex( transVerts.value( j ) - transTangents.value( j ) * normalScale / 2 ); - } - - glEnd(); - - if ( i >= 0 ) { - glDepthFunc( GL_ALWAYS ); - glHighlightColor(); - glBegin( GL_LINES ); - glVertex( transVerts.value( i ) ); - glVertex( transVerts.value( i ) + transTangents.value( i ) * normalScale * 2 ); - glVertex( transVerts.value( i ) ); - glVertex( transVerts.value( i ) - transTangents.value( i ) * normalScale / 2 ); - glEnd(); - } - } - - if ( n == "Bitangents" ) { - float normalScale = bounds().radius / 20; - normalScale /= 2.0f; - - if ( normalScale < 0.1f ) - normalScale = 0.1f; - - glBegin( GL_LINES ); - - for ( int j = 0; j < transVerts.count() && j < transBitangents.count(); j++ ) { - glVertex( transVerts.value( j ) ); - glVertex( transVerts.value( j ) + transBitangents.value( j ) * normalScale * 2 ); - glVertex( transVerts.value( j ) ); - glVertex( transVerts.value( j ) - transBitangents.value( j ) * normalScale / 2 ); - } - - glEnd(); - - if ( i >= 0 ) { - glDepthFunc( GL_ALWAYS ); - glHighlightColor(); - glBegin( GL_LINES ); - glVertex( transVerts.value( i ) ); - glVertex( transVerts.value( i ) + transBitangents.value( i ) * normalScale * 2 ); - glVertex( transVerts.value( i ) ); - glVertex( transVerts.value( i ) - transBitangents.value( i ) * normalScale / 2 ); - glEnd(); - } - } - - if ( n == "Faces" || n == "Triangles" ) { - glLineWidth( 1.5f ); - - for ( const Triangle& tri : triangles ) { - glBegin( GL_TRIANGLES ); - glVertex( transVerts.value( tri.v1() ) ); - glVertex( transVerts.value( tri.v2() ) ); - glVertex( transVerts.value( tri.v3() ) ); - //glVertex( transVerts.value( tri.v1() ) ); - glEnd(); - } - - if ( i >= 0 ) { - glPolygonMode( GL_FRONT_AND_BACK, GL_FILL ); - glDepthFunc( GL_ALWAYS ); - glHighlightColor(); - Triangle tri = triangles.value( i ); - glBegin( GL_TRIANGLES ); - glVertex( transVerts.value( tri.v1() ) ); - glVertex( transVerts.value( tri.v2() ) ); - glVertex( transVerts.value( tri.v3() ) ); - //glVertex( transVerts.value( tri.v1() ) ); - glEnd(); - glPolygonMode( GL_FRONT_AND_BACK, GL_LINE ); - } - } + // Add selection ranges for Partitions in skinBlock + auto otherPartRoot = skinBlock.child("Partitions"); + for ( int iPart = 0, nOtherPartititions = otherPartRoot.childCount(); iPart < nOtherPartititions; iPart++ ) { + const NifSkopeFlagsType PART_RANGE_FLAGS = TriangleRange::FLAG_HIGHLIGHT | TriangleRange::FLAG_DEEP; - if ( n == "Faces" || n == "Strips" || n == "Strip Lengths" ) { - glLineWidth( 1.5f ); + auto rangeTris = blockTriRanges.value( iPart, nullptr ); + if ( rangeTris ) + addTriangleRange( otherPartRoot[iPart], PART_RANGE_FLAGS, rangeTris->start, rangeTris->length ); - for ( const TriStrip& strip : tristrips ) { - quint16 a = strip.value( 0 ); - quint16 b = strip.value( 1 ); + auto rangeStrips = blockStripRanges.value( iPart, nullptr ); + if ( rangeStrips ) + addStripRange( otherPartRoot[iPart], PART_RANGE_FLAGS, rangeStrips->start, rangeStrips->length, NifFieldConst() ); - for ( int v = 2; v < strip.count(); v++ ) { - quint16 c = strip[v]; - - if ( a != b && b != c && c != a ) { - glBegin( GL_LINE_STRIP ); - glVertex( transVerts.value( a ) ); - glVertex( transVerts.value( b ) ); - glVertex( transVerts.value( c ) ); - glVertex( transVerts.value( a ) ); - glEnd(); - } - - a = b; - b = c; + // Fallback + if ( !rangeTris && !rangeStrips ) + addTriangleRange( otherPartRoot[iPart], PART_RANGE_FLAGS, 0, 0 ); } - } - - if ( i >= 0 && !tristrips.isEmpty() ) { - TriStrip strip = tristrips[i]; - quint16 a = strip.value( 0 ); - quint16 b = strip.value( 1 ); - - for ( int v = 2; v < strip.count(); v++ ) { - quint16 c = strip[v]; - - if ( a != b && b != c && c != a ) { - glDepthFunc( GL_ALWAYS ); - glHighlightColor(); - glBegin( GL_LINE_STRIP ); - glVertex( transVerts.value( a ) ); - glVertex( transVerts.value( b ) ); - glVertex( transVerts.value( c ) ); - glVertex( transVerts.value( a ) ); - glEnd(); + } else if ( skinDataBlock.child("Has Vertex Weights").value() ) { + for ( int bind = 0, nListedBones = nodeListRoot.childCount(); bind < nListedBones; bind++ ) { + auto inData = nodeListRoot[bind].child("Vertex Weights"); + int nWeights = inData.childCount(); + if ( nWeights <= 0 ) + continue; + + auto firstWeight = inData[0]; + int iIndexField = firstWeight["Index"].row(); + int iWeightField = firstWeight["Weight"].row(); + if ( iIndexField < 0 || iWeightField < 0 ) + continue; + + auto & outWeights = bones[bind].vertexWeights; + outWeights.reserve( nWeights ); + for ( auto wentry : inData.iter() ) { + float w = wentry[iWeightField].value(); + if ( w == 0.0f ) + continue; + int vind = wentry[iIndexField].value(); + if ( vind < 0 || vind >= numVerts ) { + wentry[iIndexField].reportError( tr("Invalid vertex index %1.").arg(vind) ); + continue; + } + outWeights << VertexWeight( vind, w ); } - - a = b; - b = c; } } } - if ( n == "Partitions" ) { + // LODs + if ( block.hasName("BSLODTriShape") ) { + initLodData(); + } - for ( int c = 0; c < partitions.count(); c++ ) { - if ( c == i ) - glHighlightColor(); - else - glNormalColor(); + // Bounding sphere + addBoundSphereSelection( dataBlock.child("Bounding Sphere") ); +} - QVector vmap = partitions[c].vertexMap; +void Mesh::transformShapes() +{ + if ( isHidden() ) + return; - for ( const Triangle& tri : partitions[c].triangles ) { - glBegin( GL_LINE_STRIP ); - glVertex( transVerts.value( vmap.value( tri.v1() ) ) ); - glVertex( transVerts.value( vmap.value( tri.v2() ) ) ); - glVertex( transVerts.value( vmap.value( tri.v3() ) ) ); - glVertex( transVerts.value( vmap.value( tri.v1() ) ) ); - glEnd(); - } - for ( const TriStrip& strip : partitions[c].tristrips ) { - quint16 a = vmap.value( strip.value( 0 ) ); - quint16 b = vmap.value( strip.value( 1 ) ); - - for ( int v = 2; v < strip.count(); v++ ) { - quint16 c = vmap.value( strip[v] ); - - if ( a != b && b != c && c != a ) { - glBegin( GL_LINE_STRIP ); - glVertex( transVerts.value( a ) ); - glVertex( transVerts.value( b ) ); - glVertex( transVerts.value( c ) ); - glVertex( transVerts.value( a ) ); - glEnd(); - } + Node::transformShapes(); - a = b; - b = c; - } - } - } + if ( doSkinning() ) { + // TODO (Gavrant): I've no idea why it requires different transforms depending on whether it's partitioned or not. + Transform baseTrans = iSkinPart.isValid() ? scene->view : ( viewTrans() * skeletonTrans ); + applySkinningTransforms( baseTrans ); + } else { + applyRigidTransforms(); } - if ( n == "Bone List" ) { - if ( nif->isArray( idx ) ) { - for ( int i = 0; i < nif->rowCount( idx ); i++ ) - boneSphere( nif, idx.child( i, 0 ) ); - } else { - boneSphere( nif, idx ); - } - } + // Colors + MaterialProperty * matprop = findProperty(); + applyColorTransforms( matprop ? matprop->alphaValue() : 1.0f ); +} - glDisable( GL_POLYGON_OFFSET_FILL ); +BoundSphere Mesh::bounds() const +{ + if ( needUpdateBounds ) { + needUpdateBounds = false; + boundSphere = BoundSphere( verts ); + } - if ( transformRigid ) - glPopMatrix(); + return worldTrans() * boundSphere; } QString Mesh::textStats() const diff --git a/src/gl/glmesh.h b/src/gl/glmesh.h index 6bcf9108e..ba3427bf2 100644 --- a/src/gl/glmesh.h +++ b/src/gl/glmesh.h @@ -35,39 +35,30 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "gl/glshape.h" // Inherited -//! @file glmesh.h Mesh -//! A mesh +// Nodes of types NiMesh, NiTriShape, NiTriStrips, NiTriBasedGeom (pre-FO4/SKSE) class Mesh : public Shape { - public: - Mesh( Scene * s, const QModelIndex & b ) : Shape( s, b ) { } + Mesh( Scene * _scene, NifFieldConst _block ); // Node void transformShapes() override; - void drawShapes( NodeList * secondPass = nullptr, bool presort = false ) override; - void drawSelection() const override; - BoundSphere bounds() const override; QString textStats() const override; // TODO (Gavrant): move to Shape // end Node - // Shape - - void drawVerts() const override; - QModelIndex vertexAt( int ) const override; - protected: void updateImpl( const NifModel * nif, const QModelIndex & index ) override; - void updateData( const NifModel * nif ) override; + void updateDataImpl() override; - void updateData_NiMesh( const NifModel * nif ); - void updateData_NiTriShape( const NifModel * nif ); +private: + void updateData_NiMesh(); + void updateData_NiTriShape(); }; #endif diff --git a/src/gl/glnode.cpp b/src/gl/glnode.cpp index 7335e6b21..5567d12e4 100644 --- a/src/gl/glnode.cpp +++ b/src/gl/glnode.cpp @@ -56,19 +56,11 @@ int Node::SELECTING = 0; static QColor highlightColor; static QColor wireframeColor; + /* * Node list */ -NodeList::NodeList() -{ -} - -NodeList::NodeList( const NodeList & other ) -{ - operator=( other ); -} - NodeList::~NodeList() { clear(); @@ -76,71 +68,66 @@ NodeList::~NodeList() void NodeList::clear() { - foreach ( Node * n, nodes ) { - del( n ); - } + for ( Node * node : nodes ) + detach( node ); + nodes.clear(); } NodeList & NodeList::operator=( const NodeList & other ) { clear(); - for ( Node * n : other.list() ) { - add( n ); - } + + nodes.reserve( other.nodes.count() ); + for ( Node * node : other.nodes ) + attach( node ); + return *this; } -void NodeList::add( Node * n ) +void NodeList::add( Node * node ) { - if ( n && !nodes.contains( n ) ) { - ++n->ref; - nodes.append( n ); - } + if ( node && !nodes.contains( node ) ) + attach( node ); } -void NodeList::del( Node * n ) +void NodeList::del( Node * node ) { - if ( nodes.contains( n ) ) { - int cnt = nodes.removeAll( n ); - - if ( n->ref <= cnt ) { - delete n; - } else { - n->ref -= cnt; - } + int i = nodes.indexOf( node ); + if ( i >= 0 ) { + detach( node ); + nodes.removeAt( i ); } } -Node * NodeList::get( const QModelIndex & index ) const +void NodeList::validate() { - for ( Node * n : nodes ) { - if ( n->index().isValid() && n->index() == index ) - return n; + for ( int i = nodes.count() - 1; i >= 0; i-- ) { + auto node = nodes[i]; + if ( !node->isValid() ) { + detach( node ); + nodes.removeAt( i ); + } } - return nullptr; } -void NodeList::validate() +Node * NodeList::get( NifFieldConst nodeBlock ) const { - QList rem; - for ( Node * n : nodes ) { - if ( !n->isValid() ) - rem.append( n ); - } - foreach ( Node * n, rem ) { - del( n ); + for ( Node * node : nodes ) { + if ( node->isValid() && node->block == nodeBlock ) + return node; } + return nullptr; } + bool compareNodes( const Node * node1, const Node * node2 ) { bool p1 = node1->isPresorted(); bool p2 = node2->isPresorted(); // Presort meshes - if ( p1 && p2 ) { + if ( p1 && p2 ) return node1->id() < node2->id(); - } return p2; } @@ -154,20 +141,15 @@ bool compareNodesAlpha( const Node * node1, const Node * node2 ) bool p2 = node2->isPresorted(); // Presort meshes - if ( p1 && p2 ) { + if ( p1 && p2 ) return node1->id() < node2->id(); - } - - bool a1 = node1->findProperty(); - bool a2 = node2->findProperty(); - float d1 = node1->viewDepth(); - float d2 = node2->viewDepth(); + bool a1 = node1->findProperty() != nullptr; + bool a2 = node2->findProperty() != nullptr; // Alpha sort meshes - if ( a1 == a2 ) { - return (d1 < d2); - } + if ( a1 == a2 ) + return node1->viewDepth() < node2->viewDepth(); return a2; } @@ -182,12 +164,13 @@ void NodeList::alphaSort() std::stable_sort( nodes.begin(), nodes.end(), compareNodesAlpha ); } + /* * Node */ - -Node::Node( Scene * s, const QModelIndex & iBlock) : IControllable( s, iBlock ), parent( 0 ), ref( 0 ) +Node::Node( Scene * _scene, NifFieldConst _block ) + : IControllable( _scene, _block ), parent( 0 ), ref( 0 ) { nodeId = 0; flags.bits = 0; @@ -233,7 +216,6 @@ void Node::glNormalColor() const glColor( Color4( cfg.wireframe ) ); } - void Node::clear() { IControllable::clear(); @@ -246,34 +228,32 @@ void Node::clear() properties.clear(); } -Controller * Node::findController( const QString & proptype, const QString & ctrltype, const QString & var1, const QString & var2 ) +Controller * Node::findPropertyController( const QString & propType, const QString & ctrlType, const QString & var1, const QString & var2 ) const { - if ( proptype != "" && !proptype.isEmpty() ) { - for ( Property * prp : properties.list() ) { - if ( prp->typeId() == proptype ) { - return prp->findController( ctrltype, var1, var2 ); - } + if ( !propType.isEmpty() && propType != QStringLiteral("") ) { + for ( Property * prp : properties.hash() ) { + if ( prp->typeId() == propType ) + return prp->findController( ctrlType, var1, var2 ); } return nullptr; } - return IControllable::findController( ctrltype, var1, var2 ); + return IControllable::findController( ctrlType, var1, var2 ); } -Controller * Node::findController( const QString & proptype, const QModelIndex & index ) +Controller * Node::findPropertyController( const QString & propType, NifFieldConst ctrlBlock ) const { - Controller * c = nullptr; - - for ( Property * prp : properties.list() ) { - if ( prp->typeId() == proptype ) { - if ( c ) - break; - - c = prp->findController( index ); + if ( !propType.isEmpty() && ctrlBlock ) { + for ( Property * prp : properties.hash() ) { + if ( prp->typeId() == propType ) { + auto c = prp->findController( ctrlBlock ); + if ( c ) + return c; + } } } - return c; + return nullptr; } void Node::updateImpl( const NifModel * nif, const QModelIndex & index ) @@ -327,26 +307,21 @@ void Node::makeParent( Node * newParent ) parent->children.add( this ); } -void Node::setController( const NifModel * nif, const QModelIndex & iController ) +Controller * Node::createController( NifFieldConst controllerBlock ) { - QString cname = nif->itemName( iController ); - - if ( cname == "NiTransformController" ) { - Controller * ctrl = new TransformController( this, iController ); - registerController(nif, ctrl); - } else if ( cname == "NiMultiTargetTransformController" ) { - Controller * ctrl = new MultiTargetTransformController( this, iController ); - registerController(nif, ctrl); - } else if ( cname == "NiControllerManager" ) { - Controller * ctrl = new ControllerManager( this, iController ); - registerController(nif, ctrl); - } else if ( cname == "NiKeyframeController" ) { - Controller * ctrl = new KeyframeController( this, iController ); - registerController(nif, ctrl); - } else if ( cname == "NiVisController" ) { - Controller * ctrl = new VisibilityController( this, iController ); - registerController(nif, ctrl); - } + if ( controllerBlock.hasName("NiTransformController", "NiKeyframeController") ) + return new TransformController( this, controllerBlock ); + + if ( controllerBlock.hasName("NiMultiTargetTransformController") ) + return new MultiTargetTransformController( this, controllerBlock ); + + if ( controllerBlock.hasName("NiControllerManager") ) + return new ControllerManager( this, controllerBlock ); + + if ( controllerBlock.hasName("NiVisController") ) + return new VisibilityController( this, controllerBlock ); + + return nullptr; } void Node::activeProperties( PropertyList & list ) const @@ -508,8 +483,7 @@ void Node::draw() return; if ( Node::SELECTING ) { - int s_nodeId = ID2COLORKEY( nodeId ); - glColor4ubv( (GLubyte *)&s_nodeId ); + glSelectionBufferColor( nodeId ); glLineWidth( 5 ); // make hitting a line a litlle bit more easy } else { glEnable( GL_DEPTH_TEST ); @@ -575,8 +549,7 @@ void Node::drawSelection() const auto n = scene->currentIndex.data( NifSkopeDisplayRole ).toString(); if ( Node::SELECTING ) { - int s_nodeId = ID2COLORKEY( nodeId ); - glColor4ubv( (GLubyte *)&s_nodeId ); + glSelectionBufferColor( nodeId ); glLineWidth( 5 ); } else { glEnable( GL_DEPTH_TEST ); @@ -790,15 +763,13 @@ void drawHvkShape( const NifModel * nif, const QModelIndex & iShape, QStackgetBlockNumber( iShape ) ); - glColor4ubv( (GLubyte *)&s_nodeId ); + glSelectionBufferColor( nif, iShape ); } drawSphere( Vector3(), nif->get( iShape, "Radius" ) ); } else if ( name == "bhkMultiSphereShape" ) { if ( Node::SELECTING ) { - int s_nodeId = ID2COLORKEY( nif->getBlockNumber( iShape ) ); - glColor4ubv( (GLubyte *)&s_nodeId ); + glSelectionBufferColor( nif, iShape ); } QModelIndex iSpheres = nif->getIndex( iShape, "Spheres" ); @@ -808,16 +779,14 @@ void drawHvkShape( const NifModel * nif, const QModelIndex & iShape, QStackgetBlockNumber( iShape ) ); - glColor4ubv( (GLubyte *)&s_nodeId ); + glSelectionBufferColor( nif, iShape ); } Vector3 v = nif->get( iShape, "Dimensions" ); drawBox( v, -v ); } else if ( name == "bhkCapsuleShape" ) { if ( Node::SELECTING ) { - int s_nodeId = ID2COLORKEY( nif->getBlockNumber( iShape ) ); - glColor4ubv( (GLubyte *)&s_nodeId ); + glSelectionBufferColor( nif, iShape ); } drawCapsule( nif->get( iShape, "First Point" ), nif->get( iShape, "Second Point" ), nif->get( iShape, "Radius" ) ); @@ -827,8 +796,7 @@ void drawHvkShape( const NifModel * nif, const QModelIndex & iShape, QStackgetBlockNumber( iShape ) ); - glColor4ubv( (GLubyte *)&s_nodeId ); + glSelectionBufferColor( nif, iShape ); } drawNiTSS( nif, iShape ); @@ -844,8 +812,7 @@ void drawHvkShape( const NifModel * nif, const QModelIndex & iShape, QStackgetBlockNumber( iShape ) ); - glColor4ubv( (GLubyte *)&s_nodeId ); + glSelectionBufferColor( nif, iShape ); } drawConvexHull( nif, iShape, 1.0 ); @@ -873,8 +840,7 @@ void drawHvkShape( const NifModel * nif, const QModelIndex & iShape, QStackgetBlockIndex( nif->getLink( iShape, "Shape" ) ), stack, scene, origin_color3fv ); } else if ( name == "bhkPackedNiTriStripsShape" || name == "hkPackedNiTriStripsData" ) { if ( Node::SELECTING ) { - int s_nodeId = ID2COLORKEY( nif->getBlockNumber( iShape ) ); - glColor4ubv( (GLubyte *)&s_nodeId ); + glSelectionBufferColor( nif, iShape ); } QModelIndex iData = nif->getBlockIndex( nif->getLink( iShape, "Data" ) ); @@ -1016,8 +982,7 @@ void drawHvkShape( const NifModel * nif, const QModelIndex & iShape, QStackgetBlockNumber( iShape ) ); - glColor4ubv( (GLubyte *)&s_nodeId ); + glSelectionBufferColor( nif, iShape ); } drawCMS( nif, iShape ); @@ -1068,8 +1033,7 @@ void drawHvkConstraint( const NifModel * nif, const QModelIndex & iConstraint, c Color3 color_b( 0.6f, 0.8f, 0.0f ); if ( Node::SELECTING ) { - int s_nodeId = ID2COLORKEY( nif->getBlockNumber( iConstraint ) ); - glColor4ubv( (GLubyte *)&s_nodeId ); + glSelectionBufferColor( nif, iConstraint ); glLineWidth( 5 ); // make hitting a line a litlle bit more easy } else { if ( scene->currentBlock == nif->getBlockIndex( iConstraint ) ) { @@ -1374,8 +1338,7 @@ void Node::drawHavok() glMultMatrix( bt ); if ( Node::SELECTING ) { - int s_nodeId = ID2COLORKEY( nodeId ); - glColor4ubv( (GLubyte *)&s_nodeId ); + glSelectionBufferColor( nodeId ); } else { glColor( Color3( 1.0f, 0.0f, 0.0f ) ); glDisable( GL_LIGHTING ); @@ -1429,8 +1392,7 @@ void Node::drawHavok() } if ( Node::SELECTING ) { - int s_nodeId = ID2COLORKEY( nif->getBlockNumber( iBSMultiBoundData ) ); - glColor4ubv( (GLubyte *)&s_nodeId ); + glSelectionBufferColor( nif, iBSMultiBoundData ); glLineWidth( 5 ); } else { glColor( Color4( 1.0f, 1.0f, 1.0f, 0.6f ) ); @@ -1462,8 +1424,7 @@ void Node::drawHavok() glMultMatrix( worldTrans() ); if ( Node::SELECTING ) { - int s_nodeId = ID2COLORKEY( nif->getBlockNumber( iBound ) ); - glColor4ubv( (GLubyte *)&s_nodeId ); + glSelectionBufferColor( nif, iBound ); } else { glColor( Color3( 1.0f, 0.0f, 0.0f ) ); glDisable( GL_LIGHTING ); @@ -1538,8 +1499,7 @@ void Node::drawHavok() drawHvkShape( nif, nif->getBlockIndex( nif->getLink( iBody, "Shape" ) ), shapeStack, scene, colors[ color_index ] ); if ( Node::SELECTING && scene->hasOption(Scene::ShowAxes) ) { - int s_nodeId = ID2COLORKEY( nif->getBlockNumber( iBody ) ); - glColor4ubv( (GLubyte *)&s_nodeId ); + glSelectionBufferColor( nif, iBody ); glDepthFunc( GL_ALWAYS ); drawAxes( Vector3( nif->get( iBody, "Center" ) ), 1.0f / bhkScaleMult( nif ), false ); glDepthFunc( GL_LEQUAL ); @@ -1715,9 +1675,7 @@ void drawFurnitureMarker( const NifModel * nif, const QModelIndex & iPosition ) } if ( Node::SELECTING ) { - GLint id = ( nif->getBlockNumber( iPosition ) & 0xffff ) | ( ( iPosition.row() & 0xffff ) << 16 ); - int s_nodeId = ID2COLORKEY( id ); - glColor4ubv( (GLubyte *)&s_nodeId ); + glSelectionBufferColor( iPosition.row(), nif->getBlockNumber( iPosition ) ); } for ( int n = 0; n < i; n++ ) { @@ -1889,8 +1847,8 @@ BoundSphere Node::bounds() const } -LODNode::LODNode( Scene * scene, const QModelIndex & iBlock ) - : Node( scene, iBlock ) +LODNode::LODNode( Scene * _scene, NifFieldConst _block ) + : Node( _scene, _block ) { } @@ -1956,8 +1914,8 @@ void LODNode::transform() } -BillboardNode::BillboardNode( Scene * scene, const QModelIndex & iBlock ) - : Node( scene, iBlock ) +BillboardNode::BillboardNode( Scene * _scene, NifFieldConst _block ) + : Node( _scene, _block ) { } diff --git a/src/gl/glnode.h b/src/gl/glnode.h index 92d0fe077..4c6fd012f 100644 --- a/src/gl/glnode.h +++ b/src/gl/glnode.h @@ -33,7 +33,7 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #ifndef GLNODE_H #define GLNODE_H -#include "gl/icontrollable.h" // Inherited +#include "gl/glcontrollable.h" // Inherited #include "gl/glproperty.h" #include @@ -46,43 +46,50 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. class Node; class NifModel; +// A list of Nodes without duplicates and with reference counting class NodeList final { public: - NodeList(); - NodeList( const NodeList & other ); + NodeList() {} + NodeList( const NodeList & other ) { operator=( other ); } ~NodeList(); - void add( Node * ); - void del( Node * ); - - Node * get( const QModelIndex & idx ) const; - - void validate(); - void clear(); NodeList & operator=( const NodeList & other ); + void add( Node * node ); + void del( Node * node ); + bool has( Node * node ) const { return nodes.contains( node ); } + + void validate(); + const QVector & list() const { return nodes; } + Node * get( NifFieldConst nodeBlock ) const; + void sort(); void alphaSort(); -protected: +private: QVector nodes; + + void attach( Node * node ); + void detach( Node * node ); }; + class Node : public IControllable { friend class ControllerManager; - friend class KeyframeController; - friend class MultiTargetTransformController; - friend class TransformController; - friend class VisibilityController; friend class NodeList; friend class LODNode; + // Interpolator managers + friend class TransformInterpolator; + friend class BSplineInterpolator; + friend class VisibilityInterpolator; + typedef union { quint16 bits; @@ -94,7 +101,7 @@ class Node : public IControllable } NodeFlags; public: - Node( Scene * scene, const QModelIndex & iBlock ); + Node( Scene * _scene, NifFieldConst _block ); static int SELECTING; @@ -139,15 +146,14 @@ class Node : public IControllable template T * findProperty() const; void activeProperties( PropertyList & list ) const; - Controller * findController( const QString & proptype, const QString & ctrltype, const QString & var1, const QString & var2 ); - - Controller * findController( const QString & proptype, const QModelIndex & index ); + Controller * findPropertyController( const QString & propType, const QString & ctrlType, const QString & var1, const QString & var2 ) const; + Controller * findPropertyController( const QString & propType, NifFieldConst ctrlBlock ) const; public slots: void updateSettings(); protected: - void setController( const NifModel * nif, const QModelIndex & controller ) override; + Controller * createController( NifFieldConst controllerBlock ) override; void updateImpl( const NifModel * nif, const QModelIndex & block ) override; // Old Options API @@ -194,7 +200,7 @@ template inline T * Node::findProperty() const class LODNode : public Node { public: - LODNode( Scene * scene, const QModelIndex & block ); + LODNode( Scene * _scene, NifFieldConst _block ); // IControllable @@ -216,10 +222,25 @@ class LODNode : public Node class BillboardNode : public Node { public: - BillboardNode( Scene * scene, const QModelIndex & block ); + BillboardNode( Scene * _scene, NifFieldConst _block ); const Transform & viewTrans() const override; }; +// Inlines - NodeList + +inline void NodeList::attach( Node * node ) +{ + node->ref++; + nodes.append( node ); +} + +inline void NodeList::detach( Node * node ) +{ + Q_ASSERT( node->ref > 0 ); + if ( --node->ref <= 0 ) + delete node; +} + #endif diff --git a/src/gl/glparticles.cpp b/src/gl/glparticles.cpp index b7a21cbbb..476831092 100644 --- a/src/gl/glparticles.cpp +++ b/src/gl/glparticles.cpp @@ -43,6 +43,11 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * Particle */ +Particles::Particles( Scene * _scene, NifFieldConst _block ) + : Node( _scene, _block ) +{ +} + void Particles::clear() { Node::clear(); @@ -74,15 +79,12 @@ void Particles::updateImpl( const NifModel * nif, const QModelIndex & index ) updateData = true; } -void Particles::setController( const NifModel * nif, const QModelIndex & index ) +Controller * Particles::createController( NifFieldConst controllerBlock ) { - auto contrName = nif->itemName(index); - if ( contrName == "NiParticleSystemController" || contrName == "NiBSPArrayController" ) { - Controller * ctrl = new ParticleController( this, index ); - registerController(nif, ctrl); - } else { - Node::setController( nif, index ); - } + if ( controllerBlock.hasName("NiParticleSystemController", "NiBSPArrayController") ) + return new ParticleController( this, controllerBlock ); + + return Node::createController( controllerBlock ); } void Particles::transform() @@ -132,6 +134,10 @@ void Particles::drawShapes( NodeList * secondPass, bool presort ) if ( isHidden() ) return; + // TODO: The code right below is a quick and dirty fix for Particles polluting the selection memory buffer. + // Rewrite it in a more elegant way. + if ( Node::SELECTING ) + return; AlphaProperty * aprop = findProperty(); @@ -161,7 +167,7 @@ void Particles::drawShapes( NodeList * secondPass, bool presort ) // setup texturing - glProperty( findProperty() ); + glProperty( findProperty() ); // setup z buffer @@ -224,4 +230,6 @@ void Particles::drawShapes( NodeList * secondPass, bool presort ) p++; } + + glDisable( GL_TEXTURE_2D ); } diff --git a/src/gl/glparticles.h b/src/gl/glparticles.h index 085bbbc29..1d3ee4592 100644 --- a/src/gl/glparticles.h +++ b/src/gl/glparticles.h @@ -43,10 +43,10 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. class Particles : public Node { - friend class ParticleController; + friend class ParticleInterpolator; public: - Particles( Scene * s, const QModelIndex & b ) : Node( s, b ) {} + Particles( Scene * _scene, NifFieldConst _block ); void clear() override; void transform() override; @@ -58,7 +58,7 @@ class Particles : public Node BoundSphere bounds() const override; protected: - void setController( const NifModel * nif, const QModelIndex & controller ) override; + Controller * createController( NifFieldConst controllerBlock ) override; void updateImpl( const NifModel * nif, const QModelIndex & index ) override; QPersistentModelIndex iData; diff --git a/src/gl/glproperty.cpp b/src/gl/glproperty.cpp index a5637d0a5..1eec95d01 100644 --- a/src/gl/glproperty.cpp +++ b/src/gl/glproperty.cpp @@ -37,7 +37,6 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "gl/glscene.h" #include "gl/gltex.h" #include "io/material.h" -#include "model/nifmodel.h" #include @@ -45,70 +44,93 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. //! @file glproperty.cpp Encapsulation of NiProperty blocks defined in nif.xml //! Helper function that checks texture sets -bool checkSet( int s, const QVector > & texcoords ) +static inline bool checkSet( int s, const QVector > & texcoords ) { return s >= 0 && s < texcoords.count() && texcoords[s].count(); } Property * Property::create( Scene * scene, const NifModel * nif, const QModelIndex & index ) { - Property * property = 0; - - if ( nif->isNiBlock( index, "NiAlphaProperty" ) ) { - property = new AlphaProperty( scene, index ); - } else if ( nif->isNiBlock( index, "NiZBufferProperty" ) ) { - property = new ZBufferProperty( scene, index ); - } else if ( nif->isNiBlock( index, "NiTexturingProperty" ) ) { - property = new TexturingProperty( scene, index ); - } else if ( nif->isNiBlock( index, "NiTextureProperty" ) ) { - property = new TextureProperty( scene, index ); - } else if ( nif->isNiBlock( index, "NiMaterialProperty" ) ) { - property = new MaterialProperty( scene, index ); - } else if ( nif->isNiBlock( index, "NiSpecularProperty" ) ) { - property = new SpecularProperty( scene, index ); - } else if ( nif->isNiBlock( index, "NiWireframeProperty" ) ) { - property = new WireframeProperty( scene, index ); - } else if ( nif->isNiBlock( index, "NiVertexColorProperty" ) ) { - property = new VertexColorProperty( scene, index ); - } else if ( nif->isNiBlock( index, "NiStencilProperty" ) ) { - property = new StencilProperty( scene, index ); - } else if ( nif->isNiBlock( index, "BSLightingShaderProperty" ) ) { - property = new BSLightingShaderProperty( scene, index ); - } else if ( nif->isNiBlock( index, "BSShaderLightingProperty" ) ) { - property = new BSShaderLightingProperty( scene, index ); - } else if ( nif->isNiBlock( index, "BSEffectShaderProperty" ) ) { - property = new BSEffectShaderProperty( scene, index ); - } else if ( nif->isNiBlock( index, "BSWaterShaderProperty" ) ) { - property = new BSWaterShaderProperty( scene, index ); - } else if ( nif->isNiBlock( index, "BSShaderNoLightingProperty" ) ) { - property = new BSShaderLightingProperty( scene, index ); - } else if ( nif->isNiBlock( index, "BSShaderPPLightingProperty" ) ) { - property = new BSShaderLightingProperty( scene, index ); - } else if ( index.isValid() ) { -#ifndef QT_NO_DEBUG - NifItem * item = static_cast( index.internalPointer() ); - - if ( item ) - qCWarning( nsNif ) << tr( "Unknown property: %1" ).arg( item->name() ); - else - qCWarning( nsNif ) << tr( "Unknown property: I can't determine its name" ); -#endif + Property * property = nullptr; + + static const QSet oldShaderTypes = { + // Fallout 3 - lighting shaders + QStringLiteral("BSShaderLightingProperty"), + QStringLiteral("BSShaderNoLightingProperty"), + QStringLiteral("BSShaderPPLightingProperty"), + QStringLiteral("Lighting30ShaderProperty"), + QStringLiteral("SkyShaderProperty"), + QStringLiteral("TileShaderProperty"), + + // Fallout 3 - other shaders + QStringLiteral("WaterShaderProperty"), + QStringLiteral("TallGrassShaderProperty"), + + // Other ancient shaders from nif.xml + QStringLiteral("DistantLODShaderProperty"), + QStringLiteral("HairShaderProperty"), + QStringLiteral("BSDistantTreeShaderProperty"), + QStringLiteral("VolumetricFogShaderProperty"), + }; + + auto block = nif->field( index ); + if ( !block ) { + // Do nothing + + } else if ( !block.isBlock() ) { + nif->reportError( tr("Property::create: item '%1' is not a block.").arg( block.repr() ) ); + + } else if ( block.hasName("NiAlphaProperty") ) { + property = new AlphaProperty( scene, block ); + + } else if ( block.hasName("NiZBufferProperty") ) { + property = new ZBufferProperty( scene, block ); + + } else if ( block.hasName("NiTexturingProperty") ) { + property = new TexturingProperty( scene, block ); + + } else if ( block.hasName("NiTextureProperty") ) { + property = new TextureProperty( scene, block ); + + } else if ( block.hasName("NiMaterialProperty") ) { + property = new MaterialProperty( scene, block ); + + } else if ( block.hasName("NiSpecularProperty") ) { + property = new SpecularProperty( scene, block ); + + } else if ( block.hasName("NiWireframeProperty") ) { + property = new WireframeProperty( scene, block ); + + } else if ( block.hasName("NiVertexColorProperty") ) { + property = new VertexColorProperty( scene, block ); + + } else if ( block.hasName("NiStencilProperty") ) { + property = new StencilProperty( scene, block ); + + } else if ( block.hasName("BSLightingShaderProperty") ) { + property = new BSLightingShaderProperty( scene, block ); + + } else if ( block.hasName("BSEffectShaderProperty") ) { + property = new BSEffectShaderProperty( scene, block ); + + } else if ( block.hasName("BSWaterShaderProperty", "BSSkyShaderProperty") ) { + property = new SkyrimSimpleShaderProperty( scene, block ); + + } else if ( oldShaderTypes.contains( block.name() ) ) { + property = new BSShaderProperty( scene, block ); + + } else { + nif->reportError( tr("Property::create: Could not create Property from a block of type '%1'.").arg( block.name() ) ); } if ( property ) - property->update( nif, index ); + property->update(); return property; } -PropertyList::PropertyList() -{ -} -PropertyList::PropertyList( const PropertyList & other ) -{ - operator=( other ); -} +// PropertyList class PropertyList::~PropertyList() { @@ -117,70 +139,34 @@ PropertyList::~PropertyList() void PropertyList::clear() { - for ( Property * p : properties ) { - if ( --p->ref <= 0 ) - delete p; - } + for ( Property * p : properties ) + detach( p, 1 ); properties.clear(); } PropertyList & PropertyList::operator=( const PropertyList & other ) { clear(); - for ( Property * p : other.properties ) { - add( p ); - } + for ( Property * p : other.properties ) + attach( p ); return *this; } -bool PropertyList::contains( Property * p ) const +void PropertyList::add( Property * prop ) { - if ( !p ) - return false; - - QList props = properties.values( p->type() ); - return props.contains( p ); + if ( prop && !properties.contains( prop->type(), prop ) ) + attach( prop ); } -void PropertyList::add( Property * p ) +void PropertyList::del( Property * prop ) { - if ( p && !contains( p ) ) { - ++p->ref; - properties.insert( p->type(), p ); + if ( prop ) { + int cnt = properties.remove( prop->type(), prop ); + if ( cnt > 0 ) + detach( prop, cnt ); } } -void PropertyList::del( Property * p ) -{ - if ( !p ) - return; - - QHash::iterator i = properties.find( p->type() ); - - while ( p && i != properties.end() && i.key() == p->type() ) { - if ( i.value() == p ) { - i = properties.erase( i ); - - if ( --p->ref <= 0 ) - delete p; - } else { - ++i; - } - } -} - -Property * PropertyList::get( const QModelIndex & index ) const -{ - if ( !index.isValid() ) - return 0; - - for ( Property * p : properties ) { - if ( p->index() == index ) - return p; - } - return 0; -} - void PropertyList::validate() { QList rem; @@ -188,25 +174,39 @@ void PropertyList::validate() if ( !p->isValid() ) rem.append( p ); } - for ( Property * p : rem ) { + for ( Property * p : rem ) del( p ); - } } void PropertyList::merge( const PropertyList & other ) { for ( Property * p : other.properties ) { if ( !properties.contains( p->type() ) ) - add( p ); + attach( p ); } } +Property * PropertyList::get( const QModelIndex & iPropBlock ) const +{ + if ( iPropBlock.isValid() ) { + for ( Property * p : properties ) { + if ( p->index() == iPropBlock ) + return p; + } + } + + return nullptr; +} + + +// AlphaProperty class + void AlphaProperty::updateImpl( const NifModel * nif, const QModelIndex & index ) { Property::updateImpl( nif, index ); if ( index == iBlock ) { - unsigned short flags = nif->get( iBlock, "Flags" ); + auto flags = block["Flags"].value(); alphaBlend = flags & 1; @@ -226,23 +226,22 @@ void AlphaProperty::updateImpl( const NifModel * nif, const QModelIndex & index alphaTest = flags & ( 1 << 9 ); alphaFunc = testMap[ ( flags >> 10 ) & 0x7 ]; - alphaThreshold = float( nif->get( iBlock, "Threshold" ) ) / 255.0; + alphaThreshold = float( block["Threshold"].value() ) / 255.0; alphaSort = ( flags & 0x2000 ) == 0; // Temporary Weapon Blood fix for FO4 - if ( nif->getBSVersion() >= 130 ) + if ( modelBSVersion() >= 130 ) alphaTest |= (flags == 20547); } } -void AlphaProperty::setController( const NifModel * nif, const QModelIndex & controller ) +Controller * AlphaProperty::createController( NifFieldConst controllerBlock ) { - auto contrName = nif->itemName(controller); - if ( contrName == "BSNiAlphaPropertyTestRefController" ) { - Controller * ctrl = new AlphaController( this, controller ); - registerController(nif, ctrl); - } + if ( controllerBlock.hasName("BSNiAlphaPropertyTestRefController") ) + return new AlphaController_Alpha( this, controllerBlock ); + + return nullptr; } void glProperty( AlphaProperty * p ) @@ -262,12 +261,15 @@ void glProperty( AlphaProperty * p ) } } + +// ZBufferProperty class + void ZBufferProperty::updateImpl( const NifModel * nif, const QModelIndex & index ) { Property::updateImpl( nif, index ); if ( index == iBlock ) { - int flags = nif->get( iBlock, "Flags" ); + auto flags = block["Flags"].value(); depthTest = flags & 1; depthMask = flags & 2; static const GLenum depthMap[8] = { @@ -275,9 +277,9 @@ void ZBufferProperty::updateImpl( const NifModel * nif, const QModelIndex & inde }; // This was checking version 0x10000001 ? - if ( nif->checkVersion( 0x04010012, 0x14000005 ) ) { - depthFunc = depthMap[ nif->get < int > ( iBlock, "Function" ) & 0x07 ]; - } else if ( nif->checkVersion( 0x14010003, 0 ) ) { + if ( modelVersionInRange( 0x04010012, 0x14000005 ) ) { + depthFunc = depthMap[ block["Function"].value() & 0x07 ]; + } else if ( modelVersion() >= 0x14010003 ) { depthFunc = depthMap[ (flags >> 2 ) & 0x07 ]; } else { depthFunc = GL_LEQUAL; @@ -304,110 +306,118 @@ void glProperty( ZBufferProperty * p ) } } -/* - TexturingProperty -*/ + +// TexturingProperty class void TexturingProperty::updateImpl( const NifModel * nif, const QModelIndex & index ) { Property::updateImpl( nif, index ); if ( index == iBlock ) { - static const char * texnames[numTextures] = { - "Base Texture", "Dark Texture", "Detail Texture", "Gloss Texture", "Glow Texture", "Bump Map Texture", "Decal 0 Texture", "Decal 1 Texture", "Decal 2 Texture", "Decal 3 Texture" + static const QString FIELD_NAMES[NUM_TEXTURES] = { + QStringLiteral("Base Texture"), + QStringLiteral("Dark Texture"), + QStringLiteral("Detail Texture"), + QStringLiteral("Gloss Texture"), + QStringLiteral("Glow Texture"), + QStringLiteral("Bump Map Texture"), + QStringLiteral("Decal 0 Texture"), + QStringLiteral("Decal 1 Texture"), + QStringLiteral("Decal 2 Texture"), + QStringLiteral("Decal 3 Texture") }; - for ( int t = 0; t < numTextures; t++ ) { - QModelIndex iTex = nif->getIndex( iBlock, texnames[t] ); + for ( int t = 0; t < NUM_TEXTURES; t++ ) { + auto texEntry = block.child( FIELD_NAMES[t] ); + if ( !texEntry ) { + textures[t].sourceBlock = NifFieldConst(); + continue; + } - if ( iTex.isValid() ) { - textures[t].iSource = nif->getBlockIndex( nif->getLink( iTex, "Source" ), "NiSourceTexture" ); - textures[t].coordset = nif->get( iTex, "UV Set" ); + textures[t].sourceBlock = texEntry.child("Source").linkBlock("NiSourceTexture"); + textures[t].coordset = texEntry.child("UV Set").value(); - int filterMode = 0, clampMode = 0; - if ( nif->checkVersion( 0, 0x14010002 ) ) { - filterMode = nif->get( iTex, "Filter Mode" ); - clampMode = nif->get( iTex, "Clamp Mode" ); - } else if ( nif->checkVersion( 0x14010003, 0 ) ) { - auto flags = nif->get( iTex, "Flags" ); - filterMode = ((flags & 0x0F00) >> 0x08); - clampMode = ((flags & 0xF000) >> 0x0C); - textures[t].coordset = (flags & 0x00FF); - } + int filterMode = 0, clampMode = 0; + if ( modelVersion() <= 0x14010002 ) { + filterMode = texEntry["Filter Mode"].value(); + clampMode = texEntry["Clamp Mode"].value(); + } else { + auto flags = texEntry["Flags"].value(); + filterMode = ((flags & 0x0F00) >> 0x08); + clampMode = ((flags & 0xF000) >> 0x0C); + textures[t].coordset = (flags & 0x00FF); + } - float af = 1.0; - float max_af = get_max_anisotropy(); - // Let User Settings decide for trilinear - if ( filterMode == GL_LINEAR_MIPMAP_LINEAR ) - af = max_af; - - // Override with value in NIF for 20.5+ - if ( nif->checkVersion( 0x14050004, 0 ) ) - af = std::min( max_af, (float)nif->get( iTex, "Max Anisotropy" ) ); - - textures[t].maxAniso = std::max( 1.0f, std::min( af, max_af ) ); - - // See OpenGL docs on glTexParameter and GL_TEXTURE_MIN_FILTER option - // See also http://gregs-blog.com/2008/01/17/opengl-texture-filter-parameters-explained/ - switch ( filterMode ) { - case 0: - textures[t].filter = GL_NEAREST; - break; // nearest - case 1: - textures[t].filter = GL_LINEAR; - break; // bilinear - case 2: - textures[t].filter = GL_LINEAR_MIPMAP_LINEAR; - break; // trilinear - case 3: - textures[t].filter = GL_NEAREST_MIPMAP_NEAREST; - break; // nearest from nearest - case 4: - textures[t].filter = GL_NEAREST_MIPMAP_LINEAR; - break; // interpolate from nearest - case 5: - textures[t].filter = GL_LINEAR_MIPMAP_NEAREST; - break; // bilinear from nearest - default: - textures[t].filter = GL_LINEAR; - break; - } + float af = 1.0; + float max_af = get_max_anisotropy(); + // Let User Settings decide for trilinear + if ( filterMode == GL_LINEAR_MIPMAP_LINEAR ) + af = max_af; - switch ( clampMode ) { - case 0: - textures[t].wrapS = GL_CLAMP; - textures[t].wrapT = GL_CLAMP; - break; - case 1: - textures[t].wrapS = GL_CLAMP; - textures[t].wrapT = GL_REPEAT; - break; - case 2: - textures[t].wrapS = GL_REPEAT; - textures[t].wrapT = GL_CLAMP; - break; - default: - textures[t].wrapS = GL_REPEAT; - textures[t].wrapT = GL_REPEAT; - break; - } + // Override with value in NIF for 20.5+ + if ( modelVersion() >= 0x14050004 ) + af = std::min( max_af, float( texEntry["Max Anisotropy"].value() ) ); - textures[t].hasTransform = nif->get( iTex, "Has Texture Transform" ); - - if ( textures[t].hasTransform ) { - textures[t].translation = nif->get( iTex, "Translation" ); - textures[t].tiling = nif->get( iTex, "Scale" ); - textures[t].rotation = nif->get( iTex, "Rotation" ); - textures[t].center = nif->get( iTex, "Center" ); - } else { - // we don't really need to set these since they won't be applied in bind() unless hasTransform is set - textures[t].translation = Vector2(); - textures[t].tiling = Vector2( 1.0, 1.0 ); - textures[t].rotation = 0.0; - textures[t].center = Vector2( 0.5, 0.5 ); - } + textures[t].maxAniso = std::max( 1.0f, std::min( af, max_af ) ); + + // See OpenGL docs on glTexParameter and GL_TEXTURE_MIN_FILTER option + // See also http://gregs-blog.com/2008/01/17/opengl-texture-filter-parameters-explained/ + switch ( filterMode ) { + case 0: + textures[t].filter = GL_NEAREST; + break; // nearest + case 1: + textures[t].filter = GL_LINEAR; + break; // bilinear + case 2: + textures[t].filter = GL_LINEAR_MIPMAP_LINEAR; + break; // trilinear + case 3: + textures[t].filter = GL_NEAREST_MIPMAP_NEAREST; + break; // nearest from nearest + case 4: + textures[t].filter = GL_NEAREST_MIPMAP_LINEAR; + break; // interpolate from nearest + case 5: + textures[t].filter = GL_LINEAR_MIPMAP_NEAREST; + break; // bilinear from nearest + default: + textures[t].filter = GL_LINEAR; + break; + } + + switch ( clampMode ) { + case 0: + textures[t].wrapS = GL_CLAMP; + textures[t].wrapT = GL_CLAMP; + break; + case 1: + textures[t].wrapS = GL_CLAMP; + textures[t].wrapT = GL_REPEAT; + break; + case 2: + textures[t].wrapS = GL_REPEAT; + textures[t].wrapT = GL_CLAMP; + break; + default: + textures[t].wrapS = GL_REPEAT; + textures[t].wrapT = GL_REPEAT; + break; + } + + textures[t].hasTransform = texEntry.child("Has Texture Transform").value(); + + if ( textures[t].hasTransform ) { + textures[t].translation = texEntry["Translation"].value(); + textures[t].tiling = texEntry["Scale"].value(); + textures[t].rotation = texEntry["Rotation"].value(); + textures[t].center = texEntry["Center"].value(); } else { - textures[t].iSource = QModelIndex(); + // we don't really need to set these since they won't be applied in bind() unless hasTransform is set + textures[t].translation = Vector2(); + textures[t].tiling = Vector2( 1.0, 1.0 ); + textures[t].rotation = 0.0; + textures[t].center = Vector2( 0.5, 0.5 ); } } } @@ -417,11 +427,11 @@ bool TexturingProperty::bind( int id, const QString & fname ) { GLuint mipmaps = 0; - if ( id >= 0 && id <= (numTextures - 1) ) { + if ( id >= 0 && id < NUM_TEXTURES ) { if ( !fname.isEmpty() ) mipmaps = scene->bindTexture( fname ); else - mipmaps = scene->bindTexture( textures[ id ].iSource ); + mipmaps = scene->bindTexture( textures[id].sourceBlock.toIndex() ); if ( mipmaps == 0 ) return false; @@ -476,38 +486,30 @@ bool TexturingProperty::bind( int id, const QVector > & texcoor QString TexturingProperty::fileName( int id ) const { - if ( id >= 0 && id <= (numTextures - 1) ) { - QModelIndex iSource = textures[id].iSource; - auto nif = NifModel::fromValidIndex(iSource); - if ( nif ) { - return nif->get( iSource, "File Name" ); - } - } + if ( id >= 0 && id < NUM_TEXTURES ) + return textures[id].sourceBlock.child("File Name").value(); return QString(); } int TexturingProperty::coordSet( int id ) const { - if ( id >= 0 && id <= (numTextures - 1) ) { + if ( id >= 0 && id < NUM_TEXTURES ) { return textures[id].coordset; } return -1; } - -//! Set the appropriate Controller -void TexturingProperty::setController( const NifModel * nif, const QModelIndex & iController ) +Controller * TexturingProperty::createController( NifFieldConst controllerBlock ) { - auto contrName = nif->itemName(iController); - if ( contrName == "NiFlipController" ) { - Controller * ctrl = new TexFlipController( this, iController ); - registerController(nif, ctrl); - } else if ( contrName == "NiTextureTransformController" ) { - Controller * ctrl = new TexTransController( this, iController ); - registerController(nif, ctrl); - } + if ( controllerBlock.hasName("NiFlipController") ) + return new TextureFlipController_Texturing( this, controllerBlock ); + + if ( controllerBlock.hasName("NiTextureTransformController") ) + return new TextureTransformController( this, controllerBlock ); + + return nullptr; } int TexturingProperty::getId( const QString & texname ) @@ -535,16 +537,15 @@ void glProperty( TexturingProperty * p ) } } -/* - TextureProperty -*/ + +// TextureProperty void TextureProperty::updateImpl( const NifModel * nif, const QModelIndex & index ) { Property::updateImpl( nif, index ); if ( index == iBlock ) { - iImage = nif->getBlockIndex( nif->getLink( iBlock, "Image" ), "NiImage" ); + imageBlock = block["Image"].linkBlock( "NiImage" ); } } @@ -580,21 +581,15 @@ bool TextureProperty::bind( const QVector > & texcoords ) QString TextureProperty::fileName() const { - auto nif = NifModel::fromValidIndex(iImage); - if ( nif ) - return nif->get( iImage, "File Name" ); - - return QString(); + return imageBlock.child("File Name").value(); } - -void TextureProperty::setController( const NifModel * nif, const QModelIndex & iController ) +Controller * TextureProperty::createController( NifFieldConst controllerBlock ) { - auto contrName = nif->itemName(iController); - if ( contrName == "NiFlipController" ) { - Controller * ctrl = new TexFlipController( this, iController ); - registerController(nif, ctrl); - } + if ( controllerBlock.hasName("NiFlipController") ) + return new TextureFlipController_Texture( this, controllerBlock ); + + return nullptr; } void glProperty( TextureProperty * p ) @@ -604,43 +599,45 @@ void glProperty( TextureProperty * p ) } } -/* - MaterialProperty -*/ + +// MaterialProperty and SpecularProperty classes void MaterialProperty::updateImpl( const NifModel * nif, const QModelIndex & index ) { Property::updateImpl( nif, index ); if ( index == iBlock ) { - alpha = nif->get( iBlock, "Alpha" ); - if ( alpha < 0.0 ) - alpha = 0.0; - if ( alpha > 1.0 ) - alpha = 1.0; + alpha = std::clamp( block["Alpha"].value(), 0.0f, 1.0f ); - ambient = Color4( nif->get( iBlock, "Ambient Color" ) ); - diffuse = Color4( nif->get( iBlock, "Diffuse Color" ) ); - specular = Color4( nif->get( iBlock, "Specular Color" ) ); - emissive = Color4( nif->get( iBlock, "Emissive Color" ) ); + ambient = Color4( block.child("Ambient Color").value() ); + diffuse = Color4( block.child("Diffuse Color").value() ); + specular = Color4( block.child("Specular Color").value() ); + emissive = Color4( block.child("Emissive Color").value() ); // OpenGL needs shininess clamped otherwise it generates GL_INVALID_VALUE - shininess = std::min( std::max( nif->get( iBlock, "Glossiness" ), 0.0f ), 128.0f ); + shininess = std::clamp( block["Glossiness"].value(), 0.0f, 128.0f ); } } -void MaterialProperty::setController( const NifModel * nif, const QModelIndex & iController ) +Controller * MaterialProperty::createController( NifFieldConst controllerBlock ) { - auto contrName = nif->itemName(iController); - if ( contrName == "NiAlphaController" ) { - Controller * ctrl = new AlphaController( this, iController ); - registerController(nif, ctrl); - } else if ( contrName == "NiMaterialColorController" ) { - Controller * ctrl = new MaterialColorController( this, iController ); - registerController(nif, ctrl); - } + if ( controllerBlock.hasName("NiAlphaController") ) + return new AlphaController_Material( this, controllerBlock ); + + if ( controllerBlock.hasName("NiMaterialColorController") ) + return new MaterialColorController( this, controllerBlock ); + + return nullptr; } +void SpecularProperty::updateImpl( const NifModel * nif, const QModelIndex & index ) +{ + Property::updateImpl( nif, index ); + + if ( index == iBlock ) { + spec = block["Flags"].value() != 0; + } +} void glProperty( MaterialProperty * p, SpecularProperty * s ) { @@ -667,21 +664,15 @@ void glProperty( MaterialProperty * p, SpecularProperty * s ) } } -void SpecularProperty::updateImpl( const NifModel * nif, const QModelIndex & index ) -{ - Property::updateImpl( nif, index ); - if ( index == iBlock ) { - spec = nif->get( iBlock, "Flags" ) != 0; - } -} +// WireframeProperty class void WireframeProperty::updateImpl( const NifModel * nif, const QModelIndex & index ) { Property::updateImpl( nif, index ); if ( index == iBlock ) { - wire = nif->get( iBlock, "Flags" ) != 0; + wire = block["Flags"].value() != 0; } } @@ -695,21 +686,24 @@ void glProperty( WireframeProperty * p ) } } + +// VertexColorProperty class + void VertexColorProperty::updateImpl( const NifModel * nif, const QModelIndex & index ) { Property::updateImpl( nif, index ); if ( index == iBlock ) { - if ( nif->checkVersion( 0, 0x14010001 ) ) { - vertexmode = nif->get( iBlock, "Vertex Mode" ); + if ( modelVersion() <= 0x14010001 ) { + vertexmode = block["Vertex Mode"].value(); // 0 : source ignore // 1 : source emissive // 2 : source ambient + diffuse - lightmode = nif->get( iBlock, "Lighting Mode" ); + lightmode = block["Lighting Mode"].value(); // 0 : emissive // 1 : emissive + ambient + diffuse } else { - auto flags = nif->get( iBlock, "Flags" ); + auto flags = block["Flags"].value(); vertexmode = (flags & 0x0030) >> 4; lightmode = (flags & 0x0008) >> 3; } @@ -756,36 +750,39 @@ void glProperty( VertexColorProperty * p, bool vertexcolors ) } } + +// StencilProperty class + void StencilProperty::updateImpl( const NifModel * nif, const QModelIndex & index ) { using namespace Stencil; Property::updateImpl( nif, index ); if ( index == iBlock ) { - static const GLenum funcMap[8] = { + static const GLenum funcMap[TEST_MAX] = { GL_NEVER, GL_GEQUAL, GL_NOTEQUAL, GL_GREATER, GL_LEQUAL, GL_EQUAL, GL_LESS, GL_ALWAYS }; - static const GLenum opMap[6] = { + static const GLenum opMap[ACTION_MAX] = { GL_KEEP, GL_ZERO, GL_REPLACE, GL_INCR, GL_DECR, GL_INVERT }; int drawMode = 0; - if ( nif->checkVersion( 0, 0x14000005 ) ) { - drawMode = nif->get( iBlock, "Draw Mode" ); - func = funcMap[std::min(nif->get( iBlock, "Stencil Function" ), (quint32)TEST_MAX - 1 )]; - failop = opMap[std::min( nif->get( iBlock, "Fail Action" ), (quint32)ACTION_MAX - 1 )]; - zfailop = opMap[std::min( nif->get( iBlock, "Z Fail Action" ), (quint32)ACTION_MAX - 1 )]; - zpassop = opMap[std::min( nif->get( iBlock, "Pass Action" ), (quint32)ACTION_MAX - 1 )]; - stencil = (nif->get( iBlock, "Stencil Enabled" ) & ENABLE_MASK); + if ( modelVersion() <= 0x14000005 ) { + drawMode = block["Draw Mode"].value(); + func = funcMap[std::min( block["Stencil Function"].value(), quint32(TEST_MAX) - 1 )]; + failop = opMap[std::min( block["Fail Action"].value(), quint32(ACTION_MAX) - 1 )]; + zfailop = opMap[std::min( block["Z Fail Action"].value(), quint32(ACTION_MAX) - 1 )]; + zpassop = opMap[std::min( block["Pass Action"].value(), quint32(ACTION_MAX) - 1 )]; + stencil = ( block["Stencil Enabled"].value() & ENABLE_MASK ); } else { - auto flags = nif->get( iBlock, "Flags" ); + auto flags = block["Flags"].value(); drawMode = (flags & DRAW_MASK) >> DRAW_POS; - func = funcMap[(flags & TEST_MASK) >> TEST_POS]; - failop = opMap[(flags & FAIL_MASK) >> FAIL_POS]; - zfailop = opMap[(flags & ZFAIL_MASK) >> ZFAIL_POS]; - zpassop = opMap[(flags & ZPASS_MASK) >> ZPASS_POS]; - stencil = (flags & ENABLE_MASK); + func = funcMap[(flags & TEST_MASK) >> TEST_POS]; + failop = opMap[(flags & FAIL_MASK) >> FAIL_POS]; + zfailop = opMap[(flags & ZFAIL_MASK) >> ZFAIL_POS]; + zpassop = opMap[(flags & ZPASS_MASK) >> ZPASS_POS]; + stencil = (flags & ENABLE_MASK); } switch ( drawMode ) { @@ -804,8 +801,8 @@ void StencilProperty::updateImpl( const NifModel * nif, const QModelIndex & inde break; } - ref = nif->get( iBlock, "Stencil Ref" ); - mask = nif->get( iBlock, "Stencil Mask" ); + ref = block.child("Stencil Ref").value(); + mask = block.child("Stencil Mask").value(); } } @@ -833,102 +830,453 @@ void glProperty( StencilProperty * p ) } } -/* - BSShaderLightingProperty -*/ -BSShaderLightingProperty::~BSShaderLightingProperty() -{ - if ( material ) - delete material; -} +// Shader flags + +typedef uint32_t ShaderFlagsType; + + +// Fallout 3 shader flags + +enum class Fallout3_ShaderFlags1 : ShaderFlagsType // BSShaderFlags in nif.xml +{ + Specular = 1u << 0, + Skinned = 1u << 1, + LowDetail = 1u << 2, + VertexAlpha = 1u << 3, + Unknown4 = 1u << 4, + SinglePass = 1u << 5, + Empty = 1u << 6, + EnvMap = 1u << 7, + AlphaTexture = 1u << 8, + Unknown9 = 1u << 9, + FaceGen = 1u << 10, + Parallax15 = 1u << 11, + Unknown12 = 1u << 12, + NonProjectiveShadows = 1u << 13, + Unknown14 = 1u << 14, + Refraction = 1u << 15, + FireRefraction = 1u << 16, + EyeEnvMap = 1u << 17, + Hair = 1u << 18, + DynamicAlpha = 1u << 19, + LocalMapHideSecret = 1u << 20, + WindowEnvMap = 1u << 21, + TreeBillboard = 1u << 22, + ShadowFrustrum = 1u << 23, + MultipleTextures = 1u << 24, + RemappableTextures = 1u << 25, + DecalSinglePass = 1u << 26, + DynamicDecalSinglePass = 1u << 27, + ParallaxOcclusion = 1u << 28, + ExternalEmittance = 1u << 29, + ShadowMap = 1u << 30, + ZBufferTest = 1u << 31, +}; -void BSShaderLightingProperty::updateImpl( const NifModel * nif, const QModelIndex & index ) +enum class Fallout3_ShaderFlags2 : ShaderFlagsType // BSShaderFlags2 in nif.xml +{ + ZBufferWrite = 1u << 0, + LODLandscape = 1u << 1, + LODBuilding = 1u << 2, + NoFade = 1u << 3, + RefractionTint = 1u << 4, + VertexColors = 1u << 5, + Unknown6 = 1u << 6, + FirstLightIsPointLight = 1u << 7, + SecondLight = 1u << 8, + ThirdLight = 1u << 9, + VertexLighting = 1u << 10, + UniformScale = 1u << 11, + FitSlope = 1u << 12, + BillboardAndEnvMapLightFade = 1u << 13, + NoLODLandBlend = 1u << 14, + EnvMapLightFade = 1u << 15, + Wireframe = 1u << 16, + VATSSelection = 1u << 17, + ShowInLocalMap = 1u << 18, + PremultAlpha = 1u << 19, + SkipNormalMaps = 1u << 20, + AlphaDecal = 1u << 21, + NoTraansparencyMultisampling = 1u << 22, + Unknown23 = 1u << 23, + Unknown24 = 1u << 24, + Unknown25 = 1u << 25, + Unknown26 = 1u << 26, + Unknown27 = 1u << 27, + Unknown28 = 1u << 28, + Unknown29 = 1u << 29, + Unknown30 = 1u << 30, + Unknown31 = 1u << 31, +}; + +class Fallout3_ShaderFlags { - Property::updateImpl( nif, index ); +public: + ShaderFlagsType flags1 = 0x82000000; + ShaderFlagsType flags2 = 0x1; - if ( index == iBlock ) { - iTextureSet = nif->getBlockIndex( nif->getLink( iBlock, "Texture Set" ), "BSShaderTextureSet" ); - iWetMaterial = nif->getIndex( iBlock, "Root Material" ); +private: + bool has( Fallout3_ShaderFlags1 f ) const { return flags1 & ShaderFlagsType(f); } + bool has( Fallout3_ShaderFlags2 f ) const { return flags2 & ShaderFlagsType(f); } +public: + bool vertexColors() const { return has( Fallout3_ShaderFlags2::VertexColors ); } + bool vertexAlpha() const { return has( Fallout3_ShaderFlags1::VertexAlpha ); } + bool depthTest() const { return has( Fallout3_ShaderFlags1::ZBufferTest ); } + bool depthWrite() const { return has( Fallout3_ShaderFlags2::ZBufferWrite ); } +}; + + +// New shader flags (Skyrim and FO4) + +enum class Skyrim_ShaderFlags1 : ShaderFlagsType // SkyrimShaderPropertyFlags1 in nif.xml +{ + Specular = 1u << 0, + Skinned = 1u << 1, + TempRefraction = 1u << 2, + VertexAlpha = 1u << 3, + GreyscaleToPaletteColor = 1u << 4, + GreyscaleToPaletteAlpha = 1u << 5, + UseFalloff = 1u << 6, + EnvMap = 1u << 7, + RecieveShadows = 1u << 8, + CastShadows = 1u << 9, + FaceGenDetailMap = 1u << 10, + Parallax = 1u << 11, + ModelSpaceNormals = 1u << 12, + NonProjectiveShadows = 1u << 13, + Landscape = 1u << 14, + Refraction = 1u << 15, + FireRefraction = 1u << 16, + EyeEnvMap = 1u << 17, + HairSoftLighting = 1u << 18, + ScreendoorAlphaFade = 1u << 19, + LocalMapHideSecret = 1u << 20, + FaceGenRGBTint = 1u << 21, + OwnEmit = 1u << 22, + ProjectedUV = 1u << 23, + MultipleTextures = 1u << 24, + RemappableTextures = 1u << 25, + Decal = 1u << 26, + DynamicDecal = 1u << 27, + ParallaxOcclusion = 1u << 28, + ExternalEmittance = 1u << 29, + SoftEffect = 1u << 30, + ZBufferTest = 1u << 31, +}; + +enum class Skyrim_ShaderFlags2 : ShaderFlagsType // SkyrimShaderPropertyFlags2 in nif.xml +{ + ZBufferWrite = 1u << 0, + LODLandscape = 1u << 1, + LODObjects = 1u << 2, + NoFade = 1u << 3, + DoubleSided = 1u << 4, + VertexColors = 1u << 5, + GlowMap = 1u << 6, + AssumeShadowMask = 1u << 7, + PackedTangent = 1u << 8, + MultiIndexSnow = 1u << 9, + VertexLighting = 1u << 10, + UniformScale = 1u << 11, + FitSlope = 1u << 12, + Billboard = 1u << 13, + NoLODLandBlend = 1u << 14, + EnvMapLightFade = 1u << 15, + Wireframe = 1u << 16, + WeaponBlood = 1u << 17, + HideOnLocalMap = 1u << 18, + PremultAlpha = 1u << 19, + CloudLOD = 1u << 20, + AnisotropicLighting = 1u << 21, + NoTraansparencyMultisampling = 1u << 22, + Unused23 = 1u << 23, + MultiLayerParallax = 1u << 24, + SoftLighting = 1u << 25, + RimLighting = 1u << 26, + BackLighting = 1u << 27, + Unused28 = 1u << 28, + TreeAnim = 1u << 29, + EffectLighting = 1u << 30, + HiDefLODObjects = 1u << 31, +}; + +enum class Fallout4_ShaderFlags1 : ShaderFlagsType // Fallout4ShaderPropertyFlags1 in nif.xml +{ + Specular = 1u << 0, + Skinned = 1u << 1, + TempRefraction = 1u << 2, + VertexAlpha = 1u << 3, + GreyscaleToPaletteColor = 1u << 4, + GreyscaleToPaletteAlpha = 1u << 5, + UseFalloff = 1u << 6, + EnvMap = 1u << 7, + RGBFalloff = 1u << 8, + CastShadows = 1u << 9, + Face = 1u << 10, + UIMaskRects = 1u << 11, + ModelSpaceNormals = 1u << 12, + NonProjectiveShadows = 1u << 13, + Landscape = 1u << 14, + Refraction = 1u << 15, + FireRefraction = 1u << 16, + EyeEnvMap = 1u << 17, + Hair = 1u << 18, + ScreendoorAlphaFade = 1u << 19, + LocalMapHideSecret = 1u << 20, + SkinTint = 1u << 21, + OwnEmit = 1u << 22, + ProjectedUV = 1u << 23, + MultipleTextures = 1u << 24, + Tessellate = 1u << 25, + Decal = 1u << 26, + DynamicDecal = 1u << 27, + CharacterLighting = 1u << 28, + ExternalEmittance = 1u << 29, + SoftEffect = 1u << 30, + ZBufferTest = 1u << 31, +}; + +enum class Fallout4_ShaderFlags2 : ShaderFlagsType // Fallout4ShaderPropertyFlags2 in nif.xml +{ + ZBufferWrite = 1u << 0, + LODLandscape = 1u << 1, + LODObjects = 1u << 2, + NoFade = 1u << 3, + DoubleSided = 1u << 4, + VertexColors = 1u << 5, + GlowMap = 1u << 6, + TransformChanged = 1u << 7, + DismembermentMeatcuff = 1u << 8, + Tint = 1u << 9, + GrassVertexLighting = 1u << 10, + GrassUniformScale = 1u << 11, + GrassFitSlope = 1u << 12, + GrassBillboard = 1u << 13, + NoLODLandBlend = 1u << 14, + Dismemberment = 1u << 15, + Wireframe = 1u << 16, + WeaponBlood = 1u << 17, + HideOnLocalMap = 1u << 18, + PremultAlpha = 1u << 19, + VATSTarget = 1u << 20, + AnisotropicLighting = 1u << 21, + SkewSpecularAlpha = 1u << 22, + MenuScreen = 1u << 23, + MultiLayerParallax = 1u << 24, + AlphaTest = 1u << 25, + GradientRemap = 1u << 26, + VATSTargetDrawAll = 1u << 27, + PipboyScreen = 1u << 28, + TreeAnim = 1u << 29, + EffectLighting = 1u << 30, + RefractionWritesDepth = 1u << 31, +}; + +class NewShaderFlags +{ +public: + bool isFO4 = false; + ShaderFlagsType flags1 = 0; + ShaderFlagsType flags2 = 0; + + void setFO4( bool _isFO4, bool isEffectsShader ); + +private: + bool has( Skyrim_ShaderFlags1 f ) const { return ( flags1 & ShaderFlagsType(f) ); } + bool has( Skyrim_ShaderFlags2 f ) const { return ( flags2 & ShaderFlagsType(f) ); } + bool has( Fallout4_ShaderFlags1 f ) const { return ( flags1 & ShaderFlagsType(f) ); } + bool has( Fallout4_ShaderFlags2 f ) const { return ( flags2 & ShaderFlagsType(f) ); } + + bool has( Skyrim_ShaderFlags1 f_sky, Fallout4_ShaderFlags1 f_fo4) const { return isFO4 ? has(f_fo4) : has(f_sky); } + bool has( Skyrim_ShaderFlags2 f_sky, Fallout4_ShaderFlags2 f_fo4) const { return isFO4 ? has(f_fo4) : has(f_sky); } + +public: + bool vertexColors() const { return has( Skyrim_ShaderFlags2::VertexColors, Fallout4_ShaderFlags2::VertexColors ); } + bool vertexAlpha() const { return has( Skyrim_ShaderFlags1::VertexAlpha, Fallout4_ShaderFlags1::VertexAlpha ); } + bool treeAnim() const { return has( Skyrim_ShaderFlags2::TreeAnim, Fallout4_ShaderFlags2::TreeAnim ); } + bool doubleSided() const { return has( Skyrim_ShaderFlags2::DoubleSided, Fallout4_ShaderFlags2::DoubleSided ); } + bool depthTest() const { return has( Skyrim_ShaderFlags1::ZBufferTest, Fallout4_ShaderFlags1::ZBufferTest ); } + bool depthWrite() const { return has( Skyrim_ShaderFlags2::ZBufferWrite, Fallout4_ShaderFlags2::ZBufferWrite ); } + bool specular() const { return has( Skyrim_ShaderFlags1::Specular, Fallout4_ShaderFlags1::Specular ); } + bool ownEmit() const { return has( Skyrim_ShaderFlags1::OwnEmit, Fallout4_ShaderFlags1::OwnEmit ); } + bool envMap() const { return has( Skyrim_ShaderFlags1::EnvMap, Fallout4_ShaderFlags1::EnvMap ); } + bool eyeEnvMap() const { return has( Skyrim_ShaderFlags1::EyeEnvMap, Fallout4_ShaderFlags1::EyeEnvMap ); } + bool glowMap() const { return has( Skyrim_ShaderFlags2::GlowMap, Fallout4_ShaderFlags2::GlowMap ); } + bool skyrimParallax() const { return ( !isFO4 && has( Skyrim_ShaderFlags1::Parallax ) ); } + bool skyrimBackLighting() const { return ( !isFO4 && has( Skyrim_ShaderFlags2::BackLighting ) ); } + bool skyrimRimLighting() const { return ( !isFO4 && has( Skyrim_ShaderFlags2::RimLighting ) ); } + bool skyrimSoftLighting() const { return ( !isFO4 && has( Skyrim_ShaderFlags2::SoftLighting ) ); } + bool skyrimMultiLayerParalax() const { return ( !isFO4 && has( Skyrim_ShaderFlags2::MultiLayerParallax ) ); } + bool refraction() const { return has( Skyrim_ShaderFlags1::Refraction, Fallout4_ShaderFlags1::Refraction ); } + bool greyscaleToPaletteColor() const { return has( Skyrim_ShaderFlags1::GreyscaleToPaletteColor, Fallout4_ShaderFlags1::GreyscaleToPaletteColor ); } + bool greyscaleToPaletteAlpha() const { return has( Skyrim_ShaderFlags1::GreyscaleToPaletteAlpha, Fallout4_ShaderFlags1::GreyscaleToPaletteAlpha ); } + bool useFalloff() const { return has( Skyrim_ShaderFlags1::UseFalloff, Fallout4_ShaderFlags1::UseFalloff ); } + bool rgbFalloff() const { return ( isFO4 && has( Fallout4_ShaderFlags1::RGBFalloff ) ); } + bool weaponBlood() const { return has( Skyrim_ShaderFlags2::WeaponBlood, Fallout4_ShaderFlags2::WeaponBlood ); } + bool effectLighting() const { return has( Skyrim_ShaderFlags2::EffectLighting, Fallout4_ShaderFlags2::EffectLighting ); } +}; + +void NewShaderFlags::setFO4( bool _isFO4, bool isEffectsShader ) +{ + isFO4 = _isFO4; + if ( isEffectsShader ) { + flags1 = 0x80000000; + flags2 = 0x20; + } else if ( isFO4 ) { + flags1 = 0x80400201; + flags2 = 1; + } else { + flags1 = 0x82400301; + flags2 = 0x8021; } } -void BSShaderLightingProperty::resetParams() -{ - flags1 = ShaderFlags::SLSF1_ZBuffer_Test; - flags2 = ShaderFlags::SLSF2_ZBuffer_Write; +static const QMap Fallout4_CRCFlagMap = { + // SF1 + { 1563274220u, uint64_t(Fallout4_ShaderFlags1::CastShadows) }, + { 1740048692u, uint64_t(Fallout4_ShaderFlags1::ZBufferTest) }, + { 3744563888u, uint64_t(Fallout4_ShaderFlags1::Skinned) }, + { 2893749418u, uint64_t(Fallout4_ShaderFlags1::EnvMap) }, + { 2333069810u, uint64_t(Fallout4_ShaderFlags1::VertexAlpha) }, + { 314919375u, uint64_t(Fallout4_ShaderFlags1::Face) }, + { 442246519u, uint64_t(Fallout4_ShaderFlags1::GreyscaleToPaletteColor) }, + { 2901038324u, uint64_t(Fallout4_ShaderFlags1::GreyscaleToPaletteAlpha) }, + { 3849131744u, uint64_t(Fallout4_ShaderFlags1::Decal) }, + { 1576614759u, uint64_t(Fallout4_ShaderFlags1::DynamicDecal) }, + { 2262553490u, uint64_t(Fallout4_ShaderFlags1::OwnEmit) }, + { 1957349758u, uint64_t(Fallout4_ShaderFlags1::Refraction) }, + { 1483897208u, uint64_t(Fallout4_ShaderFlags1::SkinTint) }, + { 3448946507u, uint64_t(Fallout4_ShaderFlags1::RGBFalloff) }, + { 2150459555u, uint64_t(Fallout4_ShaderFlags1::ExternalEmittance) }, + { 2548465567u, uint64_t(Fallout4_ShaderFlags1::ModelSpaceNormals) }, + { 3980660124u, uint64_t(Fallout4_ShaderFlags1::UseFalloff) }, + { 3503164976u, uint64_t(Fallout4_ShaderFlags1::SoftEffect) }, + + // SF2 + { 3166356979u, uint64_t(Fallout4_ShaderFlags2::ZBufferWrite) << 32 }, + { 2399422528u, uint64_t(Fallout4_ShaderFlags2::GlowMap) << 32 }, + { 759557230u, uint64_t(Fallout4_ShaderFlags2::DoubleSided) << 32 }, + { 348504749u, uint64_t(Fallout4_ShaderFlags2::VertexColors) << 32 }, + { 2994043788u, uint64_t(Fallout4_ShaderFlags2::NoFade) << 32 }, + { 2078326675u, uint64_t(Fallout4_ShaderFlags2::WeaponBlood) << 32 }, + { 3196772338u, uint64_t(Fallout4_ShaderFlags2::TransformChanged) << 32 }, + { 3473438218u, uint64_t(Fallout4_ShaderFlags2::EffectLighting) << 32 }, + { 2896726515u, uint64_t(Fallout4_ShaderFlags2::LODObjects) << 32 }, + + // TODO + { 731263983u, 0 }, // PBR + { 902349195u, 0 }, // REFRACTION FALLOFF + { 3030867718u, 0 }, // INVERTED_FADE_PATTERN + { 1264105798u, 0 }, // HAIRTINT + { 3707406987u, 0 }, // NO_EXPOSURE +}; - uvScale.reset(); - uvOffset.reset(); - clampMode = CLAMP_S_CLAMP_T; +static void readNewShaderFlags( NewShaderFlags & flags, BSShaderProperty * prop, bool isEffectsShader ) +{ + // Read flags fields + if ( prop->modelBSVersion() >= 151 ) { + flags.isFO4 = true; + + auto sfs = prop->block["SF1"].array() + prop->block["SF2"].array(); + uint64_t allFlags = 0; + for ( auto sf : sfs ) + allFlags |= Fallout4_CRCFlagMap.value( sf, 0 ); + flags.flags1 = allFlags & uint64_t(MAXUINT32); + flags.flags2 = allFlags >> 32; + + } else { // bsVersion < 151 + auto flagField1 = prop->block["Shader Flags 1"]; + auto flagField2 = prop->block["Shader Flags 2"]; + + if ( flagField1.hasStrType("SkyrimShaderPropertyFlags1") ) { + flags.setFO4( false, isEffectsShader ); + flags.flags1 = flagField1.value(); + } else if ( flagField1.hasStrType("Fallout4ShaderPropertyFlags1") ) { + flags.setFO4( true, isEffectsShader ); + flags.flags1 = flagField1.value(); + } else { + if ( flagField1 ) + flagField1.reportError( Property::tr("Unsupported value type '%1'.").arg( flagField1.strType() ) ); + // Fallback setVersion + flags.setFO4( !flagField1 && flagField2.hasStrType("Fallout4ShaderPropertyFlags2"), isEffectsShader ); + } - hasVertexColors = false; - hasVertexAlpha = false; + if ( flagField2.hasStrType("SkyrimShaderPropertyFlags2") ) { + if ( flags.isFO4 ) { + flagField2.reportError( Property::tr("Unexpected value type '%1'.").arg( flagField2.strType() ) ); + } else { + flags.flags2 = flagField2.value(); + } + } else if ( flagField2.hasStrType("Fallout4ShaderPropertyFlags2") ) { + if ( flags.isFO4 ) { + flags.flags2 = flagField2.value(); + } else { + flagField2.reportError( Property::tr("Unexpected value type '%1'.").arg( flagField2.strType() ) ); + } + } else if ( flagField2 ) { + flagField2.reportError( Property::tr("Unsupported value type '%1'.").arg( flagField2.strType() ) ); + } + } - depthTest = false; - depthWrite = false; - isDoubleSided = false; - isVertexAlphaAnimation = false; + // Set common vertex flags in the property + if ( prop->modelBSVersion() >= 130 ) { + // Always do vertex colors, incl. alphas, for FO4 and newer if colors present + prop->vertexColorMode = ShaderColorMode::FromData; + prop->hasVertexAlpha = true; + } else { + prop->vertexColorMode = flags.vertexColors() ? ShaderColorMode::Yes : ShaderColorMode::No; + prop->hasVertexAlpha = flags.vertexAlpha(); + } + prop->isVertexAlphaAnimation = flags.treeAnim(); } -void glProperty( BSShaderLightingProperty * p ) + +// BSShaderProperty class + +BSShaderProperty::~BSShaderProperty() { - if ( p && p->scene->hasOption(Scene::DoTexturing) && p->bind(0) ) { - glEnable( GL_TEXTURE_2D ); - } + if ( material ) + delete material; } -void BSShaderLightingProperty::clear() +void BSShaderProperty::clear() { Property::clear(); - setMaterial(nullptr); + setMaterial( nullptr ); } -void BSShaderLightingProperty::setMaterial( Material * newMaterial ) +bool BSShaderProperty::isTranslucent() const { - if (newMaterial && !newMaterial->isValid()) { - delete newMaterial; - newMaterial = nullptr; - } - if ( material && material != newMaterial ) { - delete material; - } - material = newMaterial; + return false; } -bool BSShaderLightingProperty::bind( int id, const QString & fname, TexClampMode mode ) +bool BSShaderProperty::bind( int id, const QString & fname, TextureClampMode mode ) { - GLuint mipmaps = 0; - - if ( !fname.isEmpty() ) - mipmaps = scene->bindTexture( fname ); - else - mipmaps = scene->bindTexture( this->fileName( id ) ); - + auto mipmaps = scene->bindTexture( fname.isEmpty() ? fileName(id) : fname ); if ( mipmaps == 0 ) return false; - switch ( mode ) { - case TexClampMode::CLAMP_S_CLAMP_T: + case TextureClampMode::ClampS_ClampT: glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP ); glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP ); break; - case TexClampMode::CLAMP_S_WRAP_T: + case TextureClampMode::ClampS_WrapT: glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP ); glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT ); break; - case TexClampMode::WRAP_S_CLAMP_T: + case TextureClampMode::WrapS_ClampT: glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT ); glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP ); break; - case TexClampMode::MIRRORED_S_MIRRORED_T: + case TextureClampMode::MirrorS_MirrorT: glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT ); glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT ); break; - case TexClampMode::WRAP_S_WRAP_T: + case TextureClampMode::WrapS_WrapT: default: glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT ); glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT ); @@ -945,12 +1293,12 @@ bool BSShaderLightingProperty::bind( int id, const QString & fname, TexClampMode return true; } -bool BSShaderLightingProperty::bind( int id, const QVector > & texcoords ) +bool BSShaderProperty::bind( int id, const QVector > & texcoords ) { if ( checkSet( 0, texcoords ) && bind( id ) ) { glEnable( GL_TEXTURE_2D ); glEnableClientState( GL_TEXTURE_COORD_ARRAY ); - glTexCoordPointer( 2, GL_FLOAT, 0, texcoords[ 0 ].data() ); + glTexCoordPointer( 2, GL_FLOAT, 0, texcoords[0].data() ); return true; } @@ -958,16 +1306,9 @@ bool BSShaderLightingProperty::bind( int id, const QVector > & return false; } -bool BSShaderLightingProperty::bindCube( int id, const QString & fname ) +bool BSShaderProperty::bindCube( const QString & fname ) { - Q_UNUSED( id ); - - GLuint result = 0; - - if ( !fname.isEmpty() ) - result = scene->bindTexture( fname ); - - if ( result == 0 ) + if ( fname.isEmpty() || scene->bindTexture( fname ) == 0 ) return false; glEnable( GL_TEXTURE_CUBE_MAP_SEAMLESS ); @@ -984,202 +1325,152 @@ bool BSShaderLightingProperty::bindCube( int id, const QString & fname ) return true; } -enum +void BSShaderProperty::setMaterial( Material * newMaterial ) { - BGSM1_DIFFUSE = 0, - BGSM1_NORMAL, - BGSM1_SPECULAR, - BGSM1_G2P, - BGSM1_ENV, - BGSM20_GLOW = 4, - BGSM1_GLOW = 5, - BGSM1_ENVMASK = 5, - BGSM20_REFLECT, - BGSM20_LIGHTING, - - BGSM1_MAX = 9, - BGSM20_MAX = 10 -}; + if ( newMaterial && !newMaterial->isValid() ) { + delete newMaterial; + newMaterial = nullptr; + } + if ( material && material != newMaterial ) { + delete material; + } + material = newMaterial; +} -QString BSShaderLightingProperty::fileName( int id ) const +Material * BSShaderProperty::createMaterial() { - const NifModel * nif; + return nullptr; +} - // Fallout 4 - nif = NifModel::fromValidIndex(iWetMaterial); - if ( nif ) { - // BSLSP - auto m = static_cast(material); - if ( m && m->isValid() ) { - auto tex = m->textures(); - if ( tex.count() >= BGSM1_MAX ) { - switch ( id ) { - case 0: // Diffuse - if ( !tex[BGSM1_DIFFUSE].isEmpty() ) - return tex[BGSM1_DIFFUSE]; - break; - case 1: // Normal - if ( !tex[BGSM1_NORMAL].isEmpty() ) - return tex[BGSM1_NORMAL]; - break; - case 2: // Glow - if ( tex.count() == BGSM1_MAX && m->bGlowmap && !tex[BGSM1_GLOW].isEmpty() ) - return tex[BGSM1_GLOW]; - - if ( tex.count() == BGSM20_MAX && m->bGlowmap && !tex[BGSM20_GLOW].isEmpty() ) - return tex[BGSM20_GLOW]; - break; - case 3: // Greyscale - if ( m->bGrayscaleToPaletteColor && !tex[BGSM1_G2P].isEmpty() ) - return tex[BGSM1_G2P]; - break; - case 4: // Cubemap - if ( tex.count() == BGSM1_MAX && m->bEnvironmentMapping && !tex[BGSM1_ENV].isEmpty() ) - return tex[BGSM1_ENV]; - break; - case 5: // Env Mask - if ( m->bEnvironmentMapping && !tex[BGSM1_ENVMASK].isEmpty() ) - return tex[BGSM1_ENVMASK]; - break; - case 7: // Specular - if ( m->bSpecularEnabled && !tex[BGSM1_SPECULAR].isEmpty() ) - return tex[BGSM1_SPECULAR]; - break; - } - } - if ( tex.count() >= BGSM20_MAX ) { - switch ( id ) { - case 8: - if ( m->bSpecularEnabled && !tex[BGSM20_REFLECT].isEmpty() ) - return tex[BGSM20_REFLECT]; - break; - case 9: - if ( m->bSpecularEnabled && !tex[BGSM20_LIGHTING].isEmpty() ) - return tex[BGSM20_LIGHTING]; - break; - } - } - } +void BSShaderProperty::setTexturePath( int id, const QString & texPath ) +{ + Q_ASSERT( id >= 0 ); - return QString(); + int nPaths = texturePaths.count(); + if ( id < nPaths ) { + texturePaths[id] = texPath; + } else if ( !texPath.isEmpty() ) { + if ( id > nPaths ) + texturePaths.resize( id ); // Get them up to (id) count so the next append would get (id) index + texturePaths.append( texPath ); } +} - // From iTextureSet - nif = NifModel::fromValidIndex(iTextureSet); - if ( nif ) { - if ( id >= 0 && id < nif->get(iTextureSet, "Num Textures") ) { - QModelIndex iTextures = nif->getIndex(iTextureSet, "Textures"); - return nif->get( iTextures.child(id, 0) ); - } +void BSShaderProperty::setTexturePathsFromTextureBlock() +{ + texturePaths = textureBlock["Textures"].array(); +} - return QString(); - } +void BSShaderProperty::updateImpl( const NifModel * nif, const QModelIndex & index ) +{ + Property::updateImpl( nif, index ); - // From material - auto m = static_cast(material); - if ( m ) { - if (m->isValid()) { - auto tex = m->textures(); - return tex[id]; - } + if ( index == iBlock ) { + textureBlock = block.child("Texture Set").linkBlock("BSShaderTextureSet"); + iTextureSet = textureBlock.toIndex(); - return QString(); - } + setMaterial( createMaterial() ); - // Handle niobject name="BSEffectShaderProperty... - nif = NifModel::fromIndex( iBlock ); - if ( nif ) { - switch ( id ) { - case 0: - return nif->get( iBlock, "Source Texture" ); - case 1: - return nif->get( iBlock, "Greyscale Texture" ); - case 2: - return nif->get( iBlock, "Env Map Texture" ); - case 3: - return nif->get( iBlock, "Normal Texture" ); - case 4: - return nif->get( iBlock, "Env Mask Texture" ); - case 6: - return nif->get( iBlock, "Reflectance Texture" ); - case 7: - return nif->get( iBlock, "Lighting Texture" ); - } + resetData(); + updateData(); + + } else if ( index == iTextureSet ) { + resetData(); + updateData(); } - - return QString(); } -int BSShaderLightingProperty::getId( const QString & id ) +void BSShaderProperty::resetData() { - const static QHash hash{ - { "base", 0 }, - { "dark", 1 }, - { "detail", 2 }, - { "gloss", 3 }, - { "glow", 4 }, - { "bumpmap", 5 }, - { "decal0", 6 }, - { "decal1", 7 } - }; + uvScale.reset(); + uvOffset.reset(); + clampMode = TextureClampMode::WrapS_WrapT; + + vertexColorMode = ShaderColorMode::FromData; + hasVertexAlpha = false; - return hash.value( id, -1 ); + depthTest = false; + depthWrite = false; + isDoubleSided = false; + isVertexAlphaAnimation = false; + + texturePaths.clear(); } -void BSShaderLightingProperty::setFlags1( const NifModel * nif ) +void BSShaderProperty::updateData() { - if ( nif->getBSVersion() >= 151 ) { - auto sf1 = nif->getArray( iBlock, "SF1" ); - auto sf2 = nif->getArray( iBlock, "SF2" ); - sf1.append( sf2 ); + Fallout3_ShaderFlags flags; + NifFieldConst flagField; - uint64_t flags = 0; - for ( auto sf : sf1 ) { - flags |= ShaderFlags::CRC_TO_FLAG.value( sf, 0 ); - } - flags1 = ShaderFlags::SF1( (uint32_t)flags ); + flagField = block.child("Shader Flags"); + if ( flagField.hasStrType("BSShaderFlags") ) { + flags.flags1 = flagField.value(); + } else if ( flagField ) { + flagField.reportError( tr("Unsupported value type '%1'.").arg( flagField.strType() ) ); + } + + flagField = block.child("Shader Flags 2"); + if ( flagField.hasStrType("BSShaderFlags2") ) { + flags.flags2 = flagField.value(); + } else if ( flagField ) { + flagField.reportError( tr("Unsupported value type '%1'.").arg( flagField.strType() ) ); + } + + // Judging by the amount of vanilla FO3 shapes with colors in the data and w/o Fallout3_ShaderFlags2::VertexColors in the shader flags, + // vertex colors are applied in the game even if Fallout3_ShaderFlags2::VertexColors is not set. + vertexColorMode = flags.vertexColors() ? ShaderColorMode::Yes : ShaderColorMode::FromData; + + if ( block.inherits("WaterShaderProperty") ) { + hasVertexAlpha = true; } else { - flags1 = ShaderFlags::SF1( nif->get(iBlock, "Shader Flags 1") ); + hasVertexAlpha = flags.vertexAlpha(); } + + depthTest = flags.depthTest(); + depthWrite = flags.depthWrite(); + + auto clampField = block.child("Texture Clamp Mode"); + if ( clampField ) + clampMode = TextureClampMode( clampField.value() ); + + // Textures + if ( textureBlock ) { + setTexturePathsFromTextureBlock(); + + } else if ( block.hasName("SkyShaderProperty", "TileShaderProperty", "TallGrassShaderProperty") ) { + setTexturePath( 0, block["File Name"] ); + + } else if ( block.hasName("BSShaderNoLightingProperty") ) { + setTexturePath( 2, block["File Name"] ); // The texture glow map + } } -void BSShaderLightingProperty::setFlags2( const NifModel * nif ) +void glProperty( BSShaderProperty * p ) { - if ( nif->getBSVersion() >= 151 ) { - auto sf1 = nif->getArray( iBlock, "SF1" ); - auto sf2 = nif->getArray( iBlock, "SF2" ); - sf1.append( sf2 ); - - uint64_t flags = 0; - for ( auto sf : sf1 ) { - flags |= ShaderFlags::CRC_TO_FLAG.value( sf, 0 ); - } - flags2 = ShaderFlags::SF2( (uint32_t)(flags >> 32) ); - } else { - flags2 = ShaderFlags::SF2( nif->get(iBlock, "Shader Flags 2") ); + if ( p && p->scene->hasOption(Scene::DoTexturing) && p->bind(0) ) { + glEnable( GL_TEXTURE_2D ); } } -/* - BSLightingShaderProperty -*/ -void BSLightingShaderProperty::updateImpl( const NifModel * nif, const QModelIndex & index ) +// BSLightingShaderProperty class + +bool BSLightingShaderProperty::isTranslucent() const { - BSShaderLightingProperty::updateImpl( nif, index ); + return alpha < 1.0 || hasRefraction; +} - if ( index == iBlock ) { - setMaterial(name.endsWith(".bgsm", Qt::CaseInsensitive) ? new ShaderMaterial(name, scene->game) : nullptr); - updateParams(nif); - } - else if ( index == iTextureSet ) { - updateParams(nif); - } +Material * BSLightingShaderProperty::createMaterial() +{ + if ( name.endsWith(".bgsm", Qt::CaseInsensitive) ) + return new ShaderMaterial( name, scene->getGame() ); + + return nullptr; } -void BSLightingShaderProperty::resetParams() +void BSLightingShaderProperty::resetData() { - BSShaderLightingProperty::resetParams(); + BSShaderProperty::resetData(); hasGlowMap = false; hasEmittance = false; @@ -1225,23 +1516,35 @@ void BSLightingShaderProperty::resetParams() backlightPower = 0.0; } -void BSLightingShaderProperty::updateParams( const NifModel * nif ) -{ - resetParams(); - - setFlags1( nif ); - setFlags2( nif ); +enum class Skyrim_ShaderType : uint32_t // BSLightingShaderType in nif.xml +{ + Default = 0, + EnvMap = 1, + Glow = 2, + HeightMap = 3, + FaceTint = 4, + SkinTint = 5, + HairTint = 6, + ParallaxOcclusion = 7, + MultiTextureLandscape = 8, + LODLandscape = 9, + Snow = 10, + MultiLayerParallax = 11, + TreeAnim = 12, + LODObjects = 13, + SnowSparkle = 14, + LODObjectsHD = 15, + EyeEnvMap = 16, + Cloud = 17, + LODLandscapeNoise = 18, + MultiTextureLandscapeLODBlend = 19, + Dismemberment = 20, // FO4 +}; - if ( nif->getBSVersion() >= 151 ) { - shaderType = ShaderFlags::ShaderType::ST_EnvironmentMap; - hasVertexAlpha = true; - hasVertexColors = true; - } else { - shaderType = ShaderFlags::ShaderType( nif->get(iBlock, "Shader Type") ); - hasVertexAlpha = hasSF1(ShaderFlags::SLSF1_Vertex_Alpha); - hasVertexColors = hasSF2(ShaderFlags::SLSF2_Vertex_Colors); - } - isVertexAlphaAnimation = hasSF2(ShaderFlags::SLSF2_Tree_Anim); +void BSLightingShaderProperty::updateData() +{ + NewShaderFlags flags; + readNewShaderFlags( flags, this, false ); ShaderMaterial * m = ( material && material->isValid() ) ? static_cast(material) : nullptr; if ( m ) { @@ -1258,20 +1561,19 @@ void BSLightingShaderProperty::updateParams( const NifModel * nif ) emissiveMult = m->fEmittanceMult; if ( m->bTileU && m->bTileV ) - clampMode = TexClampMode::WRAP_S_WRAP_T; + clampMode = TextureClampMode::WrapS_WrapT; else if ( m->bTileU ) - clampMode = TexClampMode::WRAP_S_CLAMP_T; + clampMode = TextureClampMode::WrapS_ClampT; else if ( m->bTileV ) - clampMode = TexClampMode::CLAMP_S_WRAP_T; + clampMode = TextureClampMode::ClampS_WrapT; else - clampMode = TexClampMode::CLAMP_S_CLAMP_T; + clampMode = TextureClampMode::ClampS_ClampT; fresnelPower = m->fFresnelPower; greyscaleColor = m->bGrayscaleToPaletteColor; paletteScale = m->fGrayscaleToPaletteScale; - hasSpecularMap = m->bSpecularEnabled && (!m->textureList[2].isEmpty() - || (nif->getBSVersion() >= 151 && !m->textureList[7].isEmpty())); + hasSpecularMap = m->bSpecularEnabled && ( !m->textureList[2].isEmpty() || (modelBSVersion() >= 151 && !m->textureList[7].isEmpty()) ); hasGlowMap = m->bGlowmap; hasEmittance = m->bEmitEnabled; hasBacklight = m->bBackLighting; @@ -1292,120 +1594,172 @@ void BSLightingShaderProperty::updateParams( const NifModel * nif ) } else { // m == nullptr - auto textures = nif->getArray(iTextureSet, "Textures"); + Skyrim_ShaderType shaderType; + if ( modelBSVersion() >= 151 ) { + shaderType = Skyrim_ShaderType::EnvMap; + } else { + shaderType = Skyrim_ShaderType::Default; + auto typeField = block["Shader Type"]; + if ( typeField.hasStrType("BSLightingShaderType") ) { // Skyrim or newer shader type + shaderType = Skyrim_ShaderType( typeField.value() ); + } else if ( typeField ) { + typeField.reportError( tr("Unsupported value type '%1'.").arg( typeField.strType() ) ); + } + } - isDoubleSided = hasSF2( ShaderFlags::SLSF2_Double_Sided ); - depthTest = hasSF1( ShaderFlags::SLSF1_ZBuffer_Test ); - depthWrite = hasSF2( ShaderFlags::SLSF2_ZBuffer_Write ); + auto texturesRoot = textureBlock["Textures"]; + auto hasTexture = [texturesRoot](int index) -> bool { + return !texturesRoot.child( index ).value().isEmpty(); + }; + + isDoubleSided = flags.doubleSided(); + depthTest = flags.depthTest(); + depthWrite = flags.depthWrite(); - alpha = nif->get( iBlock, "Alpha" ); + alpha = block["Alpha"].value(); - uvScale.set( nif->get(iBlock, "UV Scale") ); - uvOffset.set( nif->get(iBlock, "UV Offset") ); - clampMode = TexClampMode( nif->get( iBlock, "Texture Clamp Mode" ) ); + uvScale.set( block["UV Scale"].value() ); + uvOffset.set( block["UV Offset"].value() ); + clampMode = TextureClampMode( block["Texture Clamp Mode"].value() ); // Specular - if ( hasSF1( ShaderFlags::SLSF1_Specular ) ) { - specularColor = nif->get(iBlock, "Specular Color"); - specularGloss = nif->get( iBlock, "Glossiness" ); + if ( flags.specular() ) { + specularColor = block["Specular Color"].value(); + specularGloss = block.child("Glossiness").value(); if ( specularGloss == 0.0f ) // FO4 - specularGloss = nif->get( iBlock, "Smoothness" ); - specularStrength = nif->get(iBlock, "Specular Strength"); + specularGloss = block.child("Smoothness").value(); + specularStrength = block["Specular Strength"].value(); } // Emissive - emissiveColor = nif->get( iBlock, "Emissive Color" ); - emissiveMult = nif->get( iBlock, "Emissive Multiple" ); + emissiveColor = block["Emissive Color"].value(); + emissiveMult = block["Emissive Multiple"].value(); - hasEmittance = hasSF1( ShaderFlags::SLSF1_Own_Emit ); - hasGlowMap = isST(ShaderFlags::ST_GlowShader) && hasSF2( ShaderFlags::SLSF2_Glow_Map ) && !textures.value( 2, "" ).isEmpty(); + hasEmittance = flags.ownEmit(); + hasGlowMap = ( shaderType == Skyrim_ShaderType::Glow ) && flags.glowMap() && hasTexture(2); // Version Dependent settings - if ( nif->getBSVersion() < 130 ) { - lightingEffect1 = nif->get( iBlock, "Lighting Effect 1" ); - lightingEffect2 = nif->get( iBlock, "Lighting Effect 2" ); - - innerThickness = nif->get( iBlock, "Parallax Inner Layer Thickness" ); - outerRefractionStrength = nif->get( iBlock, "Parallax Refraction Scale" ); - outerReflectionStrength = nif->get( iBlock, "Parallax Envmap Strength" ); - innerTextureScale.set( nif->get(iBlock, "Parallax Inner Layer Texture Scale") ); - - hasSpecularMap = hasSF1( ShaderFlags::SLSF1_Specular ) && !textures.value( 7, "" ).isEmpty(); - hasHeightMap = isST( ShaderFlags::ST_Heightmap ) && hasSF1( ShaderFlags::SLSF1_Parallax ) && !textures.value( 3, "" ).isEmpty(); - hasBacklight = hasSF2( ShaderFlags::SLSF2_Back_Lighting ); - hasRimlight = hasSF2( ShaderFlags::SLSF2_Rim_Lighting ); - hasSoftlight = hasSF2( ShaderFlags::SLSF2_Soft_Lighting ); - hasMultiLayerParallax = hasSF2( ShaderFlags::SLSF2_Multi_Layer_Parallax ); - hasRefraction = hasSF1( ShaderFlags::SLSF1_Refraction ); - - hasTintMask = isST( ShaderFlags::ST_FaceTint ); + if ( modelBSVersion() < 130 ) { + lightingEffect1 = block["Lighting Effect 1"].value(); + lightingEffect2 = block["Lighting Effect 2"].value(); + + innerThickness = block.child("Parallax Inner Layer Thickness").value(); + outerRefractionStrength = block.child("Parallax Refraction Scale").value(); + outerReflectionStrength = block.child("Parallax Envmap Strength").value(); + innerTextureScale.set( block.child("Parallax Inner Layer Texture Scale").value() ); + + hasSpecularMap = flags.specular() && hasTexture(7); + hasHeightMap = (shaderType == Skyrim_ShaderType::HeightMap) && flags.skyrimParallax() && hasTexture(3); + hasBacklight = flags.skyrimBackLighting(); + hasRimlight = flags.skyrimRimLighting(); + hasSoftlight = flags.skyrimSoftLighting(); + hasMultiLayerParallax = flags.skyrimMultiLayerParalax(); + hasRefraction = flags.refraction(); + + hasTintMask = (shaderType == Skyrim_ShaderType::FaceTint); hasDetailMask = hasTintMask; - if ( isST( ShaderFlags::ST_HairTint ) ) - setTintColor( nif, "Hair Tint Color" ); - else if ( isST( ShaderFlags::ST_SkinTint ) ) - setTintColor( nif, "Skin Tint Color" ); + if ( shaderType == Skyrim_ShaderType::HairTint ) { + hasTintColor = true; + tintColor = block["Hair Tint Color"].value(); + } else if ( shaderType == Skyrim_ShaderType::SkinTint ) { + hasTintColor = true; + tintColor = block["Skin Tint Color"].value(); + } } else { - hasSpecularMap = hasSF1( ShaderFlags::SLSF1_Specular ); - greyscaleColor = hasSF1( ShaderFlags::SLSF1_Greyscale_To_PaletteColor ); - paletteScale = nif->get( iBlock, "Grayscale to Palette Scale" ); - lightingEffect1 = nif->get( iBlock, "Subsurface Rolloff" ); - backlightPower = nif->get( iBlock, "Backlight Power" ); - fresnelPower = nif->get( iBlock, "Fresnel Power" ); + hasSpecularMap = flags.specular(); + greyscaleColor = flags.greyscaleToPaletteColor(); + paletteScale = block["Grayscale to Palette Scale"].value(); + lightingEffect1 = block.child("Subsurface Rolloff").value(); + backlightPower = block.child("Backlight Power").value(); + fresnelPower = block["Fresnel Power"].value(); } // Environment Map, Mask and Reflection Scale hasEnvironmentMap = - ( isST(ShaderFlags::ST_EnvironmentMap) && hasSF1(ShaderFlags::SLSF1_Environment_Mapping) ) - || ( isST(ShaderFlags::ST_EyeEnvmap) && hasSF1(ShaderFlags::SLSF1_Eye_Environment_Mapping) ) - || ( nif->getBSVersion() == 100 && hasMultiLayerParallax ); + ( shaderType == Skyrim_ShaderType::EnvMap && flags.envMap() ) + || ( shaderType == Skyrim_ShaderType::EyeEnvMap && flags.eyeEnvMap() ) + || ( modelBSVersion() == 100 && hasMultiLayerParallax ); - useEnvironmentMask = hasEnvironmentMap && !textures.value( 5, "" ).isEmpty(); + useEnvironmentMask = hasEnvironmentMap && hasTexture(5); - if ( isST( ShaderFlags::ST_EnvironmentMap ) ) - environmentReflection = nif->get( iBlock, "Environment Map Scale" ); - else if ( isST( ShaderFlags::ST_EyeEnvmap ) ) - environmentReflection = nif->get( iBlock, "Eye Cubemap Scale" ); + if ( shaderType == Skyrim_ShaderType::EnvMap ) + environmentReflection = block.child("Environment Map Scale").value(); + else if ( shaderType == Skyrim_ShaderType::EyeEnvMap ) + environmentReflection = block["Eye Cubemap Scale"].value(); } -} -void BSLightingShaderProperty::setController( const NifModel * nif, const QModelIndex & iController ) -{ - auto contrName = nif->itemName(iController); - if ( contrName == "BSLightingShaderPropertyFloatController" ) { - Controller * ctrl = new LightingFloatController( this, iController ); - registerController(nif, ctrl); - } else if ( contrName == "BSLightingShaderPropertyColorController" ) { - Controller * ctrl = new LightingColorController( this, iController ); - registerController(nif, ctrl); + // Textures + if ( block.child("Root Material") ) { + if ( m ) { + constexpr int BGSM1_MAX = 9; + constexpr int BGSM20_MAX = 10; + + auto tex = m->textures(); + auto nMatTextures = tex.count(); + if ( nMatTextures >= BGSM1_MAX ) { + setTexturePath( 0, tex[0] ); // Diffuse + setTexturePath( 1, tex[1] ); // Normal + if ( m->bGlowmap ) { + if ( nMatTextures == BGSM1_MAX ) { + setTexturePath( 2, tex[5] ); // Glow + } else if ( nMatTextures == BGSM20_MAX ) { + setTexturePath( 2, tex[4] ); // Glow + } + } + if ( m->bGrayscaleToPaletteColor ) { + setTexturePath( 3, tex[3] ); // Greyscale + } + if ( m->bEnvironmentMapping ) { + if ( nMatTextures == BGSM1_MAX ) + setTexturePath( 4, tex[4] ); // Cubemap + setTexturePath( 5, tex[5] ); // Env Mask + } + if ( m->bSpecularEnabled ) { + setTexturePath( 7, tex[2] ); // Specular + if ( nMatTextures >= BGSM20_MAX ) { + setTexturePath( 8, tex[6] ); // Reflect + setTexturePath( 9, tex[7] ); // Lighting + } + } + } + } + + } else { + setTexturePathsFromTextureBlock(); } } -void BSLightingShaderProperty::setTintColor( const NifModel* nif, const QString & itemName ) +Controller * BSLightingShaderProperty::createController( NifFieldConst controllerBlock ) { - hasTintColor = true; - tintColor = nif->get(iBlock, itemName); + if ( controllerBlock.hasName("BSLightingShaderPropertyFloatController") ) + return new LightingFloatController( this, controllerBlock ); + + if ( controllerBlock.hasName("BSLightingShaderPropertyColorController") ) + return new LightingColorController( this, controllerBlock ); + + return nullptr; } -/* - BSEffectShaderProperty -*/ -void BSEffectShaderProperty::updateImpl( const NifModel * nif, const QModelIndex & index ) +// BSEffectShaderProperty class + +bool BSEffectShaderProperty::isTranslucent() const { - BSShaderLightingProperty::updateImpl( nif, index ); + return alpha() < 1.0; +} - if ( index == iBlock ) { - setMaterial(name.endsWith(".bgem", Qt::CaseInsensitive) ? new EffectMaterial(name, scene->game) : nullptr); - updateParams(nif); - } - else if ( index == iTextureSet ) - updateParams(nif); +Material * BSEffectShaderProperty::createMaterial() +{ + if ( name.endsWith(".bgem", Qt::CaseInsensitive) ) + return new EffectMaterial( name, scene->getGame() ); + + return nullptr; } -void BSEffectShaderProperty::resetParams() +void BSEffectShaderProperty::resetData() { - BSShaderLightingProperty::resetParams(); + BSShaderProperty::resetData(); hasSourceTexture = false; hasGreyscaleMap = false; @@ -1420,11 +1774,11 @@ void BSEffectShaderProperty::resetParams() hasWeaponBlood = false; - falloff.startAngle = 1.0f; - falloff.stopAngle = 0.0f; + falloff.startAngle = 1.0f; + falloff.stopAngle = 0.0f; falloff.startOpacity = 1.0f; - falloff.stopOpacity = 0.0f; - falloff.softDepth = 1.0f; + falloff.stopOpacity = 0.0f; + falloff.softDepth = 1.0f; lumEmittance = 0.0; @@ -1435,16 +1789,10 @@ void BSEffectShaderProperty::resetParams() environmentReflection = 0.0; } -void BSEffectShaderProperty::updateParams( const NifModel * nif ) +void BSEffectShaderProperty::updateData() { - resetParams(); - - setFlags1( nif ); - setFlags2( nif ); - - hasVertexAlpha = hasSF1( ShaderFlags::SLSF1_Vertex_Alpha ); - hasVertexColors = hasSF2( ShaderFlags::SLSF2_Vertex_Colors ); - isVertexAlphaAnimation = hasSF2(ShaderFlags::SLSF2_Tree_Anim); + NewShaderFlags flags; + readNewShaderFlags( flags, this, true ); EffectMaterial * m = ( material && material->isValid() ) ? static_cast(material) : nullptr; if ( m ) { @@ -1471,13 +1819,13 @@ void BSEffectShaderProperty::updateParams( const NifModel * nif ) uvOffset.set(m->fUOffset, m->fVOffset); if ( m->bTileU && m->bTileV ) - clampMode = TexClampMode::WRAP_S_WRAP_T; + clampMode = TextureClampMode::WrapS_WrapT; else if ( m->bTileU ) - clampMode = TexClampMode::WRAP_S_CLAMP_T; + clampMode = TextureClampMode::WrapS_ClampT; else if ( m->bTileV ) - clampMode = TexClampMode::CLAMP_S_WRAP_T; + clampMode = TextureClampMode::ClampS_WrapT; else - clampMode = TexClampMode::CLAMP_S_CLAMP_T; + clampMode = TextureClampMode::ClampS_ClampT; emissiveColor = Color4(m->cBaseColor, m->fAlpha); emissiveMult = m->fBaseColorScale; @@ -1485,79 +1833,102 @@ void BSEffectShaderProperty::updateParams( const NifModel * nif ) if ( m->bEffectLightingEnabled ) lightingInfluence = m->fLightingInfluence; - falloff.startAngle = m->fFalloffStartAngle; - falloff.stopAngle = m->fFalloffStopAngle; + falloff.startAngle = m->fFalloffStartAngle; + falloff.stopAngle = m->fFalloffStopAngle; falloff.startOpacity = m->fFalloffStartOpacity; - falloff.stopOpacity = m->fFalloffStopOpacity; - falloff.softDepth = m->fSoftDepth; + falloff.stopOpacity = m->fFalloffStopOpacity; + falloff.softDepth = m->fSoftDepth; } else { // m == nullptr - hasSourceTexture = !nif->get( iBlock, "Source Texture" ).isEmpty(); - hasGreyscaleMap = !nif->get( iBlock, "Greyscale Texture" ).isEmpty(); + hasSourceTexture = !block["Source Texture"].value().isEmpty(); + hasGreyscaleMap = !block["Greyscale Texture"].value().isEmpty(); - greyscaleAlpha = hasSF1( ShaderFlags::SLSF1_Greyscale_To_PaletteAlpha ); - greyscaleColor = hasSF1( ShaderFlags::SLSF1_Greyscale_To_PaletteColor ); - useFalloff = hasSF1( ShaderFlags::SLSF1_Use_Falloff ); + greyscaleAlpha = flags.greyscaleToPaletteAlpha(); + greyscaleColor = flags.greyscaleToPaletteColor(); + useFalloff = flags.useFalloff(); - depthTest = hasSF1( ShaderFlags::SLSF1_ZBuffer_Test ); - depthWrite = hasSF2( ShaderFlags::SLSF2_ZBuffer_Write ); - isDoubleSided = hasSF2( ShaderFlags::SLSF2_Double_Sided ); + depthTest = flags.depthTest(); + depthWrite = flags.depthWrite(); + isDoubleSided = flags.doubleSided(); - if ( nif->getBSVersion() < 130 ) { - hasWeaponBlood = hasSF2( ShaderFlags::SLSF2_Weapon_Blood ); + if ( modelBSVersion() < 130 ) { + hasWeaponBlood = flags.weaponBlood(); } else { - hasEnvironmentMap = !nif->get( iBlock, "Env Map Texture" ).isEmpty(); - hasEnvironmentMask = !nif->get(iBlock, "Env Mask Texture").isEmpty(); - hasNormalMap = !nif->get( iBlock, "Normal Texture" ).isEmpty(); + hasEnvironmentMap = !block["Env Map Texture"].value().isEmpty(); + hasEnvironmentMask = !block["Env Mask Texture"].value().isEmpty(); + hasNormalMap = !block["Normal Texture"].value().isEmpty(); - environmentReflection = nif->get( iBlock, "Environment Map Scale" ); + environmentReflection = block["Environment Map Scale"].value(); // Receive Shadows -> RGB Falloff for FO4 - hasRGBFalloff = hasSF1( ShaderFlags::SF1( 1 << 8 ) ); + hasRGBFalloff = flags.rgbFalloff(); } - uvScale.set( nif->get(iBlock, "UV Scale") ); - uvOffset.set( nif->get(iBlock, "UV Offset") ); - clampMode = TexClampMode( nif->get( iBlock, "Texture Clamp Mode" ) ); + uvScale.set( block["UV Scale"].value() ); + uvOffset.set( block["UV Offset"].value() ); + clampMode = TextureClampMode( block["Texture Clamp Mode"].value() ); + + emissiveColor = block["Base Color"].value(); + emissiveMult = block["Base Color Scale"].value(); - emissiveColor = nif->get(iBlock, "Base Color"); - emissiveMult = nif->get(iBlock, "Base Color Scale"); + if ( flags.effectLighting() ) + lightingInfluence = float( block["Lighting Influence"].value() ) / 255.0; - if ( hasSF2( ShaderFlags::SLSF2_Effect_Lighting ) ) - lightingInfluence = (float)nif->get( iBlock, "Lighting Influence" ) / 255.0; + falloff.startAngle = block["Falloff Start Angle"].value(); + falloff.stopAngle = block["Falloff Stop Angle"].value(); + falloff.startOpacity = block["Falloff Start Opacity"].value(); + falloff.stopOpacity = block["Falloff Stop Opacity"].value(); + falloff.softDepth = block["Soft Falloff Depth"].value(); + } - falloff.startAngle = nif->get( iBlock, "Falloff Start Angle" ); - falloff.stopAngle = nif->get( iBlock, "Falloff Stop Angle" ); - falloff.startOpacity = nif->get( iBlock, "Falloff Start Opacity" ); - falloff.stopOpacity = nif->get( iBlock, "Falloff Stop Opacity" ); - falloff.softDepth = nif->get( iBlock, "Soft Falloff Depth" ); + // Textures + if ( material ) { + if ( m ) + texturePaths = m->textures().toVector(); + } else { + setTexturePath( 0, block["Source Texture"] ); + setTexturePath( 1, block["Greyscale Texture"] ); + setTexturePath( 2, block.child("Env Map Texture") ); + setTexturePath( 3, block.child("Normal Texture") ); + setTexturePath( 4, block.child("Env Mask Texture") ); + setTexturePath( 6, block.child("Reflectance Texture") ); + setTexturePath( 7, block.child("Lighting Texture") ); } } -void BSEffectShaderProperty::setController( const NifModel * nif, const QModelIndex & iController ) +Controller * BSEffectShaderProperty::createController( NifFieldConst controllerBlock ) { - auto contrName = nif->itemName(iController); - if ( contrName == "BSEffectShaderPropertyFloatController" ) { - Controller * ctrl = new EffectFloatController( this, iController ); - registerController(nif, ctrl); - } else if ( contrName == "BSEffectShaderPropertyColorController" ) { - Controller * ctrl = new EffectColorController( this, iController ); - registerController(nif, ctrl); - } + if ( controllerBlock.hasName("BSEffectShaderPropertyFloatController") ) + return new EffectFloatController( this, controllerBlock ); + + if ( controllerBlock.hasName("BSEffectShaderPropertyColorController") ) + return new EffectColorController( this, controllerBlock ); + + return nullptr; } -/* - BSWaterShaderProperty -*/ -unsigned int BSWaterShaderProperty::getWaterShaderFlags() const +// SkyrimSimpleShaderProperty class + +bool SkyrimSimpleShaderProperty::isTranslucent() const { - return (unsigned int)waterShaderFlags; + return block.inherits("BSSkyShaderProperty"); } -void BSWaterShaderProperty::setWaterShaderFlags( unsigned int val ) +void SkyrimSimpleShaderProperty::updateData() { - waterShaderFlags = WaterShaderFlags::SF1( val ); -} + NewShaderFlags flags; + readNewShaderFlags( flags, this, false ); + depthTest = flags.depthTest(); + depthWrite = flags.depthWrite(); + isDoubleSided = flags.doubleSided(); + + uvScale.set( block["UV Scale"].value() ); + uvOffset.set( block["UV Offset"].value() ); + + if ( block.inherits("BSSkyShaderProperty") ) { + setTexturePath( 0, block["Source Texture"] ); + } +} diff --git a/src/gl/glproperty.h b/src/gl/glproperty.h index a0a5df242..cc088b05b 100644 --- a/src/gl/glproperty.h +++ b/src/gl/glproperty.h @@ -33,12 +33,11 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #ifndef GLPROPERTY_H #define GLPROPERTY_H -#include "icontrollable.h" // Inherited +#include "glcontrollable.h" // Inherited #include "data/niftypes.h" +#include "model/nifmodel.h" #include -#include -#include //! @file glproperty.h Property, PropertyList @@ -48,9 +47,8 @@ typedef int GLint; typedef unsigned int GLuint; typedef float GLfloat; - class Material; -class NifModel; + //! Controllable properties attached to nodes and meshes class Property : public IControllable @@ -59,7 +57,7 @@ class Property : public IControllable protected: //! Protected constructor; see IControllable() - Property( Scene * scene, const QModelIndex & index ) : IControllable( scene, index ) {} + Property( Scene * _scene, NifFieldConst _block ) : IControllable( _scene, _block ) {} int ref = 0; @@ -93,45 +91,44 @@ class Property : public IControllable //! Associate a Property subclass with a Property::Type #define REGISTER_PROPERTY( CLASSNAME, TYPENAME ) template <> inline Property::Type Property::_type() { return Property::TYPENAME; } + //! A list of [Properties](@ref Property) class PropertyList final { public: - PropertyList(); - PropertyList( const PropertyList & other ); + PropertyList() {} + PropertyList( const PropertyList & other ) { operator=( other ); } ~PropertyList(); - void add( Property * ); - void del( Property * ); - bool contains( Property * ) const; + void clear(); - Property * get( const QModelIndex & idx ) const; + PropertyList & operator=( const PropertyList & other ); - template T * get() const; - template bool contains() const; + void add( Property * prop ); + void del( Property * prop ); void validate(); - void clear(); + void merge( const PropertyList & list ); - PropertyList & operator=( const PropertyList & other ); + const QMultiHash & hash() const { return properties; } - QList list() const { return properties.values(); } + Property * get( const QModelIndex & iPropBlock ) const; - void merge( const PropertyList & list ); + template T * get() const; + template bool contains() const; -protected: +private: QMultiHash properties; + + void attach( Property * prop ); + void detach( Property * prop, int cnt ); }; template inline T * PropertyList::get() const { Property * p = properties.value( Property::_type() ); - - if ( p ) - return p->cast(); - - return nullptr; + return p ? p->cast() : nullptr; } template inline bool PropertyList::contains() const @@ -139,14 +136,29 @@ template inline bool PropertyList::contains() const return properties.contains( Property::_type() ); } +inline void PropertyList::attach( Property * prop ) +{ + ++prop->ref; + properties.insert( prop->type(), prop ); +} + +inline void PropertyList::detach( Property * prop, int cnt ) +{ + Q_ASSERT( cnt > 0 && prop->ref >= cnt ); + prop->ref -= cnt; + if ( prop->ref <= 0 ) + delete prop; +} + + //! A Property that specifies alpha blending class AlphaProperty final : public Property { public: - AlphaProperty( Scene * scene, const QModelIndex & index ) : Property( scene, index ) {} + AlphaProperty( Scene * scene, NifFieldConst block ) : Property( scene, block ) {} Type type() const override final { return Alpha; } - QString typeId() const override final { return "NiAlphaProperty"; } + QString typeId() const override final { return QStringLiteral("NiAlphaProperty"); } bool hasAlphaBlend() const { return alphaBlend; } bool hasAlphaTest() const { return alphaTest; } @@ -156,8 +168,8 @@ class AlphaProperty final : public Property friend void glProperty( AlphaProperty * ); protected: - void setController( const NifModel * nif, const QModelIndex & controller ) override final; - void updateImpl( const NifModel * nif, const QModelIndex & block ) override final; + Controller * createController( NifFieldConst controllerBlock ) override final; + void updateImpl( const NifModel * nif, const QModelIndex & index ) override final; bool alphaBlend = false, alphaTest = false, alphaSort = false; GLenum alphaSrc = 0, alphaDst = 0, alphaFunc = 0; @@ -165,14 +177,15 @@ class AlphaProperty final : public Property REGISTER_PROPERTY( AlphaProperty, Alpha ) + //! A Property that specifies depth testing class ZBufferProperty final : public Property { public: - ZBufferProperty( Scene * scene, const QModelIndex & index ) : Property( scene, index ) {} + ZBufferProperty( Scene * scene, NifFieldConst block ) : Property( scene, block ) {} Type type() const override final { return ZBuffer; } - QString typeId() const override final { return "NiZBufferProperty"; } + QString typeId() const override final { return QStringLiteral("NiZBufferProperty"); } bool test() const { return depthTest; } bool mask() const { return depthMask; } @@ -184,24 +197,22 @@ class ZBufferProperty final : public Property bool depthMask = false; GLenum depthFunc = 0; - void updateImpl( const NifModel * nif, const QModelIndex & block ) override final; + void updateImpl( const NifModel * nif, const QModelIndex & index ) override final; }; REGISTER_PROPERTY( ZBufferProperty, ZBuffer ) -//! Number of textures; base + dark + detail + gloss + glow + bump + 4 decals -#define numTextures 10 //! A Property that specifies (multi-)texturing class TexturingProperty final : public Property { - friend class TexFlipController; - friend class TexTransController; + friend class TextureFlipInterpolator_Texturing; + friend class TextureTransformInterpolator; //! The properties of each texture slot struct TexDesc { - QPersistentModelIndex iSource; + NifFieldConst sourceBlock; GLenum filter = 0; GLint wrapS = 0, wrapT = 0; int coordset = 0; @@ -216,10 +227,13 @@ class TexturingProperty final : public Property }; public: - TexturingProperty( Scene * scene, const QModelIndex & index ) : Property( scene, index ) {} + //! Number of textures; base + dark + detail + gloss + glow + bump + 4 decals + static constexpr int NUM_TEXTURES = 10; + + TexturingProperty( Scene * scene, NifFieldConst block ) : Property( scene, block ) {} Type type() const override final { return Texturing; } - QString typeId() const override final { return "NiTexturingProperty"; } + QString typeId() const override final { return QStringLiteral("NiTexturingProperty"); } friend void glProperty( TexturingProperty * ); @@ -234,24 +248,25 @@ class TexturingProperty final : public Property static int getId( const QString & id ); protected: - TexDesc textures[numTextures]; + TexDesc textures[NUM_TEXTURES]; - void setController( const NifModel * nif, const QModelIndex & controller ) override final; - void updateImpl( const NifModel * nif, const QModelIndex & block ) override final; + Controller * createController( NifFieldConst controllerBlock ) override final; + void updateImpl( const NifModel * nif, const QModelIndex & index ) override final; }; REGISTER_PROPERTY( TexturingProperty, Texturing ) + //! A Property that specifies a texture class TextureProperty final : public Property { - friend class TexFlipController; + friend class TextureFlipInterpolator_Texture; public: - TextureProperty( Scene * scene, const QModelIndex & index ) : Property( scene, index ) {} + TextureProperty( Scene * scene, NifFieldConst block ) : Property( scene, block ) {} Type type() const override final { return Texture; } - QString typeId() const override final { return "NiTextureProperty"; } + QString typeId() const override final { return QStringLiteral("NiTextureProperty"); } friend void glProperty( TextureProperty * ); @@ -261,25 +276,26 @@ class TextureProperty final : public Property QString fileName() const; protected: - QPersistentModelIndex iImage; + NifFieldConst imageBlock; - void setController( const NifModel * nif, const QModelIndex & controller ) override final; - void updateImpl( const NifModel * nif, const QModelIndex & block ) override final; + Controller * createController( NifFieldConst controllerBlock ) override final; + void updateImpl( const NifModel * nif, const QModelIndex & index ) override final; }; REGISTER_PROPERTY( TextureProperty, Texture ) + //! A Property that specifies a material class MaterialProperty final : public Property { - friend class AlphaController; - friend class MaterialColorController; + friend class AlphaInterpolator_Material; + friend class MaterialColorInterpolator; public: - MaterialProperty( Scene * scene, const QModelIndex & index ) : Property( scene, index ) {} + MaterialProperty( Scene * scene, NifFieldConst block ) : Property( scene, block ) {} Type type() const override final { return MaterialProp; } - QString typeId() const override final { return "NiMaterialProperty"; } + QString typeId() const override final { return QStringLiteral("NiMaterialProperty"); } friend void glProperty( class MaterialProperty *, class SpecularProperty * ); @@ -289,20 +305,21 @@ class MaterialProperty final : public Property Color4 ambient, diffuse, specular, emissive; GLfloat shininess = 0, alpha = 0; - void setController( const NifModel * nif, const QModelIndex & controller ) override final; - void updateImpl( const NifModel * nif, const QModelIndex & block ) override final; + Controller * createController( NifFieldConst controllerBlock ) override final; + void updateImpl( const NifModel * nif, const QModelIndex & index ) override final; }; REGISTER_PROPERTY( MaterialProperty, MaterialProp ) + //! A Property that specifies specularity class SpecularProperty final : public Property { public: - SpecularProperty( Scene * scene, const QModelIndex & index ) : Property( scene, index ) {} + SpecularProperty( Scene * scene, NifFieldConst block ) : Property( scene, block ) {} Type type() const override final { return Specular; } - QString typeId() const override final { return "NiSpecularProperty"; } + QString typeId() const override final { return QStringLiteral("NiSpecularProperty"); } friend void glProperty( class MaterialProperty *, class SpecularProperty * ); @@ -314,14 +331,15 @@ class SpecularProperty final : public Property REGISTER_PROPERTY( SpecularProperty, Specular ) + //! A Property that specifies wireframe drawing class WireframeProperty final : public Property { public: - WireframeProperty( Scene * scene, const QModelIndex & index ) : Property( scene, index ) {} + WireframeProperty( Scene * scene, NifFieldConst block ) : Property( scene, block ) {} Type type() const override final { return Wireframe; } - QString typeId() const override final { return "NiWireframeProperty"; } + QString typeId() const override final { return QStringLiteral("NiWireframeProperty"); } friend void glProperty( WireframeProperty * ); @@ -333,14 +351,15 @@ class WireframeProperty final : public Property REGISTER_PROPERTY( WireframeProperty, Wireframe ) + //! A Property that specifies vertex color handling class VertexColorProperty final : public Property { public: - VertexColorProperty( Scene * scene, const QModelIndex & index ) : Property( scene, index ) {} + VertexColorProperty( Scene * scene, NifFieldConst block ) : Property( scene, block ) {} Type type() const override final { return VertexColor; } - QString typeId() const override final { return "NiVertexColorProperty"; } + QString typeId() const override final { return QStringLiteral("NiVertexColorProperty"); } friend void glProperty( VertexColorProperty *, bool vertexcolors ); @@ -353,6 +372,7 @@ class VertexColorProperty final : public Property REGISTER_PROPERTY( VertexColorProperty, VertexColor ) + namespace Stencil { enum TestFunc @@ -393,10 +413,10 @@ namespace Stencil class StencilProperty final : public Property { public: - StencilProperty( Scene * scene, const QModelIndex & index ) : Property( scene, index ) {} + StencilProperty( Scene * scene, NifFieldConst block ) : Property( scene, block ) {} Type type() const override final { return Stencil; } - QString typeId() const override final { return "NiStencilProperty"; } + QString typeId() const override final { return QStringLiteral("NiStencilProperty"); } friend void glProperty( StencilProperty * ); @@ -435,195 +455,15 @@ class StencilProperty final : public Property REGISTER_PROPERTY( StencilProperty, Stencil ) -namespace ShaderFlags +enum class TextureClampMode : uint32_t { - enum SF1 : unsigned int - { - SLSF1_Specular = 1, // 0! - SLSF1_Skinned = 1 << 1, // 1 - SLSF1_Temp_Refraction = 1 << 2, // 2 - SLSF1_Vertex_Alpha = 1 << 3, // 3 - SLSF1_Greyscale_To_PaletteColor = 1 << 4, // 4 - SLSF1_Greyscale_To_PaletteAlpha = 1 << 5, // 5 - SLSF1_Use_Falloff = 1 << 6, // 6 - SLSF1_Environment_Mapping = 1 << 7, // 7 - SLSF1_Recieve_Shadows = 1 << 8, // 8 - SLSF1_Cast_Shadows = 1 << 9, // 9 - SLSF1_Facegen_Detail_Map = 1 << 10, // 10 - SLSF1_Parallax = 1 << 11, // 11 - SLSF1_Model_Space_Normals = 1 << 12, // 12 - SLSF1_Non_Projective_Shadows = 1 << 13, // 13 - SLSF1_Landscape = 1 << 14, // 14 - SLSF1_Refraction = 1 << 15, // 15! - SLSF1_Fire_Refraction = 1 << 16, // 16 - SLSF1_Eye_Environment_Mapping = 1 << 17, // 17 - SLSF1_Hair_Soft_Lighting = 1 << 18, // 18 - SLSF1_Screendoor_Alpha_Fade = 1 << 19, // 19 - SLSF1_Localmap_Hide_Secret = 1 << 20, // 20 - SLSF1_FaceGen_RGB_Tint = 1 << 21, // 21 - SLSF1_Own_Emit = 1 << 22, // 22 - SLSF1_Projected_UV = 1 << 23, // 23 - SLSF1_Multiple_Textures = 1 << 24, // 24 - SLSF1_Remappable_Textures = 1 << 25, // 25 - SLSF1_Decal = 1 << 26, // 26 - SLSF1_Dynamic_Decal = 1 << 27, // 27 - SLSF1_Parallax_Occlusion = 1 << 28, // 28 - SLSF1_External_Emittance = 1 << 29, // 29 - SLSF1_Soft_Effect = 1 << 30, // 30 - SLSF1_ZBuffer_Test = (unsigned int)(1 << 31), // 31 - }; - - enum SF2 : unsigned int - { - SLSF2_ZBuffer_Write = 1, // 0! - SLSF2_LOD_Landscape = 1 << 1, // 1 - SLSF2_LOD_Objects = 1 << 2, // 2 - SLSF2_No_Fade = 1 << 3, // 3 - SLSF2_Double_Sided = 1 << 4, // 4 - SLSF2_Vertex_Colors = 1 << 5, // 5 - SLSF2_Glow_Map = 1 << 6, // 6 - SLSF2_Assume_Shadowmask = 1 << 7, // 7 - SLSF2_Packed_Tangent = 1 << 8, // 8 - SLSF2_Multi_Index_Snow = 1 << 9, // 9 - SLSF2_Vertex_Lighting = 1 << 10, // 10 - SLSF2_Uniform_Scale = 1 << 11, // 11 - SLSF2_Fit_Slope = 1 << 12, // 12 - SLSF2_Billboard = 1 << 13, // 13 - SLSF2_No_LOD_Land_Blend = 1 << 14, // 14 - SLSF2_EnvMap_Light_Fade = 1 << 15, // 15! - SLSF2_Wireframe = 1 << 16, // 16 - SLSF2_Weapon_Blood = 1 << 17, // 17 - SLSF2_Hide_On_Local_Map = 1 << 18, // 18 - SLSF2_Premult_Alpha = 1 << 19, // 19 - SLSF2_Cloud_LOD = 1 << 20, // 20 - SLSF2_Anisotropic_Lighting = 1 << 21, // 21 - SLSF2_No_Transparency_Multisampling = 1 << 22, // 22 - SLSF2_Unused01 = 1 << 23, // 23 - SLSF2_Multi_Layer_Parallax = 1 << 24, // 24 - SLSF2_Soft_Lighting = 1 << 25, // 25 - SLSF2_Rim_Lighting = 1 << 26, // 26 - SLSF2_Back_Lighting = 1 << 27, // 27 - SLSF2_Unused02 = 1 << 28, // 28 - SLSF2_Tree_Anim = 1 << 29, // 29 - SLSF2_Effect_Lighting = 1 << 30, // 30 - SLSF2_HD_LOD_Objects = (unsigned int)(1 << 31), // 31 - }; - - enum ShaderType : unsigned int - { - ST_Default, - ST_EnvironmentMap, - ST_GlowShader, - ST_Heightmap, - ST_FaceTint, - ST_SkinTint, - ST_HairTint, - ST_ParallaxOccMaterial, - ST_WorldMultitexture, - ST_WorldMap1, - ST_Unknown10, - ST_MultiLayerParallax, - ST_Unknown12, - ST_WorldMap2, - ST_SparkleSnow, - ST_WorldMap3, - ST_EyeEnvmap, - ST_Unknown17, - ST_WorldMap4, - ST_WorldLODMultitexture - }; - - enum BSShaderCRC32 : unsigned int - { - // Lighting + Effect - // SF1 - ZBUFFER_TEST = 1740048692, - SKINNED = 3744563888, - ENVMAP = 2893749418, - VERTEX_ALPHA = 2333069810, - GRAYSCALE_TO_PALETTE_COLOR = 442246519, - DECAL = 3849131744, - DYNAMIC_DECAL = 1576614759, - HAIRTINT = 1264105798, - SKIN_TINT = 1483897208, - EMIT_ENABLED = 2262553490, - REFRACTION = 1957349758, - REFRACTION_FALLOFF = 902349195, - RGB_FALLOFF = 3448946507, - EXTERNAL_EMITTANCE = 2150459555, - // SF2 - ZBUFFER_WRITE = 3166356979, - GLOWMAP = 2399422528, - TWO_SIDED = 759557230, - VERTEXCOLORS = 348504749, - NOFADE = 2994043788, - LOD_OBJECTS = 2896726515, - // Lighting only - PBR = 731263983, - FACE = 314919375, - CAST_SHADOWS = 1563274220, - MODELSPACENORMALS = 2548465567, - TRANSFORM_CHANGED = 3196772338, - INVERTED_FADE_PATTERN = 3030867718, - // Effect only - EFFECT_LIGHTING = 3473438218, - FALLOFF = 3980660124, - SOFT_EFFECT = 3503164976, - GRAYSCALE_TO_PALETTE_ALPHA = 2901038324, - WEAPON_BLOOD = 2078326675, - NO_EXPOSURE = 3707406987 - }; - - static QMap CRC_TO_FLAG = { - // SF1 - {CAST_SHADOWS, SLSF1_Cast_Shadows}, - {ZBUFFER_TEST, SLSF1_ZBuffer_Test}, - {SKINNED, SLSF1_Skinned}, - {ENVMAP, SLSF1_Environment_Mapping}, - {VERTEX_ALPHA, SLSF1_Vertex_Alpha}, - {FACE, SLSF1_Facegen_Detail_Map}, - {GRAYSCALE_TO_PALETTE_COLOR, SLSF1_Greyscale_To_PaletteColor}, - {GRAYSCALE_TO_PALETTE_ALPHA, SLSF1_Greyscale_To_PaletteAlpha}, - {DECAL, SLSF1_Decal}, - {DYNAMIC_DECAL, SLSF1_Dynamic_Decal}, - {EMIT_ENABLED, SLSF1_Own_Emit}, - {REFRACTION, SLSF1_Refraction}, - {SKIN_TINT, SLSF1_FaceGen_RGB_Tint}, - {RGB_FALLOFF, SLSF1_Recieve_Shadows}, - {EXTERNAL_EMITTANCE, SLSF1_External_Emittance}, - {MODELSPACENORMALS, SLSF1_Model_Space_Normals}, - {FALLOFF, SLSF1_Use_Falloff}, - {SOFT_EFFECT, SLSF1_Soft_Effect}, - // SF2 - {ZBUFFER_WRITE, (uint64_t)SLSF2_ZBuffer_Write << 32}, - {GLOWMAP, (uint64_t)SLSF2_Glow_Map << 32}, - {TWO_SIDED, (uint64_t)SLSF2_Double_Sided << 32}, - {VERTEXCOLORS, (uint64_t)SLSF2_Vertex_Colors << 32}, - {NOFADE, (uint64_t)SLSF2_No_Fade << 32}, - {WEAPON_BLOOD, (uint64_t)SLSF2_Weapon_Blood << 32}, - {TRANSFORM_CHANGED, (uint64_t)SLSF2_Assume_Shadowmask << 32}, - {EFFECT_LIGHTING, (uint64_t)SLSF2_Effect_Lighting << 32}, - {LOD_OBJECTS, (uint64_t)SLSF2_LOD_Objects << 32}, - - // TODO - {PBR, 0}, - {REFRACTION_FALLOFF, 0}, - {INVERTED_FADE_PATTERN, 0}, - {HAIRTINT, 0}, - {NO_EXPOSURE, 0}, - }; -} - -enum TexClampMode : unsigned int -{ - CLAMP_S_CLAMP_T = 0, - CLAMP_S_WRAP_T = 1, - WRAP_S_CLAMP_T = 2, - WRAP_S_WRAP_T = 3, - MIRRORED_S_MIRRORED_T = 4 + ClampS_ClampT = 0, + ClampS_WrapT = 1, + WrapS_ClampT = 2, + WrapS_WrapT = 3, + MirrorS_MirrorT = 4 }; - struct UVScale { float x; @@ -632,7 +472,7 @@ struct UVScale UVScale() { reset(); } void reset() { x = y = 1.0f; } void set( float _x, float _y ) { x = _x; y = _y; } - void set( const Vector2 & v) { x = v[0]; y = v[1]; } + void set( const Vector2 & v ) { x = v[0]; y = v[1]; } }; struct UVOffset @@ -642,48 +482,48 @@ struct UVOffset UVOffset() { reset(); } void reset() { x = y = 0.0f; } - void set(float _x, float _y) { x = _x; y = _y; } - void set(const Vector2 & v) { x = v[0]; y = v[1]; } + void set( float _x, float _y ) { x = _x; y = _y; } + void set( const Vector2 & v ) { x = v[0]; y = v[1]; } +}; + +enum class ShaderColorMode +{ + No, + Yes, + FromData // Always matches the vertex color flag in the parent shape }; -//! A Property that specifies shader lighting (Bethesda-specific) -class BSShaderLightingProperty : public Property +//! A base Property for BSShaderProperty and its descendants, with support for simple FO3 shaders +class BSShaderProperty : public Property { + friend void glProperty( BSShaderProperty * ); + public: - BSShaderLightingProperty( Scene * scene, const QModelIndex & index ) : Property( scene, index ) { } - ~BSShaderLightingProperty(); + BSShaderProperty( Scene * scene, NifFieldConst block ) : Property( scene, block ) { } + ~BSShaderProperty(); Type type() const override final { return ShaderLighting; } - QString typeId() const override { return "BSShaderLightingProperty"; } - - friend void glProperty( BSShaderLightingProperty * ); + QString typeId() const override { return QStringLiteral("BSShaderProperty"); } void clear() override; - bool bind( int id, const QString & fname = QString(), TexClampMode mode = TexClampMode::WRAP_S_WRAP_T ); - bool bind( int id, const QVector > & texcoords ); - - bool bindCube( int id, const QString & fname = QString() ); - //! Checks if the params of the shader depend on data from block - bool isParamBlock( const QModelIndex & block ) { return ( block == iBlock || block == iTextureSet ); } + bool isParamBlock( const QModelIndex & block ) const { return ( block == iBlock || block == iTextureSet ); } - QString fileName( int id ) const; - //int coordSet( int id ) const; + QString fileName( int id ) const { return texturePaths.value(id); } - static int getId( const QString & id ); + virtual bool isTranslucent() const; - //! Has Shader Flag 1 - bool hasSF1( ShaderFlags::SF1 flag ) { return flags1 & flag; }; - //! Has Shader Flag 2 - bool hasSF2( ShaderFlags::SF2 flag ) { return flags2 & flag; }; + bool bind( int id, const QString & fname, TextureClampMode mode ); + bool bind( int id ) { return bind( id, QString(), clampMode ); } + bool bind( int id, const QVector > & texcoords ); + + bool bindCube( const QString & fname = QString() ); - void setFlags1( const NifModel * nif ); - void setFlags2( const NifModel * nif ); - bool hasVertexColors = false; - bool hasVertexAlpha = false; // TODO Gavrant: it's unused + ShaderColorMode vertexColorMode = ShaderColorMode::FromData; + bool hasVertexAlpha = false; bool depthTest = false; bool depthWrite = false; @@ -692,38 +532,40 @@ class BSShaderLightingProperty : public Property UVScale uvScale; UVOffset uvOffset; - TexClampMode clampMode = CLAMP_S_CLAMP_T; + TextureClampMode clampMode = TextureClampMode::WrapS_WrapT; Material * getMaterial() const { return material; } protected: - ShaderFlags::SF1 flags1 = ShaderFlags::SLSF1_ZBuffer_Test; - ShaderFlags::SF2 flags2 = ShaderFlags::SLSF2_ZBuffer_Write; - - //QVector textures; - QPersistentModelIndex iTextureSet; - QPersistentModelIndex iWetMaterial; + QPersistentModelIndex iTextureSet; // TODO: Remove + NifFieldConst textureBlock; Material * material = nullptr; void setMaterial( Material * newMaterial ); + virtual Material * createMaterial(); - void updateImpl( const NifModel * nif, const QModelIndex & block ) override; - virtual void resetParams(); + QVector texturePaths; + void setTexturePath( int id, const QString & texPath ); + void setTexturePath( int id, NifFieldConst pathField ) { setTexturePath( id, pathField.value() ); } + void setTexturePathsFromTextureBlock(); + + void updateImpl( const NifModel * nif, const QModelIndex & index ) override final; + virtual void resetData(); + virtual void updateData(); }; -REGISTER_PROPERTY( BSShaderLightingProperty, ShaderLighting ) +REGISTER_PROPERTY( BSShaderProperty, ShaderLighting ) -//! A Property that inherits BSLightingShaderProperty (Skyrim-specific) -class BSLightingShaderProperty final : public BSShaderLightingProperty +//! A Property that inherits BSLightingShaderProperty (Skyrim and newer) +class BSLightingShaderProperty final : public BSShaderProperty { public: - BSLightingShaderProperty( Scene * scene, const QModelIndex & index ) : BSShaderLightingProperty( scene, index ) { } + BSLightingShaderProperty( Scene * scene, NifFieldConst block ) : BSShaderProperty( scene, block ) { } - QString typeId() const override final { return "BSLightingShaderProperty"; } + QString typeId() const override final { return QStringLiteral("BSLightingShaderProperty"); } - //! Is Shader Type - bool isST( ShaderFlags::ShaderType st ) { return shaderType == st; }; + bool isTranslucent() const override final; bool hasGlowMap = false; bool hasEmittance = false; @@ -769,27 +611,26 @@ class BSLightingShaderProperty final : public BSShaderLightingProperty float backlightPower = 0.0; protected: - void setController( const NifModel * nif, const QModelIndex & controller ) override final; - void updateImpl( const NifModel * nif, const QModelIndex & block ) override; - void resetParams() override; - void updateParams( const NifModel * nif ); - void setTintColor( const NifModel* nif, const QString & itemName ); - - ShaderFlags::ShaderType shaderType = ShaderFlags::ST_Default; + Controller * createController( NifFieldConst controllerBlock ) override final; + Material * createMaterial() override final; + void resetData() override final; + void updateData() override final; }; REGISTER_PROPERTY( BSLightingShaderProperty, ShaderLighting ) -//! A Property that inherits BSEffectShaderProperty (Skyrim-specific) -class BSEffectShaderProperty final : public BSShaderLightingProperty +//! A Property that inherits BSEffectShaderProperty (Skyrim and newer) +class BSEffectShaderProperty final : public BSShaderProperty { public: - BSEffectShaderProperty( Scene * scene, const QModelIndex & index ) : BSShaderLightingProperty( scene, index ) { } + BSEffectShaderProperty( Scene * scene, NifFieldConst block ) : BSShaderProperty( scene, block ) { } + + QString typeId() const override final { return QStringLiteral("BSEffectShaderProperty"); } - QString typeId() const override final { return "BSEffectShaderProperty"; } + float alpha() const { return emissiveColor.alpha(); } - float getAlpha() const { return emissiveColor.alpha(); } + bool isTranslucent() const override final; bool hasSourceTexture = false; bool hasGreyscaleMap = false; @@ -825,47 +666,29 @@ class BSEffectShaderProperty final : public BSShaderLightingProperty float environmentReflection = 0.0; protected: - void setController( const NifModel * nif, const QModelIndex & controller ) override final; - void updateImpl( const NifModel * nif, const QModelIndex & block ) override; - void resetParams() override; - void updateParams( const NifModel * nif ); + Controller * createController( NifFieldConst controllerBlock ) override final; + Material * createMaterial() override final; + void resetData() override final; + void updateData() override final; }; REGISTER_PROPERTY( BSEffectShaderProperty, ShaderLighting ) -namespace WaterShaderFlags -{ - enum SF1 : unsigned int - { - SWSF1_UNKNOWN0 = 1, - SWSF1_Bypass_Refraction_Map = 1 << 1, - SWSF1_Water_Toggle = 1 << 2, - SWSF1_UNKNOWN3 = 1 << 3, - SWSF1_UNKNOWN4 = 1 << 4, - SWSF1_UNKNOWN5 = 1 << 5, - SWSF1_Highlight_Layer_Toggle = 1 << 6, - SWSF1_Enabled = 1 << 7 - }; -} - -//! A Property that inherits BSWaterShaderProperty (Skyrim-specific) -class BSWaterShaderProperty final : public BSShaderLightingProperty +//! A Property for simple Skyrim (and newer) shaders +class SkyrimSimpleShaderProperty final : public BSShaderProperty { public: - BSWaterShaderProperty( Scene * scene, const QModelIndex & index ) : BSShaderLightingProperty( scene, index ) { } + SkyrimSimpleShaderProperty( Scene * scene, NifFieldConst block ) : BSShaderProperty( scene, block ) { } - QString typeId() const override final { return "BSWaterShaderProperty"; } + QString typeId() const override final { return QStringLiteral("SkyrimSimpleShaderProperty"); } - unsigned int getWaterShaderFlags() const; - - void setWaterShaderFlags( unsigned int ); + bool isTranslucent() const override final; protected: - WaterShaderFlags::SF1 waterShaderFlags = WaterShaderFlags::SF1(0); + void updateData() override final; }; -REGISTER_PROPERTY( BSWaterShaderProperty, ShaderLighting ) - +REGISTER_PROPERTY( SkyrimSimpleShaderProperty, ShaderLighting ) #endif diff --git a/src/gl/glscene.cpp b/src/gl/glscene.cpp index 2e8724a64..293cca9dc 100644 --- a/src/gl/glscene.cpp +++ b/src/gl/glscene.cpp @@ -53,6 +53,8 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Scene::Scene( TexCache * texcache, QOpenGLContext * context, QOpenGLFunctions * functions, QObject * parent ) : QObject( parent ) { + setGame( Game::OTHER ); + renderer = new Renderer( context, functions ); currentBlock = currentIndex = QModelIndex(); @@ -65,8 +67,6 @@ Scene::Scene( TexCache * texcache, QOpenGLContext * context, QOpenGLFunctions * options = ( DoLighting | DoTexturing | DoMultisampling | DoBlending | DoVertexColors | DoSpecular | DoGlow | DoCubeMapping ); - lodLevel = Level0; - visMode = VisNone; selMode = SelObject; @@ -103,6 +103,12 @@ Scene::~Scene() delete renderer; } +void Scene::setGame( Game::GameMode newGame ) +{ + game = newGame; + lodLevel = defaultLodLevel(); +} + void Scene::updateShaders() { renderer->updateShaders(); @@ -124,7 +130,7 @@ void Scene::clear( bool flushTextures ) sceneBoundsValid = timeBoundsValid = false; - game = Game::OTHER; + setGame( Game::OTHER ); } void Scene::update( const NifModel * nif, const QModelIndex & index ) @@ -137,7 +143,7 @@ void Scene::update( const NifModel * nif, const QModelIndex & index ) if ( !block.isValid() ) return; - for ( Property * prop : properties.list() ) + for ( Property * prop : properties.hash() ) prop->update( nif, block ); for ( Node * node : nodes.list() ) @@ -146,11 +152,11 @@ void Scene::update( const NifModel * nif, const QModelIndex & index ) properties.validate(); nodes.validate(); - for ( Property * p : properties.list() ) - p->update( nif, p->index() ); + for ( Property * p : properties.hash() ) + p->update(); for ( Node * n : nodes.list() ) - n->update( nif, n->index() ); + n->update(); roots.clear(); for ( const auto link : nif->getRootLinks() ) { @@ -158,7 +164,7 @@ void Scene::update( const NifModel * nif, const QModelIndex & index ) if ( iBlock.isValid() ) { Node * node = getNode( nif, iBlock ); if ( node ) { - node->makeParent( 0 ); + node->makeParent( nullptr ); roots.add( node ); } } @@ -197,11 +203,21 @@ void Scene::updateSelectMode( QAction * action ) emit sceneUpdated(); } -void Scene::updateLodLevel( int level ) +int Scene::maxLodLevel() const +{ + return ( game == Game::STARFIELD ) ? MAX_LOD_LEVEL_STARFIELD : MAX_LOD_LEVEL_DEFAULT; +} + +int Scene::defaultLodLevel() const +{ + return ( game == Game::STARFIELD ) ? 0 : 2; +} + +void Scene::updateLodLevel( int newLevel ) { - if ( game != Game::STARFIELD ) - level = std::max(level, 2); - lodLevel = LodLevel( level ); + if ( newLevel < 0 || newLevel > maxLodLevel() ) + newLevel = defaultLodLevel(); + lodLevel = newLevel; } void Scene::make( NifModel * nif, bool flushTextures ) @@ -211,7 +227,7 @@ void Scene::make( NifModel * nif, bool flushTextures ) if ( !nif ) return; - game = Game::GameManager::get_game(nif->getVersionNumber(), nif->getUserVersion(), nif->getBSVersion()); + setGame( Game::GameManager::get_game(nif->getVersionNumber(), nif->getUserVersion(), nif->getBSVersion()) ); update( nif, QModelIndex() ); @@ -228,45 +244,54 @@ void Scene::make( NifModel * nif, bool flushTextures ) Node * Scene::getNode( const NifModel * nif, const QModelIndex & iNode ) { if ( !nif || !iNode.isValid() ) - return 0; + return nullptr; + + return getNode( nif->field( iNode ) ); +} - Node * node = nodes.get( iNode ); +Node * Scene::getNode( NifFieldConst nodeBlock ) +{ + if ( !nodeBlock ) + return nullptr; + Node * node = nodes.get( nodeBlock ); if ( node ) return node; - auto nodeName = nif->itemName(iNode); - if ( nif->blockInherits( iNode, "NiNode" ) ) { - if ( nodeName == "NiLODNode" ) - node = new LODNode( this, iNode ); - else if ( nodeName == "NiBillboardNode" ) - node = new BillboardNode( this, iNode ); - else - node = new Node( this, iNode ); - } else if ( nodeName == "NiTriShape" || nodeName == "NiTriStrips" || nif->blockInherits( iNode, "NiTriBasedGeom" ) ) { - node = new Mesh( this, iNode ); - shapes += static_cast(node); - } else if ( nif->checkVersion( 0x14050000, 0 ) && nodeName == "NiMesh" ) { - node = new Mesh( this, iNode ); - } - //else if ( nif->blockInherits( iNode, "AParticleNode" ) || nif->blockInherits( iNode, "AParticleSystem" ) ) - else if ( nif->blockInherits( iNode, "NiParticles" ) ) { - // ... where did AParticleSystem go? - node = new Particles( this, iNode ); - } else if ( nif->blockInherits( iNode, "BSTriShape" ) ) { - node = new BSShape( this, iNode ); - shapes += static_cast(node); - } else if ( nif->blockInherits(iNode, "BSGeometry") ) { - node = new BSMesh(this, iNode); - shapes += static_cast(node); - } else if ( nif->blockInherits( iNode, "NiAVObject" ) ) { - if ( nodeName == "BSTreeNode" ) - node = new Node( this, iNode ); + if ( !nodeBlock.isBlock() ) { + nodeBlock.model()->reportError( tr("Scene::getNode: item '%1' is not a block.").arg( nodeBlock.repr() ) ); + + } else if ( nodeBlock.inherits("NiNode") ) { + if ( nodeBlock.hasName("NiLODNode") ) { + node = new LODNode( this, nodeBlock ); + } else if ( nodeBlock.hasName("NiBillboardNode") ) { + node = new BillboardNode( this, nodeBlock ); + } else { + node = new Node( this, nodeBlock ); + } + + } else if ( nodeBlock.hasName("NiTriShape", "NiTriStrips") || nodeBlock.inherits("NiTriBasedGeom") ) { + node = new Mesh( this, nodeBlock ); + + } else if ( nodeBlock.model()->checkVersion( 0x14050000, 0 ) && nodeBlock.hasName("NiMesh") ) { + node = new Mesh( this, nodeBlock ); + + // } else if ( nodeBlock.inherits("AParticleNode", "AParticleSystem") ) { + // ... where did AParticleSystem go? + + } else if ( nodeBlock.inherits("NiParticles") ) { + node = new Particles( this, nodeBlock ); + + } else if ( nodeBlock.inherits("BSTriShape") ) { + node = new BSShape( this, nodeBlock ); + + } else if ( nodeBlock.inherits("BSGeometry") ) { + node = new BSMesh( this, nodeBlock ); } if ( node ) { nodes.add( node ); - node->update( nif, iNode ); + node->update(); } return node; @@ -299,7 +324,7 @@ void Scene::setSequence( const QString & seqname ) for ( Node * node : nodes.list() ) { node->setSequence( seqname ); } - for ( Property * prop : properties.list() ) { + for ( Property * prop : properties.hash() ) { prop->setSequence( seqname ); } @@ -315,7 +340,7 @@ void Scene::transform( const Transform & trans, float time ) viewTrans.clear(); bhkBodyTrans.clear(); - for ( Property * prop : properties.list() ) { + for ( Property * prop : properties.hash() ) { prop->transform(); } for ( Node * node : roots.list() ) { @@ -399,6 +424,12 @@ void Scene::drawSelection() const } } +int Scene::registerShape( Shape * shape ) +{ + shapes.append( shape ); + return shapes.count() - 1; +} + BoundSphere Scene::bounds() const { if ( !sceneBoundsValid ) { @@ -420,7 +451,7 @@ void Scene::updateTimeBounds() const for ( Node * node : nodes.list() ) { node->timeBounds( tMin, tMax ); } - for ( Property * prop : properties.list() ) { + for ( Property * prop : properties.hash() ) { prop->timeBounds( tMin, tMax ); } } else { diff --git a/src/gl/glscene.h b/src/gl/glscene.h index 63e10a2db..57dbbde5c 100644 --- a/src/gl/glscene.h +++ b/src/gl/glscene.h @@ -89,10 +89,15 @@ class Scene final : public QObject int bindTexture( const QModelIndex & index ); Node * getNode( const NifModel * nif, const QModelIndex & iNode ); + Node * getNode( NifFieldConst nodeBlock ); Property * getProperty( const NifModel * nif, const QModelIndex & iProperty ); Property * getProperty( const NifModel * nif, const QModelIndex & iParentBlock, const QString & itemName, const QString & mustInherit ); - Game::GameMode game = Game::OTHER; +private: + Game::GameMode game; +public: + void setGame( Game::GameMode newGame ); + inline Game::GameMode getGame() const { return game; } enum SceneOption { @@ -148,15 +153,13 @@ class Scene final : public QObject inline bool isSelModeObject() const { return ( selMode & SelObject ); } inline bool isSelModeVertex() const { return ( selMode & SelVertex ); } - enum LodLevel - { - Level0 = 0, - Level1 = 1, - Level2 = 2, - Level3 = 3 - }; + static constexpr int MAX_LOD_LEVEL_STARFIELD = 3; + static constexpr int MAX_LOD_LEVEL_DEFAULT = 2; + + int lodLevel; - LodLevel lodLevel; + int maxLodLevel() const; + int defaultLodLevel() const; Renderer * renderer; @@ -186,6 +189,7 @@ class Scene final : public QObject QPersistentModelIndex currentIndex; QVector shapes; + int registerShape( Shape * shape ); BoundSphere bounds() const; diff --git a/src/gl/glshape.cpp b/src/gl/glshape.cpp index cc71b670b..b497c6094 100644 --- a/src/gl/glshape.cpp +++ b/src/gl/glshape.cpp @@ -34,24 +34,27 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "gl/controllers.h" #include "gl/glscene.h" -#include "model/nifmodel.h" +#include "gl/renderer.h" #include "io/material.h" +#include "lib/nvtristripwrapper.h" -#include -#include -Shape::Shape( Scene * s, const QModelIndex & b ) : Node( s, b ) +Shape::Shape( Scene * _scene, NifFieldConst _block ) + : Node( _scene, _block ) { - shapeNumber = s->shapes.count(); + shapeNumber = scene->registerShape( this ); +} + +Shape::~Shape() +{ + qDeleteAll( selections ); } void Shape::clear() { Node::clear(); - resetSkinning(); - resetVertexData(); - resetSkeletonData(); + resetBlockData(); transVerts.clear(); transNorms.clear(); @@ -65,46 +68,343 @@ void Shape::clear() bsesp = nullptr; alphaProperty = nullptr; - isLOD = false; isDoubleSided = false; } void Shape::transform() { - if ( needUpdateData ) { - needUpdateData = false; + if ( needUpdateData ) + updateData(); + + Node::transform(); +} + +bool Shape::isEditorMarker() const +{ + // TODO: replace it with a check if BSXFlags has "EditorMarkers present" flag? + return name.contains( QStringLiteral("EditorMarker") ); +} + +bool Shape::doSkinning() const +{ + return isSkinned && bones.count() && scene->hasOption(Scene::DoSkinning); +} + +void Shape::fillViewModeWeights( double * outWeights, bool & outIsSkinned, const int * modeAxes ) +{ + if ( isEditorMarker() || isHidden() ) + return; + + if ( needUpdateData ) + updateData(); + + if ( doSkinning() && ( triangles.count() || stripTriangles.count() ) ) + outIsSkinned = true; - auto nif = NifModel::fromValidIndex( iBlock ); - if ( nif ) { - needUpdateBounds = true; // Force update bounds - updateData(nif); + Transform vertTransform = worldTrans(); - if ( isVertexAlphaAnimation ) { - int nColors = colors.count(); - for ( int i = 0; i < nColors; i++ ) - colors[i].setRGBA( colors[i].red(), colors[i].green(), colors[i].blue(), 1 ); + auto processTri = [this, &outWeights, vertTransform]( const Triangle & t, const int * modeAxes ) { + auto p1 = vertTransform * verts[ t[0] ]; + auto p2 = vertTransform * verts[ t[1] ]; + auto p3 = vertTransform * verts[ t[2] ]; + + // resv = triangle normal vector * triangle area (to give bigger triangles more weight in the result) + // where: + // cpv = Vector3::crossproduct( p2 - p1, p3 - p1 ) + // triangle normal vector = cpv.normalize (or cpv / cpv.length) + // triangle area = cpv.length * 0.5 + // So resv resolves to ( cpv / cpv.length * cpv.length * 0.5 ) -> ( cpv * 0.5 ). + // And since the 0.5 multiplier does not affect the ratio of the result weights, we can drop it too, simplifying all this to just ( cpv ). + auto resv = Vector3::crossproduct( p2 - p1, p3 - p1 ); + + for ( int i = 0; i < 3; i++, modeAxes += 2 ) { + auto axisv = resv[i]; + if ( axisv > 0.0f ) { + outWeights[ modeAxes[0] ] += axisv; + if ( isDoubleSided ) + outWeights[ modeAxes[1] ] += axisv; + } else if ( axisv < 0.0f ) { + outWeights[ modeAxes[1] ] -= axisv; + if ( isDoubleSided ) + outWeights[ modeAxes[0] ] -= axisv; } + } + }; + + for ( const Triangle & t : triangles ) + processTri( t, modeAxes ); + for ( const Triangle & t : stripTriangles ) + processTri( t, modeAxes ); +} + + +constexpr GLfloat BIG_VERTEX_SIZE = 8.5f; +constexpr GLfloat SMALL_VERTEX_SIZE = 5.5f; +constexpr GLfloat WIREFRAME_LINE_WIDTH = 1.0f; +constexpr GLfloat VECTOR_LINE_WIDTH = 1.5f; +constexpr float VECTOR_SCALE_DIV = 20.0f; +constexpr float VECTOR_MIN_SCALE = 0.5f; // 1.0f; +constexpr float VECTOR_MAX_SCALE = 25.0f; +const Color4 BOUND_SPHERE_COLOR( 1, 1, 1, 0.4f ); +const Color4 BOUND_SPHERE_CENTER_COLOR( 1, 1, 1, 1 ); + +void Shape::drawShapes( NodeList * secondPass, bool presort ) +{ + if ( numVerts <= 0 || isHidden() ) + return; + + if ( !scene->hasOption(Scene::ShowMarkers) && isEditorMarker() ) + return; + + // BSOrderedNode + // TODO (Gavrant): I don't understand the purpose of this. Also, in the old code BSShape did not do this. + presorted |= presort; + + // Draw translucent meshes in second pass + if ( secondPass && drawInSecondPass ) { + secondPass->add( this ); + return; + } + + // TODO: Option to hide Refraction and other post effects + + // rigid mesh? then pass the transformation on to the gl layer + if ( transformRigid ) { + glPushMatrix(); + glMultMatrix( viewTrans() ); + } + + // Render polygon fill slightly behind alpha transparency, and alpha transparency - behind wireframe + glEnable( GL_POLYGON_OFFSET_FILL ); + if ( drawInSecondPass ) + glPolygonOffset( 0.5f, 1.0f ); + else + glPolygonOffset( 1.0f, 2.0f ); + + glEnableClientState( GL_VERTEX_ARRAY ); + glVertexPointer( 3, GL_FLOAT, 0, transVerts.constData() ); + + if ( !Node::SELECTING) { // Normal rendering + if ( transNorms.count() > 0 ) { + glEnableClientState( GL_NORMAL_ARRAY ); + glNormalPointer( GL_FLOAT, 0, transNorms.constData() ); + } + + if ( transColors.count() ) { + glEnableClientState( GL_COLOR_ARRAY ); + glColorPointer( 4, GL_FLOAT, 0, transColors.constData() ); + } else { + glColor( Color3( 1.0f, 1.0f, 1.0f ) ); + } + + if ( sRGB ) + glEnable( GL_FRAMEBUFFER_SRGB ); + else + glDisable( GL_FRAMEBUFFER_SRGB ); + + shader = scene->renderer->setupProgram( this, shader ); + } else { // Selection rendering + if ( scene->isSelModeObject() ) { + glSelectionBufferColor( nodeId ); } else { - clear(); - return; + glColor4f( 0, 0, 0, 1 ); } + + glDisable( GL_LIGHTING ); + glDisable( GL_FRAMEBUFFER_SRGB ); } - Node::transform(); + if ( isDoubleSided ) + glDisable( GL_CULL_FACE ); + else + glEnable( GL_CULL_FACE ); + + // Draw triangles and strips + int lodLevel = isLOD ? scene->lodLevel : -1; + if ( lodLevel >= 0 && lodLevel < lodLevels.count() ) { + glDrawTriangles( lodLevels[lodLevel] ); + } else { + glDrawTriangles( triangles ); + } + + glDrawTriangles( stripTriangles ); + + // Post-drawing triangles and strips + if ( !Node::SELECTING ) + scene->renderer->stopProgram(); + + glDisableClientState( GL_VERTEX_ARRAY ); + glDisableClientState( GL_NORMAL_ARRAY ); + glDisableClientState( GL_COLOR_ARRAY ); + + glDisable( GL_POLYGON_OFFSET_FILL ); + + if ( Node::SELECTING && scene->isSelModeVertex() ) { + glPointSize( BIG_VERTEX_SIZE ); + + glBegin( GL_POINTS ); + + auto pVerts = transVerts.data(); + for ( int i = 0; i < numVerts; i++, pVerts++ ) { + glSelectionBufferColor( shapeNumber, i ); + glVertex( pVerts ); + } + + glEnd(); + } + + if ( transformRigid ) + glPopMatrix(); } -void Shape::setController( const NifModel * nif, const QModelIndex & iController ) +void Shape::drawSelection() const { - QString contrName = nif->itemName(iController); - if ( contrName == "NiGeomMorpherController" ) { - Controller * ctrl = new MorphController( this, iController ); - registerController(nif, ctrl); - } else if ( contrName == "NiUVController" ) { - Controller * ctrl = new UVController( this, iController ); - registerController(nif, ctrl); + // TODO (Gavrant): move glDisable GL_FRAMEBUFFER_SRGB to drawShapes? + glDisable( GL_FRAMEBUFFER_SRGB ); + + if ( scene->hasOption(Scene::ShowNodes) ) + Node::drawSelection(); + + if ( isHidden() ) + return; + + auto nif = NifModel::fromValidIndex( iBlock ); + if ( !nif ) + return; + + drawSelectionMode = DrawSelectionMode::NO; + + auto selectedField = nif->field( scene->currentIndex ); + auto selectedBlock = selectedField.block(); + bool dataSelected; + if ( selectedBlock ) { + QModelIndex iSelBlock = selectedBlock.toIndex(); + dataSelected = ( iSelBlock == iBlock || iSelBlock == iData || iSelBlock == iSkin || iSelBlock == iSkinData || iSelBlock == iSkinPart || iSelBlock == iExtraData ); } else { - Node::setController( nif, iController ); + dataSelected = false; + } + + if ( dataSelected && selectedField != selectedBlock ) { + int selectedLevel = selectedField.ancestorLevel( selectedBlock ); + + for ( auto pSelection : selections ) { + if ( pSelection->block != selectedBlock || pSelection->level > selectedLevel ) + continue; + int iSubLevel = selectedField.ancestorLevel( pSelection->rootField ); + if ( iSubLevel >= 0 && pSelection->process( selectedField, iSubLevel ) ) + return; + } + } + + // Fallback + if ( scene->isSelModeVertex() ) { + drawSelection_vertices( VertexSelectionType::VERTICES ); + } else if ( dataSelected && scene->isSelModeObject() ) { + drawSelection_triangles(); + } +} + +QModelIndex Shape::vertexAt( int vertexIndex ) const +{ + if ( mainVertexRoot ) { + auto resField = mainVertexRoot.child( vertexIndex ); + if ( resField.hasStrType("BSVertexData", "BSVertexDataSSE") ) { + auto pointField = resField.child("Vertex"); + if ( pointField ) + resField = pointField; + } + + return resField.toIndex(); + } + + return QModelIndex(); +} + +template static inline void normalizeVectorSize( QVector & v, int nRequiredSize, bool hasVertexData ) +{ + if ( hasVertexData ) { + if ( v.count() < nRequiredSize ) + v.resize( nRequiredSize ); + } else { + v.clear(); + } +} + +static void validateTriangles( QVector & tris, QVector & triMap, int numVerts ) +{ + // Validate triangle data + int nTotalTris = tris.count(); + int nValidTris = 0; + if ( nTotalTris > 0 ) { + triMap.fill( -1, nTotalTris ); + for ( int i = 0; i < nTotalTris; i++ ) { + const auto & t = tris[i]; + if ( t[0] < numVerts && t[1] < numVerts && t[2] < numVerts ) + triMap[i] = nValidTris++; + } + + if ( nValidTris < nTotalTris ) { + if ( nValidTris > 0 ) { + for ( int i = nTotalTris - 1; i >= 0; i-- ) { + if ( triMap[i] < 0 ) + tris.remove(i); + } + } else { + tris.clear(); + } + } + } +} + +void Shape::updateData() +{ + needUpdateData = false; + + if ( !isValid() ) { + clear(); + return; } + + needUpdateBounds = true; // Force update bounds + resetBlockData(); + + updateDataImpl(); + + numVerts = verts.count(); + + // Validate vertex data + normalizeVectorSize( norms, numVerts, hasVertexNormals ); + normalizeVectorSize( tangents, numVerts, hasVertexTangents ); + normalizeVectorSize( bitangents, numVerts, hasVertexBitangents ); + + for ( auto & uvset : coords ) + normalizeVectorSize( uvset, numVerts, hasVertexUVs ); + + // Validate triangles + validateTriangles( triangles, triangleMap, numVerts ); + validateTriangles( stripTriangles, stripMap, numVerts ); + + // Update selections + for ( auto pSelection : selections ) + pSelection->postUpdate(); + + // Sort selections from the highest level to the lowest, + // so the deeper a selection is within its block, the closer to the start the selection would be. + std::sort( selections.begin(), selections.end(), []( ShapeSelectionBase * a, ShapeSelectionBase * b ) { return a->level > b->level; }); + + if ( isLOD ) + emit model->lodSliderChanged( true ); +} + +Controller * Shape::createController( NifFieldConst controllerBlock ) +{ + if ( controllerBlock.hasName("NiGeomMorpherController") ) + return new MorphController( this, controllerBlock ); + + if ( controllerBlock.hasName("NiUVController") ) + return new UVController( this, controllerBlock ); + + return Node::createController( controllerBlock ); } void Shape::updateImpl( const NifModel * nif, const QModelIndex & index ) @@ -116,12 +416,12 @@ void Shape::updateImpl( const NifModel * nif, const QModelIndex & index ) bslsp = nullptr; bsesp = nullptr; - bssp = properties.get(); + bssp = properties.get(); if ( bssp ) { auto shaderType = bssp->typeId(); - if ( shaderType == "BSLightingShaderProperty" ) + if ( shaderType == QStringLiteral("BSLightingShaderProperty") ) bslsp = bssp->cast(); - else if ( shaderType == "BSEffectShaderProperty" ) + else if ( shaderType == QStringLiteral("BSEffectShaderProperty") ) bsesp = bssp->cast(); } @@ -137,74 +437,191 @@ void Shape::updateImpl( const NifModel * nif, const QModelIndex & index ) updateShader(); } + + // TODO: trigger update data if any of the shape's bones nodes are updated (or its sceleton root?) } -void Shape::boneSphere( const NifModel * nif, const QModelIndex & index ) const +TriangleRange * Shape::addTriangles( NifFieldConst rangeRoot, const QVector & tris ) { - Node * root = findParent( 0 ); - Node * bone = root ? root->findChild( bones.value( index.row() ) ) : 0; - if ( !bone ) - return; + int iStart = triangles.count(); + triangles << tris; + return addTriangleRange( rangeRoot, TriangleRange::FLAG_ARRAY, iStart ); +} - Transform boneT = Transform( nif, index ); - Transform t = scene->hasOption(Scene::DoSkinning) ? viewTrans() : Transform(); - t = t * skeletonTrans * bone->localTrans( 0 ) * boneT; +StripRange * Shape::addStrip( NifFieldConst stripPointsRoot, const QVector & stripTris, NifFieldConst vertexMapField ) +{ + int iStart = stripTriangles.count(); + stripTriangles << stripTris; + return addStripRange( stripPointsRoot, TriangleRange::FLAG_ARRAY | TriangleRange::FLAG_HIGHLIGHT, iStart, vertexMapField ); +} - auto bSphere = BoundSphere( nif, index ); - if ( bSphere.radius > 0.0 ) { - glColor4f( 1, 1, 1, 0.33f ); - auto pos = boneT.rotation.inverted() * (bSphere.center - boneT.translation); - drawSphereSimple( t * pos, bSphere.radius, 36 ); +StripRange * Shape::addStrips( NifFieldConst stripsRoot, NifSkopeFlagsType rangeFlags ) +{ + if ( stripsRoot ) { + int iStart = stripTriangles.count(); + for ( auto pointsRoot : stripsRoot.iter() ) + addStrip( pointsRoot, triangulateStrip( pointsRoot.array() ), NifFieldConst() ); + return addStripRange( stripsRoot, rangeFlags, iStart ); } + + return nullptr; } -void Shape::resetSkinning() +void Shape::initSkinBones( NifFieldConst nodeMapRoot, NifFieldConst nodeListRoot, NifFieldConst block ) { - isSkinned = false; - iSkin = iSkinData = iSkinPart = QModelIndex(); + reportFieldCountMismatch( nodeMapRoot, nodeListRoot, block ); + int nTotalBones = std::max( nodeMapRoot.childCount(), nodeListRoot.childCount() ); + bones.reserve( nTotalBones ); + Node * root = findParent( skeletonRoot ); + for ( int bind = 0; bind < nTotalBones; bind++ ) { + auto boneNodeLink = nodeMapRoot.child(bind).link(); + const Node * boneNode = ( root && boneNodeLink >= 0 ) ? root->findChild( boneNodeLink ) : nullptr; + bones << SkinBone( nodeListRoot.child(bind), boneNode ); + } + + addBoneSelection( nodeMapRoot, nullptr ); + addBoneSelection( nodeListRoot, nullptr ); +} + +void Shape::applySkinningTransforms( const Transform & skinTransform ) +{ + transformRigid = false; + + transVerts.resize( numVerts ); + transVerts.fill( Vector3() ); + transNorms.resize( norms.count() ); + transNorms.fill( Vector3() ); + transTangents.resize( tangents.count() ); + transTangents.fill( Vector3() ); + transBitangents.resize( bitangents.count() ); + transBitangents.fill( Vector3() ); + + for ( SkinBone & bone : bones ) { + Transform t = bone.localTransform( skinTransform, skeletonRoot ) * bone.transform; + + for ( const VertexWeight & vw : bone.vertexWeights ) { + transVerts[vw.vertex] += t * verts[vw.vertex] * vw.weight; + if ( hasVertexNormals ) + transNorms[vw.vertex] += t.rotation * norms[vw.vertex] * vw.weight; + if ( hasVertexTangents ) + transTangents[vw.vertex] += t.rotation * tangents[vw.vertex] * vw.weight; + if ( hasVertexBitangents ) + transBitangents[vw.vertex] += t.rotation * bitangents[vw.vertex] * vw.weight; + } + } + + for ( auto & v : transNorms ) + v.normalize(); + for ( auto & v : transTangents ) + v.normalize(); + for ( auto & v : transBitangents ) + v.normalize(); + + boundSphere = BoundSphere( transVerts ); + boundSphere.applyInv( viewTrans() ); + needUpdateBounds = false; +} + +void Shape::applyRigidTransforms() +{ + transformRigid = true; + + transVerts = verts; + transNorms = norms; + transTangents = tangents; + transBitangents = bitangents; +} + +void Shape::applyColorTransforms( float alphaBlend ) +{ + auto shaderColorMode = bssp ? bssp->vertexColorMode : ShaderColorMode::FromData; + bool doVCs = ( shaderColorMode == ShaderColorMode::FromData ) ? hasVertexColors : ( shaderColorMode == ShaderColorMode::Yes ); + + if ( doVCs && numVerts > 0 ) { + transColors = colors; + if ( alphaBlend != 1.0f ) { + for ( auto & c : transColors ) + c.setAlpha( c.alpha() * alphaBlend ); + } else if ( bssp && ( bssp->isVertexAlphaAnimation || !bssp->hasVertexAlpha ) ) { + for ( auto & c : transColors ) + c.setAlpha( 1.0f ); + } + } else { + transColors.clear(); + } + + // Cover any mismatches between shader and data flags or wrong color numbers in the data by appending "bad colors" until transColors.count() == numVerts. + if ( transColors.count() < numVerts && ( doVCs || hasVertexColors ) ) { + transColors.reserve( numVerts ); + for ( int i = transColors.count(); i < numVerts; i++ ) + transColors << Color4( 0, 0, 0, 1 ); + } } -void Shape::resetVertexData() +void Shape::resetBlockData() { + // Vertex data numVerts = 0; - iData = iTangentData = QModelIndex(); + iData = iExtraData = QModelIndex(); + + hasVertexNormals = false; + hasVertexTangents = false; + hasVertexBitangents = false; + hasVertexUVs = false; + hasVertexColors = false; + + sRGB = false; + + isLOD = false; + lodLevels.clear(); // No need for qDeleteAll here, selections cleanup will take care of it + // Vertex data verts.clear(); norms.clear(); colors.clear(); coords.clear(); tangents.clear(); bitangents.clear(); + + mainVertexRoot = NifFieldConst(); + + // Triangle data triangles.clear(); - tristrips.clear(); -} + triangleMap.clear(); -void Shape::resetSkeletonData() -{ + // Strip data + stripTriangles.clear(); + stripMap.clear(); + + // Skinning data + isSkinned = false; + iSkin = iSkinData = iSkinPart = QModelIndex(); + + // Skeleton data skeletonRoot = 0; skeletonTrans = Transform(); bones.clear(); - weights.clear(); - partitions.clear(); + + qDeleteAll( selections ); + selections.clear(); } void Shape::updateShader() { - if ( bslsp ) - translucent = (bslsp->alpha < 1.0) || bslsp->hasRefraction; - else if ( bsesp ) - translucent = (bsesp->getAlpha() < 1.0) && !alphaProperty; - else - translucent = false; + if ( bslsp ) { + translucent = bslsp->isTranslucent(); + } else { + translucent = !alphaProperty && bssp && bssp->isTranslucent(); + } drawInSecondPass = false; - if ( translucent ) + if ( translucent ) { drawInSecondPass = true; - else if ( alphaProperty && (alphaProperty->hasAlphaBlend() || alphaProperty->hasAlphaTest()) ) + } else if ( alphaProperty && (alphaProperty->hasAlphaBlend() || alphaProperty->hasAlphaTest()) ) { drawInSecondPass = true; - else if ( bssp ) { + } else if ( bssp ) { Material * mat = bssp->getMaterial(); if ( mat && (mat->hasAlphaBlend() || mat->hasAlphaTest() || mat->hasDecal()) ) drawInSecondPass = true; @@ -214,11 +631,626 @@ void Shape::updateShader() depthTest = bssp->depthTest; depthWrite = bssp->depthWrite; isDoubleSided = bssp->isDoubleSided; - isVertexAlphaAnimation = bssp->isVertexAlphaAnimation; } else { depthTest = true; depthWrite = true; isDoubleSided = false; - isVertexAlphaAnimation = false; } } + +void Shape::initLodData() +{ + static constexpr int NUM_LODS = 3; + static const QString FIELD_NAMES[NUM_LODS] = { + QStringLiteral("LOD0 Size"), + QStringLiteral("LOD1 Size"), + QStringLiteral("LOD2 Size") + }; + + isLOD = true; + + lodLevels.resize( NUM_LODS ); + const NifSkopeFlagsType RANGE_FLAGS = TriangleRange::FLAG_HIGHLIGHT; + for ( int iLod = 0, iLodStart = 0; iLod < NUM_LODS; iLod++ ) { + auto lodField = block[FIELD_NAMES[iLod]]; + auto nLodSize = lodField.value(); + // TODO: report if iStart + nLodSize exceeds nTriangles + int iLodEnd = iLodStart + nLodSize; + + if ( iLodStart != 0 ) { + // One range for rendering actual triangles... + lodLevels[iLod] = addTriangleRange( NifFieldConst(), RANGE_FLAGS, 0, iLodEnd ); + // Another range for tracking selection... + addTriangleRange( lodField, RANGE_FLAGS, iLodStart, nLodSize ); + } else { + // Micro-optimization: one single range both for rendering actual triangles and for tracking selection... + lodLevels[iLod] = addTriangleRange( lodField, RANGE_FLAGS, 0, iLodEnd ); + } + + iLodStart = iLodEnd; + } + // TODO: report if the lods do not cover all triangles +} + + +// Shape: drawSelection helpers + +void Shape::drawSelection_begin( DrawSelectionMode newMode ) const +{ + if ( newMode == drawSelectionMode ) + return; + + auto getUseViewTrans = [this]( DrawSelectionMode drawMode ) -> bool { + if ( drawMode == DrawSelectionMode::NO ) + return false; + if ( drawMode == DrawSelectionMode::BOUND_SPHERE ) + return true; + return transformRigid; + }; + + bool oldUseViewTrans = getUseViewTrans( drawSelectionMode ); + bool newUseViewTrans = getUseViewTrans( newMode ); + if ( oldUseViewTrans != newUseViewTrans ) { + if ( newUseViewTrans ) { + glPushMatrix(); + glMultMatrix( viewTrans() ); + } else { + glPopMatrix(); + } + } + + // Cleanup of the previous selection mode + switch( drawSelectionMode ) + { + case DrawSelectionMode::NO: // First call of drawSelection_begin + glDisable( GL_LIGHTING ); + glDisable( GL_COLOR_MATERIAL ); + glDisable( GL_TEXTURE_2D ); + glDisable( GL_NORMALIZE ); + glEnable( GL_DEPTH_TEST ); + glDepthFunc( GL_LEQUAL ); + glDepthMask( GL_FALSE ); + glEnable( GL_BLEND ); + glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA ); + glDisable( GL_ALPHA_TEST ); + glDisable( GL_CULL_FACE ); + break; + case DrawSelectionMode::WIREFRAME: + glDisableClientState( GL_VERTEX_ARRAY ); + glDisable( GL_POLYGON_OFFSET_FILL ); + break; + case DrawSelectionMode::VERTICES: + glDisableClientState( GL_VERTEX_ARRAY ); + break; + } + + // Init the new selection mode + switch ( newMode ) + { + case DrawSelectionMode::VERTICES: + glPolygonMode( GL_FRONT_AND_BACK, GL_POINT ); // ??? + glPointSize( scene->isSelModeVertex() ? BIG_VERTEX_SIZE : SMALL_VERTEX_SIZE ); + glEnableClientState( GL_VERTEX_ARRAY ); + glVertexPointer( 3, GL_FLOAT, 0, transVerts.constData() ); + break; + case DrawSelectionMode::VECTORS: + glPolygonMode( GL_FRONT_AND_BACK, GL_LINE ); // ??? + glLineWidth( VECTOR_LINE_WIDTH ); + break; + case DrawSelectionMode::WIREFRAME: + glEnable( GL_POLYGON_OFFSET_FILL ); + glPolygonOffset( 0.03f, 0.03f ); + glPolygonMode( GL_FRONT_AND_BACK, GL_LINE ); + glLineWidth( WIREFRAME_LINE_WIDTH ); + glEnableClientState( GL_VERTEX_ARRAY ); + glVertexPointer( 3, GL_FLOAT, 0, transVerts.constData() ); + break; + case DrawSelectionMode::BOUND_SPHERE: + glPointSize( BIG_VERTEX_SIZE ); + glPolygonMode( GL_FRONT_AND_BACK, GL_LINE ); // ??? + glLineWidth( WIREFRAME_LINE_WIDTH ); + break; + } + + drawSelectionMode = newMode; +} + +static inline void drawSingleSelection_begin( const Color4 & color ) +{ + // Semi-transparent highlight color + no depth test (always visible) + glColor4f( color.red(), color.green(), color.blue(), color.alpha() * 0.5f ); + glDepthFunc( GL_ALWAYS ); +} + +static inline void drawSingleSelection_end() +{ + glDepthFunc( GL_LEQUAL ); +} + +void Shape::drawSelection_triangles() const +{ + drawSelection_begin( DrawSelectionMode::WIREFRAME ); + + glNormalColor(); + glDrawTriangles( triangles ); + glDrawTriangles( stripTriangles ); + + drawSelection_end(); +} + +void Shape::drawSelection_triangles( const TriangleRange * range ) const +{ + if ( range->realLength > 0 ) { + drawSelection_begin( DrawSelectionMode::WIREFRAME ); + + glNormalColor(); + glDrawTriangles( range ); + + drawSelection_end(); + } +} + +void Shape::drawSelection_trianglesHighlighted( const TriangleRange * range ) const +{ + if ( range->realLength > 0 ) { + drawSelection_begin( DrawSelectionMode::WIREFRAME ); + + glNormalColor(); + const auto & rangeTris = range->triangles(); + int iRangeEnd = range->realEnd(); + const TriangleRange * pParent = range->parentRange; + if ( pParent ) { + glDrawTriangles( rangeTris, pParent->realStart, range->realStart - pParent->realStart ); + glDrawTriangles( rangeTris, iRangeEnd, pParent->realEnd() - iRangeEnd ); + } else { + glDrawTriangles( rangeTris, 0, range->realStart ); + glDrawTriangles( rangeTris, iRangeEnd, rangeTris.count() - iRangeEnd ); + glDrawTriangles( range->otherTriangles() ); + } + + glHighlightColor(); + glDrawTriangles( range ); + + drawSelection_end(); + } +} + +void Shape::drawSelection_triangles( const TriangleRange * partition, int iSelectedTri ) const +{ + if ( partition->realLength > 0 ) { + drawSelection_begin( DrawSelectionMode::WIREFRAME ); + + glNormalColor(); + glDrawTriangles( partition ); + + if ( iSelectedTri >= partition->realStart && iSelectedTri < partition->realEnd() ) { + glHighlightColor(); + glPolygonMode( GL_FRONT_AND_BACK, GL_FILL ); + // First pass: draw the triangle with the normal highlight color (and with cull face for single-sided shapes). + if ( !isDoubleSided ) { + glEnable( GL_CULL_FACE ); + glDrawTriangles( partition->triangles(), iSelectedTri, 1 ); + glDisable( GL_CULL_FACE ); + } else { + glDrawTriangles( partition->triangles(), iSelectedTri, 1 ); + } + // Second pass: draw the triangle with the semitransparent highlight color, w/o cull face and with no depth test. + // This makes the selected triangle noticeable even if it is facing away from the camera or is obstructed by other triangles. + drawSingleSelection_begin( Color4(cfg.highlight) ); + glDrawTriangles( partition->triangles(), iSelectedTri, 1 ); + drawSingleSelection_end(); + } + + drawSelection_end(); + } +} + +bool Shape::drawSelection_vectors_init( VertexSelectionType type, DrawVectorsData & outData ) const +{ + if ( scene->isSelModeObject() ) { + outData.drawNormals = ( type == VertexSelectionType::NORMALS ) && hasVertexNormals; + outData.drawTangents = ( type == VertexSelectionType::TANGENTS || type == VertexSelectionType::EXTRA_TANGENTS ) && hasVertexTangents; + outData.drawBitangents = ( type == VertexSelectionType::BITANGENTS || type == VertexSelectionType::EXTRA_TANGENTS ) && hasVertexBitangents; + + if ( outData.drawNormals || outData.drawTangents || outData.drawBitangents ) { + outData.vectorScale = std::clamp( bounds().radius / VECTOR_SCALE_DIV, VECTOR_MIN_SCALE, VECTOR_MAX_SCALE ); + return true; + } + } + + return false; +} + +void Shape::drawSelection_vectors( int iStart, int nLength, const DrawVectorsData & drawData ) const +{ + if ( nLength > 0 ) { + glBegin( GL_LINES ); + + auto drawVectors = [this, drawData, iStart, nLength]( const QVector & vectors ) { + auto pVertices = transVerts.constData() + iStart; + auto pVectors = vectors.constData() + iStart; + for ( int i = 0; i < nLength; i++, pVertices++, pVectors++ ) { + glVertex( pVertices ); + glVertex( (*pVertices) + (*pVectors) * drawData.vectorScale ); + } + }; + + if ( drawData.drawNormals ) + drawVectors( transNorms ); + if ( drawData.drawTangents ) + drawVectors( transTangents ); + if ( drawData.drawBitangents ) + drawVectors( transBitangents ); + + glEnd(); + } +} + +void Shape::drawSelection_vertices( VertexSelectionType type ) const +{ + if ( numVerts > 0 ) { + drawSelection_begin( DrawSelectionMode::VERTICES ); + + glNormalColor(); + glDrawArrays( GL_POINTS, 0, numVerts ); + + DrawVectorsData drawData; + if ( drawSelection_vectors_init( type, drawData ) ) { + drawSelection_begin( DrawSelectionMode::VECTORS ); + drawSelection_vectors( 0, numVerts, drawData ); + } + + drawSelection_end(); + } +} + +void Shape::drawSelection_vertices( VertexSelectionType type, int iSelectedVertex ) const +{ + if ( iSelectedVertex >= 0 && iSelectedVertex < numVerts ) { + int iNextVertex = iSelectedVertex + 1; + DrawVectorsData drawData; + bool bDrawVectors = drawSelection_vectors_init( type, drawData ); + + drawSelection_begin( DrawSelectionMode::VERTICES ); + glNormalColor(); + if ( iSelectedVertex > 0 ) + glDrawArrays( GL_POINTS, 0, iSelectedVertex ); + if ( iNextVertex < numVerts ) + glDrawArrays( GL_POINTS, iNextVertex, numVerts - iNextVertex ); + + if ( bDrawVectors ) { + drawSelection_begin( DrawSelectionMode::VECTORS ); + drawSelection_vectors( 0, iSelectedVertex, drawData ); + drawSelection_vectors( iNextVertex, numVerts - iNextVertex, drawData ); + } + + drawSelection_begin( DrawSelectionMode::VERTICES ); + glHighlightColor(); + glDrawArrays( GL_POINTS, iSelectedVertex, 1 ); + drawSingleSelection_begin( Color4(cfg.highlight) ); + glDrawArrays( GL_POINTS, iSelectedVertex, 1 ); + drawSingleSelection_end(); + + if ( bDrawVectors ) { + drawSelection_begin( DrawSelectionMode::VECTORS ); + glHighlightColor(); + drawSelection_vectors( iSelectedVertex, 1, drawData ); + } + + drawSelection_end(); + } else { + drawSelection_vertices( type ); + } +} + +void Shape::drawSelection_sphere( const BoundSphere & sphere, const Transform & transform, bool hightlightCenter ) const +{ + auto drawCenter = [sphere, transform]( const Color4 & color ) { + Vector3 vc = transform * sphere.center; + + glColor( color ); + glBegin( GL_POINTS ); + glVertex( vc ); + glEnd(); + + drawSingleSelection_begin( color ); + glBegin( GL_POINTS ); + glVertex( vc ); + glEnd(); + drawSingleSelection_end(); + }; + + if ( sphere.radius > 0.01f ) { + glColor( BOUND_SPHERE_COLOR ); + drawSphereNew( sphere.center, sphere.radius, 12, transform ); + } else if ( !hightlightCenter ) { + drawCenter( BOUND_SPHERE_CENTER_COLOR ); + } + + if ( hightlightCenter ) { + drawCenter( Color4(cfg.highlight) ); + } +} + +void Shape::drawSelection_boundSphere( const BoundSphereSelection * selSphere, bool hightlightCenter ) const +{ + drawSelection_begin( DrawSelectionMode::WIREFRAME ); + glNormalColor(); + glDrawTriangles( triangles ); + glDrawTriangles( stripTriangles ); + + drawSelection_begin( DrawSelectionMode::BOUND_SPHERE ); + if ( selSphere->absoluteTransform ) { + glPopMatrix(); + + glPushMatrix(); + glMultMatrix( scene->view * selSphere->transform ); + drawSelection_sphere( selSphere->sphere, Transform(), hightlightCenter ); + // glPopMatrix(); // leave calling glPopMatrix to drawSelection_end() + } else { + drawSelection_sphere( selSphere->sphere, selSphere->transform, hightlightCenter ); + } + drawSelection_end(); +} + +void Shape::drawSelection_bone( const BoneSelection * selection, int iSelectedBone, bool drawBoundSphere, bool hightlightSphereCenter ) const +{ + if ( iSelectedBone < 0 || iSelectedBone >= bones.count() ) + return; + const SkinBone & bone = bones[iSelectedBone]; + + // Bones' triangles + const TriangleRange * range = selection->triRange; + int nTotalTris = range ? range->realLength : ( triangles.count() + stripTriangles.count() ); + if ( nTotalTris > 0 ) { + QVector boneVerticesMap( numVerts ); + for ( const auto & vw : bone.vertexWeights) { + if ( vw.weight > 0.0f ) + boneVerticesMap[vw.vertex] = true; + } + + QVector boneTriangles; + boneTriangles.reserve( nTotalTris ); + QVector otherTriangles; + otherTriangles.reserve( nTotalTris ); + + auto regTri = [boneVerticesMap, &boneTriangles, &otherTriangles]( const Triangle & t ) { + if ( boneVerticesMap[t.v1()] && boneVerticesMap[t.v2()] && boneVerticesMap[t.v3()] ) + boneTriangles << t; + else + otherTriangles << t; + }; + if ( range ) { + const auto & tris = range->triangles(); + for ( int tind = range->realStart, iEnd = range->realEnd(); tind < iEnd; tind++ ) + regTri( tris[tind] ); + } else { + for ( const auto & t : triangles ) + regTri( t ); + for ( const auto & t : stripTriangles ) + regTri( t ); + } + + drawSelection_begin( DrawSelectionMode::WIREFRAME ); + glNormalColor(); + glDrawTriangles( otherTriangles ); + glHighlightColor(); + glDrawTriangles( boneTriangles ); + } + + // Bound sphere + if ( drawBoundSphere ) { + Transform boneT = bone.localTransform( skeletonTrans, skeletonRoot ); // * bone.transform + drawSelection_begin( DrawSelectionMode::BOUND_SPHERE ); + drawSelection_sphere( bone.boundSphere, boneT, hightlightSphereCenter ); + } + + drawSelection_end(); +} + + +// ShapeSelectionBase class + +ShapeSelectionBase::ShapeSelectionBase( Shape * _shape, NifFieldConst _rootField, NifFieldConst _mapField ) + : shape( _shape ), rootField( _rootField ), block( _rootField.block() ), mapField( _mapField ) +{ + level = rootField.ancestorLevel( block ); + _shape->selections.append( this ); +} + +int ShapeSelectionBase::remapIndex( int i ) const +{ + if ( !mapField ) + return i; + + auto mapEntry = mapField.child( i ); + if ( mapEntry ) + return mapEntry.value(); + return -1; +} + + +// VertexSelection class + +bool VertexSelection::process( NifFieldConst selectedField, int iSubLevel ) const +{ + switch( type ) + { + case VertexSelectionType::VERTICES: + case VertexSelectionType::NORMALS: + case VertexSelectionType::TANGENTS: + case VertexSelectionType::BITANGENTS: + case VertexSelectionType::BS_VERTEX_DATA: + if ( iSubLevel == 0 ) { + shape->drawSelection_vertices( type ); + return true; + } else { // iSubLevel > 0 + int iVertex = remapIndex( selectedField.ancestorAt( iSubLevel - 1 ).row() ); + VertexSelectionType drawType = type; + + if ( type == VertexSelectionType::BS_VERTEX_DATA && iSubLevel == 2 ) { + if ( selectedField.hasName("Normal") ) + drawType = VertexSelectionType::NORMALS; + else if ( selectedField.hasName("Tangent") ) + drawType = VertexSelectionType::TANGENTS; + else if ( selectedField.hasName("Bitangent X", "Bitangent Y", "Bitangent Z") ) + drawType = VertexSelectionType::BITANGENTS; + } + + shape->drawSelection_vertices( drawType, iVertex ); + return true; + } + break; + case VertexSelectionType::EXTRA_TANGENTS: + shape->drawSelection_vertices( type ); + return true; + case VertexSelectionType::VERTEX_ROOT: + if ( iSubLevel == 0 ) { + shape->drawSelection_vertices( type ); + return true; + } + break; + } + + return false; +} + + +// TriangleRange class + +const QVector & TriangleRange::triangles() const +{ + return shape->triangles; +} + +const QVector & TriangleRange::triangleMap() const +{ + return shape->triangleMap; +} + +const QVector & TriangleRange::otherTriangles() const +{ + return shape->stripTriangles; +} + +void TriangleRange::postUpdate() +{ + const auto & triMap = triangleMap(); + + int nTotalTris = triMap.count(); + int nValidTris = triangles().count(); + + int iFirst = std::max( start, 0 ); + int iLast = ( nValidTris > 0 ) ? ( std::min( start + length, nTotalTris ) - 1 ) : -1; + + if ( nValidTris < nTotalTris ) { + while ( iFirst <= iLast && triMap[iFirst] < 0 ) { + iFirst++; + } + while ( iLast >= iFirst && triMap[iLast] < 0 ) { + iLast--; + } + } + + if ( iFirst <= iLast ) { + realStart = triMap[iFirst]; + realLength = triMap[iLast] - realStart + 1; + } else { + realStart = 0; // Whatever... + realLength = 0; + } +} + +bool TriangleRange::process( NifFieldConst selectedField, int iSubLevel ) const +{ + if ( shape->scene->isSelModeObject() ) { + if ( isArray() && iSubLevel == 1 ) { + int iSelectedTri = triangleMap().value( start + selectedField.row(), -1 ); + shape->drawSelection_triangles( this, iSelectedTri ); + return true; + } else if ( iSubLevel == 0 || isDeep() ) { + if ( isHighlight() ) + shape->drawSelection_trianglesHighlighted( this ); + else + shape->drawSelection_triangles( this ); + return true; + } + } + + return false; +} + + +// StripRange class + +const QVector & StripRange::triangles() const +{ + return shape->stripTriangles; +} + +const QVector & StripRange::triangleMap() const +{ + return shape->stripMap; +} + +const QVector & StripRange::otherTriangles() const +{ + return shape->triangles; +} + +bool StripRange::process( NifFieldConst selectedField, int iSubLevel ) const +{ + if ( isArray() && iSubLevel == 1 ) { + int iVertex = remapIndex( selectedField.value() ); + shape->drawSelection_vertices( VertexSelectionType::VERTICES, iVertex ); + return true; + } else if ( iSubLevel == 0 || isDeep() ) { + if ( shape->scene->isSelModeObject() ) { + if ( isHighlight() ) + shape->drawSelection_trianglesHighlighted( this ); + else + shape->drawSelection_triangles( this ); + return true; + } + } + + return false; +} + + +// BoundSphereSelection class + +bool BoundSphereSelection::process( NifFieldConst selectedField, int iSubLevel ) const +{ + if ( shape->scene->isSelModeObject() ) { + bool highlightCenter = ( iSubLevel == 1 ) && selectedField.hasName("Center"); + shape->drawSelection_boundSphere( this, highlightCenter ); + return true; + } + + return false; +} + + +// BoneSelection class + +bool BoneSelection::process( NifFieldConst selectedField, int iSubLevel ) const +{ + if ( shape->scene->isSelModeObject() ) { + if ( iSubLevel == 0 ) { + if ( triRange ) + shape->drawSelection_triangles( triRange ); + else + shape->drawSelection_triangles(); + return true; + } else { // iSubLevel > 0 + int iBone = remapIndex( selectedField.ancestorAt( iSubLevel - 1 ).row() ); + bool drawBoundSphere = ( iSubLevel >= 2 && selectedField.ancestorAt( iSubLevel - 2 ).hasName("Bounding Sphere") ); + bool highlightSphereCenter = drawBoundSphere && ( iSubLevel == 3 ) && selectedField.hasName("Center"); + shape->drawSelection_bone( this, iBone, drawBoundSphere, highlightSphereCenter ); + return true; + } + } + + return false; +} diff --git a/src/gl/glshape.h b/src/gl/glshape.h index c5b732676..09dfb32e7 100644 --- a/src/gl/glshape.h +++ b/src/gl/glshape.h @@ -36,22 +36,208 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "gl/glnode.h" // Inherited #include "gl/gltools.h" -#include -#include -#include //! @file glshape.h Shape -class NifModel; +class Shape; +// A set of vertices weighted to a bone ( BSSkinBoneTrans ) +class SkinBone +{ +public: + Transform transform; + const Node * node; + BoundSphere boundSphere; + QVector vertexWeights; + + inline SkinBone() + : node( nullptr ) {} + inline SkinBone( NifFieldConst boneDataEntry, const Node * boneNode ) + : transform( boneDataEntry ), node( boneNode ), boundSphere( boneDataEntry ) {} + + Transform localTransform( const Transform & parentTransform, int skeletonRoot ) const; +}; + + +// Base class for shape selection entries +class ShapeSelectionBase +{ + friend class Shape; + +public: + const Shape * shape; + NifFieldConst rootField; + NifFieldConst block; + int level; // The level of rootField in block. For sorting. + NifFieldConst mapField; // Vertex or bone map field to convert child indices + + ShapeSelectionBase() = delete; +protected: + ShapeSelectionBase( Shape * shape, NifFieldConst rootField, NifFieldConst mapField ); + + virtual bool process( NifFieldConst selectedField, int iSubLevel ) const = 0; + virtual void postUpdate() {}; + int remapIndex( int i ) const; +}; + + +enum class VertexSelectionType +{ + VERTICES, // Array of shape's vertices, with each child of rootField representing a vertex + NORMALS, // Same as VERTICES, but show also normals + TANGENTS, // Same as VERTICES, but show also tangents + BITANGENTS, // Same as VERTICES, but show also bitangents + BS_VERTEX_DATA, // Array of SSE+ vertex data structures + EXTRA_TANGENTS, // Byte array from extra data block storing tangents and bitangents of the shape + VERTEX_ROOT, // If rootField (not its children) is selected, then show all vertices of the shape +}; + + +// A vertex selection +class VertexSelection final : public ShapeSelectionBase +{ + friend class Shape; + +public: + VertexSelectionType type; + + VertexSelection() = delete; +private: + VertexSelection( Shape * _shape, NifFieldConst _rootField, VertexSelectionType _type, NifFieldConst _mapField ) + : ShapeSelectionBase( _shape, _rootField, _mapField ), type( _type ) {} + +protected: + bool process( NifFieldConst selectedField, int iSubLevel ) const override; +}; + + +// A range of triangles, with start and length (triangle arrays, skin partitions, LOD levels, ...) +class TriangleRange : public ShapeSelectionBase +{ + friend class Shape; + +public: + enum Flags : NifSkopeFlagsType + { + // rootField is a simple array of triangles or strip points + FLAG_ARRAY = 1U << 0, + // Draw wireframes of all triangles/strips in the shape, with the range highlighted + FLAG_HIGHLIGHT = 1U << 1, + // The range is considered selected if any of rootField's children is selected, no matter how deep it is + FLAG_DEEP = 1U << 2, + }; + NifSkopeFlagsType flags; + const TriangleRange * parentRange = nullptr; + + int start; + int length; + int realStart = 0; + int realLength = 0; + + TriangleRange() = delete; +protected: + TriangleRange( Shape * _shape, NifFieldConst _rootField, NifSkopeFlagsType _flags, int _start, int _length, NifFieldConst _mapField ) + : ShapeSelectionBase( _shape, _rootField, _mapField ), flags( _flags ), start( _start ), length( _length ) {} + +public: + bool isArray() const { return ( flags & FLAG_ARRAY ); } + bool isHighlight() const { return ( flags & FLAG_HIGHLIGHT ); } + bool isDeep() const { return ( flags & FLAG_DEEP ); } + + int realEnd() const { return realStart + realLength; } + + virtual const QVector & triangles() const; + virtual const QVector & triangleMap() const; + virtual const QVector & otherTriangles() const; + +protected: + void postUpdate() override; + bool process( NifFieldConst selectedField, int iSubLevel ) const override; +}; + +inline void glDrawTriangles( const TriangleRange * range ) +{ + if ( range ) + glDrawTriangles( range->triangles(), range->realStart, range->realLength ); +} + + +// A selection range for strip triangles of the shape +class StripRange final : public TriangleRange +{ + friend class Shape; + +public: + StripRange() = delete; +private: + StripRange( Shape * _shape, NifFieldConst _rootField, NifSkopeFlagsType _flags, int _start, int _length, NifFieldConst _mapField ) + : TriangleRange( _shape, _rootField, _flags, _start, _length, _mapField ) {} + +public: + const QVector & triangles() const override; + const QVector & triangleMap() const override; + const QVector & otherTriangles() const override; + +protected: + bool process( NifFieldConst selectedField, int iSubLevel ) const override; +}; + + +// Bounding sphere selection +class BoundSphereSelection final : public ShapeSelectionBase +{ + friend class Shape; + +public: + BoundSphere sphere; + Transform transform; + bool absoluteTransform = false; + + BoundSphereSelection() = delete; +private: + BoundSphereSelection( Shape * _shape, NifFieldConst _rootField ) + : ShapeSelectionBase( _shape, _rootField, NifFieldConst() ), sphere( _rootField ) {} + +protected: + bool process( NifFieldConst selectedField, int iSubLevel ) const override; +}; + + +// Skin bone selection +class BoneSelection final : public ShapeSelectionBase +{ + friend class Shape; + +public: + const TriangleRange * triRange; + + BoneSelection() = delete; +private: + BoneSelection( Shape * _shape, NifFieldConst _rootField, TriangleRange * _triRange, NifFieldConst _boneMapField ) + : ShapeSelectionBase( _shape, _rootField, _boneMapField ), triRange( _triRange ) {} + +protected: + bool process( NifFieldConst selectedField, int iSubLevel ) const override; +}; + + +// Base class for shape nodes class Shape : public Node { + friend class MorphInterpolator; friend class MorphController; - friend class UVController; + friend class UVInterpolator; friend class Renderer; + friend class ShapeSelectionBase; + friend class VertexSelection; + friend class TriangleRange; + friend class StripRange; + friend class BoundSphereSelection; + friend class BoneSelection; public: - Shape( Scene * s, const QModelIndex & b ); + Shape( Scene * _scene, NifFieldConst _block ); + virtual ~Shape(); // IControllable @@ -60,22 +246,28 @@ class Shape : public Node // end IControllable - virtual void drawVerts() const {}; - virtual QModelIndex vertexAt( int ) const { return QModelIndex(); }; + void drawShapes( NodeList * secondPass, bool presort ) override; + void drawSelection() const override; + + QModelIndex vertexAt( int vertexIndex ) const; + + bool isEditorMarker() const; + bool doSkinning() const; + + void fillViewModeWeights( double * outWeights, bool & outIsSkinned, const int * modeAxes ); protected: int shapeNumber; - void setController( const NifModel * nif, const QModelIndex & controller ) override; + Controller * createController( NifFieldConst controllerBlock ) override; void updateImpl( const NifModel * nif, const QModelIndex & index ) override; - virtual void updateData( const NifModel* nif ) = 0; - - void boneSphere( const NifModel * nif, const QModelIndex & index ) const; + void updateData(); + virtual void updateDataImpl() = 0; //! Shape data QPersistentModelIndex iData; //! Tangent data - QPersistentModelIndex iTangentData; + QPersistentModelIndex iExtraData; //! Does the data need updating? bool needUpdateData = false; @@ -86,8 +278,6 @@ class Shape : public Node //! Skin partition QPersistentModelIndex iSkinPart; - void resetSkinning(); - int numVerts = 0; //! Vertices @@ -102,14 +292,40 @@ class Shape : public Node QVector bitangents; //! UV coordinate sets QVector coords; + + QVector selections; + NifFieldConst mainVertexRoot; + + VertexSelection * addVertexSelection( NifFieldConst rootField, VertexSelectionType type, NifFieldConst mapField = NifFieldConst() ); + //! Triangles QVector triangles; - //! Strip points - QVector tristrips; + //! Map of triangle indices in the shape data to their indices in the QVector + QVector triangleMap; //! Sorted triangles - QVector sortedTriangles; + QVector sortedTriangles; // TODO: get rid of it - void resetVertexData(); + TriangleRange * addTriangleRange( NifFieldConst rangeRoot, NifSkopeFlagsType rangeFlags, int iStart, int nTris ); + TriangleRange * addTriangleRange( NifFieldConst rangeRoot, NifSkopeFlagsType rangeFlags, int iStart ); + + TriangleRange * addTriangles( NifFieldConst rangeRoot, const QVector & tris ); + TriangleRange * addTriangles( NifFieldConst arrayRoot ); + + //! Strip points + QVector stripTriangles; + QVector stripMap; + + StripRange * addStripRange( NifFieldConst rangeRoot, NifSkopeFlagsType rangeFlags, int iStart, int nStrips, NifFieldConst vertexMapField ); + StripRange * addStripRange( NifFieldConst rangeRoot, NifSkopeFlagsType rangeFlags, int iStart, NifFieldConst vertexMapField ); + StripRange * addStripRange( NifFieldConst rangeRoot, NifSkopeFlagsType rangeFlags, int iStart ); + + StripRange * addStrip( NifFieldConst stripPointsRoot, const QVector & stripTris, NifFieldConst vertexMapField ); + StripRange * addStrips( NifFieldConst stripsRoot, NifSkopeFlagsType rangeFlags ); + + BoundSphereSelection * addBoundSphereSelection( NifFieldConst rootField ); + + BoneSelection * addBoneSelection( NifFieldConst rootField, TriangleRange * triRange, NifFieldConst boneMapField = NifFieldConst() ); + BoneSelection * addPartitionBoneSelection( NifFieldConst rootField, TriangleRange * triRange ); //! Is the transform rigid or weighted? bool transformRigid = true; @@ -129,17 +345,22 @@ class Shape : public Node int skeletonRoot = 0; Transform skeletonTrans; - QVector bones; - QVector weights; - QVector partitions; + QVector bones; + + void initSkinBones( NifFieldConst nodeMapRoot, NifFieldConst nodeListRoot, NifFieldConst block ); + + void applySkinningTransforms( const Transform & skinTransform ); + void applyRigidTransforms(); - void resetSkeletonData(); + void applyColorTransforms( float alphaBlend = 1.0f ); + + void resetBlockData(); //! Holds the name of the shader, or "" if no shader QString shader = ""; //! Shader property - BSShaderLightingProperty * bssp = nullptr; + BSShaderProperty * bssp = nullptr; //! Skyrim shader property BSLightingShaderProperty * bslsp = nullptr; //! Skyrim effect shader property @@ -149,11 +370,16 @@ class Shape : public Node //! Is shader set to double sided? bool isDoubleSided = false; - //! Is shader set to animate using vertex alphas? - bool isVertexAlphaAnimation = false; + + bool hasVertexNormals = false; + bool hasVertexTangents = false; + bool hasVertexBitangents = false; + bool hasVertexUVs = false; //! Is "Has Vertex Colors" set to Yes bool hasVertexColors = false; + bool sRGB = false; + bool depthTest = true; bool depthWrite = true; bool drawInSecondPass = false; @@ -165,6 +391,113 @@ class Shape : public Node mutable bool needUpdateBounds = false; bool isLOD = false; + QVector lodLevels; + void initLodData(); + + + // drawSelection helpers +private: + + enum class DrawSelectionMode { + NO, + VERTICES, + VECTORS, + WIREFRAME, + BOUND_SPHERE, + }; + mutable DrawSelectionMode drawSelectionMode = DrawSelectionMode::NO; + + void drawSelection_begin( DrawSelectionMode newMode ) const; + void drawSelection_end() const; + + void drawSelection_triangles() const; + // Draw only the triangles from range + void drawSelection_triangles( const TriangleRange * range ) const; + // Draw all triangles of the shape, with the triangles from range highlighted with HighlightColor + void drawSelection_trianglesHighlighted( const TriangleRange * range ) const; + void drawSelection_triangles( const TriangleRange * partition, int iSelectedTri ) const; + + struct DrawVectorsData + { + bool drawNormals; + bool drawTangents; + bool drawBitangents; + float vectorScale; + }; + bool drawSelection_vectors_init( VertexSelectionType type, DrawVectorsData & outData ) const; + void drawSelection_vectors( int iStart, int nLength, const DrawVectorsData & drawData ) const; + + void drawSelection_vertices( VertexSelectionType type ) const; + void drawSelection_vertices( VertexSelectionType type, int iSelectedVertex ) const; + + void drawSelection_sphere( const BoundSphere & sphere, const Transform & transform, bool hightlightCenter ) const; + void drawSelection_boundSphere( const BoundSphereSelection * selSphere, bool hightlightCenter ) const; + void drawSelection_bone( const BoneSelection * selection, int iSelectedBone, bool drawBoundSphere, bool hightlightSphereCenter ) const; }; + +// Inlines + +inline Transform SkinBone::localTransform( const Transform & parentTransform, int skeletonRoot ) const +{ + if ( node ) + return parentTransform * node->localTrans( skeletonRoot ); + return parentTransform; +} + +inline void Shape::drawSelection_end() const +{ + drawSelection_begin( DrawSelectionMode::NO ); +} + +inline VertexSelection * Shape::addVertexSelection( NifFieldConst rootField, VertexSelectionType type, NifFieldConst mapField ) +{ + return rootField ? new VertexSelection( this, rootField, type, mapField ) : nullptr; +} + +inline TriangleRange * Shape::addTriangleRange( NifFieldConst rangeRoot, NifSkopeFlagsType rangeFlags, int iStart, int nTris ) +{ + return new TriangleRange( this, rangeRoot, rangeFlags, iStart, nTris, NifFieldConst() ); +} + +inline TriangleRange * Shape::addTriangleRange( NifFieldConst rangeRoot, NifSkopeFlagsType rangeFlags, int iStart ) +{ + return addTriangleRange( rangeRoot, rangeFlags, iStart, triangles.count() - iStart ); +} + +inline TriangleRange * Shape::addTriangles( NifFieldConst arrayRoot ) +{ + return arrayRoot ? addTriangles( arrayRoot, arrayRoot.array() ) : nullptr; +} + +inline StripRange * Shape::addStripRange( NifFieldConst rangeRoot, NifSkopeFlagsType rangeFlags, int iStart, int nStrips, NifFieldConst vertexMapField ) +{ + return new StripRange( this, rangeRoot, rangeFlags, iStart, nStrips, vertexMapField ); +} + +inline StripRange * Shape::addStripRange( NifFieldConst rangeRoot, NifSkopeFlagsType rangeFlags, int iStart, NifFieldConst vertexMapField ) +{ + return addStripRange( rangeRoot, rangeFlags, iStart, stripTriangles.count() - iStart, vertexMapField ); +} + +inline StripRange * Shape::addStripRange( NifFieldConst rangeRoot, NifSkopeFlagsType rangeFlags, int iStart ) +{ + return addStripRange( rangeRoot, rangeFlags, iStart, NifFieldConst() ); +} + +inline BoundSphereSelection * Shape::addBoundSphereSelection( NifFieldConst rootField ) +{ + return rootField ? new BoundSphereSelection( this, rootField ) : nullptr; +} + +inline BoneSelection * Shape::addBoneSelection( NifFieldConst rootField, TriangleRange * triRange, NifFieldConst boneMapField ) +{ + return rootField ? new BoneSelection( this, rootField, triRange, boneMapField ) : nullptr; +} + +inline BoneSelection * Shape::addPartitionBoneSelection( NifFieldConst rootField, TriangleRange * triRange ) +{ + return addBoneSelection( rootField, triRange, rootField ); +} + #endif diff --git a/src/gl/gltexloaders.cpp b/src/gl/gltexloaders.cpp index 57c91c046..b06633c9d 100644 --- a/src/gl/gltexloaders.cpp +++ b/src/gl/gltexloaders.cpp @@ -1008,8 +1008,8 @@ gli::texture load_if_valid( const char * data, unsigned int size ) dx DX; - format Format( static_cast(FORMAT_INVALID) ); - if ( (Header.Format.flags & (dx::DDPF_RGB | dx::DDPF_ALPHAPIXELS | dx::DDPF_ALPHA | dx::DDPF_YUV | dx::DDPF_LUMINANCE)) && Format == static_cast(gli::FORMAT_INVALID) && Header.Format.bpp != 0 ) { + format Format( static_cast(FORMAT_UNDEFINED) ); + if ( (Header.Format.flags & (dx::DDPF_RGB | dx::DDPF_ALPHAPIXELS | dx::DDPF_ALPHA | dx::DDPF_YUV | dx::DDPF_LUMINANCE)) && Format == static_cast(gli::FORMAT_UNDEFINED) && Header.Format.bpp != 0 ) { switch ( Header.Format.bpp ) { default: break; @@ -1080,13 +1080,13 @@ gli::texture load_if_valid( const char * data, unsigned int size ) break; } } - } else if ( (Header.Format.flags & dx::DDPF_FOURCC) && (Header.Format.fourCC != dx::D3DFMT_DX10) && (Header.Format.fourCC != dx::D3DFMT_GLI1) && (Format == static_cast(gli::FORMAT_INVALID)) ) { + } else if ( (Header.Format.flags & dx::DDPF_FOURCC) && (Header.Format.fourCC != dx::D3DFMT_DX10) && (Header.Format.fourCC != dx::D3DFMT_GLI1) && (Format == static_cast(gli::FORMAT_UNDEFINED)) ) { dx::d3dfmt const FourCC = remap_four_cc( Header.Format.fourCC ); Format = DX.find( FourCC ); } else if ( Header.Format.fourCC == dx::D3DFMT_DX10 || Header.Format.fourCC == dx::D3DFMT_GLI1 ) Format = DX.find( Header.Format.fourCC, Header10.Format ); - if ( Format == static_cast(FORMAT_INVALID) ) + if ( Format == static_cast(FORMAT_UNDEFINED) ) return texture(); size_t const MipMapCount = (Header.Flags & DDSD_MIPMAPCOUNT) ? Header.MipMapLevels : 1; diff --git a/src/gl/gltools.cpp b/src/gl/gltools.cpp index d4ae5d522..5e5a80dad 100644 --- a/src/gl/gltools.cpp +++ b/src/gl/gltools.cpp @@ -33,6 +33,7 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "gltools.h" #include "model/nifmodel.h" +#include "lib/Miniball.hpp" #include #include @@ -46,108 +47,10 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. //! \file gltools.cpp GL helper functions -BoneWeights::BoneWeights( const NifModel * nif, const QModelIndex & index, int b, int vcnt ) -{ - trans = Transform( nif, index ); - auto sph = BoundSphere( nif, index ); - center = sph.center; - radius = sph.radius; - bone = b; - - QModelIndex idxWeights = nif->getIndex( index, "Vertex Weights" ); - if ( vcnt && idxWeights.isValid() ) { - for ( int c = 0; c < nif->rowCount( idxWeights ); c++ ) { - QModelIndex idx = idxWeights.child( c, 0 ); - weights.append( VertexWeight( nif->get( idx, "Index" ), nif->get( idx, "Weight" ) ) ); - } - } -} - -void BoneWeights::setTransform( const NifModel * nif, const QModelIndex & index ) -{ - trans = Transform( nif, index ); - auto sph = BoundSphere( nif, index ); - center = sph.center; - radius = sph.radius; -} - -BoneWeightsUNorm::BoneWeightsUNorm(QVector> weights, int v) -{ - weightsUNORM.resize(weights.size()); - for ( int i = 0; i < weights.size(); i++ ) { - weightsUNORM[i] = BoneWeightUNORM16(weights[i].first, weights[i].second / 65535.0); - } -} - - -SkinPartition::SkinPartition( const NifModel * nif, const QModelIndex & index ) -{ - numWeightsPerVertex = nif->get( index, "Num Weights Per Vertex" ); - - vertexMap = nif->getArray( index, "Vertex Map" ); - - if ( vertexMap.isEmpty() ) { - vertexMap.resize( nif->get( index, "Num Vertices" ) ); - - for ( int x = 0; x < vertexMap.count(); x++ ) - vertexMap[x] = x; - } - - boneMap = nif->getArray( index, "Bones" ); - - QModelIndex iWeights = nif->getIndex( index, "Vertex Weights" ); - QModelIndex iBoneIndices = nif->getIndex( index, "Bone Indices" ); - - weights.resize( vertexMap.count() * numWeightsPerVertex ); - - for ( int v = 0; v < vertexMap.count(); v++ ) { - for ( int w = 0; w < numWeightsPerVertex; w++ ) { - QModelIndex iw = iWeights.child( v, 0 ).child( w, 0 ); - QModelIndex ib = iBoneIndices.child( v, 0 ).child( w, 0 ); - - weights[ v * numWeightsPerVertex + w ].first = ( ib.isValid() ? nif->get( ib ) : 0 ); - weights[ v * numWeightsPerVertex + w ].second = ( iw.isValid() ? nif->get( iw ) : 0 ); - } - } - - QModelIndex iStrips = nif->getIndex( index, "Strips" ); - - for ( int s = 0; s < nif->rowCount( iStrips ); s++ ) { - tristrips << nif->getArray( iStrips.child( s, 0 ) ); - } - - triangles = nif->getArray( index, "Triangles" ); -} - -QVector SkinPartition::getRemappedTriangles() const -{ - QVector tris; - - for ( const auto& t : triangles ) - tris << Triangle( vertexMap[t.v1()], vertexMap[t.v2()], vertexMap[t.v3()] ); - - return tris; -} - -QVector> SkinPartition::getRemappedTristrips() const -{ - QVector> tris; - - for ( const auto& t : tristrips ) { - QVector points; - for ( const auto& p : t ) - points << vertexMap[p]; - tris << points; - } - - return tris; -} - /* * Bound Sphere */ - BoundSphere::BoundSphere() { radius = -1; @@ -166,6 +69,7 @@ BoundSphere::BoundSphere( const BoundSphere & other ) BoundSphere::BoundSphere( const NifModel * nif, const QModelIndex & index ) { + // TODO: Replace with BoundSphere( NifFieldConst sphereRoot ) everywhere auto idx = index; auto sph = nif->getIndex( idx, "Bounding Sphere" ); if ( sph.isValid() ) @@ -175,26 +79,40 @@ BoundSphere::BoundSphere( const NifModel * nif, const QModelIndex & index ) radius = nif->get( idx, "Radius" ); } +BoundSphere::BoundSphere( NifFieldConst sphereRoot ) +{ + NifFieldConst sphere = sphereRoot.child("Bounding Sphere"); + if ( sphere ) + sphereRoot = sphere; + + center = sphereRoot["Center"].value(); + radius = sphereRoot["Radius"].value(); +} + BoundSphere::BoundSphere( const QVector & verts ) { if ( verts.isEmpty() ) { center = Vector3(); radius = -1; } else { - center = Vector3(); - for ( const Vector3& v : verts ) { - center += v; - } - center /= verts.count(); - - radius = 0; - for ( const Vector3& v : verts ) { - float d = ( center - v ).squaredLength(); - - if ( d > radius ) - radius = d; - } - radius = sqrt( radius ); + // Convert QVector to a QVector of pointers to Vector3 data. + QVector vptrs; + vptrs.reserve( verts.count() ); + for ( const auto & v : verts ) + vptrs << v.data(); + + // Create an instance of Miniball. + typedef const float * const * PointIterator; + typedef const float * CoordIterator; + Miniball::Miniball< Miniball::CoordAccessor > + mb(3, vptrs.begin(), vptrs.end()); + + const float * pCenter = mb.center(); + center[0] = pCenter[0]; + center[1] = pCenter[1]; + center[2] = pCenter[2]; + + radius = sqrt(mb.squared_radius()); } } @@ -408,7 +326,7 @@ QModelIndex bhkGetRBInfo( const NifModel * nif, const QModelIndex & index, const return{ x, y, z }; } -void drawAxesOverlay( const Vector3 & c, float axis, QVector axesOrder ) +void drawAxesOverlay( const Vector3 & c, float axis, const QVector & axesOrder, double uiScale ) { glPushMatrix(); glTranslate( c ); @@ -416,7 +334,7 @@ void drawAxesOverlay( const Vector3 & c, float axis, QVector axesOrder ) glDisable( GL_LIGHTING ); glDepthFunc( GL_ALWAYS ); - glLineWidth( 2.0f ); + glLineWidth( 2.0 * uiScale ); glBegin( GL_LINES ); // Render the X axis @@ -534,6 +452,8 @@ void drawGrid( int s /* grid size */, int line /* line spacing */, int sub /* # glDisable( GL_BLEND ); } +const int CIRCLE_SEGMENTS = 120; + void drawCircle( const Vector3 & c, const Vector3 & n, float r, int sd ) { Vector3 x = Vector3::crossproduct( n, Vector3( n[1], n[2], n[0] ) ); @@ -541,6 +461,32 @@ void drawCircle( const Vector3 & c, const Vector3 & n, float r, int sd ) drawArc( c, x * r, y * r, -PI, +PI, sd ); } +static float angleSection( double fullAngle, int nSection, int nTotalSections ) +{ + if ( nSection == 0 ) + return 0.0f; + if ( nSection == nTotalSections ) + return fullAngle; + return fullAngle * double(nSection) / double(nTotalSections); +} + +static float angleSection( double angleStart, double angleEnd, int nSection, int nTotalSections ) +{ + if ( nSection == 0 ) + return angleStart; + if ( nSection == nTotalSections ) + return angleEnd; + return ( (angleEnd - angleStart) * double(nSection) / double(nTotalSections) ) + angleStart; +} + +void glVertexArc( const Vector3 & center, const Vector3 & arcv1, const Vector3 & arcv2, double angleStart, double angleEnd, int nSections ) +{ + for ( int i = 0; i <= nSections; i++ ) { + float angle = angleSection( angleStart, angleEnd, i, nSections ); + glVertex( center + arcv1 * sin( angle ) + arcv2 * cos( angle ) ); + } +} + void drawArc( const Vector3 & c, const Vector3 & x, const Vector3 & y, float an, float ax, int sd ) { glBegin( GL_LINE_STRIP ); @@ -782,6 +728,32 @@ void drawSphere( const Vector3 & c, float r, int sd ) } } +void drawSphereNew( const Vector3 & center, float radius, int segments, const Transform & transform ) +{ + Transform radTrans( transform ); + radTrans.translation = Vector3(); + + Vector3 c = transform * center; + + for ( int z = 1; z < segments; z++ ) { + float angle = angleSection( PI, z, segments ); + Vector3 cz = c + radTrans * Vector3( 0, 0, radius * cos(angle) ); + float rz = radius * sin(angle); + + glBegin( GL_LINE_STRIP ); + glVertexArc( cz, radTrans * Vector3( rz, 0, 0 ), radTrans * Vector3( 0, rz, 0 ), -PI, PI, CIRCLE_SEGMENTS ); + glEnd(); + } + + Vector3 vz = radTrans * Vector3( 0, 0, radius ); + for ( int x = 0; x < segments; x++ ) { + float angle = angleSection( PI, x, segments ); + glBegin(GL_LINE_STRIP); + glVertexArc( c, radTrans * Vector3( radius * cos(angle), radius * sin(angle), 0 ), vz, -PI, PI, CIRCLE_SEGMENTS ); + glEnd(); + } +} + void drawCapsule( const Vector3 & a, const Vector3 & b, float r, int sd ) { Vector3 d = b - a; @@ -1094,6 +1066,26 @@ void drawCMS( const NifModel * nif, const QModelIndex & iShape, bool solid ) } } +void glSelectionBufferColor( int lowId ) +{ + if ( lowId >= 0 && lowId < 0xFFFF ) { + uint32_t idColor = lowId + 1; + glColor3ubv( (GLubyte *) &idColor ); + } else { + glColor4f( 0, 0, 0, 1 ); + } +} + +void glSelectionBufferColor( int highId, int lowId ) +{ + if ( highId >= 0 && highId < 0xFF && lowId >= 0 && lowId <= 0xFFFF ) { + uint32_t idColor = ( (highId + 1) << 16 ) + lowId; + glColor3ubv( (GLubyte *) &idColor ); + } else { + glColor4f( 0, 0, 0, 1 ); + } +} + // Renders text using the font initialized in the primary view class void renderText( const Vector3 & c, const QString & str ) { @@ -1117,4 +1109,3 @@ void renderText( double x, double y, double z, const QString & str ) glCallLists( cstr.size(), GL_UNSIGNED_BYTE, cstr.constData() ); glPopAttrib(); } - diff --git a/src/gl/gltools.h b/src/gl/gltools.h index 69c8df9c9..bc46cef9f 100644 --- a/src/gl/gltools.h +++ b/src/gl/gltools.h @@ -34,6 +34,7 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #define GLTOOLS_H #include "data/niftypes.h" +#include "model/nifmodel.h" #include #include @@ -41,13 +42,6 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. //! @file gltools.h BoundSphere, VertexWeight, BoneWeights, SkinPartition - -using TriStrip = QVector; -Q_DECLARE_TYPEINFO(TriStrip, Q_MOVABLE_TYPE); -using TexCoords = QVector; -Q_DECLARE_TYPEINFO(TexCoords, Q_MOVABLE_TYPE); - - //! A bounding sphere for an object, typically a Mesh class BoundSphere final { @@ -55,6 +49,7 @@ class BoundSphere final BoundSphere(); BoundSphere( const BoundSphere & ); BoundSphere( const NifModel * nif, const QModelIndex & ); + BoundSphere( NifFieldConst sphereRoot ); BoundSphere( const Vector3 & center, float radius ); BoundSphere( const QVector & vertices ); @@ -81,77 +76,14 @@ class VertexWeight final { public: VertexWeight() - { vertex = 0; weight = 0.0; } + : vertex( 0 ), weight( 0.0f ) {} VertexWeight( int v, float w ) - { vertex = v; weight = w; } + : vertex( v ), weight( w ) {} int vertex; float weight; }; -//! A bone, weight pair -class BoneWeightUNORM16 final -{ -public: - BoneWeightUNORM16() - { - bone = 0; weight = 0.0; - } - BoneWeightUNORM16(quint16 b, float w) - { - bone = b; weight = w; - } - - quint16 bone; - float weight; -}; - -//! A set of vertices weighted to a bone -class BoneWeights -{ -public: - BoneWeights() {} - BoneWeights( const NifModel * nif, const QModelIndex & index, int b, int vcnt = 0 ); - - void setTransform( const NifModel * nif, const QModelIndex & index ); - - Transform trans; - Vector3 center; - float radius = 0; - Vector3 tcenter; - int bone = 0; - QVector weights; -}; - -class BoneWeightsUNorm : public BoneWeights -{ -public: - BoneWeightsUNorm() {} - BoneWeightsUNorm(QVector> weights, int v); - - QVector weightsUNORM; -}; - -//! A skin partition -class SkinPartition final -{ -public: - SkinPartition() { numWeightsPerVertex = 0; } - SkinPartition( const NifModel * nif, const QModelIndex & index ); - - QVector getRemappedTriangles() const; - QVector> getRemappedTristrips() const; - - QVector boneMap; - QVector vertexMap; - - int numWeightsPerVertex; - QVector > weights; - - QVector triangles; - QVector > tristrips; -}; - float bhkScale( const NifModel * nif ); float bhkInvScale( const NifModel * nif ); float bhkScaleMult( const NifModel * nif ); @@ -164,7 +96,7 @@ QModelIndex bhkGetRBInfo( const NifModel * nif, const QModelIndex & index, const QVector sortAxes( QVector axesDots ); void drawAxes( const Vector3 & c, float axis, bool color = true ); -void drawAxesOverlay( const Vector3 & c, float axis, QVector axesOrder = {2, 1, 0} ); +void drawAxesOverlay( const Vector3 & c, float axis, const QVector & axesOrder, double uiScale ); void drawGrid( int s, int line, int sub ); void drawBox( const Vector3 & a, const Vector3 & b ); void drawCircle( const Vector3 & c, const Vector3 & n, float r, int sd = 16 ); @@ -173,6 +105,7 @@ void drawSolidArc( const Vector3 & c, const Vector3 & n, const Vector3 & x, cons void drawCone( const Vector3 & c, Vector3 n, float a, int sd = 16 ); void drawRagdollCone( const Vector3 & pivot, const Vector3 & twist, const Vector3 & plane, float coneAngle, float minPlaneAngle, float maxPlaneAngle, int sd = 16 ); void drawSphereSimple( const Vector3 & c, float r, int sd = 36 ); +void drawSphereNew( const Vector3 & center, float radius, int segments, const Transform & transform ); void drawSphere( const Vector3 & c, float r, int sd = 8 ); void drawCapsule( const Vector3 & a, const Vector3 & b, float r, int sd = 5 ); void drawDashLine( const Vector3 & a, const Vector3 & b, int sd = 15 ); @@ -207,6 +140,11 @@ inline void glVertex( const Vector4 & v ) glVertex3fv( v.data() ); } +inline void glVertex( const Vector3 * pv ) +{ + glVertex3fv( pv->data() ); +} + inline void glNormal( const Vector3 & v ) { glNormal3fv( v.data() ); @@ -227,6 +165,15 @@ inline void glColor( const Color4 & c ) glColor4fv( c.data() ); } +void glSelectionBufferColor( int lowId ); + +inline void glSelectionBufferColor( const NifModel * nif, const QModelIndex & index ) +{ + glSelectionBufferColor( nif->getBlockNumber(index) ); +} + +void glSelectionBufferColor( int highId, int lowId ); + inline void glMaterial( GLenum x, GLenum y, const Color4 & c ) { glMaterialfv( x, y, c.data() ); @@ -252,6 +199,16 @@ inline void glMultMatrix( const Transform & t ) glMultMatrix( t.toMatrix4() ); } +inline void glDrawTriangles( const QVector & tris, int iStartOffset, int nTris ) +{ + if ( nTris > 0) + glDrawElements( GL_TRIANGLES, nTris * 3, GL_UNSIGNED_SHORT, tris.constData() + iStartOffset ); +} + +inline void glDrawTriangles( const QVector & tris ) +{ + glDrawTriangles( tris, 0, tris.count() ); +} inline GLuint glClosestMatch( GLuint * buffer, GLint hits ) { @@ -272,7 +229,4 @@ inline GLuint glClosestMatch( GLuint * buffer, GLint hits ) void renderText( double x, double y, double z, const QString & str ); void renderText( const Vector3 & c, const QString & str ); -#define ID2COLORKEY( id ) (id + 1) -#define COLORKEY2ID( id ) (id - 1) - #endif diff --git a/src/gl/renderer.cpp b/src/gl/renderer.cpp index 07355a7ae..2890a8f3a 100644 --- a/src/gl/renderer.cpp +++ b/src/gl/renderer.cpp @@ -493,7 +493,7 @@ QString Renderer::setupProgram( Shape * mesh, const QString & hint ) QVector iBlocks; iBlocks << mesh->index(); iBlocks << mesh->iData; - for ( Property * p : props.list() ) { + for ( Property * p : props.hash() ) { iBlocks.append( p->index() ); } @@ -559,46 +559,37 @@ void Renderer::Program::uni4m( UniformType var, const Matrix4 & val ) f->glUniformMatrix4fv( uniformLocations[var], 1, 0, val.data() ); } -bool Renderer::Program::uniSampler( BSShaderLightingProperty * bsprop, UniformType var, +void Renderer::Program::uniSampler( BSShaderProperty * bsprop, UniformType var, int textureSlot, int & texunit, const QString & alternate, - uint clamp, const QString & forced ) + TextureClampMode clamp, const QString & forced ) { GLint uniSamp = uniformLocations[var]; if ( uniSamp >= 0 ) { // TODO: On stream 155 bsprop->fileName can reference incorrect strings because // the BSSTS is not filled out nor linked from the BSSP - QString fname = (forced.isEmpty()) ? bsprop->fileName( textureSlot ) : forced; + QString fname = forced.isEmpty() ? bsprop->fileName( textureSlot ) : forced; if ( fname.isEmpty() ) fname = alternate; if ( !fname.isEmpty() && (!activateTextureUnit( texunit ) - || !(bsprop->bind( textureSlot, fname, TexClampMode(clamp) ) - || bsprop->bind( textureSlot, alternate, TexClampMode(3) ))) ) - return uniSamplerBlank( var, texunit ); - - f->glUniform1i( uniSamp, texunit++ ); - - return true; + || !(bsprop->bind( textureSlot, fname, clamp ) + || bsprop->bind( textureSlot, alternate, TextureClampMode::WrapS_WrapT ))) ) + { + uniSamplerBlank( var, texunit ); + } else { + f->glUniform1i( uniSamp, texunit++ ); + } } - - return true; } -bool Renderer::Program::uniSamplerBlank( UniformType var, int & texunit ) +void Renderer::Program::uniSamplerBlank( UniformType var, int & texunit ) { GLint uniSamp = uniformLocations[var]; - if ( uniSamp >= 0 ) { - if ( !activateTextureUnit( texunit ) ) - return false; - + if ( uniSamp >= 0 && activateTextureUnit( texunit ) ) { glBindTexture( GL_TEXTURE_2D, 0 ); f->glUniform1i( uniSamp, texunit++ ); - - return true; } - - return true; } static QString white = "shaders/white.dds"; @@ -624,18 +615,15 @@ bool Renderer::setupProgram( Program * prog, Shape * mesh, const PropertyList & auto nifVersion = nif->getBSVersion(); auto scene = mesh->scene; + auto bsprop = mesh->bssp; auto lsp = mesh->bslsp; auto esp = mesh->bsesp; - Material * mat = nullptr; - if ( lsp ) - mat = lsp->getMaterial(); - else if ( esp ) - mat = esp->getMaterial(); + Material * mat = bsprop ? bsprop->getMaterial() : nullptr; QString default_n = (nifVersion >= 151) ? ::default_ns : ::default_n; - // TODO: Temp for pre CDB material reading + // TODO: Temp for pre CDB material reading (Starfield) if ( !mat && nifVersion >= 172 ) { if ( lsp ) { mesh->depthWrite = true; @@ -649,14 +637,11 @@ bool Renderer::setupProgram( Program * prog, Shape * mesh, const PropertyList & // texturing TexturingProperty * texprop = props.get(); - BSShaderLightingProperty * bsprop = mesh->bssp; - // BSShaderLightingProperty * bsprop = props.get(); - // TODO: BSLSP has been split off from BSShaderLightingProperty so it needs + // BSShaderProperty * bsprop = props.get(); + // TODO: BSLSP has been split off from BSShaderProperty so it needs // to be accessible from here - TexClampMode clamp = TexClampMode::WRAP_S_WRAP_T; - if ( lsp ) - clamp = lsp->clampMode; + TextureClampMode clamp = bsprop ? bsprop->clampMode : TextureClampMode::WrapS_WrapT; int texunit = 0; if ( bsprop ) { @@ -668,10 +653,10 @@ bool Renderer::setupProgram( Program * prog, Shape * mesh, const PropertyList & if ( scene->hasOption(Scene::DoErrorColor) && nifVersion < 172 ) // TODO: Hide error color until CDB reading alt = magenta; - bool result = prog->uniSampler( bsprop, SAMP_BASE, 0, texunit, alt, clamp, forced ); + prog->uniSampler( bsprop, SAMP_BASE, 0, texunit, alt, clamp, forced ); } else { GLint uniBaseMap = prog->uniformLocations[SAMP_BASE]; - if ( uniBaseMap >= 0 && (texprop || (bsprop && lsp)) ) { + if ( uniBaseMap >= 0 && (texprop || lsp) ) { if ( !activateTextureUnit( texunit ) || (texprop && !texprop->bind( 0 )) ) prog->uniSamplerBlank( SAMP_BASE, texunit ); else @@ -750,7 +735,7 @@ bool Renderer::setupProgram( Program * prog, Shape * mesh, const PropertyList & prog->uni4m( MAT_WORLD, mesh->worldTrans().toMatrix4() ); prog->uni1i( G2P_COLOR, lsp->greyscaleColor ); - prog->uniSampler( bsprop, SAMP_GRAYSCALE, 3, texunit, "", TexClampMode::MIRRORED_S_MIRRORED_T ); + prog->uniSampler( bsprop, SAMP_GRAYSCALE, 3, texunit, "", TextureClampMode::MirrorS_MirrorT ); prog->uni1i( HAS_TINT_COLOR, lsp->hasTintColor ); if ( lsp->hasTintColor ) { @@ -840,9 +825,10 @@ bool Renderer::setupProgram( Program * prog, Shape * mesh, const PropertyList & if ( fname.isEmpty() ) fname = cube; - if ( !activateTextureUnit( texunit ) || !bsprop->bindCube( 4, fname ) ) - if ( !activateTextureUnit( texunit ) || !bsprop->bindCube( 4, cube ) ) - return false; + if ( !activateTextureUnit( texunit ) ) + return false; + if ( !bsprop->bindCube( fname ) && !bsprop->bindCube( cube ) ) + return false; fn->glUniform1i( uniCubeMap, texunit++ ); } @@ -865,8 +851,6 @@ bool Renderer::setupProgram( Program * prog, Shape * mesh, const PropertyList & prog->uni4m( MAT_WORLD, mesh->worldTrans().toMatrix4() ); - clamp = esp->clampMode; - prog->uniSampler( bsprop, SAMP_BASE, 0, texunit, white, clamp ); prog->uni1i( DOUBLE_SIDE, esp->isDoubleSided ); @@ -900,7 +884,7 @@ bool Renderer::setupProgram( Program * prog, Shape * mesh, const PropertyList & prog->uni1f( FALL_DEPTH, esp->falloff.softDepth ); // BSEffectShader textures - prog->uniSampler( bsprop, SAMP_GRAYSCALE, 1, texunit, "", TexClampMode::MIRRORED_S_MIRRORED_T ); + prog->uniSampler( bsprop, SAMP_GRAYSCALE, 1, texunit, "", TextureClampMode::MirrorS_MirrorT ); if ( nifVersion >= 130 ) { @@ -924,10 +908,10 @@ bool Renderer::setupProgram( Program * prog, Shape * mesh, const PropertyList & if ( fname.isEmpty() ) fname = cube; - if ( !activateTextureUnit( texunit ) || !bsprop->bindCube( 2, fname ) ) - if ( !activateTextureUnit( texunit ) || !bsprop->bindCube( 2, cube ) ) - return false; - + if ( !activateTextureUnit( texunit ) ) + return false; + if ( !bsprop->bindCube( fname ) && !bsprop->bindCube( cube ) ) + return false; fn->glUniform1i( uniCubeMap, texunit++ ); } @@ -1308,7 +1292,7 @@ void Renderer::setupFixedFunction( Shape * mesh, const PropertyList & props ) } else if ( TextureProperty * texprop = props.get() ) { // old single texture property texprop->bind( mesh->coords ); - } else if ( BSShaderLightingProperty * texprop = props.get() ) { + } else if ( BSShaderProperty * texprop = props.get() ) { // standard multi texturing property int stage = 0; @@ -1331,6 +1315,13 @@ void Renderer::setupFixedFunction( Shape * mesh, const PropertyList & props ) glTexEnvi( GL_TEXTURE_ENV, GL_OPERAND1_ALPHA, GL_SRC_ALPHA ); glTexEnvf( GL_TEXTURE_ENV, GL_RGB_SCALE, 1.0 ); + + if ( mesh->translucent ) { + glEnable( GL_BLEND ); + glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA ); + // If mesh is alpha tested, override threshold + glAlphaFunc( GL_GREATER, 0.1f ); + } } } else { glDisable( GL_TEXTURE_2D ); diff --git a/src/gl/renderer.h b/src/gl/renderer.h index 90e1b52dd..606b0282e 100644 --- a/src/gl/renderer.h +++ b/src/gl/renderer.h @@ -35,6 +35,8 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include +#include "gl/glproperty.h" + #include #include #include @@ -46,9 +48,7 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. //! @file renderer.h Renderer, Renderer::ConditionSingle, Renderer::ConditionGroup, Renderer::Shader, Renderer::Program -class NifModel; class Shape; -class PropertyList; class QOpenGLContext; class QOpenGLFunctions; @@ -354,9 +354,9 @@ public slots: void uni1i( UniformType var, int val ); void uni3m( UniformType var, const Matrix & val ); void uni4m( UniformType var, const Matrix4 & val ); - bool uniSampler( class BSShaderLightingProperty * bsprop, UniformType var, int textureSlot, - int & texunit, const QString & alternate, uint clamp, const QString & forced = {} ); - bool uniSamplerBlank( UniformType var, int & texunit ); + void uniSampler( BSShaderProperty * bsprop, UniformType var, int textureSlot, + int & texunit, const QString & alternate, TextureClampMode clamp, const QString & forced = {} ); + void uniSamplerBlank( UniformType var, int & texunit ); }; QMap shaders; diff --git a/src/glview.cpp b/src/glview.cpp index c0addf922..9506d8fc3 100644 --- a/src/glview.cpp +++ b/src/glview.cpp @@ -40,28 +40,15 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "model/nifmodel.h" #include "ui/settingsdialog.h" #include "ui/widgets/fileselect.h" +#include "ui/UiUtils.h" #include -#include -#include -#include -#include #include -#include #include -#include #include -#include -#include -#include #include #include -#include -#include -#include -#include #include -#include #include #include @@ -316,7 +303,8 @@ void GLView::initializeGL() // Made viewport and aspect member variables. // They were being updated every single frame instead of only when resizing. //glGetIntegerv( GL_VIEWPORT, viewport ); - aspect = (GLdouble)width() / (GLdouble)height(); + if ( viewportWidth < 0 ) + cacheViewportSize(); // Check for errors while ( ( err = glGetError() ) != GL_NO_ERROR ) @@ -332,10 +320,8 @@ void GLView::updateShaders() update(); } -void GLView::glProjection( int x, int y ) +void GLView::glProjection( [[maybe_unused]] int x, [[maybe_unused]] int y ) { - Q_UNUSED( x ); Q_UNUSED( y ); - glMatrixMode( GL_PROJECTION ); glLoadIdentity(); @@ -351,7 +337,7 @@ void GLView::glProjection( int x, int y ) GLdouble nr = fabs( bs.center[2] ) - bounds * 1.5; GLdouble fr = fabs( bs.center[2] ) + bounds * 1.5; - if ( perspectiveMode || (view == ViewWalk) ) { + if ( perspectiveMode || view == ViewWalk ) { // Perspective View if ( nr < 1.0 * scale() ) nr = 1.0 * scale(); @@ -360,24 +346,22 @@ void GLView::glProjection( int x, int y ) if ( nr > fr ) { // add: swap them when needed - GLfloat tmp = nr; - nr = fr; - fr = tmp; + std::swap( nr, fr ); } - if ( (fr - nr) < 0.00001f ) { + if ( (fr - nr) < 0.00001 ) { // add: ensure distance nr = 1.0 * scale(); fr = 2.0 * scale(); } - GLdouble h2 = tan( ( cfg.fov / Zoom ) / 360 * M_PI ) * nr; - GLdouble w2 = h2 * aspect; + GLdouble h2 = tan( double( cfg.fov ) * M_PI / ( Zoom * 360.0 ) ) * nr; + GLdouble w2 = h2 * aspectWidth / aspectHeight; glFrustum( -w2, +w2, -h2, +h2, nr, fr ); } else { // Orthographic View GLdouble h2 = Dist / Zoom; - GLdouble w2 = h2 * aspect; + GLdouble w2 = h2 * aspectWidth / aspectHeight; glOrtho( -w2, +w2, -h2, +h2, nr, fr ); } @@ -421,22 +405,34 @@ void GLView::paintGL() if ( doCompile ) { textures->setNifFolder( model->getFolder() ); scene->make( model ); - scene->transform( Transform(), scene->timeMin() ); + + auto tMin = scene->timeMin(); + auto tMax = scene->timeMax(); + + scene->transform( Transform(), tMin ); axis = (scene->bounds().radius <= 0) ? 1024.0 * scale() : scene->bounds().radius; - if ( scene->timeMin() != scene->timeMax() ) { - if ( time < scene->timeMin() || time > scene->timeMax() ) - time = scene->timeMin(); + if ( doLoadReset ) { + setViewMode( getDefaultViewMode() ); + center(); // Force (re)center the view on the model because setViewMode not always does this. + + time = tMin; + } + + if ( tMin != tMax ) { + if ( time < tMin || time > tMax ) + time = tMin; emit sequencesUpdated(); - } else if ( scene->timeMax() == 0 ) { + } else if ( tMax == 0 ) { // No Animations in this NIF emit sequencesDisabled( true ); } - emit sceneTimeChanged( time, scene->timeMin(), scene->timeMax() ); + emit sceneTimeChanged( time, tMin, tMax ); doCompile = false; + doLoadReset = false; } // Center the model @@ -627,7 +623,6 @@ void GLView::paintGL() if ( scene->hasOption(Scene::ShowAxes) ) { // Resize viewport to small corner of screen - int axesSize = std::min( width() / 10, 125 ); glViewport( 0, 0, axesSize, axesSize ); // Reset matrices @@ -635,9 +630,9 @@ void GLView::paintGL() glLoadIdentity(); // Square frustum - auto nr = 1.0; - auto fr = 250.0; - GLdouble h2 = tan( cfg.fov / 360 * M_PI ) * nr; + double nr = 1.0; + double fr = 250.0; + GLdouble h2 = tan( double( cfg.fov ) * M_PI / 360.0 ) * nr; GLdouble w2 = h2; glFrustum( -w2, +w2, -h2, +h2, nr, fr ); @@ -651,7 +646,7 @@ void GLView::paintGL() auto viewTransOrig = viewTrans.translation; // Zoom out slightly - viewTrans.translation = { 0, 0, -150.0 }; + viewTrans.translation = { 0, 0, -140.0 }; // Load modified viewTrans glLoadMatrix( viewTrans ); @@ -663,12 +658,12 @@ void GLView::paintGL() auto vtr = viewTrans.rotation; QVector axesDots = { vtr( 2, 0 ), vtr( 2, 1 ), vtr( 2, 2 ) }; - drawAxesOverlay( { 0, 0, 0 }, 50.0, sortAxes( axesDots ) ); + drawAxesOverlay( { 0, 0, 0 }, 50.0, sortAxes( axesDots ), uiScale ); glPopMatrix(); // Restore viewport size - glViewport( 0, 0, width(), height() ); + glViewport( 0, 0, viewportWidth, viewportHeight ); // Restore matrices glProjection(); } @@ -696,15 +691,13 @@ void GLView::paintGL() } -void GLView::resizeGL( int width, int height ) +void GLView::resizeGL( [[maybe_unused]] int width, [[maybe_unused]] int height ) { - resize( width, height ); - makeCurrent(); if ( !isValid() ) return; - aspect = (GLdouble)width / (GLdouble)height; - glViewport( 0, 0, width, height ); + cacheViewportSize(); + glViewport( 0, 0, viewportWidth, viewportHeight ); glDisable(GL_FRAMEBUFFER_SRGB); qglClearColor(clearColor()); @@ -773,9 +766,8 @@ void GLView::setVisMode( Scene::VisMode mode, bool checked ) typedef void (Scene::* DrawFunc)( void ); -int indexAt( /*GLuint *buffer,*/ NifModel * model, Scene * scene, QList drawFunc, int cycle, const QPoint & pos, int & furn ) +uint32_t indexAt( Scene * scene, const QList & drawFunc, const QPoint & pos ) { - Q_UNUSED( model ); Q_UNUSED( cycle ); // Color Key O(1) selection // Open GL 3.0 says glRenderMode is deprecated // ATI OpenGL API implementation of GL_SELECT corrupts NifSkope memory @@ -831,31 +823,8 @@ int indexAt( /*GLuint *buffer,*/ NifModel * model, Scene * scene, QList> 0 - // G = (id & 0x0000FF00) >> 8 - // B = (id & 0x00FF0000) >> 16 - - int choose = COLORKEY2ID( a ); - - // Pick BSFurnitureMarker - if ( choose > 0 ) { - auto furnBlock = model->getBlockIndex( model->index( 3, 0, model->getBlockIndex( choose & 0x0ffff ) ), "BSFurnitureMarker" ); - - if ( furnBlock.isValid() ) { - furn = choose >> 16; - choose &= 0x0ffff; - } - } - - //qDebug() << "Key:" << a << " R" << pixel.red() << " G" << pixel.green() << " B" << pixel.blue(); - return choose; + // Decode RGB to id + return ( pixel.blue() << 16 ) | ( pixel.green() << 8 ) | pixel.red(); } QModelIndex GLView::indexAt( const QPoint & pos, int cycle ) @@ -889,8 +858,7 @@ QModelIndex GLView::indexAt( const QPoint & pos, int cycle ) df << &Scene::drawShapes; - int choose = -1, furn = -1; - choose = ::indexAt( model, scene, df, cycle, pos, /*out*/ furn ); + uint32_t idColor = ::indexAt( scene, df, pos ); glPopAttrib(); glMatrixMode( GL_MODELVIEW ); @@ -900,22 +868,23 @@ QModelIndex GLView::indexAt( const QPoint & pos, int cycle ) QModelIndex chooseIndex; - if ( scene->isSelModeVertex() ) { - // Vertex - int block = choose >> 16; - int vert = choose - (block << 16); + if ( idColor != 0 ) { + int lowId = idColor & 0xFFFF; + int highId = idColor >> 16; - auto shape = scene->shapes.value( block ); - if ( shape ) - chooseIndex = shape->vertexAt( vert ); - } else if ( choose != -1 ) { - // Block Index - chooseIndex = model->getBlockIndex( choose ); - - if ( furn != -1 ) { - // Furniture Row @ Block Index - chooseIndex = model->index( furn, 0, model->index( 3, 0, chooseIndex ) ); - } + if ( scene->isSelModeVertex() ) { + if ( highId > 0 ) { // Shape vertex selection + auto shape = scene->shapes.value( highId - 1 ); + if ( shape ) + chooseIndex = shape->vertexAt( lowId ); + } + } else { + if ( highId > 0 ) { // Furniture row selection + chooseIndex = model->block( lowId ).child("Positions").child( highId - 1 ).toIndex(); + } else { // Block selection + chooseIndex = model->getBlockIndex( lowId - 1 ); + } + } } return chooseIndex; @@ -927,18 +896,17 @@ void GLView::center() update(); } -void GLView::move( float x, float y, float z ) +void GLView::moveBy( float x, float y, float z ) { Pos += Matrix::euler( deg2rad(Rot[0]), deg2rad(Rot[1]), deg2rad(Rot[2]) ).inverted() * Vector3( x, y, z ); - updateViewpoint(); update(); + resetViewMode(); } -void GLView::rotate( float x, float y, float z ) +void GLView::rotateBy( float x, float y, float z ) { - Rot += Vector3( x, y, z ); - updateViewpoint(); - update(); + setRotation( Rot[0] + x, Rot[1] + y, Rot[2] + z ); + resetViewMode(); } void GLView::setCenter() @@ -949,7 +917,7 @@ void GLView::setCenter() // Center on selected node BoundSphere bs = node->bounds(); - this->setPosition( -bs.center ); + setPosition( -bs.center ); if ( bs.radius > 0 ) { setDistance( bs.radius * 1.2 ); @@ -965,8 +933,6 @@ void GLView::setCenter() setZoom( 1.0 ); setPosition( -bs.center ); - - setOrientation( view ); } } @@ -996,7 +962,9 @@ void GLView::setProjection( bool isPersp ) void GLView::setRotation( float x, float y, float z ) { - Rot = { x, y, z }; + Rot[0] = normDeg( x ); + Rot[1] = normDeg( y ); + Rot[2] = normDeg( z ); update(); } @@ -1014,90 +982,130 @@ void GLView::setZoom( float z ) } -void GLView::flipOrientation() +void GLView::flipView() { - ViewState tmp = ViewDefault; - switch ( view ) { case ViewTop: - tmp = ViewBottom; + setViewMode( ViewBottom ); break; case ViewBottom: - tmp = ViewTop; + setViewMode( ViewTop ); break; case ViewLeft: - tmp = ViewRight; + setViewMode( ViewRight ); break; case ViewRight: - tmp = ViewLeft; + setViewMode( ViewLeft ); break; case ViewFront: - tmp = ViewBack; + setViewMode( ViewBack ); break; case ViewBack: - tmp = ViewFront; + setViewMode( ViewFront ); break; - case ViewUser: default: - { - // TODO: Flip any other view also? - } + setRotation( 180.0f - Rot[0], Rot[1], Rot[2] + 180.0f ); + resetViewMode(); break; } - - setOrientation( tmp, false ); } -void GLView::setOrientation( GLView::ViewState state, bool recenter ) +void GLView::setViewMode( GLView::ViewState newView ) { - if ( state == view ) + if ( view == newView ) return; - switch ( state ) { + auto oldView = view; + view = newView; + + auto onDirViewModeChange = [this, oldView]( float rotX, float rotY, float rotZ ) { + setRotation( rotX, rotY, rotZ ); + if ( oldView != ViewBottom && oldView != ViewTop && oldView != ViewBack && oldView != ViewFront && oldView != ViewRight && oldView != ViewLeft ) + center(); + }; + + switch ( view ) { case ViewBottom: - setRotation( 180, 0, 0 ); // Bottom + onDirViewModeChange( 180, 0, 0 ); // Bottom break; case ViewTop: - setRotation( 0, 0, 0 ); // Top + onDirViewModeChange( 0, 0, 0 ); // Top break; case ViewBack: - setRotation( -90, 0, 0 ); // Back + onDirViewModeChange( -90, 0, 0 ); // Back break; case ViewFront: - setRotation( -90, 0, 180 ); // Front + onDirViewModeChange( -90, 0, 180 ); // Front break; case ViewRight: - setRotation( -90, 0, 90 ); // Right + onDirViewModeChange( -90, 0, 90 ); // Right break; case ViewLeft: - setRotation( -90, 0, -90 ); // Left + onDirViewModeChange( -90, 0, -90 ); // Left break; - default: + case ViewWalk: + center(); break; } - view = state; + emit viewModeChanged(); +} - // Recenter - if ( recenter ) - center(); +void GLView::resetViewMode() +{ + if ( view != ViewDefault && view != ViewWalk ) + setViewMode( ViewDefault ); } -void GLView::updateViewpoint() +GLView::ViewState GLView::getDefaultViewMode() { - switch ( view ) { - case ViewTop: - case ViewBottom: - case ViewLeft: - case ViewRight: - case ViewFront: - case ViewBack: - case ViewUser: - emit viewpointChanged(); - break; - default: - break; + const GLView::ViewState defaultMode = GLView::ViewFront; + GLView::ViewState resultMode = defaultMode; + + if ( scene->shapes.count() > 0 ) { + double modeWeights[GLView::ViewLast + 1] = { 0.0 }; + bool hasSkinnedShapes = false; + + const int modeAxesX[] = { + GLView::ViewTop, GLView::ViewBottom, + GLView::ViewLeft, GLView::ViewRight, + GLView::ViewFront, GLView::ViewBack, + }; + const int modeAxesY[] = { + GLView::ViewFront, GLView::ViewBack, + GLView::ViewTop, GLView::ViewBottom, + GLView::ViewLeft, GLView::ViewRight, + }; + const int modeAxesZ[] = { + GLView::ViewLeft, GLView::ViewRight, + GLView::ViewFront, GLView::ViewBack, + GLView::ViewTop, GLView::ViewBottom, + }; + + const int * modeAxes; + switch( cfg.upAxis ) { + case XAxis: modeAxes = modeAxesX; break; + case YAxis: modeAxes = modeAxesY; break; + default: modeAxes = modeAxesZ; break; + } + + for ( auto shape : scene->shapes ) + shape->fillViewModeWeights( modeWeights, hasSkinnedShapes, modeAxes ); + + if ( !hasSkinnedShapes ) { + modeWeights[defaultMode] *= 16.0; // Inflate the weight of the default mode + + int maxIndex = defaultMode; + for ( int i = 0, n = sizeof(modeWeights) / sizeof(*modeWeights); i < n; i++ ) { + if ( modeWeights[i] > modeWeights[maxIndex] ) + maxIndex = i; + } + if ( modeWeights[maxIndex] > 0.0 ) + resultMode = GLView::ViewState(maxIndex); + } } + + return resultMode; } void GLView::flush() @@ -1192,6 +1200,7 @@ void GLView::modelChanged() return; doCompile = true; + doLoadReset = true; //doCenter = true; update(); } @@ -1252,6 +1261,8 @@ void GLView::saveUserView() settings.setValue( "Dist", Dist ); settings.endGroup(); settings.endGroup(); + + setViewMode( ViewUser ); } void GLView::loadUserView() @@ -1264,6 +1275,8 @@ void GLView::loadUserView() setDistance( settings.value( "Dist" ).toDouble() ); settings.endGroup(); settings.endGroup(); + + setViewMode( ViewUser ); } void GLView::advanceGears() @@ -1312,20 +1325,20 @@ void GLView::advanceGears() // keys based on user preferences of what app they would like to // emulate for the control scheme // Rotation - if ( kbd[ Qt::Key_Up ] ) rotate( -cfg.rotSpd * dT, 0, 0 ); - if ( kbd[ Qt::Key_Down ] ) rotate( +cfg.rotSpd * dT, 0, 0 ); - if ( kbd[ Qt::Key_Left ] ) rotate( 0, 0, -cfg.rotSpd * dT ); - if ( kbd[ Qt::Key_Right ] ) rotate( 0, 0, +cfg.rotSpd * dT ); + if ( kbd[ Qt::Key_Up ] ) rotateBy( -cfg.rotSpd * dT, 0, 0 ); + if ( kbd[ Qt::Key_Down ] ) rotateBy( +cfg.rotSpd * dT, 0, 0 ); + if ( kbd[ Qt::Key_Left ] ) rotateBy( 0, 0, -cfg.rotSpd * dT ); + if ( kbd[ Qt::Key_Right ] ) rotateBy( 0, 0, +cfg.rotSpd * dT ); // Fix movement speed for Starfield scale dT *= scale(); // Movement - if ( kbd[ Qt::Key_A ] ) move( +cfg.moveSpd * dT, 0, 0 ); - if ( kbd[ Qt::Key_D ] ) move( -cfg.moveSpd * dT, 0, 0 ); - if ( kbd[ Qt::Key_W ] ) move( 0, 0, +cfg.moveSpd * dT ); - if ( kbd[ Qt::Key_S ] ) move( 0, 0, -cfg.moveSpd * dT ); - if ( kbd[ Qt::Key_Q ] ) move( 0, +cfg.moveSpd * dT, 0 ); - if ( kbd[ Qt::Key_E ] ) move( 0, -cfg.moveSpd * dT, 0 ); + if ( kbd[ Qt::Key_A ] ) moveBy( +cfg.moveSpd * dT, 0, 0 ); + if ( kbd[ Qt::Key_D ] ) moveBy( -cfg.moveSpd * dT, 0, 0 ); + if ( kbd[ Qt::Key_W ] ) moveBy( 0, 0, +cfg.moveSpd * dT ); + if ( kbd[ Qt::Key_S ] ) moveBy( 0, 0, -cfg.moveSpd * dT ); + if ( kbd[ Qt::Key_Q ] ) moveBy( 0, +cfg.moveSpd * dT, 0 ); + if ( kbd[ Qt::Key_E ] ) moveBy( 0, -cfg.moveSpd * dT, 0 ); // Zoom //if ( kbd[ Qt::Key_R ] ) setDistance( Dist / ZOOM_QE_KEY_MULT ); @@ -1336,231 +1349,260 @@ void GLView::advanceGears() if ( kbd[ Qt::Key_PageDown ] ) setZoom( Zoom / ZOOM_PAGE_KEY_MULT ); if ( mouseMov[0] != 0 || mouseMov[1] != 0 || mouseMov[2] != 0 ) { - move( mouseMov[0], mouseMov[1], mouseMov[2] ); + moveBy( mouseMov[0], mouseMov[1], mouseMov[2] ); mouseMov = Vector3(); } if ( mouseRot[0] != 0 || mouseRot[1] != 0 || mouseRot[2] != 0 ) { - rotate( mouseRot[0], mouseRot[1], mouseRot[2] ); + rotateBy( mouseRot[0], mouseRot[1], mouseRot[2] ); mouseRot = Vector3(); } } +void GLView::resetAnimation() +{ + if ( animState & AnimPlay ) { + time = scene->timeMin(); + animState &= ~AnimPlay; + emit sequenceStopped(); + emit sceneTimeChanged( time, scene->timeMin(), scene->timeMax() ); + update(); + } +} -// TODO: Separate widget -void GLView::saveImage() +ScreenshotDialog::ScreenshotDialog( GLView * parent ) + : ToolDialog( parent, tr("Save Screenshot"), ToolDialog::HResize, 500 ), // parent + view( parent ) { - auto dlg = new QDialog( qApp->activeWindow() ); - QGridLayout * lay = new QGridLayout( dlg ); - dlg->setWindowTitle( tr( "Save View" ) ); - dlg->setLayout( lay ); - dlg->setMinimumWidth( 400 ); - - QString date = QDateTime::currentDateTime().toString( "yyyyMMdd_HH-mm-ss" ); - QString name = model->getFilename(); - - QString nifFolder = model->getFolder(); - // TODO: Default extension in Settings - QString filename = name + (!name.isEmpty() ? "_" : "") + date + ".jpg"; - - // Default: NifSkope directory - // TODO: User-configurable default screenshot path in Options - QString nifskopePath = "screenshots/" + filename; - // Absolute: NIF directory - QString nifPath = nifFolder + (!nifFolder.isEmpty() ? "/" : "") + filename; - - FileSelector * file = new FileSelector( FileSelector::SaveFile, tr( "File" ), QBoxLayout::LeftToRight ); - file->setParent( dlg ); - // TODO: Default extension in Settings - file->setFilter( { "Images (*.jpg *.png *.webp *.bmp)", "JPEG (*.jpg)", "PNG (*.png)", "WebP (*.webp)", "BMP (*.bmp)" } ); - file->setFile( nifskopePath ); - lay->addWidget( file, 0, 0, 1, -1 ); - - auto grpDir = new QButtonGroup( dlg ); - - QRadioButton * nifskopeDir = new QRadioButton( tr( "NifSkope Directory" ), dlg ); - nifskopeDir->setChecked( true ); - nifskopeDir->setToolTip( tr( "Save to NifSkope screenshots directory" ) ); + setSettingsFolder( "Screenshot" ); - QRadioButton * niffileDir = new QRadioButton( tr( "NIF Directory" ), dlg ); - niffileDir->setChecked( false ); - niffileDir->setDisabled( nifFolder.isEmpty() ); - niffileDir->setToolTip( tr( "Save to NIF file directory" ) ); + appScreenshotsPath = QFileInfo( QApplication::applicationDirPath() + "/screenshots" ).absoluteFilePath(); + const QString & modelDirPath = modelFolder(); + bool hasModelFile = !modelDirPath.isEmpty(); - grpDir->addButton( nifskopeDir ); - grpDir->addButton( niffileDir ); - grpDir->setExclusive( true ); + QVBoxLayout * mainLayout = addVBoxLayout( this ); - lay->addWidget( nifskopeDir, 1, 0, 1, 1 ); - lay->addWidget( niffileDir, 1, 1, 1, 1 ); + // Image path + QString imgName; + if ( hasModelFile ) { + imgName += view->model->getFilename(); + if ( !imgName.isEmpty() ) + imgName += "-"; + } + imgName += QDateTime::currentDateTime().toString( "yyyy-MM-dd-HHmmss" ) + "." + settingsStrValue( "Format", "jpg" ); - // Save JPEG Quality - QSettings settings; - int jpegQuality = settings.value( "JPEG/Quality", 90 ).toInt(); - settings.setValue( "JPEG/Quality", jpegQuality ); - - QHBoxLayout * pixBox = new QHBoxLayout; - pixBox->setAlignment( Qt::AlignRight ); - QSpinBox * pixQuality = new QSpinBox( dlg ); - pixQuality->setRange( -1, 100 ); - pixQuality->setSingleStep( 10 ); - pixQuality->setValue( jpegQuality ); - pixQuality->setSpecialValueText( tr( "Auto" ) ); - pixQuality->setMaximumWidth( pixQuality->minimumSizeHint().width() ); - pixBox->addWidget( new QLabel( tr( "JPEG Quality" ), dlg ) ); - pixBox->addWidget( pixQuality ); - lay->addLayout( pixBox, 1, 2, Qt::AlignRight ); - - - // Image Size radio button lambda - auto btnSize = [dlg]( const QString & name ) { - auto btn = new QRadioButton( name, dlg ); - btn->setCheckable( true ); - - return btn; - }; + pathWidget = new FileSelector( FileSelector::SaveFile, "...", QBoxLayout::LeftToRight ); + pathWidget->setFilter( { "Images (*.jpg *.png *.webp *.bmp)", "JPEG (*.jpg)", "PNG (*.png)", "WebP (*.webp)", "BMP (*.bmp)" } ); + pathWidget->setFile( imgName ); + switchToDirectory( ( hasModelFile && settingsIntValue( "ModelDirectory", 0 ) ) ? modelDirPath : appScreenshotsPath ); + connect( pathWidget, &FileSelector::sigEdited, this, &ScreenshotDialog::onPathEdit ); + connect( pathWidget, &FileSelector::sigActivated, this, &ScreenshotDialog::onPathEdit ); + mainLayout->addWidget( pathWidget ); - // Get max viewport size for platform - GLint dims; - glGetIntegerv( GL_MAX_VIEWPORT_DIMS, &dims ); - int maxSize = dims; + // Directory selectors + QHBoxLayout * dirSelLayout = addHBoxLayout( mainLayout ); - // Default size - auto btnOneX = btnSize( "1x" ); - btnOneX->setChecked( true ); - // Disable any of these that would exceed the max viewport size of the platform - auto btnTwoX = btnSize( "2x" ); - btnTwoX->setDisabled( (width() * 2) > maxSize || (height() * 2) > maxSize ); - auto btnFourX = btnSize( "4x" ); - btnFourX->setDisabled( (width() * 4) > maxSize || (height() * 4) > maxSize ); - auto btnEightX = btnSize( "8x" ); - btnEightX->setDisabled( (width() * 8) > maxSize || (height() * 8) > maxSize ); - - - auto grpBox = new QGroupBox( tr( "Image Size" ), dlg ); - auto grpBoxLayout = new QHBoxLayout; - grpBoxLayout->addWidget( btnOneX ); - grpBoxLayout->addWidget( btnTwoX ); - grpBoxLayout->addWidget( btnFourX ); - grpBoxLayout->addWidget( btnEightX ); - grpBoxLayout->addWidget( new QLabel( "Caution:
4x and 8x may be memory intensive.", dlg ) ); - grpBoxLayout->addStretch( 1 ); - grpBox->setLayout( grpBoxLayout ); - - auto grpSize = new QButtonGroup( dlg ); - grpSize->addButton( btnOneX, 1 ); - grpSize->addButton( btnTwoX, 2 ); - grpSize->addButton( btnFourX, 4 ); - grpSize->addButton( btnEightX, 8 ); - - grpSize->setExclusive( true ); - - lay->addWidget( grpBox, 2, 0, 1, -1 ); + QPushButton * btnAppDir = addPushButton( dirSelLayout, tr("NifSkope Directory") ); + lockPushButtonSize( btnAppDir ); + btnAppDir->setToolTip( tr("Switch to NifSkope screenshots directory") ); + connect( btnAppDir, &QPushButton::clicked, this, &ScreenshotDialog::onAppDirClicked ); + + QPushButton * btnModelDir = addPushButton( dirSelLayout, tr("NIF Directory") ); + lockPushButtonSize( btnModelDir ); + btnModelDir->setToolTip( tr("Switch to the current NIF file directory") ); + btnModelDir->setEnabled( hasModelFile ); + connect( btnModelDir, &QPushButton::clicked, this, &ScreenshotDialog::onNifDirClicked ); + + addStretch( dirSelLayout, 1 ); + // JPEG/WebP Quality + qualityLabel = addLabel( dirSelLayout, tr("Quality:"), true ); + qualityBox = addSpinBox( dirSelLayout, 0, 100, settingsIntValue( "Quality", 95 ) ); + updateQualityUI( imagePathInfo().suffix() ); - QHBoxLayout * hBox = new QHBoxLayout; - QPushButton * btnOk = new QPushButton( tr( "Save" ), dlg ); - QPushButton * btnCancel = new QPushButton( tr( "Cancel" ), dlg ); - hBox->addWidget( btnOk ); - hBox->addWidget( btnCancel ); - lay->addLayout( hBox, 3, 0, 1, -1 ); + // Image scale + QHBoxLayout * imageScaleLayout = addHBoxLayout(); + imageScaleGroup = beginRadioGroup(); - // Set FileSelector to NifSkope dir (relative) - connect( nifskopeDir, &QRadioButton::clicked, [=]() - { - file->setText( nifskopePath ); - file->setFile( nifskopePath ); + GLint dims; + glGetIntegerv( GL_MAX_VIEWPORT_DIMS, &dims ); // Get max viewport size for platform + int maxSize = dims; + + for ( int sz = 1; sz <= 8; sz *= 2 ) { + QRadioButton * btn = addRadioButton( imageScaleLayout, QString::number( sz ) + "x", sz ); + if ( sz == 1 ) { + btn->setChecked( true ); + } else { // sz > 1 + btn->setDisabled( ( view->viewportWidth * sz ) > maxSize || ( view->viewportHeight * sz ) > maxSize ); } - ); - // Set FileSelector to NIF File dir (absolute) - connect( niffileDir, &QRadioButton::clicked, [=]() - { - file->setText( nifPath ); - file->setFile( nifPath ); + } + addLabel( imageScaleLayout, tr("Caution:
4x and 8x may be memory intensive.") ); + addStretch( imageScaleLayout, 1 ); + + addGroupBox( mainLayout, tr("Image Size"), imageScaleLayout ); + + // Main buttons + beginMainButtonLayout( mainLayout ); + + QPushButton * btnSave = addMainButton( tr("Save"), true ); + connect( btnSave, &QPushButton::clicked, this, &ScreenshotDialog::onSaveClicked ); + addCloseButton( tr("Cancel") ); +} + +const QString & ScreenshotDialog::modelFolder() const +{ + return view->model->getFolder(); +} + +QFileInfo ScreenshotDialog::imagePathInfo() const +{ + if ( pathWidget ) + return QFileInfo( pathWidget->file() ); + return QFileInfo(); +} + +static bool isJpegExtension( const QString & extension ) +{ + return extension.compare( QStringLiteral("jpg"), Qt::CaseInsensitive ) == 0 || extension.compare( QStringLiteral("jpeg"), Qt::CaseInsensitive ) == 0; +} + +static bool isWebpExtension( const QString & extension ) +{ + return extension.compare( QStringLiteral("webp"), Qt::CaseInsensitive ) == 0; +} + +void ScreenshotDialog::updateQualityUI( const QString & extension ) +{ + bool qualityEnabled = isJpegExtension( extension ) || isWebpExtension( extension ); + if ( qualityLabel ) + qualityLabel->setEnabled( qualityEnabled ); + if ( qualityBox ) + qualityBox->setEnabled( qualityEnabled ); +} + +void ScreenshotDialog::onPathEdit() +{ + updateQualityUI( imagePathInfo().suffix() ); +} + +void ScreenshotDialog::onAppDirClicked() +{ + switchToDirectory( appScreenshotsPath ); +} + +void ScreenshotDialog::onNifDirClicked() +{ + const QString & modelDirPath = modelFolder(); + if ( !modelDirPath.isEmpty() ) + switchToDirectory( modelDirPath ); +} + +void ScreenshotDialog::switchToDirectory( const QString & dirPath ) +{ + QString newPath = QDir( dirPath ).filePath( imagePathInfo().fileName() ); + pathWidget->setText( QDir::toNativeSeparators( newPath ) ); +} + +void ScreenshotDialog::onSaveClicked() +{ + QFileInfo pathInfo = imagePathInfo(); + QString outPath = QDir::toNativeSeparators( pathInfo.absoluteFilePath() ); + QString outExt = pathInfo.suffix(); + QDir outDir( pathInfo.absolutePath() ); + const QString & modelDirPath = modelFolder(); + int quality = qualityBox->value(); + int imageScale = std::max( imageScaleGroup->checkedId(), 1 ); + + setSettingsStrValue( "Format", outExt ); + setSettingsIntValue( "Quality", quality ); + setSettingsIntValue( "ModelDirectory", outDir == QDir( modelDirPath ) ); + + QDir appScreenshotsDir( appScreenshotsPath ); + if ( outDir == appScreenshotsDir && !appScreenshotsDir.exists() ) { + if ( !QDir().mkdir( appScreenshotsPath ) ) { + QFileInfo appScreenshotsInfo( appScreenshotsPath ); + Message::critical( this, tr("Could not create \"%1\" folder in \"%2\".").arg( appScreenshotsInfo.fileName(), appScreenshotsInfo.absolutePath() ) ); + return; } - ); - - // Validate on OK - connect( btnOk, &QPushButton::clicked, [&]() - { - // Save JPEG Quality - QSettings settings; - settings.setValue( "JPEG/Quality", pixQuality->value() ); - - // TODO: Set up creation of screenshots directory in Options - if ( nifskopeDir->isChecked() ) { - QDir workingDir; - workingDir.mkpath( "screenshots" ); - } + } - // Supersampling - int ss = grpSize->checkedId(); + if ( imageScale > 1 ) // Image scales 2x or greater can take a significant amount of time to save... + setCursor( Qt::WaitCursor ); - int w, h; + QImage img = view->captureScreenshot( imageScale ); - w = width(); - h = height(); + QImageWriter writer( outPath ); - // Resize viewport for supersampling - if ( ss > 1 ) { - w *= ss; - h *= ss; + // Set Compression for formats that can use it + writer.setCompression( 1 ); - resizeGL( w, h ); - } - - QOpenGLFramebufferObjectFormat fboFmt; - fboFmt.setTextureTarget( GL_TEXTURE_2D ); - fboFmt.setInternalTextureFormat( GL_RGB ); - fboFmt.setMipmap( false ); - fboFmt.setAttachment( QOpenGLFramebufferObject::Attachment::Depth ); - fboFmt.setSamples( 16 / ss ); + // Handle JPEG/WebP Quality exclusively + // PNG will not use compression if Quality is set + if ( isJpegExtension( outExt ) ) { + writer.setFormat( "jpg" ); + writer.setQuality( quality ); + } else if ( isWebpExtension( outExt ) ) { + writer.setFormat( "webp" ); + writer.setQuality( quality ); + } - QOpenGLFramebufferObject fbo( w, h, fboFmt ); - fbo.bind(); + if ( writer.write( img ) ) { + close(); + } else { + Message::critical( this, tr("Could not save \"%1\":\n\n").arg( outPath ) + writer.errorString() ); + } - update(); - updateGL(); + if ( imageScale > 1 ) + setCursor( Qt::ArrowCursor ); +} - fbo.release(); +void GLView::saveImage() +{ + auto dlg = new ScreenshotDialog( this ); + dlg->open( true ); +} - QImage * img = new QImage(fbo.toImage()); +QImage GLView::captureScreenshot( int imageScale ) +{ + // Supersampling + int oldw = width(); + int oldh = height(); - // Return viewport to original size - if ( ss > 1 ) - resizeGL( width(), height() ); + // Resize viewport for supersampling + if ( imageScale > 1 ) { + int neww = oldw * imageScale; + int newh = oldh * imageScale; + globalScale = imageScale; + resize( neww, newh ); + resizeGL( neww, newh ); + } - QImageWriter writer( file->file() ); + QOpenGLFramebufferObjectFormat fboFmt; + fboFmt.setTextureTarget( GL_TEXTURE_2D ); + fboFmt.setInternalTextureFormat( GL_RGB ); + fboFmt.setMipmap( false ); + fboFmt.setAttachment( QOpenGLFramebufferObject::Attachment::Depth ); + fboFmt.setSamples( 16 ); - // Set Compression for formats that can use it - writer.setCompression( 1 ); + QOpenGLFramebufferObject fbo( viewportWidth, viewportHeight, fboFmt ); + fbo.bind(); - // Handle JPEG/WebP Quality exclusively - // PNG will not use compression if Quality is set - if ( file->file().endsWith( ".jpg", Qt::CaseInsensitive ) ) { - writer.setFormat( "jpg" ); - writer.setQuality( 50 + pixQuality->value() / 2 ); - } else if ( file->file().endsWith( ".webp", Qt::CaseInsensitive ) ) { - writer.setFormat( "webp" ); - writer.setQuality( 75 + pixQuality->value() / 4 ); - } + updateGL(); - if ( writer.write( *img ) ) { - dlg->accept(); - } else { - Message::critical( this, tr( "Could not save %1" ).arg( file->file() ) ); - } + fbo.release(); - delete img; - img = nullptr; - } - ); - connect( btnCancel, &QPushButton::clicked, dlg, &QDialog::reject ); + auto img = fbo.toImage(); - if ( dlg->exec() != QDialog::Accepted ) { - return; + // Return viewport to the original size + if ( imageScale > 1 ) { + globalScale = 1; + resize( oldw, oldh ); + resizeGL( oldw, oldh ); + updateGL(); } + + return img; } @@ -1813,6 +1855,17 @@ void GLView::wheelEvent( QWheelEvent * event ) } } +void GLView::cacheViewportSize() +{ + auto vportSize = UIUtils::widgetRealSize( this ); + + viewportWidth = std::max( vportSize.width(), 0 ); + viewportHeight = std::max( vportSize.height(), 0 ); + aspectWidth = viewportWidth; + aspectHeight = viewportHeight > 0 ? viewportHeight : 1; + uiScale = UIUtils::widgetUIScaleFactor( this ) * globalScale; + axesSize = std::min( qRound( uiScale * 125 ), std::min( viewportWidth / 10, viewportHeight ) ); +} void GLGraphicsView::setupViewport( QWidget * viewport ) { diff --git a/src/glview.h b/src/glview.h index dedb318ed..20251e798 100644 --- a/src/glview.h +++ b/src/glview.h @@ -34,11 +34,13 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #define GLVIEW #include "gl/glscene.h" +#include "ui/ToolDialog.h" #include // Inherited #include #include #include +#include #include @@ -62,6 +64,7 @@ class GLView final : public QGLWidget friend class NifSkope; friend class GLGraphicsView; + friend class ScreenshotDialog; private: GLView( const QGLFormat & format, QWidget * parent, const QGLWidget * shareWidget = 0 ); @@ -102,7 +105,9 @@ class GLView final : public QGLWidget ViewFront, ViewBack, ViewWalk, - ViewUser + ViewUser, + + ViewLast = ViewUser }; enum DebugMode @@ -123,13 +128,13 @@ class GLView final : public QGLWidget Scene * getScene(); void updateShaders(); - void updateViewpoint(); + void resetViewMode(); void flush(); void center(); - void move( float, float, float ); - void rotate( float, float, float ); + void moveBy( float x, float y, float z ); + void rotateBy( float x, float y, float z ); void setCenter(); void setDistance( float ); @@ -139,12 +144,13 @@ class GLView final : public QGLWidget void setRotation( float, float, float ); void setZoom( float ); - void setOrientation( GLView::ViewState, bool recenter = true ); - void flipOrientation(); + void setViewMode( GLView::ViewState state ); + void flipView(); + GLView::ViewState getDefaultViewMode(); void setDebugMode( DebugMode ); - float scale() { return (scene->game == Game::STARFIELD) ? 1.0 / 32.0 : 1.0; }; + float scale() { return (scene->getGame() == Game::STARFIELD) ? 1.0 / 32.0 : 1.0; }; QColor clearColor() const; @@ -156,12 +162,13 @@ class GLView final : public QGLWidget QSize minimumSizeHint() const override final { return { 50, 50 }; } QSize sizeHint() const override final { return { 400, 400 }; } -public slots: void setCurrentIndex( const QModelIndex & ); - void setSceneTime( float ); - void setSceneSequence( const QString & ); void saveUserView(); void loadUserView(); + +public slots: + void setSceneTime( float ); + void setSceneSequence( const QString & ); void setBrightness( int ); void setAmbient( int ); void setDeclination( int ); @@ -176,7 +183,7 @@ public slots: void clicked( const QModelIndex & ); void paintUpdate(); void sceneTimeChanged( float t, float mn, float mx ); - void viewpointChanged(); + void viewModeChanged(); void sequenceStopped(); void sequenceChanged( const QString & ); @@ -212,8 +219,12 @@ public slots: void mouseReleaseEvent( QMouseEvent * ) override final; void wheelEvent( QWheelEvent * ) override final; + void resetAnimation(); + protected slots: void saveImage(); +protected: + QImage captureScreenshot( int imageScale ); private: NifModel * model; @@ -237,8 +248,15 @@ protected slots: GLdouble grid; Transform viewTrans; - GLdouble aspect; - + int viewportWidth = -1; + int viewportHeight = 1; + int globalScale = 1; + double uiScale = 1.0; + GLdouble aspectWidth = 0; + GLdouble aspectHeight = 1; + int axesSize = 0; + void cacheViewportSize(); + QHash kbd; QPoint lastPos; QPoint pressPos; @@ -250,6 +268,7 @@ protected slots: QString fnDragTex, fnDragTexOrg; bool doCompile; + bool doLoadReset; bool doCenter; QTimer * lightVisTimer; @@ -313,4 +332,35 @@ protected slots: }; +class ScreenshotDialog final : public ToolDialog +{ + Q_OBJECT + +public: + ScreenshotDialog( GLView * parent ); + +protected slots: + void onPathEdit(); + void onAppDirClicked(); + void onNifDirClicked(); + void onSaveClicked(); + +private: + GLView * view; + QString appScreenshotsPath; + + class FileSelector * pathWidget = nullptr; + QLabel * qualityLabel = nullptr; + QSpinBox * qualityBox = nullptr; + QButtonGroup * imageScaleGroup = nullptr; + + QFileInfo imagePathInfo() const; + + const QString & modelFolder() const; + + void updateQualityUI( const QString & extension ); + + void switchToDirectory( const QString & dirPath ); +}; + #endif diff --git a/src/io/MeshFile.cpp b/src/io/MeshFile.cpp index 85e0bae0e..3de1d2aae 100644 --- a/src/io/MeshFile.cpp +++ b/src/io/MeshFile.cpp @@ -132,17 +132,12 @@ quint32 MeshFile::readMesh() for ( int i = 0; i < coords[0].count(); i++ ) { uint16_t u, v; - union { float f; uint32_t i; } uu, vu; - in >> u; in >> v; - uu.i = half_to_float(u); - vu.i = half_to_float(v); - Vector2 coord; - coord[0] = uu.f; - coord[1] = vu.f; + coord[0] = halfToFloat( u ); + coord[1] = halfToFloat( v ); coords[0][i] = coord; } @@ -154,16 +149,12 @@ quint32 MeshFile::readMesh() for ( int i = 0; i < coords[1].count(); i++ ) { uint16_t u, v; - union { float f; uint32_t i; } uu, vu; - in >> u; in >> v; - uu.i = half_to_float(u); - vu.i = half_to_float(v); Vector2 coord; - coord[0] = uu.f; - coord[1] = vu.f; + coord[0] = halfToFloat( u ); + coord[1] = halfToFloat( v ); coords[1][i] = coord; } @@ -228,7 +219,7 @@ quint32 MeshFile::readMesh() weightsUNORM.append({0, 0}); } } - weights[i] = BoneWeightsUNorm(weightsUNORM, i); + weights[i] = BoneWeightsUNorm(weightsUNORM); } quint32 numLODs; diff --git a/src/io/MeshFile.h b/src/io/MeshFile.h index 126bde561..c8d287e0c 100644 --- a/src/io/MeshFile.h +++ b/src/io/MeshFile.h @@ -1,7 +1,7 @@ #pragma once #include "data/niftypes.h" -#include "gl/gltools.h" +#include "gl/BSMesh.h" #include #include diff --git a/src/io/material.cpp b/src/io/material.cpp index 9e097a9ac..913146404 100644 --- a/src/io/material.cpp +++ b/src/io/material.cpp @@ -162,11 +162,6 @@ bool Material::isValid() const return readable && !data.isEmpty(); } -QStringList Material::textures() const -{ - return textureList; -} - QString Material::getPath() const { return localPath; diff --git a/src/io/material.h b/src/io/material.h index a7cec3dea..8d63b48ef 100644 --- a/src/io/material.h +++ b/src/io/material.h @@ -57,7 +57,7 @@ class Material : public QObject bool hasAlphaBlend() const { return (bAlphaBlend != 0); } bool hasAlphaTest() const { return (bAlphaTest != 0); } bool hasDecal() const { return (bDecal != 0); } - QStringList textures() const; + const QStringList & textures() const { return textureList; } QString getPath() const; protected: @@ -128,7 +128,7 @@ class ShaderMaterial : public Material Q_OBJECT friend class Renderer; - friend class BSShaderLightingProperty; + friend class BSShaderProperty; friend class BSLightingShaderProperty; public: @@ -211,7 +211,7 @@ class EffectMaterial : public Material Q_OBJECT friend class Renderer; - friend class BSShaderLightingProperty; + friend class BSShaderProperty; friend class BSEffectShaderProperty; public: diff --git a/src/io/nifstream.cpp b/src/io/nifstream.cpp index 70636dbea..f4beebf69 100644 --- a/src/io/nifstream.cpp +++ b/src/io/nifstream.cpp @@ -43,6 +43,27 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. //! @file nifstream.cpp NIF file I/O +constexpr int CHAR8_STRING_SIZE = 8; + +static inline void shortString_prepareForWrite( QByteArray & str ) +{ + str.replace( "\\r", "\r" ); + str.replace( "\\n", "\n" ); + + if ( str.size() > 254 ) + str.resize( 254 ); +} + +static inline uint8_t floatToNormByte( float f ) +{ + return round( ( (double(f) + 1.0) / 2.0 ) * 255.0 ); +} + +static inline float normByteToFloat( uint8_t u ) +{ + return (double(u) / 255.0) * 2.0 - 1.0; +} + /* * NifIStream */ @@ -61,23 +82,69 @@ void NifIStream::init() maxLength = 0x8000; } +bool NifIStream::readSizedString( NifValue & val ) +{ + auto valString = static_cast(val.val.data); + if ( !valString ) + return false; + + int32_t len; + *dataStream >> len; + if ( len > maxLength || len < 0 ) { + *valString = tr( "" ).arg( len, 0, 16 ); + return false; + } + + QByteArray byteString = device->read( len ); + if ( byteString.size() != len ) + return false; + *valString = QString( byteString ); + return true; +} + +bool NifIStream::readLineString( QByteArray & outString, int maxLineLength ) +{ + outString.reserve( maxLineLength ); + + for ( int counter = 0; ; counter++ ) { + char ch; + if ( !device->getChar( &ch ) ) + return false; + if ( ch == '\n') + break; + if ( counter >= maxLineLength ) + return false; + outString.append( ch ); + } + + return true; +} + bool NifIStream::read( NifValue & val ) { - if ( val.isCount() ) - val.val.u64 = 0; + #define _DEVICE_READ_DATA( data, dataSize ) ( device->read( (char *)(data), (dataSize) ) == (dataSize) ) + #define _DEVICE_READ_ARRAY( arr ) ( device->read( (char *)(arr), sizeof(arr) ) == sizeof(arr) ) + #define _DEVICE_READ_VALUE( val ) ( device->read( (char *)(&(val)), sizeof(val) ) == sizeof(val) ) + + // TODO (Gavrant): + // - What's the point of having 2 different ways of reading streams - dataStream and device? + // - tHeaderString and tLineString: why the max char number is capped at 79 and 254 respectively? If it has a point, then why those caps are not enforced in ::write() and ::size() below? + // - tShortString: Why ::write() converts "\\n" to "\n" and "\\r" to "\r", but ::read() doesn't do the opposite? + // - tBlob: it looks like it never should/could be read from a stream. Return false instead? switch ( val.type() ) { case NifValue::tBool: { + val.val.u64 = 0; if ( bool32bit ) *dataStream >> val.val.u32; else *dataStream >> val.val.u08; - return (dataStream->status() == QDataStream::Ok); } case NifValue::tByte: { + val.val.u64 = 0; *dataStream >> val.val.u08; return (dataStream->status() == QDataStream::Ok); } @@ -86,6 +153,7 @@ bool NifIStream::read( NifValue & val ) case NifValue::tFlags: case NifValue::tBlockTypeIndex: { + val.val.u64 = 0; *dataStream >> val.val.u16; return (dataStream->status() == QDataStream::Ok); } @@ -93,19 +161,20 @@ bool NifIStream::read( NifValue & val ) case NifValue::tInt: case NifValue::tUInt: { + val.val.u64 = 0; *dataStream >> val.val.u32; return (dataStream->status() == QDataStream::Ok); } case NifValue::tULittle32: { - if ( bigEndian ) + val.val.u64 = 0; + if ( bigEndian ) { dataStream->setByteOrder( QDataStream::LittleEndian ); - - *dataStream >> val.val.u32; - - if ( bigEndian ) + *dataStream >> val.val.u32; dataStream->setByteOrder( QDataStream::BigEndian ); - + } else { + *dataStream >> val.val.u32; + } return (dataStream->status() == QDataStream::Ok); } case NifValue::tInt64: @@ -116,18 +185,20 @@ bool NifIStream::read( NifValue & val ) } case NifValue::tStringIndex: { + val.val.u64 = 0; *dataStream >> val.val.u32; return (dataStream->status() == QDataStream::Ok); } case NifValue::tLink: case NifValue::tUpLink: { + val.val.u64 = 0; *dataStream >> val.val.i32; - + if ( dataStream->status() != QDataStream::Ok ) + return false; if ( linkAdjust ) val.val.i32--; - - return (dataStream->status() == QDataStream::Ok); + return true; } case NifValue::tFloat: { @@ -137,393 +208,335 @@ bool NifIStream::read( NifValue & val ) } case NifValue::tHfloat: { - val.val.u64 = 0; uint16_t half; *dataStream >> half; - val.val.u32 = half_to_float( half ); - return (dataStream->status() == QDataStream::Ok); + if ( dataStream->status() != QDataStream::Ok ) + return false; + val.val.u64 = 0; + val.val.f32 = halfToFloat( half ); + return true; } case NifValue::tNormbyte: - { - quint8 v; - float fv; - *dataStream >> v; - fv = (double(v) / 255.0) * 2.0 - 1.0; - val.val.u64 = 0; - val.val.f32 = fv; - - return (dataStream->status() == QDataStream::Ok); - } + { + uint8_t v; + *dataStream >> v; + if ( dataStream->status() != QDataStream::Ok ) + return false; + val.val.u64 = 0; + val.val.f32 = normByteToFloat( v ); + return true; + } case NifValue::tByteVector3: { - quint8 x, y, z; - float xf, yf, zf; - - *dataStream >> x; - *dataStream >> y; - *dataStream >> z; - - xf = (double( x ) / 255.0) * 2.0 - 1.0; - yf = (double( y ) / 255.0) * 2.0 - 1.0; - zf = (double( z ) / 255.0) * 2.0 - 1.0; - - Vector3 * v = static_cast(val.val.data); - v->xyz[0] = xf; v->xyz[1] = yf; v->xyz[2] = zf; + auto vec = static_cast(val.val.data); + if ( !vec ) + return false; + for ( int i = 0; i < 3; i++ ) { + uint8_t v; + *dataStream >> v; + vec->xyz[i] = normByteToFloat( v ); + } return (dataStream->status() == QDataStream::Ok); } case NifValue::tUshortVector3: { - uint16_t x, y, z; - float xf, yf, zf; - - *dataStream >> x; - *dataStream >> y; - *dataStream >> z; - - xf = (float) x; - yf = (float) y; - zf = (float) z; - - Vector3 * v = static_cast(val.val.data); - v->xyz[0] = xf; v->xyz[1] = yf; v->xyz[2] = zf; + auto vec = static_cast(val.val.data); + if ( !vec ) + return false; + for ( int i = 0; i < 3; i++ ) { + uint16_t v; + *dataStream >> v; + vec->xyz[i] = float( v ); + } return (dataStream->status() == QDataStream::Ok); } case NifValue::tHalfVector3: { - uint16_t x, y, z; - union { float f; uint32_t i; } xu, yu, zu; - - *dataStream >> x; - *dataStream >> y; - *dataStream >> z; - - xu.i = half_to_float( x ); - yu.i = half_to_float( y ); - zu.i = half_to_float( z ); - - Vector3 * v = static_cast(val.val.data); - v->xyz[0] = xu.f; v->xyz[1] = yu.f; v->xyz[2] = zu.f; + auto vec = static_cast(val.val.data); + if ( !vec ) + return false; + for ( int i = 0; i < 3; i++ ) { + uint16_t v; + *dataStream >> v; + vec->xyz[i] = halfToFloat( v ); + } return (dataStream->status() == QDataStream::Ok); } case NifValue::tHalfVector2: { - uint16_t x, y; - union { float f; uint32_t i; } xu, yu; - - *dataStream >> x; - *dataStream >> y; - - xu.i = half_to_float( x ); - yu.i = half_to_float( y ); + auto vec = static_cast(val.val.data); + if ( !vec ) + return false; - Vector2 * v = static_cast(val.val.data); - v->xy[0] = xu.f; v->xy[1] = yu.f; + for ( int i = 0; i < 2; i++ ) { + uint16_t v; + *dataStream >> v; + vec->xy[i] = halfToFloat( v ); + } + return (dataStream->status() == QDataStream::Ok); + } + case NifValue::tVector2: + { + auto vec = static_cast(val.val.data); + if ( !vec ) + return false; + *dataStream >> *vec; return (dataStream->status() == QDataStream::Ok); } case NifValue::tVector3: { - Vector3 * v = static_cast(val.val.data); - *dataStream >> *v; + auto vec = static_cast(val.val.data); + if ( !vec ) + return false; + + *dataStream >> *vec; return (dataStream->status() == QDataStream::Ok); } case NifValue::tVector4: { - Vector4 * v = static_cast(val.val.data); - *dataStream >> *v; + auto vec = static_cast(val.val.data); + if ( !vec ) + return false; + + *dataStream >> *vec; return (dataStream->status() == QDataStream::Ok); } case NifValue::tTriangle: { - Triangle * t = static_cast(val.val.data); - *dataStream >> *t; + auto tri = static_cast(val.val.data); + if ( !tri ) + return false; + + *dataStream >> *tri; return (dataStream->status() == QDataStream::Ok); } case NifValue::tQuat: { - Quat * q = static_cast(val.val.data); - *dataStream >> *q; + auto quat = static_cast(val.val.data); + if ( !quat ) + return false; + + *dataStream >> *quat; return (dataStream->status() == QDataStream::Ok); } case NifValue::tQuatXYZW: { - Quat * q = static_cast(val.val.data); - return device->read( (char *)&q->wxyz[1], 12 ) == 12 && device->read( (char *)q->wxyz, 4 ) == 4; + auto quat = static_cast(val.val.data); + return quat && _DEVICE_READ_DATA( quat->wxyz + 1, sizeof(float) * 3 ) && _DEVICE_READ_DATA( quat->wxyz, sizeof(float) * 1 ); } case NifValue::tMatrix: - return device->read( (char *)static_cast(val.val.data)->m, 36 ) == 36; + { + auto matrix = static_cast(val.val.data); + return matrix && _DEVICE_READ_ARRAY( matrix->m ); + } case NifValue::tMatrix4: - return device->read( (char *)static_cast(val.val.data)->m, 64 ) == 64; - case NifValue::tVector2: { - Vector2 * v = static_cast(val.val.data); - *dataStream >> *v; - return (dataStream->status() == QDataStream::Ok); + auto matrix = static_cast(val.val.data); + return matrix && _DEVICE_READ_ARRAY( matrix->m ); } case NifValue::tColor3: - return device->read( (char *)static_cast(val.val.data)->rgb, 12 ) == 12; + { + auto color = static_cast(val.val.data); + return color && _DEVICE_READ_ARRAY( color->rgb ); + } case NifValue::tByteColor4: { - quint8 r, g, b, a; - *dataStream >> r; - *dataStream >> g; - *dataStream >> b; - *dataStream >> a; - - Color4 * c = static_cast(val.val.data); - c->setRGBA( (float)r / 255.0, (float)g / 255.0, (float)b / 255.0, (float)a / 255.0 ); + auto color = static_cast(val.val.data); + if ( !color ) + return false; + for ( int i = 0; i < 4; i++ ) { + uint8_t v; + *dataStream >> v; + color->rgba[i] = float( double(v) / 255.0 ); + } return (dataStream->status() == QDataStream::Ok); } case NifValue::tColor4: { - Color4 * c = static_cast(val.val.data); - *dataStream >> *c; - return (dataStream->status() == QDataStream::Ok); - } - case NifValue::tSizedString: - { - int len; - //device->read( (char *) &len, 4 ); - *dataStream >> len; - - if ( len > maxLength || len < 0 ) { - *static_cast(val.val.data) = tr( "" ).arg( len, 0, 16 ); return false; - } - - QByteArray string = device->read( len ); - - if ( string.size() != len ) + auto color = static_cast(val.val.data); + if ( !color ) return false; - //string.replace( "\r", "\\r" ); - //string.replace( "\n", "\\n" ); - *static_cast(val.val.data) = QString( string ); + *dataStream >> *color; + return (dataStream->status() == QDataStream::Ok); } - return true; + case NifValue::tSizedString: + case NifValue::tText: + return readSizedString( val ); case NifValue::tShortString: { - unsigned char len; - device->read( (char *)&len, 1 ); - QByteArray string = device->read( len ); - - if ( string.size() != len ) + auto valString = static_cast(val.val.data); + if ( !valString ) return false; - //string.replace( "\r", "\\r" ); - //string.replace( "\n", "\\n" ); - *static_cast(val.val.data) = QString::fromLocal8Bit( string ); - } - return true; - case NifValue::tText: - { - int len; - device->read( (char *)&len, 4 ); - - if ( len > maxLength || len < 0 ) { - *static_cast(val.val.data) = tr( "" ); return false; - } - - QByteArray string = device->read( len ); + uint8_t len; + if ( !_DEVICE_READ_VALUE( len ) ) + return false; - if ( string.size() != len ) + QByteArray byteString = device->read( len ); + if ( byteString.size() != len ) return false; - *static_cast(val.val.data) = QString( string ); + *valString = QString::fromLocal8Bit( byteString ); + return true; } - return true; case NifValue::tByteArray: { - int len; - device->read( (char *)&len, 4 ); + auto array = static_cast(val.val.data); + if ( !array ) + return false; + int32_t len; + if ( !_DEVICE_READ_VALUE( len ) ) + return false; if ( len < 0 ) return false; - *static_cast(val.val.data) = device->read( len ); - return static_cast(val.val.data)->count() == len; + *array = device->read( len ); + return array->count() == len; } case NifValue::tStringPalette: { - int len; - device->read( (char *)&len, 4 ); + auto array = static_cast(val.val.data); + if ( !array ) + return false; + int32_t len; + if ( !_DEVICE_READ_VALUE( len ) ) + return false; if ( len > 0xffff || len < 0 ) return false; - *static_cast(val.val.data) = device->read( len ); - device->read( (char *)&len, 4 ); - return true; + *array = device->read( len ); + return _DEVICE_READ_VALUE( len ); } case NifValue::tByteMatrix: { - int len1, len2; - device->read( (char *)&len1, 4 ); - device->read( (char *)&len2, 4 ); + auto valMatrix = static_cast(val.val.data); + if ( !valMatrix ) + return false; - if ( len1 < 0 || len2 < 0 ) + int32_t size1, size2; + if ( !_DEVICE_READ_VALUE( size1 ) || !_DEVICE_READ_VALUE( size2 ) ) + return false; + if ( size1 < 0 || size2 < 0 ) return false; - int len = len1 * len2; - ByteMatrix tmp( len1, len2 ); - qint64 rlen = device->read( tmp.data(), len ); - tmp.swap( *static_cast(val.val.data) ); - return (rlen == len); + ByteMatrix tmp( size1, size2 ); + int64_t totalSize = int64_t(size1) * int64_t(size2); + if ( !_DEVICE_READ_DATA( tmp.data(), totalSize ) ) + return false; + tmp.swap( *valMatrix ); + return true; } case NifValue::tHeaderString: { - QByteArray string; - int c = 0; - char chr = 0; - - while ( c++ < 80 && device->getChar( &chr ) && chr != '\n' ) - string.append( chr ); + auto valString = static_cast(val.val.data); + if ( !valString ) + return false; - if ( c >= 80 ) + QByteArray byteString; + if ( !readLineString( byteString, 79 ) ) return false; - quint32 version = 0; + uint32_t numVersion = 0; // Support NIF versions without "Version" in header string // Do for all files for now //if ( c == GAMEBRYO_FF || c == NETIMMERSE_FF || c == NEOSTEAM_FF ) { - device->peek((char *)&version, 4); + device->peek( (char *)&numVersion, 4 ); // NeoSteam Hack - if (version == 0x08F35232) - version = 0x0A010000; + if (numVersion == 0x08F35232) + numVersion = 0x0A010000; // Version didn't exist until NetImmerse 4.0 - else if (version < 0x04000000) - version = 0; + else if (numVersion < 0x04000000) + numVersion = 0; //} - *static_cast(val.val.data) = QString( string ); - bool x = model->setHeaderString( QString( string ), version ); - + *valString = QString( byteString ); + bool result = model->setHeaderString( *valString, numVersion ); init(); - return x; + return result; } case NifValue::tLineString: { - QByteArray string; - int c = 0; - char chr = 0; - - while ( c++ < 255 && device->getChar( &chr ) && chr != '\n' ) - string.append( chr ); + auto valString = static_cast(val.val.data); + if ( !valString ) + return false; - if ( c >= 255 ) + QByteArray byteString; + if ( !readLineString( byteString, 254 ) ) return false; - *static_cast(val.val.data) = QString( string ); + *valString = QString( byteString ); return true; } case NifValue::tChar8String: { - QByteArray string; - int c = 0; - char chr = 0; - - while ( c++ < 8 && device->getChar( &chr ) ) - string.append( chr ); + auto valString = static_cast(val.val.data); + if ( !valString ) + return false; - if ( c > 9 ) + char buffer[CHAR8_STRING_SIZE + 1]; + if ( !_DEVICE_READ_DATA( buffer, CHAR8_STRING_SIZE ) ) return false; + buffer[CHAR8_STRING_SIZE] = 0; - *static_cast(val.val.data) = QString( string ); + *valString = QString( buffer ); return true; } case NifValue::tFileVersion: { - if ( device->read( (char *)&val.val.u32, 4 ) != 4 ) + val.val.u64 = 0; + if ( !_DEVICE_READ_VALUE( val.val.u32 ) ) return false; - //bool x = model->setVersion( val.val.u32 ); - //init(); if ( model->inherits( "NifModel" ) && model->getVersionNumber() >= 0x14000004 ) { bool littleEndian; device->peek( (char *)&littleEndian, 1 ); bigEndian = !littleEndian; - - if ( bigEndian ) { + if ( bigEndian ) dataStream->setByteOrder( QDataStream::BigEndian ); - } } // hack for neosteam - if ( val.val.u32 == 0x08F35232 ) { + if ( val.val.u32 == 0x08F35232 ) val.val.u32 = 0x0a010000; - } return true; } case NifValue::tString: - { - if ( stringAdjust ) { - val.changeType( NifValue::tStringIndex ); - return device->read( (char *)&val.val.i32, 4 ) == 4; - } else { - val.changeType( NifValue::tSizedString ); - - int len; - device->read( (char *)&len, 4 ); - - if ( len > maxLength || len < 0 ) { - *static_cast(val.val.data) = tr( "" ); return false; - } - - QByteArray string = device->read( len ); - - if ( string.size() != len ) - return false; - - //string.replace( "\r", "\\r" ); - //string.replace( "\n", "\\n" ); - *static_cast(val.val.data) = QString( string ); - return true; - } - } case NifValue::tFilePath: - { - if ( stringAdjust ) { - val.changeType( NifValue::tStringIndex ); - return device->read( (char *)&val.val.i32, 4 ) == 4; - } else { - val.changeType( NifValue::tSizedString ); - - int len; - device->read( (char *)&len, 4 ); - - if ( len > maxLength || len < 0 ) { - *static_cast(val.val.data) = tr( "" ); return false; - } - - QByteArray string = device->read( len ); - - if ( string.size() != len ) - return false; - - *static_cast(val.val.data) = QString( string ); - return true; - } + if ( stringAdjust ) { + val.changeType( NifValue::tStringIndex ); + // val.val.u64 = 0; // val.changeType() above takes care of clearing u64 + return _DEVICE_READ_VALUE( val.val.i32 ); + } else { + val.changeType( NifValue::tSizedString ); + return readSizedString( val ); } case NifValue::tBSVertexDesc: { - *dataStream >> *static_cast(val.val.data); + auto d = static_cast(val.val.data); + if ( !d ) + return false; + + *dataStream >> *d; return (dataStream->status() == QDataStream::Ok); } case NifValue::tBlob: { - if ( val.val.data ) { - QByteArray * array = static_cast(val.val.data); - return device->read( array->data(), array->size() ) == array->size(); - } - - return false; + auto blob = static_cast(val.val.data); + return blob && _DEVICE_READ_DATA( blob->data(), blob->size() ); } case NifValue::tNone: return true; + default: + Q_ASSERT( 0 ); } return false; @@ -548,319 +561,284 @@ void NifOStream::init() bool NifOStream::write( const NifValue & val ) { + #define _DEVICE_WRITE_DATA( data, dataSize ) ( device->write( (const char *)(data), (dataSize) ) == (dataSize) ) + #define _DEVICE_WRITE_ARRAY( arr ) ( device->write( (const char *)(arr), sizeof(arr) ) == sizeof(arr) ) + #define _DEVICE_WRITE_VALUE( val ) ( device->write( (const char *)(&(val)), sizeof(val) ) == sizeof(val) ) + switch ( val.type() ) { case NifValue::tBool: - - if ( bool32bit ) - return device->write( (char *)&val.val.u32, 4 ) == 4; - else - return device->write( (char *)&val.val.u08, 1 ) == 1; - + return bool32bit ? _DEVICE_WRITE_VALUE( val.val.u32 ) : _DEVICE_WRITE_VALUE( val.val.u08 ); case NifValue::tByte: - return device->write( (char *)&val.val.u08, 1 ) == 1; + return _DEVICE_WRITE_VALUE( val.val.u08 ); case NifValue::tWord: case NifValue::tShort: case NifValue::tFlags: case NifValue::tBlockTypeIndex: - return device->write( (char *)&val.val.u16, 2 ) == 2; + return _DEVICE_WRITE_VALUE( val.val.u16 ); case NifValue::tStringOffset: case NifValue::tInt: case NifValue::tUInt: case NifValue::tULittle32: case NifValue::tStringIndex: - return device->write( (char *)&val.val.u32, 4 ) == 4; + return _DEVICE_WRITE_VALUE( val.val.u32 ); case NifValue::tInt64: case NifValue::tUInt64: - return device->write( (char *)&val.val.u64, 8 ) == 8; + return _DEVICE_WRITE_VALUE( val.val.u64 ); case NifValue::tFileVersion: { - if ( NifModel * mdl = static_cast(const_cast(model)) ) { - QString headerString = mdl->getItem( mdl->getHeaderItem(), "Header String" )->value().toString(); - quint32 version; + uint32_t version = val.val.u32; - // hack for neosteam - if ( headerString.startsWith( "NS" ) ) { + // hack for neosteam + auto nif = static_cast(model); + if ( nif ) { + QString headerString = nif->header().child("Header String").value(); + if ( headerString.startsWith( QStringLiteral("NS") ) ) version = 0x08F35232; - } else { - version = val.val.u32; - } - - return device->write( (char *)&version, 4 ) == 4; - } else { - return device->write( (char *)&val.val.u32, 4 ) == 4; } + + return _DEVICE_WRITE_VALUE( version ); } case NifValue::tLink: case NifValue::tUpLink: - - if ( !linkAdjust ) { - return device->write( (char *)&val.val.i32, 4 ) == 4; + if ( linkAdjust ) { + int32_t v = val.val.i32 + 1; + return _DEVICE_WRITE_VALUE( v ); } else { - qint32 l = val.val.i32 + 1; - return device->write( (char *)&l, 4 ) == 4; + return _DEVICE_WRITE_VALUE( val.val.i32 ); } - case NifValue::tFloat: - return device->write( (char *)&val.val.f32, 4 ) == 4; + return _DEVICE_WRITE_VALUE( val.val.f32 ); case NifValue::tHfloat: { - uint16_t half = half_from_float( val.val.u32 ); - return device->write( (char *)&half, 2 ) == 2; + uint16_t half = floatToHalf( val.val.f32 ); + return _DEVICE_WRITE_VALUE( half ); } case NifValue::tNormbyte: { - uint8_t v = round( ((val.val.f32 + 1.0) / 2.0) * 255.0 ); - - return device->write( (char*)&v, 1 ) == 1; + uint8_t v = floatToNormByte( val.val.f32 ); + return _DEVICE_WRITE_VALUE( v ); } case NifValue::tByteVector3: { - Vector3 * vec = static_cast(val.val.data); + auto vec = static_cast(val.val.data); if ( !vec ) return false; uint8_t v[3]; - v[0] = round( ((vec->xyz[0] + 1.0) / 2.0) * 255.0 ); - v[1] = round( ((vec->xyz[1] + 1.0) / 2.0) * 255.0 ); - v[2] = round( ((vec->xyz[2] + 1.0) / 2.0) * 255.0 ); - - return device->write( (char*)v, 3 ) == 3; + for ( int i = 0; i < 3; i++ ) + v[i] = floatToNormByte( vec->xyz[i] ); + return _DEVICE_WRITE_ARRAY( v ); } case NifValue::tUshortVector3: { - Vector3 * vec = static_cast(val.val.data); + auto vec = static_cast(val.val.data); if ( !vec ) return false; uint16_t v[3]; - v[0] = (uint16_t) round(vec->xyz[0]); - v[1] = (uint16_t) round(vec->xyz[1]); - v[2] = (uint16_t) round(vec->xyz[2]); - - return device->write( (char*)v, 6 ) == 3; + for ( int i = 0; i < 3; i++ ) + v[i] = (uint16_t) round( vec->xyz[i] ); + return _DEVICE_WRITE_ARRAY( v ); } case NifValue::tHalfVector3: { - Vector3 * vec = static_cast(val.val.data); + auto vec = static_cast(val.val.data); if ( !vec ) return false; - union { float f; uint32_t i; } xu, yu, zu; - - xu.f = vec->xyz[0]; - yu.f = vec->xyz[1]; - zu.f = vec->xyz[2]; - uint16_t v[3]; - v[0] = half_from_float( xu.i ); - v[1] = half_from_float( yu.i ); - v[2] = half_from_float( zu.i ); - - return device->write( (char*)v, 6 ) == 6; + for ( int i = 0; i < 3; i++) + v[i] = floatToHalf( vec->xyz[i] ); + return _DEVICE_WRITE_ARRAY( v ); } case NifValue::tHalfVector2: { - Vector2 * vec = static_cast(val.val.data); + auto vec = static_cast(val.val.data); if ( !vec ) return false; - union { float f; uint32_t i; } xu, yu; - - xu.f = vec->xy[0]; - yu.f = vec->xy[1]; - uint16_t v[2]; - v[0] = half_from_float( xu.i ); - v[1] = half_from_float( yu.i ); - - return device->write( (char*)v, 4 ) == 4; + v[0] = floatToHalf( vec->xy[0] ); + v[1] = floatToHalf( vec->xy[1] ); + return _DEVICE_WRITE_ARRAY( v ); } case NifValue::tVector3: - return device->write( (char *)static_cast(val.val.data)->xyz, 12 ) == 12; + { + auto vec = static_cast(val.val.data); + return vec && _DEVICE_WRITE_ARRAY( vec->xyz ); + } case NifValue::tVector4: - return device->write( (char *)static_cast(val.val.data)->xyzw, 16 ) == 16; + { + auto vec = static_cast(val.val.data); + return vec && _DEVICE_WRITE_ARRAY( vec->xyzw ) ; + } case NifValue::tTriangle: - return device->write( (char *)static_cast(val.val.data)->v, 6 ) == 6; + { + auto tri = static_cast(val.val.data); + return tri && _DEVICE_WRITE_ARRAY( tri->v ); + } case NifValue::tQuat: - return device->write( (char *)static_cast(val.val.data)->wxyz, 16 ) == 16; + { + auto quat = static_cast(val.val.data); + return quat && _DEVICE_WRITE_ARRAY( quat->wxyz ); + } case NifValue::tQuatXYZW: { - Quat * q = static_cast(val.val.data); - return device->write( (char *)&q->wxyz[1], 12 ) == 12 && device->write( (char *)q->wxyz, 4 ) == 4; + auto quat = static_cast(val.val.data); + return quat && _DEVICE_WRITE_DATA( quat->wxyz + 1, sizeof(float) * 3 ) && _DEVICE_WRITE_DATA( quat->wxyz, sizeof(float) * 1 ); } case NifValue::tMatrix: - return device->write( (char *)static_cast(val.val.data)->m, 36 ) == 36; + { + auto matrix = static_cast(val.val.data); + return matrix && _DEVICE_WRITE_ARRAY( matrix->m ); + } case NifValue::tMatrix4: - return device->write( (char *)static_cast(val.val.data)->m, 64 ) == 64; + { + auto matrix = static_cast(val.val.data); + return matrix && _DEVICE_WRITE_ARRAY( matrix->m ); + } case NifValue::tVector2: - return device->write( (char *)static_cast(val.val.data)->xy, 8 ) == 8; + { + auto vec = static_cast(val.val.data); + return vec && _DEVICE_WRITE_ARRAY( vec->xy ); + } case NifValue::tColor3: - return device->write( (char *)static_cast(val.val.data)->rgb, 12 ) == 12; + { + auto color = static_cast(val.val.data); + return color &&_DEVICE_WRITE_ARRAY( color->rgb ); + } case NifValue::tByteColor4: { - Color4 * color = static_cast(val.val.data); + auto color = static_cast(val.val.data); if ( !color ) return false; - quint8 c[4]; - - auto cF = color->rgba; - for ( int i = 0; i < 4; i++ ) { - c[i] = round( cF[i] * 255.0f ); - } - - return device->write( (char*)c, 4 ) == 4; + uint8_t out[4]; + for ( int i = 0; i < 4; i++ ) + out[i] = std::clamp( round( color->rgba[i] * 255.0 ), 0.0, 255.0 ); + return _DEVICE_WRITE_ARRAY( out ); } case NifValue::tColor4: - return device->write( (char *)static_cast(val.val.data)->rgba, 16 ) == 16; - case NifValue::tSizedString: { - QByteArray string = static_cast(val.val.data)->toLatin1(); - //string.replace( "\\r", "\r" ); - //string.replace( "\\n", "\n" ); - int len = string.size(); - - if ( device->write( (char *)&len, 4 ) != 4 ) - return false; - - return device->write( string.constData(), string.size() ) == string.size(); + auto color = static_cast(val.val.data); + return color && _DEVICE_WRITE_ARRAY( color->rgba ); } - case NifValue::tShortString: - { - QByteArray string = static_cast(val.val.data)->toLocal8Bit(); - string.replace( "\\r", "\r" ); - string.replace( "\\n", "\n" ); - - if ( string.size() > 254 ) - string.resize( 254 ); - - unsigned char len = string.size() + 1; - - if ( device->write( (char *)&len, 1 ) != 1 ) + case NifValue::tSizedString: + case NifValue::tText: + { + auto valString = static_cast(val.val.data); + if ( !valString ) return false; - return device->write( string.constData(), len ) == len; + QByteArray byteString = valString->toLatin1(); + int32_t len = byteString.size(); + return _DEVICE_WRITE_VALUE( len ) && _DEVICE_WRITE_DATA( byteString.constData(), len ); } - case NifValue::tText: + case NifValue::tShortString: { - QByteArray string = static_cast(val.val.data)->toLatin1(); - int len = string.size(); - - if ( device->write( (char *)&len, 4 ) != 4 ) + auto valString = static_cast(val.val.data); + if ( !valString ) return false; - return device->write( (const char *)string.constData(), string.size() ) == string.size(); + QByteArray byteString = valString->toLocal8Bit(); + shortString_prepareForWrite( byteString ); + uint8_t len = byteString.size() + 1; + return _DEVICE_WRITE_VALUE( len ) && _DEVICE_WRITE_DATA( byteString.constData(), len ); } case NifValue::tHeaderString: case NifValue::tLineString: { - QByteArray string = static_cast(val.val.data)->toLatin1(); - - if ( device->write( string.constData(), string.length() ) != string.length() ) + auto valString = static_cast(val.val.data); + if ( !valString ) return false; - return (device->write( "\n", 1 ) == 1); + QByteArray byteString = valString->toLatin1(); + int len = byteString.length(); + return _DEVICE_WRITE_DATA( byteString.constData(), len ) && _DEVICE_WRITE_DATA( "\n", 1 ); } case NifValue::tChar8String: { - QByteArray string = static_cast(val.val.data)->toLatin1(); - quint32 n = std::min( 8, string.length() ); + auto valString = static_cast(val.val.data); + if ( !valString ) + return false; - if ( device->write( string.constData(), n ) != n ) + QByteArray byteString = valString->toLatin1(); + int len = std::min( byteString.length(), CHAR8_STRING_SIZE ); + if ( !_DEVICE_WRITE_DATA( byteString.constData(), len ) ) return false; - for ( quint32 i = n; i < 8; ++i ) { - if ( device->write( "\0", 1 ) != 1 ) + // Pad it to CHAR8_STRING_SIZE bytes + for ( ; len < CHAR8_STRING_SIZE; len++ ) { + if ( !_DEVICE_WRITE_DATA( "\0", 1 ) ) return false; } - return true; } case NifValue::tByteArray: { - QByteArray * array = static_cast(val.val.data); - int len = array->count(); - - if ( device->write( (char *)&len, 4 ) != 4 ) + auto array = static_cast(val.val.data); + if ( !array ) return false; - return device->write( *array ) == len; + int32_t len = array->count(); + return _DEVICE_WRITE_VALUE( len ) && ( device->write( *array ) == len ); } case NifValue::tStringPalette: { - QByteArray * array = static_cast(val.val.data); - int len = array->count(); - - if ( device->write( (char *)&len, 4 ) != 4 ) + auto array = static_cast(val.val.data); + if ( !array ) return false; - if ( device->write( *array ) != len ) + int32_t len = array->count(); + if ( len > 0xffff ) return false; - - return device->write( (char *)&len, 4 ) == 4; + return _DEVICE_WRITE_VALUE( len ) && ( device->write( *array ) == len ) && _DEVICE_WRITE_VALUE( len ); } case NifValue::tByteMatrix: { - ByteMatrix * array = static_cast(val.val.data); - int len = array->count( 0 ); - - if ( device->write( (char *)&len, 4 ) != 4 ) + auto array = static_cast(val.val.data); + if ( !array ) return false; - len = array->count( 1 ); - - if ( device->write( (char *)&len, 4 ) != 4 ) - return false; + int32_t size1 = array->count( 0 ); + int32_t size2 = array->count( 1 ); + int64_t totalSize = int64_t(size1) * int64_t(size2); - len = array->count(); - return device->write( array->data(), len ) == len; + return _DEVICE_WRITE_VALUE( size1 ) && _DEVICE_WRITE_VALUE( size2 ) && _DEVICE_WRITE_DATA( array->data(), totalSize ); } case NifValue::tString: case NifValue::tFilePath: { if ( stringAdjust ) { if ( val.val.u32 < 0x00010000 ) { - return device->write( (char *)&val.val.u32, 4 ) == 4; + return _DEVICE_WRITE_VALUE( val.val.u32 ); } else { - int value = 0; - return device->write( (char *)&value, 4 ) == 4; + uint32_t value = 0; + return _DEVICE_WRITE_VALUE( value ); } } else { - QByteArray string; - - if ( val.val.data != 0 ) { - string = static_cast(val.val.data)->toLatin1(); - } - - //string.replace( "\\r", "\r" ); - //string.replace( "\\n", "\n" ); - int len = string.size(); - - if ( device->write( (char *)&len, 4 ) != 4 ) - return false; - - return device->write( string.constData(), string.size() ) == string.size(); + QByteArray byteString; + if ( val.val.data != 0 ) + byteString = static_cast(val.val.data)->toLatin1(); + int32_t len = byteString.size(); + return _DEVICE_WRITE_VALUE( len ) && _DEVICE_WRITE_DATA( byteString.constData(), len ); } } case NifValue::tBSVertexDesc: { auto d = static_cast(val.val.data); - if ( !d ) - return false; - - return device->write( (char*)&d->desc, 8 ) == 8; + return d && _DEVICE_WRITE_VALUE( d->desc ); } case NifValue::tBlob: - - if ( val.val.data ) { - QByteArray * array = static_cast(val.val.data); - return device->write( array->data(), array->size() ) == array->size(); + { + auto blob = static_cast(val.val.data); + return blob && _DEVICE_WRITE_DATA( blob->data(), blob->size() ); } - - return true; case NifValue::tNone: return true; + default: + Q_ASSERT( 0 ); } return false; @@ -881,12 +859,7 @@ int NifSStream::size( const NifValue & val ) { switch ( val.type() ) { case NifValue::tBool: - - if ( bool32bit ) - return 4; - else - return 1; - + return bool32bit ? 4 : 1; case NifValue::tByte: case NifValue::tNormbyte: return 1; @@ -911,104 +884,95 @@ int NifSStream::size( const NifValue & val ) case NifValue::tHfloat: return 2; case NifValue::tByteVector3: - return 3; + return 1 * 3; + case NifValue::tUshortVector3: + return 2 * 3; case NifValue::tHalfVector3: - return 6; + return 2 * 3; case NifValue::tHalfVector2: - return 4; + return 2 * 2; + case NifValue::tVector2: + return 4 * 2; case NifValue::tVector3: - return 12; + return 4 * 3; case NifValue::tVector4: - return 16; + return 4 * 4; case NifValue::tTriangle: - return 6; + return 2 * 3; case NifValue::tQuat: case NifValue::tQuatXYZW: - return 16; + return 4 * 4; case NifValue::tMatrix: - return 36; + return 4 * 3 * 3; case NifValue::tMatrix4: - return 64; - case NifValue::tVector2: + return 4 * 4 * 4; case NifValue::tBSVertexDesc: return 8; case NifValue::tColor3: - return 12; + return 4 * 3; case NifValue::tByteColor4: - return 4; + return 1 * 4; case NifValue::tColor4: - return 16; + return 4 * 4; case NifValue::tSizedString: + case NifValue::tText: { - QByteArray string = static_cast(val.val.data)->toLatin1(); - //string.replace( "\\r", "\r" ); - //string.replace( "\\n", "\n" ); - return 4 + string.size(); + auto valString = static_cast(val.val.data); + return 4 + ( valString ? valString->toLatin1().size() : 0 ); } case NifValue::tShortString: { - QByteArray string = static_cast(val.val.data)->toLatin1(); + int len = 0; - //string.replace( "\\r", "\r" ); - //string.replace( "\\n", "\n" ); - if ( string.size() > 254 ) - string.resize( 254 ); + auto valString = static_cast(val.val.data); + if ( valString ) { + QByteArray byteString = valString->toLatin1(); + shortString_prepareForWrite( byteString ); + len = byteString.size(); + } - return 1 + string.size() + 1; - } - case NifValue::tText: - { - QByteArray string = static_cast(val.val.data)->toLatin1(); - return 4 + string.size(); + return 1 + len + 1; } case NifValue::tHeaderString: case NifValue::tLineString: { - QByteArray string = static_cast(val.val.data)->toLatin1(); - return string.length() + 1; + auto valString = static_cast(val.val.data); + return ( valString ? valString->toLatin1().size() : 0 ) + 1; } case NifValue::tChar8String: - { - return 8; - } + return CHAR8_STRING_SIZE; case NifValue::tByteArray: { - QByteArray * array = static_cast(val.val.data); - return 4 + array->count(); + auto array = static_cast(val.val.data); + return 4 + ( array ? array->count() : 0 ); } case NifValue::tStringPalette: { - QByteArray * array = static_cast(val.val.data); - return 4 + array->count() + 4; + auto array = static_cast(val.val.data); + return 4 + ( array ? array->count() : 0 ) + 4; } case NifValue::tByteMatrix: { - ByteMatrix * array = static_cast(val.val.data); - return 4 + 4 + array->count(); + auto array = static_cast(val.val.data); + return 4 + 4 + ( array ? array->count() : 0 ); } case NifValue::tString: case NifValue::tFilePath: - { - if ( stringAdjust ) { - return 4; - } - QByteArray string = static_cast(val.val.data)->toLatin1(); - //string.replace( "\\r", "\r" ); - //string.replace( "\\n", "\n" ); - return 4 + string.size(); + if ( stringAdjust ) { + return 4; + } else { + auto valString = static_cast(val.val.data); + return 4 + ( valString ? valString->toLatin1().size() : 0 ); } - case NifValue::tBlob: - - if ( val.val.data ) { - QByteArray * array = static_cast(val.val.data); - return array->size(); + { + auto blob = static_cast(val.val.data); + return blob ? blob->size() : 0; } - - return 0; - case NifValue::tNone: return 0; + default: + Q_ASSERT( 0 ); } return 0; diff --git a/src/io/nifstream.h b/src/io/nifstream.h index b897db142..3272e11ea 100644 --- a/src/io/nifstream.h +++ b/src/io/nifstream.h @@ -87,6 +87,9 @@ class NifIStream final //! The maximum length of a string that can be read. int maxLength = 0x8000; + + bool readSizedString( NifValue & val ); + bool readLineString( QByteArray & outString, int maxLineLength ); }; diff --git a/src/lib/importex/3ds.cpp b/src/lib/importex/3ds.cpp index 5d8aaa0f3..167a6a9a4 100644 --- a/src/lib/importex/3ds.cpp +++ b/src/lib/importex/3ds.cpp @@ -9,6 +9,7 @@ #include "spellbook.h" #include "gl/gltex.h" +#include "spells/texture.h" #include "lib/nvtristripwrapper.h" @@ -644,24 +645,10 @@ void import3ds( NifModel * nif, const QModelIndex & index ) addLink( nif, iShape, "Properties", nif->getBlockNumber( iTexProp ) ); - nif->set( iTexProp, "Has Base Texture", 1 ); - QModelIndex iBaseMap = nif->getIndex( iTexProp, "Base Texture" ); - nif->set( iBaseMap, "Clamp Mode", 3 ); - nif->set( iBaseMap, "Filter Mode", 2 ); - if ( iTexSource.isValid() == false || objIndex != 0 || nif->itemStrType( iTexSource ) != "NiSourceTexture" ) { - iTexSource = nif->insertNiBlock( "NiSourceTexture" ); + iTexSource = QModelIndex(); } - - nif->setLink( iBaseMap, "Source", nif->getBlockNumber( iTexSource ) ); - - nif->set( iTexSource, "Pixel Layout", nif->getVersion() == "20.0.0.5" ? 6 : 5 ); - nif->set( iTexSource, "Use Mipmaps", 2 ); - nif->set( iTexSource, "Alpha Format", 3 ); - nif->set( iTexSource, "Unknown Byte", 1 ); - nif->set( iTexSource, "Unknown Byte 2", 1 ); - - nif->set( iTexSource, "Use External", 1 ); + iTexSource = NiTexturingProperty_addTexture( nif, iTexProp, iTexSource, "Base Texture" ); nif->set( iTexSource, "File Name", mat->map_Kd ); } else { //Older versions use NiTextureProperty and NiImage diff --git a/src/lib/importex/col.cpp b/src/lib/importex/col.cpp index 5d465c587..9b551047b 100644 --- a/src/lib/importex/col.cpp +++ b/src/lib/importex/col.cpp @@ -34,6 +34,7 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "gl/gltex.h" #include "gl/glscene.h" #include "model/nifmodel.h" +#include "version.h" #include "lib/nvtristripwrapper.h" @@ -843,12 +844,7 @@ void attachNiShape ( const NifModel * nif, QDomElement parentNode, int idx ) QModelIndex iPoints = nif->getIndex( iProp, "Points" ); if ( iPoints.isValid() ) { - QVector > strips; - - for ( int r = 0; r < nif->rowCount( iPoints ); r++ ) - strips.append( nif->getArray( iPoints.child( r, 0 ) ) ); - - tri = triangulate( strips ); + tri = triangulateStrips( nif, iPoints ); } else { tri = nif->getArray( iProp, "Triangles" ); } @@ -1008,7 +1004,7 @@ void exportCol( const NifModel * nif, const Scene* scene, QFileInfo fileInfo ) QDomElement contributor = doc.createElement( "contributor" ); asset.appendChild( contributor ); contributor.appendChild( doc.createElement( "author" ) ); - contributor.appendChild( textElement( "authoring_tool", QString( "NifSkope %1" ).arg( NIFSKOPE_VERSION ) ) ); + contributor.appendChild( textElement( "authoring_tool", QString( APP_NAME_FULL ) ) ); contributor.appendChild( doc.createElement( "comments" ) ); asset.appendChild( dateElement( "created", QDateTime::currentDateTime() ) ); asset.appendChild( dateElement( "modified", QDateTime::currentDateTime() ) ); diff --git a/src/lib/importex/gltf.cpp b/src/lib/importex/gltf.cpp index 7b6f8ca57..558ddadf6 100644 --- a/src/lib/importex/gltf.cpp +++ b/src/lib/importex/gltf.cpp @@ -98,7 +98,7 @@ bool exportCreateNodes(const NifModel* nif, const Scene* scene, tinygltf::Model& // Fill nodes map gltf.nodes[nodeId].append(gltfNodeID); - gltfNode.name = node->getName().toStdString(); + gltfNode.name = node->blockName().toStdString(); if ( isBSGeometry ) { gltfNode.name += ":LOD" + std::to_string(j); // Skins @@ -211,7 +211,7 @@ bool exportCreateNodes(const NifModel* nif, const Scene* scene, tinygltf::Model& for ( const auto mesh : gltf.skins ) { if ( mesh && mesh->boneNames.size() > 0 ) { auto gltfNode = tinygltf::Node(); - gltfNode.name = mesh->getName().toStdString(); + gltfNode.name = mesh->blockName().toStdString(); model.nodes.push_back(gltfNode); int skeletonRoot = gltfNodeID++; model.skins[skinID].skeleton = skeletonRoot; @@ -534,7 +534,7 @@ bool exportCreateMeshes(const NifModel* nif, const Scene* scene, tinygltf::Model auto& gltfNode = model.nodes[n[j]]; tinygltf::Mesh gltfMesh; gltfNode.mesh = meshIndex; - gltfMesh.name = QString("%1%2%3").arg(node->getName()).arg(":LOD").arg(j).toStdString(); + gltfMesh.name = QString("%1%2%3").arg(node->blockName()).arg(":LOD").arg(j).toStdString(); int materialID = gltf.materials.indexOf(mesh->materialPath) + 1; int lodLevel = (hasGPULODs) ? 0 : j; if ( exportCreatePrimitives(model, bin, mesh, gltfMesh, attributeIndex, lodLevel, materialID, gltf, skeletalLodIndex) ) { diff --git a/src/lib/importex/importex.cpp b/src/lib/importex/importex.cpp index 5c85e0789..a0cfc6629 100644 --- a/src/lib/importex/importex.cpp +++ b/src/lib/importex/importex.cpp @@ -62,15 +62,15 @@ struct ImportExportOption std::function importFn; std::function exportFn; quint32 minBSVersion = 0; - quint32 maxBSVersion = 0xFFFFFFFF; + quint32 maxBSVersion = 0; quint32 minVersion = 0; - quint32 maxVersion = 0x1404FFFF; // < 0x14050000 (20.5) + quint32 maxVersion = 0x14050000 - 1; // < 0x14050000 (20.5) }; -QVector impexOptions{ - ImportExportOption{ ".OBJ", importObj, exportObj, 0, 171 }, - ImportExportOption{ ".OBJ as Collision", importObjAsCollision, nullptr, 0, 171 }, +const QVector impexOptions{ + ImportExportOption{ ".OBJ", importObj, exportObj, 0, 172 - 1 }, + ImportExportOption{ ".OBJ as Collision", importObjAsCollision, nullptr, 1, 172 - 1 }, ImportExportOption{ ".glTF", nullptr, exportGltf, 172 }, }; @@ -80,12 +80,12 @@ void NifSkope::fillImportExportMenus() int impexIndex = 0; for ( const auto& option : impexOptions ) { if ( option.exportFn ) { - mExport->addAction(tr("Export %1").arg(option.name.c_str())); - mExport->actions().last()->setData(impexIndex); + auto a = mExport->addAction(tr("Export %1").arg(option.name.c_str())); + a->setData(impexIndex); } if ( option.importFn ) { - mImport->addAction(tr("Import %1").arg(option.name.c_str())); - mImport->actions().last()->setData(impexIndex); + auto a = mImport->addAction(tr("Import %1").arg(option.name.c_str())); + a->setData(impexIndex); } impexIndex++; } @@ -97,10 +97,8 @@ void NifSkope::updateImportExportMenu(const QMenu * menu) a->setEnabled(false); auto impex = impexOptions.value(a->data().toInt(), {}); if ( impex.importFn || impex.exportFn ) { - auto nifver = nif->getVersionNumber(); - auto bsver = nif->getBSVersion(); - if ( (nifver >= impex.minVersion && nifver <= impex.maxVersion) - && (bsver >= impex.minBSVersion && bsver <= impex.maxBSVersion) ) + if ( BaseModel::checkVersion(nif->getVersionNumber(), impex.minVersion, impex.maxVersion) + && BaseModel::checkVersion(nif->getBSVersion(), impex.minBSVersion, impex.maxBSVersion) ) { a->setEnabled(true); } diff --git a/src/lib/importex/obj.cpp b/src/lib/importex/obj.cpp index affd71cac..fd0fe3167 100644 --- a/src/lib/importex/obj.cpp +++ b/src/lib/importex/obj.cpp @@ -35,6 +35,7 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "gl/glscene.h" #include "model/nifmodel.h" #include "spells/tangentspace.h" +#include "spells/texture.h" #include "lib/nvtristripwrapper.h" @@ -96,12 +97,7 @@ static void writeData( const NifModel * nif, const QModelIndex & iData, QTextStr QModelIndex iPoints = nif->getIndex( iData, "Points" ); if ( iPoints.isValid() ) { - QVector > strips; - - for ( int r = 0; r < nif->rowCount( iPoints ); r++ ) - strips.append( nif->getArray( iPoints.child( r, 0 ) ) ); - - tris = triangulate( strips ); + tris = triangulateStrips( nif, iPoints ); } else { tris = nif->getArray( iData, "Triangles" ); } @@ -850,27 +846,10 @@ void importObjMain( NifModel * nif, const QModelIndex & index, bool collision ) // no need of NiTexturingProperty when BSShaderPPLightingProperty is present addLink( nif, iShape, "Properties", nif->getBlockNumber( iTexProp ) ); - nif->set( iTexProp, "Has Base Texture", 1 ); - iBaseMap = nif->getIndex( iTexProp, "Base Texture" ); - nif->set( iBaseMap, "Clamp Mode", 3 ); - nif->set( iBaseMap, "Filter Mode", 2 ); - } - - if ( iTexSource.isValid() == false || first_tri_shape == false || nif->itemStrType( iTexSource ) != "NiSourceTexture" ) { - if ( !cBSShaderPPLightingProperty ) - iTexSource = nif->insertNiBlock( "NiSourceTexture" ); - } - - if ( !cBSShaderPPLightingProperty ) { - // no need of NiTexturingProperty when BSShaderPPLightingProperty is present - nif->setLink( iBaseMap, "Source", nif->getBlockNumber( iTexSource ) ); - nif->set( iTexSource, "Pixel Layout", nif->getVersion() == "20.0.0.5" ? 6 : 5 ); - nif->set( iTexSource, "Use Mipmaps", 2 ); - nif->set( iTexSource, "Alpha Format", 3 ); - nif->set( iTexSource, "Unknown Byte", 1 ); - nif->set( iTexSource, "Unknown Byte 2", 1 ); - - nif->set( iTexSource, "Use External", 1 ); + if ( iTexSource.isValid() == false || first_tri_shape == false || nif->itemStrType( iTexSource ) != "NiSourceTexture" ) { + iTexSource = QModelIndex(); + } + iTexSource = NiTexturingProperty_addTexture( nif, iTexProp, iTexSource, "Base Texture" ); nif->set( iTexSource, "File Name", TexCache::stripPath( mtl.map_Kd, nif->getFolder() ) ); } } else { @@ -1061,7 +1040,8 @@ void importObjMain( NifModel * nif, const QModelIndex & index, bool collision ) nif->set( iData, "Radius", radius ); // do not stitch, because it looks better in the cs - QVector > strips = stripify( triangles ); + // FIXME: "do not stitch" in the comment above contradicts the declaration of stripifyTriangles with its ", bool stitch = true". + auto strips = stripifyTriangles( triangles ); nif->set( iData, "Num Strips", strips.count() ); nif->set( iData, "Has Points", 1 ); @@ -1074,11 +1054,11 @@ void importObjMain( NifModel * nif, const QModelIndex & index, bool collision ) nif->updateArraySize( iPoints ); int x = 0; int z = 0; - for ( const QVector & strip : strips ) { + for ( const auto & strip : strips ) { nif->set( iLengths.child( x, 0 ), strip.count() ); QModelIndex iStrip = iPoints.child( x, 0 ); nif->updateArraySize( iStrip ); - nif->setArray( iStrip, strip ); + nif->setArray( iStrip, strip ); x++; z += strip.count() - 2; } diff --git a/src/lib/nvtristripwrapper.cpp b/src/lib/nvtristripwrapper.cpp index 66a9d40d0..e75af172e 100644 --- a/src/lib/nvtristripwrapper.cpp +++ b/src/lib/nvtristripwrapper.cpp @@ -1,84 +1,79 @@ #include "nvtristripwrapper.h" -#include "data/niftypes.h" #include +#include "model/nifmodel.h" -QVector > stripify( QVector triangles, bool stitch ) +QVector stripifyTriangles( const QVector & triangles, bool stitch ) { - if ( triangles.count() <= 0 ) - return QVector >(); - - unsigned short * data = (unsigned short *)malloc( triangles.count() * 3 * sizeof( unsigned short ) ); - if ( !data ) - return QVector >(); - - for ( int t = 0; t < triangles.count(); t++ ) { - data[ t * 3 + 0 ] = triangles[t][0]; - data[ t * 3 + 1 ] = triangles[t][1]; - data[ t * 3 + 2 ] = triangles[t][2]; - } - - PrimitiveGroup * groups = 0; - unsigned short numGroups = 0; - - SetStitchStrips( stitch ); - //SetCacheSize( 64 ); - GenerateStrips( data, triangles.count() * 3, &groups, &numGroups ); - free( data ); - - QVector > strips; - - if ( !groups ) - return strips; - - for ( int g = 0; g < numGroups; g++ ) { - if ( groups[g].type == PT_STRIP ) { - QVector strip( groups[g].numIndices, 0 ); - - for ( quint32 s = 0; s < groups[g].numIndices; s++ ) { - strip[s] = groups[g].indices[s]; + QVector strips; + + int nTris = triangles.count(); + if ( nTris > 0 ) { + PrimitiveGroup * groups = nullptr; + unsigned short numGroups = 0; + SetStitchStrips( stitch ); + //SetCacheSize( 64 ); + auto pTriPoints = reinterpret_cast( triangles.constData() ); + GenerateStrips( pTriPoints, nTris * 3, &groups, &numGroups ); + + if ( groups ) { + auto pGroup = groups; + for ( int g = 0; g < numGroups; g++, pGroup++ ) { + if ( pGroup->type == PT_STRIP ) { + TriStrip strip; + + int nStripPoints = pGroup->numIndices; + strip.reserve( nStripPoints ); + auto pOut = strip.data(); + auto pIn = pGroup->indices; + for ( ; nStripPoints > 0; nStripPoints--, pOut++, pIn++ ) { + *pOut = *pIn; + } + + strips.append( strip ); + } } - strips.append( strip ); + delete [] groups; } - } - delete [] groups; + } return strips; } -QVector triangulate( QVector strip ) +QVector triangulateStrip( const TriStrip & stripPoints ) { QVector tris; - quint16 a, b = strip.value( 0 ), c = strip.value( 1 ); - bool flip = false; - - for ( int s = 2; s < strip.count(); s++ ) { - a = b; - b = c; - c = strip.value( s ); - - if ( a != b && b != c && c != a ) { - if ( !flip ) - tris.append( Triangle( a, b, c ) ); - else - tris.append( Triangle( a, c, b ) ); - } - flip = !flip; + int nStripTris = stripPoints.count() - 2; + if ( nStripTris > 0 ) { + tris.reserve( nStripTris ); + + auto pPoints = stripPoints.constData(); + for ( int i = 0; i < nStripTris; i++, pPoints++ ) { + auto a = pPoints[0]; + auto b = pPoints[1]; + auto c = pPoints[2]; + + if ( a != b && b != c && c != a ) { + if ( (i & 1) == 0 ) + tris.append( Triangle( a, b, c ) ); + else + tris.append( Triangle( a, c, b ) ); + } + } } return tris; } -QVector triangulate( QVector > strips ) +QVector triangulateStrips( const NifModel * nif, const QModelIndex & iStrips ) { QVector tris; - for ( const QVector& strip : strips ) { - tris += triangulate( strip ); + for ( int r = 0; r < nif->rowCount( iStrips ); r++ ) { + tris += triangulateStrip( nif->getArray( iStrips.child( r, 0 ) ) ); } return tris; } - diff --git a/src/lib/nvtristripwrapper.h b/src/lib/nvtristripwrapper.h index 7f7bdf8c7..ad1a61959 100644 --- a/src/lib/nvtristripwrapper.h +++ b/src/lib/nvtristripwrapper.h @@ -3,12 +3,10 @@ #include #include +#include "data/niftypes.h" - -class Triangle; - -QVector > stripify( QVector triangles, bool stitch = true ); -QVector triangulate( QVector strips ); -QVector triangulate( QVector > strips ); +QVector stripifyTriangles( const QVector & triangles, bool stitch = true ); +QVector triangulateStrip( const TriStrip & stripPoints ); +QVector triangulateStrips( const NifModel * nif, const QModelIndex & iStrips ); #endif diff --git a/src/main.cpp b/src/main.cpp index 04db24cb4..ba28c8621 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -35,6 +35,7 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "data/nifvalue.h" #include "model/nifmodel.h" #include "model/kfmmodel.h" +#include "ui/UiUtils.h" #include "gamemanager.h" @@ -46,11 +47,18 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include +#include QCoreApplication * createApplication( int &argc, char *argv[] ) { QApplication::setAttribute(Qt::AA_UseDesktopOpenGL); + // Qt::AA_EnableHighDpiScaling code below was added for the UI to scale according to the Windows settings (primarily needed for high DPI displays). + // There are some indications that the code is not needed in Qt 6, so this whole subject might be revisited after migrating to Qt 6. + #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) + QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); + #endif // Iterate over args for ( int i = 1; i < argc; ++i ) { // -no-gui: start as core app without all the GUI overhead @@ -66,17 +74,27 @@ QCoreApplication * createApplication( int &argc, char *argv[] ) * main */ +void initSettings(); + //! The main program int main( int argc, char * argv[] ) { QScopedPointer app( createApplication( argc, argv ) ); if ( auto a = qobject_cast(app.data()) ) { + // The Organization and Application names here define the default path for all QSettings in the app. + // Any change to them would need a custom code for migrating the settings to the new location on app update. + // So they must NOT be auto-updated from APP_* macros. a->setOrganizationName( "NifTools" ); + a->setApplicationName( "NifSkope 2.0" ); + a->setOrganizationDomain( "niftools.org" ); - a->setApplicationName( "NifSkope " + NifSkopeVersion::rawToMajMin( NIFSKOPE_VERSION ) ); - a->setApplicationVersion( NIFSKOPE_VERSION ); - a->setApplicationDisplayName( "NifSkope " + NifSkopeVersion::rawToDisplay( NIFSKOPE_VERSION, true ) ); + a->setApplicationVersion( APP_VER_SHORT ); + #ifdef _DEBUG + UIUtils::applicationDisplayName = QString( APP_NAME_FULL " - DEBUG" ); + #else + UIUtils::applicationDisplayName = QString( APP_NAME_FULL ); + #endif // Must set current directory or this causes issues with several features QDir::setCurrent( qApp->applicationDirPath() ); @@ -84,16 +102,16 @@ int main( int argc, char * argv[] ) // Register message handler //qRegisterMetaType( "Message" ); qInstallMessageHandler( NifSkope::MessageOutput ); + // freopen("log.txt", "w", stderr); // Register types qRegisterMetaType( "NifValue" ); QMetaType::registerComparators(); // Set locale - QSettings cfg( QString( "%1/nifskope.ini" ).arg( QCoreApplication::applicationDirPath() ), QSettings::IniFormat ); - cfg.beginGroup( "Settings" ); - NifSkope::SetAppLocale( cfg.value( "Locale", "en" ).toLocale() ); - cfg.endGroup(); + NifSkope::SetAppLocale( QLocale("en") ); + + initSettings(); // Load XML files NifModel::loadXML(); @@ -159,6 +177,113 @@ int main( int argc, char * argv[] ) return 0; } +using MigrateSettingsEntry = QPair; +using MigrateSettingsList = QVector; + +static const MigrateSettingsList migrate1_1 = { + { "auto sanitize", "File/Auto Sanitize" }, + { "list mode", "UI/List Mode" }, + { "enable animations", "GLView/Enable Animations" }, + { "perspective", "GLView/Perspective" }, + + { "Render Settings/Draw Axes", "Settings/Render/General/Startup Defaults/Show Axes" }, + { "Render Settings/Draw Collision Geometry", "Settings/Render/General/Startup Defaults/Show Collision" }, + { "Render Settings/Draw Constraints", "Settings/Render/General/Startup Defaults/Show Constraints" }, + { "Render Settings/Draw Furniture Markers", "Settings/Render/General/Startup Defaults/Show Markers" }, + { "Render Settings/Draw Nodes", "Settings/Render/General/Startup Defaults/Show Nodes" }, + // { "Render Settings/Enable Shaders", "Settings/Render/General/Use Shaders" }, + { "Render Settings/Show Hidden Objects", "Settings/Render/General/Startup Defaults/Show Hidden" }, +}; + +static const MigrateSettingsList migrate1_2 = { + { "File/Auto Sanitize", "File/Auto Sanitize" }, + { "UI/List Mode", "UI/List Mode" }, + { "GLView/Enable Animations", "GLView/Enable Animations" }, + { "GLView/Perspective", "GLView/Perspective" }, + + { "Render Settings/Draw Axes", "Settings/Render/General/Startup Defaults/Show Axes" }, + { "Render Settings/Draw Collision Geometry", "Settings/Render/General/Startup Defaults/Show Collision" }, + { "Render Settings/Draw Constraints", "Settings/Render/General/Startup Defaults/Show Constraints" }, + { "Render Settings/Draw Furniture Markers", "Settings/Render/General/Startup Defaults/Show Markers" }, + { "Render Settings/Draw Nodes", "Settings/Render/General/Startup Defaults/Show Nodes" }, + { "Render Settings/Enable Shaders", "Settings/Render/General/Use Shaders" }, + { "Render Settings/Show Hidden Objects", "Settings/Render/General/Startup Defaults/Show Hidden" }, +}; + +static void migrateSettings( QSettings & newCfg, const QString & oldCompany, const QString & oldAppName, const MigrateSettingsList & migrateKeys, bool & alreadyMigrated ) +{ + QSettings oldCfg( oldCompany, oldAppName ); + if ( !oldCfg.value("Version").isValid() ) + return; + + // The "migrated" thing left in case someone runs an older pre-Dev 10 release of NifSkope 2.0 so it would not copy the settings AGAIN. + // It could be removed if the settings root changes to something other than "NifSkope 2.0". + oldCfg.setValue( "migrated", true ); + + if ( alreadyMigrated ) + return; + alreadyMigrated = true; + + auto copyValue = [&oldCfg, &newCfg]( const QString & oldPath, const QString & newPath ) { + QVariant val = oldCfg.value( oldPath ); + if ( val.isValid() && val.type() != QVariant::ByteArray ) + newCfg.setValue( newPath, val ); + }; + + // Copy entire groups + const QStringList groupsToCopy = { "spells/", "import-export/", "XML Checker/", }; + for ( const auto & key : oldCfg.allKeys() ) { + for ( const auto & groupToCopy : groupsToCopy ) { + if ( key.startsWith( groupToCopy, Qt::CaseInsensitive ) ) { + copyValue( key, key ); + break; + } + } + } + + // Copy stuff from migrateKeys + for ( const auto & pair : migrateKeys ) + copyValue( pair.first, pair.second ); +} + +void initSettings() +{ + QSettings cfg; + + QString newCfgVer = APP_VER_SHORT; + QString oldCfgVer = cfg.value("Version").toString(); + if ( newCfgVer != oldCfgVer ) { + bool migrated = ( oldCfgVer.length() > 0 ); + migrateSettings( cfg, "NifTools", "NifSkope 1.2", migrate1_2, migrated ); + migrateSettings( cfg, "NifTools", "NifSkope", migrate1_1, migrated ); + + cfg.setValue( "Version", newCfgVer ); + } + + // Qt version update +#ifdef QT_NO_DEBUG + QString newQtVer = QT_VERSION_STR; + QString oldQtVer = cfg.value("Qt Version").toString(); + if ( newQtVer != oldQtVer ) { + auto newv = QVersionNumber::fromString( newQtVer ); + auto oldv = QVersionNumber::fromString( oldQtVer ); + if ( newv.majorVersion() != oldv.majorVersion() + || ( oldv.majorVersion() == 5 && oldv.minorVersion() < 7 ) // Gavrant: keeping byte arrays from Qt 5.7.x (Dev 7) and above seems to be rather safe? + ) { + // Check all keys and delete all QByteArrays to prevent portability problems between Qt versions + for ( const auto & key : cfg.allKeys() ) { + if ( cfg.value( key ).type() == QVariant::ByteArray ) { + // QDebug(QtInfoMsg) << "Removing Qt version-specific settings" << key + // << "while migrating settings from previous version"; + cfg.remove( key ); + } + } + } + + cfg.setValue( "Qt Version", newQtVer ); + } +#endif +} /* diff --git a/src/message.cpp b/src/message.cpp index 7859ed490..e646e400c 100644 --- a/src/message.cpp +++ b/src/message.cpp @@ -5,7 +5,10 @@ #include #include #include +#include +#include "ui/UiUtils.h" +#include "ui/ToolDialog.h" Q_LOGGING_CATEGORY( ns, "nifskope" ) Q_LOGGING_CATEGORY( nsGl, "nifskope.gl" ) @@ -25,43 +28,33 @@ Message::~Message() } //! Static helper for message box without detail text -QMessageBox* Message::message( QWidget * parent, const QString & str, QMessageBox::Icon icon ) +void Message::message( QWidget * parent, const QString & str, QMessageBox::Icon icon ) { auto msgBox = new QMessageBox( parent ); - msgBox->setWindowFlags( msgBox->windowFlags() | Qt::Tool ); msgBox->setAttribute( Qt::WA_DeleteOnClose ); - msgBox->setWindowModality( Qt::NonModal ); + UIUtils::setWindowTitle( msgBox ); msgBox->setText( str ); msgBox->setIcon( icon ); msgBox->show(); - - msgBox->activateWindow(); - - return msgBox; } //! Static helper for message box with detail text -QMessageBox* Message::message( QWidget * parent, const QString & str, const QString & err, QMessageBox::Icon icon ) +void Message::message( QWidget * parent, const QString & str, const QString & err, QMessageBox::Icon icon ) { if ( !parent ) parent = qApp->activeWindow(); auto msgBox = new QMessageBox( parent ); msgBox->setAttribute( Qt::WA_DeleteOnClose ); - msgBox->setWindowModality( Qt::NonModal ); - msgBox->setWindowFlags( msgBox->windowFlags() | Qt::Tool ); + UIUtils::setWindowTitle( msgBox ); msgBox->setText( str ); msgBox->setIcon( icon ); msgBox->setDetailedText( err ); msgBox->show(); - - msgBox->activateWindow(); - - return msgBox; } //! Static helper for installed message handler @@ -141,20 +134,63 @@ class DetailsMessageBox : public QMessageBox { public: explicit DetailsMessageBox( QWidget * parent, const QString & txt ) - : QMessageBox( parent ), m_key( txt ) { } + : QMessageBox( parent ), msgKey( txt ) + { + UIUtils::setWindowTitle( this ); + + detailFlushTimer = new QTimer( this ); + detailFlushTimer->setSingleShot( true ); + detailFlushTimer->setInterval( 20 ); + connect( detailFlushTimer, &QTimer::timeout, this, &DetailsMessageBox::flushDetailBuffer ); + } ~DetailsMessageBox(); - const QString & key() const { return m_key; } + const QString & key() const { return msgKey; } + + void setFirstDetail( const QString & detailText ) + { + if ( !detailText.isEmpty() ) { + detailBuffer = detailText + "\n"; + setDetailedText( detailBuffer ); + } else { + // detailText is empty, set the detailed text to a dummy, just for the sake of setting up "Show/Hide Details". + setDetailedText( " \n" ); + detailBuffer.clear(); // Just in case... + } + + // Auto-show detailed text on first show. + // https://stackoverflow.com/questions/36083551/qmessagebox-show-details + for ( auto btn : buttons() ) { + if ( buttonRole( btn ) == QMessageBox::ActionRole ) { + btn->click(); // "Click" it to expand the detailed text + break; + } + } + } + + void appendDetail( const QString & detailText ) + { + if ( detailText.isEmpty() ) + return; + + detailBuffer.append( detailText + "\n" ); + if ( !detailFlushTimer->isActive() ) + detailFlushTimer->start(); + } - int detailsCount() const { return m_nDetails; } - void updateDetailsCount() { m_nDetails++; } +protected slots: + void flushDetailBuffer() + { + setDetailedText( detailBuffer ); + } protected: void closeEvent( QCloseEvent * event ) override; - QString m_key; - int m_nDetails = 0; + QString msgKey; + QString detailBuffer; + QTimer * detailFlushTimer; }; static QVector messageBoxes; @@ -179,18 +215,7 @@ void Message::append( QWidget * parent, const QString & str, const QString & err } if ( msgBox ) { - // Limit the number of detail lines to MAX_DETAILS_COUNTER - // because when errors are spammed at hundreds or thousands in a row, this makes NifSkope unresponsive. - const int MAX_DETAILS_COUNTER = 50; - - if ( !err.isEmpty() && msgBox->detailsCount() <= MAX_DETAILS_COUNTER ) { - // Append strings to existing message box's Detailed Text - // Show box if it has been closed before - msgBox->show(); - QString newLine = ( msgBox->detailsCount() < MAX_DETAILS_COUNTER ) ? (err + "\n") : QString("...\n"); - msgBox->setDetailedText( msgBox->detailedText().append( newLine ) ); - msgBox->updateDetailsCount(); - } + msgBox->appendDetail( err ); } else { // Create new message box @@ -198,8 +223,7 @@ void Message::append( QWidget * parent, const QString & str, const QString & err messageBoxes.append( msgBox ); msgBox->setAttribute( Qt::WA_DeleteOnClose ); - msgBox->setWindowModality( Qt::NonModal ); - msgBox->setWindowFlags( msgBox->windowFlags() | Qt::Tool ); + ToolDialog::setDialogFlagsAndModality( msgBox, ToolDialog::NonBlocking ); // Set the min. width of the label containing str to a quarter of the screen resolution. // This makes the detailed text more readable even when str is short. @@ -214,28 +238,15 @@ void Message::append( QWidget * parent, const QString & str, const QString & err msgBox->setText( str ); msgBox->setIcon( icon ); - connect( msgBox, &QMessageBox::buttonClicked, [msgBox]( QAbstractButton * button ) { - Q_UNUSED( button ); + connect( msgBox, &QMessageBox::buttonClicked, [msgBox]( [[maybe_unused]] QAbstractButton * button ) { unregisterMessageBox( msgBox ); } ); msgBox->show(); - // setDetailedText(...) has to be after show(), + // setDetailedText() in setFirstDetail() has to be called after show(), // otherwise a "QWindowsWindow::setGeometry: Unable to set geometry ..." warning from Qt appears in Debug build. - if ( !err.isEmpty() ) { - msgBox->setDetailedText( err + "\n" ); - msgBox->updateDetailsCount(); - - // Auto-show detailed text on first show. - // https://stackoverflow.com/questions/36083551/qmessagebox-show-details - for ( auto btn : msgBox->buttons() ) { - if ( msgBox->buttonRole( btn ) == QMessageBox::ActionRole ) { - btn->click(); // "Click" it to expand the detailed text - break; - } - } - } + msgBox->setFirstDetail( err ); msgBox->activateWindow(); } diff --git a/src/message.h b/src/message.h index 2cf1b12fa..68b5a3d51 100644 --- a/src/message.h +++ b/src/message.h @@ -22,8 +22,8 @@ class Message : QObject ~Message(); public: - static QMessageBox* message( QWidget *, const QString &, QMessageBox::Icon ); - static QMessageBox* message( QWidget *, const QString &, const QString &, QMessageBox::Icon ); + static void message( QWidget *, const QString &, QMessageBox::Icon ); + static void message( QWidget *, const QString &, const QString &, QMessageBox::Icon ); static void message( QWidget *, const QString &, const QMessageLogContext *, QMessageBox::Icon ); static void append( const QString &, const QString &, QMessageBox::Icon = QMessageBox::Warning ); diff --git a/src/model/basemodel.cpp b/src/model/basemodel.cpp index 250785038..da81989be 100644 --- a/src/model/basemodel.cpp +++ b/src/model/basemodel.cpp @@ -48,12 +48,11 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * BaseModel */ -BaseModel::BaseModel( QObject * p ) : QAbstractItemModel( p ) +BaseModel::BaseModel( QObject * p, MsgMode msgMode ) : QAbstractItemModel( p ), msgMode(msgMode) { root = new NifItem( this, nullptr ); root->setIsConditionless( true ); parentWindow = qobject_cast(p); - msgMode = MSG_TEST; } BaseModel::~BaseModel() @@ -664,8 +663,12 @@ const NifItem * BaseModel::getItem( const NifItem * parent, int childIndex, bool reportError( parent, tr( "Subitem with index %1 has failed condition check." ).arg( repr ) ); } } else { - if ( reportErrors && childIndex >= 0 ) - reportError( parent, tr( "Invalid child index %1." ).arg( childIndex ) ); + if ( reportErrors ) { + if ( childIndex >= 0 ) + reportError( parent, tr( "No subitem with index %1." ).arg( childIndex ) ); + else + reportError( parent, tr( "Invalid child index %1." ).arg( childIndex ) ); + } } return nullptr; @@ -776,33 +779,40 @@ bool BaseModel::evalConditionImpl( const NifItem * item ) const return true; } -QString BaseModel::itemRepr( const NifItem * item ) const +QString BaseModel::itemRepr( const NifItem * item, const NifItem * cutoffParent ) const { if ( !item ) return QString("[NULL]"); if ( item->model() != this ) - return item->model()->itemRepr( item ); + return item->model()->itemRepr( item, cutoffParent ); if ( item == root ) return QString("[ROOT]"); + while( cutoffParent && cutoffParent->isArray() ) + cutoffParent = cutoffParent->parent(); + QString result; while( true ) { const NifItem * parent = item->parent(); if ( !parent ) { result = "???" + result; // WTF... break; - } else if ( parent == root ) { + } + if ( parent == root ) { result = topItemRepr( item ) + result; break; - } else { - QString subres; - if ( parent->isArray() ) - subres = QString(" [%1]").arg( item->row() ); - else - subres = SLASH_QSTRING + item->name(); - result = subres + result; - item = parent; } + + QString subres; + if ( parent->isArray() ) + subres = QString(" [%1]").arg( item->row() ); + else + subres = SLASH_QSTRING + item->name(); + result = subres + result; + + if ( parent == cutoffParent ) + break; + item = parent; } return result; diff --git a/src/model/basemodel.h b/src/model/basemodel.h index 8d6098fba..5e5319abd 100644 --- a/src/model/basemodel.h +++ b/src/model/basemodel.h @@ -77,7 +77,12 @@ class BaseModel : public QAbstractItemModel friend class BaseModelEval; public: - BaseModel( QObject * parent = nullptr ); + enum MsgMode + { + MSG_USER, MSG_TEST + }; + + BaseModel( QObject * parent, MsgMode msgMode ); ~BaseModel(); //! Get parent window @@ -104,21 +109,21 @@ class BaseModel : public QAbstractItemModel * This function is used to resolve external resources. * @return The folder of the last file that was loaded with loadFromFile. */ - QString getFolder() const { return folder; } + const QString & getFolder() const { return folder; } /*! If the model was loaded from a file then getFilename returns the filename. * * This function is used to resolve external resources. * @return The filename (without extension) of the last file that was loaded with loadFromFile. */ - QString getFilename() const { return filename; } + const QString & getFilename() const { return filename; } /*! If the model was loaded from a file then getFileInfo returns a QFileInfo object. * * This function is used to resolve external resources. * @return The file info of the last file that was loaded with loadFromFile. */ - QFileInfo getFileInfo() const { return fileinfo; } + const QFileInfo & getFileInfo() const { return fileinfo; } //! Updates stored file and folder information void refreshFileInfo( const QString & ); @@ -253,11 +258,6 @@ class BaseModel : public QAbstractItemModel // end QAbstractItemModel - enum MsgMode - { - MSG_USER, MSG_TEST - }; - void setMessageMode( MsgMode mode ); MsgMode getMessageMode() const { return msgMode; } @@ -269,7 +269,7 @@ class BaseModel : public QAbstractItemModel public: //! Return string representation ("path") of an item within its model (e.g., "NiTriShape [0]\Vertex Data [3]\Vertex colors"). // Mostly for messages and debug. - QString itemRepr( const NifItem * item ) const; + QString itemRepr( const NifItem * item, const NifItem * cutoffParent = nullptr ) const; //! Return string representation ("path") of a model index within its model (e.g., "NiTriShape [0]\Vertex Data [3]\Vertex colors"). // Mostly for messages and debug. @@ -315,6 +315,9 @@ class BaseModel : public QAbstractItemModel // Evaluating NifItem condition and model version public: + //! Checks if version is within since-until range. 0 for since or until means unlimited. + static bool checkVersion( quint32 version, quint32 since, quint32 until = 0 ); + //! Evaluate NifItem model version. bool evalVersion( const NifItem * item ) const; protected: @@ -662,6 +665,10 @@ inline bool BaseModel::isArray( const QModelIndex & index ) const return isArray( getItem( index ) ); } +inline bool BaseModel::checkVersion( quint32 version, quint32 since, quint32 until ) +{ + return (( since == 0 || since <= version ) && ( until == 0 || version <= until )); +} // Item getters diff --git a/src/model/kfmmodel.cpp b/src/model/kfmmodel.cpp index 6c723b33f..cf4eef1e5 100644 --- a/src/model/kfmmodel.cpp +++ b/src/model/kfmmodel.cpp @@ -42,7 +42,7 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. const QString DOT_QSTRING("."); -KfmModel::KfmModel( QObject * parent ) : BaseModel( parent ) +KfmModel::KfmModel( QObject * parent, MsgMode msgMode ) : BaseModel( parent, msgMode ) { clear(); } diff --git a/src/model/kfmmodel.h b/src/model/kfmmodel.h index f8b4df6f6..a7166ec6d 100644 --- a/src/model/kfmmodel.h +++ b/src/model/kfmmodel.h @@ -50,7 +50,7 @@ class KfmModel final : public BaseModel Q_OBJECT public: - KfmModel( QObject * parent = 0 ); + KfmModel( QObject * parent = nullptr, MsgMode msgMode = BaseModel::MSG_TEST ); // call this once on startup to load the XML descriptions static bool loadXML(); @@ -77,9 +77,6 @@ class KfmModel final : public BaseModel static QString version2string( quint32 ); static quint32 version2number( const QString & ); - // check wether the current nif file version lies in the range since~until - bool checkVersion( quint32 since, quint32 until ) const; - QString getVersion() const override final { return version2string( version ); } quint32 getVersionNumber() const override final { return version; } diff --git a/src/model/nifmodel.cpp b/src/model/nifmodel.cpp index 3060ac6c2..1d3c717d8 100644 --- a/src/model/nifmodel.cpp +++ b/src/model/nifmodel.cpp @@ -261,7 +261,7 @@ void setupArrayPseudonyms() registerMultiPseudonym("Vertex Weights", "Vertex", "Weight"); } -NifModel::NifModel( QObject * parent ) : BaseModel( parent ) +NifModel::NifModel( QObject * parent, MsgMode msgMode ) : BaseModel( parent, msgMode ) { setupArrayPseudonyms(); updateSettings(); @@ -458,8 +458,6 @@ const NifItem * NifModel::getHeaderItem() const void NifModel::updateHeader() { - emit beginUpdateHeader(); - if ( lockUpdates ) { needUpdates = UpdateType( needUpdates | utHeader ); return; @@ -633,14 +631,18 @@ bool NifModel::updateArraySizeImpl( NifItem * array ) endRemoveRows(); } - if ( nNewSize != nOldSize - && state != Loading - && ( bOldHasChildLinks || array->hasChildLinks() ) // had or has any links inside - && !array->isDescendantOf( getFooterItem() ) - ) { - updateLinks(); - updateFooter(); - emit linksChanged(); + if ( nNewSize != nOldSize ) { + if ( state != Loading + && ( bOldHasChildLinks || array->hasChildLinks() ) // had or has any links inside + && !array->isDescendantOf( getFooterItem() ) + ) { + updateLinks(); + updateFooter(); + emit linksChanged(); + } + + if ( state == Default ) + onItemValueChange( array ); } return true; @@ -2893,24 +2895,25 @@ void NifModel::convertNiBlock( const QString & identifier, const QModelIndex & i if ( !branch ) return; - const QString & btype = branch->name(); - if ( btype == identifier ) + // oldType must be QString here, not const QString &, otherwise branch->setName( identifier ) below makes oldType == identifier. + QString oldType = branch->name(); + if ( oldType == identifier ) return; - if ( !inherits( btype, identifier ) && !inherits( identifier, btype ) ) { - logMessage(tr("Cannot convert NiBlock."), tr("Block type %1 and %2 are not related").arg(btype, identifier), QMessageBox::Critical); + if ( !inherits( oldType, identifier ) && !inherits( identifier, oldType ) ) { + logMessage(tr("Cannot convert NiBlock."), tr("Block types %1 and %2 are not related").arg(oldType, identifier), QMessageBox::Critical); return; } - NifBlockPtr srcBlock = blocks.value( btype ); + NifBlockPtr srcBlock = blocks.value( oldType ); NifBlockPtr dstBlock = blocks.value( identifier ); if ( srcBlock && dstBlock ) { branch->setName( identifier ); - if ( inherits( btype, identifier ) ) { + if ( inherits( oldType, identifier ) ) { // Remove any level between the two types - for ( QString ancestor = btype; !ancestor.isNull() && ancestor != identifier; ) { + for ( QString ancestor = oldType; !ancestor.isNull() && ancestor != identifier; ) { NifBlockPtr block = blocks.value( ancestor ); if ( !block ) @@ -2923,11 +2926,11 @@ void NifModel::convertNiBlock( const QString & identifier, const QModelIndex & i ancestor = block->ancestor; } - } else if ( inherits( identifier, btype ) ) { + } else if ( inherits( identifier, oldType ) ) { // Add any level between the two types QStringList types; - for ( QString ancestor = identifier; !ancestor.isNull() && ancestor != btype; ) { + for ( QString ancestor = identifier; !ancestor.isNull() && ancestor != oldType; ) { NifBlockPtr block = blocks.value( ancestor ); if ( !block ) @@ -3052,7 +3055,7 @@ QVariant NifModelEval::operator()( const QVariant & v ) const { if ( v.type() == QVariant::String ) { QString left = v.toString(); - const NifItem * itemLeft = model->getItem( item, left, true ); + const NifItem * itemLeft = model->getItem( item, left, false ); if ( itemLeft ) { if ( itemLeft->isCount() ) diff --git a/src/model/nifmodel.h b/src/model/nifmodel.h index abf583aa0..276e470b4 100644 --- a/src/model/nifmodel.h +++ b/src/model/nifmodel.h @@ -57,6 +57,7 @@ const char * const readFail = QT_TR_NOOP( "The NIF file could not be read. See D //! Secondary string for read failure const char * const readFailFinal = QT_TR_NOOP( "Failed to load %1" ); +template class NifFieldIteratorSimple; //! The main data model for the NIF file. class NifModel final : public BaseModel @@ -67,9 +68,11 @@ class NifModel final : public BaseModel friend class NifModelEval; friend class NifOStream; friend class ArrayUpdateCommand; + friend NifField; + friend NifFieldConst; public: - NifModel( QObject * parent = 0 ); + NifModel( QObject * parent = nullptr, MsgMode msgMode = BaseModel::MSG_TEST ); static const NifModel * fromIndex( const QModelIndex & index ); static const NifModel * fromValidIndex( const QModelIndex & index ); @@ -249,6 +252,53 @@ class NifModel final : public BaseModel //! Convert a block from one type to another void convertNiBlock( const QString & identifier, const QModelIndex & index ); + // NifField +public: + //! Get field object from its model index. + NifFieldConst field( const QModelIndex & index, bool reportErrors = true ) const; + //! Get field object from its model index. + NifField field( const QModelIndex & index, bool reportErrors = true ); + + //! Get block (field object) from a link. + NifFieldConst block( quint32 link ) const; + //! Get block (field object) from a link. + NifField block( quint32 link ); + //! Get block (field object) from a model index of its item. + NifFieldConst block( const QModelIndex & index, bool reportErrors = true ) const; + //! Get block (field object) from a model index of its item. + NifField block( const QModelIndex & index, bool reportErrors = true ); +private: + const NifItem * findBlockItemByName( const QString & blockName ) const; + const NifItem * findBlockItemByName( const QLatin1String & blockName ) const; +public: + //! Get block (field object) by its name. + NifFieldConst block( const QString & blockName ) const; + //! Get block (field object) by its name. + NifField block( const QString & blockName ); + //! Get block (field object) by its name. + NifFieldConst block( const QLatin1String & blockName ) const; + //! Get block (field object) by its name. + NifField block( const QLatin1String & blockName ); + //! Get block (field object) by its name. + NifFieldConst block( const char * blockName ) const; + //! Get block (field object) by its name. + NifField block( const char * blockName ); + + //! Get the header (field object) of the model. + NifFieldConst header() const; + //! Get the header (field object) of the model. + NifField header(); + + //! Get the footer (field object) of the model. + NifFieldConst footer() const; + //! Get the footer (field object) of the model. + NifField footer(); + + //! Get block iterator of the model for use in for() loops. + NifFieldIteratorSimple blockIter() const; + //! Get block iterator of the model for use in for() loops. + NifFieldIteratorSimple blockIter(); + // Block item getters private: const NifItem * _getBlockItem( const NifItem * block, const QString & ancestor ) const; @@ -641,7 +691,6 @@ public slots: signals: void linksChanged(); void lodSliderChanged( bool ) const; - void beginUpdateHeader(); protected: // BaseModel @@ -745,783 +794,2573 @@ class NifModelEval }; -// Inlines - -inline const NifModel * NifModel::fromIndex( const QModelIndex & index ) +//! Begin/end iterator pointer for NifFieldIterator* classes below. +// Iterates a pointer to NifItem pointers, returns NifFieldTemplate on each iteration. +template +struct NifFieldIterPtr { - return static_cast( index.model() ); // qobject_cast -} + using iterator_category = std::forward_iterator_tag; + using difference_type = std::ptrdiff_t; -inline const NifModel * NifModel::fromValidIndex( const QModelIndex & index ) -{ - return index.isValid() ? NifModel::fromIndex( index ) : nullptr; -} + NifFieldIterPtr() = delete; -inline QString NifModel::createRTTIName( const QModelIndex & iBlock ) const -{ - return createRTTIName( getItem(iBlock) ); -} + NifFieldIterPtr( ItemPtr * ptr) : m_ptr( ptr ) { } -inline NifItem * NifModel::getHeaderItem() -{ - return const_cast( const_cast(this)->getHeaderItem() ); -} + NifFieldTemplate operator *() const + { + return NifFieldTemplate( *m_ptr ); + } -inline QModelIndex NifModel::getHeaderIndex() const -{ - return itemToIndex( getHeaderItem() ); -} + ItemPtr * operator ->() { return m_ptr; } -inline NifItem * NifModel::getFooterItem() -{ - return const_cast( const_cast(this)->getFooterItem() ); -} + ItemPtr * ptr() const { return m_ptr; } -inline QStringList NifModel::allNiBlocks() -{ - QStringList lst; - for ( NifBlockPtr blk : blocks ) { - if ( !blk->abstract ) - lst.append( blk->id ); + NifFieldIterPtr & operator++() + { + m_ptr++; return *this; + } + NifFieldIterPtr operator++( int ) + { + NifFieldIterPtr tmp = *this; ++(*this); return tmp; } - return lst; -} -inline bool NifModel::isAncestorOrNiBlock( const QString & name ) const -{ - return blocks.contains( name ); -} + friend bool operator == ( const NifFieldIterPtr & a, const NifFieldIterPtr & b ) + { + return a.ptr() == b.ptr(); + }; + friend bool operator != ( const NifFieldIterPtr & a, const NifFieldIterPtr & b ) + { + return a.ptr() != b.ptr(); + }; -inline bool NifModel::isNiBlock( const QString & name ) -{ - NifBlockPtr blk = blocks.value( name ); - return blk && !blk->abstract; -} +private: + ItemPtr * m_ptr; +}; -inline bool NifModel::isAncestor( const QString & name ) +//! Begin/end iterator pointer for NifFieldIteratorEval class below. +// Iterates a pointer to NifItem pointers, skipping NifItems with bad conditions, returns NifFieldTemplate on each iteration. +template +struct NifFieldEvalIterPtr { - NifBlockPtr blk = blocks.value( name ); - return blk && blk->abstract; -} + using iterator_category = std::forward_iterator_tag; + using difference_type = std::ptrdiff_t; -inline bool NifModel::isCompound( const QString & name ) -{ - return compounds.contains( name ); -} + NifFieldEvalIterPtr() = delete; -inline bool NifModel::isFixedCompound( const QString & name ) -{ - return fixedCompounds.contains( name ); -} + NifFieldEvalIterPtr( ItemPtr * ptr, ItemPtr * end ) : m_ptr( ptr ), m_end( end ) { } -inline bool NifModel::isVersionSupported( quint32 v ) -{ - return supportedVersions.contains( v ); -} + NifFieldTemplate operator *() const + { + return NifFieldTemplate( *m_ptr ); + } -inline QList NifModel::getRootLinks() const -{ - return rootLinks; -} + ItemPtr * operator ->() { return m_ptr; } + + ItemPtr * ptr() const { return m_ptr; } + + NifFieldEvalIterPtr & operator++() + { + if ( m_end ) { + // Keep incrementing m_ptr until we reach the end or a NifItem with good conditions + while ( 1 ) { + m_ptr++; + if ( m_ptr == m_end || ( *m_ptr )->model()->evalCondition( *m_ptr ) ) + break; + } + } else { + m_ptr++; + } + return *this; + } + NifFieldEvalIterPtr operator++( int ) + { + NifFieldEvalIterPtr tmp = *this; ++(*this); return tmp; + } -inline QList NifModel::getChildLinks( int block ) const -{ - return childLinks.value( block ); -} + friend bool operator == ( const NifFieldEvalIterPtr & a, const NifFieldEvalIterPtr & b ) + { + return a.ptr() == b.ptr(); + }; + friend bool operator == ( const NifFieldEvalIterPtr & a, const NifFieldIterPtr & b ) + { + return a.ptr() == b.ptr(); + }; + friend bool operator != ( const NifFieldEvalIterPtr & a, const NifFieldEvalIterPtr & b ) + { + return a.ptr() != b.ptr(); + }; + friend bool operator != ( const NifFieldEvalIterPtr & a, const NifFieldIterPtr & b ) + { + return a.ptr() != b.ptr(); + }; -inline QList NifModel::getParentLinks( int block ) const -{ - return parentLinks.value( block ); -} +private: + ItemPtr * m_ptr; + ItemPtr * m_end; // Contains the end pointer if operator ++ needs to evaluate conditions, otherwise nullptr +}; -inline bool NifModel::isLink( const NifItem * item ) const +//! Base iterator of child fields. +template +class NifFieldIteratorBase { - return item && item->isLink(); -} +public: + NifFieldIteratorBase() = delete; -inline bool NifModel::isLink( const QModelIndex & index ) const -{ - return isLink( getItem(index) ); -} + NifFieldIteratorBase( ItemPtr item ) + { + if ( item && item->childCount() > 0 ) { + m_start = const_cast( const_cast(item)->children().begin() ); + m_end = const_cast( const_cast(item)->children().end() ); + } + } -inline bool NifModel::checkVersion( quint32 since, quint32 until ) const -{ - return (( since == 0 || since <= version ) && ( until == 0 || version <= until )); -} + NifFieldIteratorBase( ItemPtr item, int iStart ) + { + if ( item ) { + if ( iStart < 0 ) + iStart = 0; + + if ( iStart < item->childCount() ) { + m_start = const_cast( const_cast(item)->children().begin() + iStart ); + m_end = const_cast( const_cast(item)->children().end() ); + } + } + } -constexpr inline int NifModel::firstBlockRow() const -{ - return 1; // The fist root's child is always the header -} + NifFieldIteratorBase( ItemPtr item, int iStart, int iLast ) + { + if ( item ) { + if ( iStart < 0 ) + iStart = 0; + + int nChildren = item->childCount(); + int iEnd = ( iLast >= nChildren ) ? nChildren : ( iLast + 1 ); + + if ( iStart < iEnd ) { + ItemPtr * ptr = const_cast( const_cast(item)->children().begin() ); + m_start = ptr + iStart; + m_end = ptr + iEnd; + } + } + } -inline int NifModel::lastBlockRow() const -{ - return root->childCount() - 2; // The last root's child is always the footer. -} + NifFieldIterPtr end() const + { + return NifFieldIterPtr( m_end ); + } -inline bool NifModel::isBlockRow( int row ) const -{ - return ( row >= firstBlockRow() && row <= lastBlockRow() ); -} +protected: + ItemPtr * m_start = nullptr; + ItemPtr * m_end = nullptr; +}; -inline int NifModel::getBlockCount() const +//! Simple iterator of child fields, without evaluating children's conditions. +template +class NifFieldIteratorSimple : public NifFieldIteratorBase { - return std::max( lastBlockRow() - firstBlockRow() + 1, 0 ); -} +public: + using NifFieldIteratorBase::m_start; -inline int NifModel::getBlockNumber( const QModelIndex & index ) const -{ - return getBlockNumber( getItem(index) ); -} + NifFieldIteratorSimple() = delete; -inline bool NifModel::isValidBlockNumber( qint32 blockNum ) const + NifFieldIteratorSimple( ItemPtr item ) : NifFieldIteratorBase( item ) {} + NifFieldIteratorSimple( ItemPtr item, int iStart ) : NifFieldIteratorBase( item, iStart ) {} + NifFieldIteratorSimple( ItemPtr item, int iStart, int iLast ) : NifFieldIteratorBase( item, iStart, iLast ) {} + + NifFieldIterPtr begin() const + { + return NifFieldIterPtr( m_start ); + } +}; + +//! Iterator of child fields with children's conditions evaluation (skips children with bad conditions). +template +class NifFieldIteratorEval final : public NifFieldIteratorBase { - return blockNum >= 0 && blockNum < getBlockCount(); -} +public: + using NifFieldIteratorBase::m_start; + using NifFieldIteratorBase::m_end; + NifFieldIteratorEval() = delete; -// Block item getters + NifFieldIteratorEval( ItemPtr item ) : NifFieldIteratorBase( item ) + { + initEvalFlag( item ); + } + NifFieldIteratorEval( ItemPtr item, int iStart ) : NifFieldIteratorBase( item, iStart ) + { + initEvalFlag( item ); + } + NifFieldIteratorEval( ItemPtr item, int iStart, int iLast ) : NifFieldIteratorBase( item, iStart, iLast ) + { + initEvalFlag( item ); + } -inline const NifItem * NifModel::getBlockItem( qint32 link, const QString & ancestor ) const -{ - return _getBlockItem( getBlockItem(link), ancestor ); -} -inline const NifItem * NifModel::getBlockItem( qint32 link, const QLatin1String & ancestor ) const -{ - return _getBlockItem( getBlockItem(link), ancestor ); -} -inline const NifItem * NifModel::getBlockItem( qint32 link, const char * ancestor ) const -{ - return _getBlockItem( getBlockItem(link), QLatin1Literal(ancestor) ); -} -inline const NifItem * NifModel::getBlockItem( qint32 link, const std::initializer_list & ancestors ) const -{ - return _getBlockItem( getBlockItem(link), ancestors ); -} -inline const NifItem * NifModel::getBlockItem( qint32 link, const QStringList & ancestors ) const -{ - return _getBlockItem( getBlockItem(link), ancestors ); -} + NifFieldEvalIterPtr begin() const + { + return NifFieldEvalIterPtr( m_start, m_evalConditions ? m_end : nullptr ); + } -inline const NifItem * NifModel::getBlockItem( const NifItem * item, const QString & ancestor ) const -{ - return _getBlockItem( getBlockItem(item), ancestor ); -} -inline const NifItem * NifModel::getBlockItem( const NifItem * item, const QLatin1String & ancestor ) const -{ - return _getBlockItem( getBlockItem(item), ancestor ); -} -inline const NifItem * NifModel::getBlockItem( const NifItem * item, const char * ancestor ) const -{ - return _getBlockItem( getBlockItem(item), QLatin1String(ancestor) ); -} -inline const NifItem * NifModel::getBlockItem( const NifItem * item, const std::initializer_list & ancestors ) const -{ - return _getBlockItem( getBlockItem(item), ancestors ); -} -inline const NifItem * NifModel::getBlockItem( const NifItem * item, const QStringList & ancestors ) const -{ - return _getBlockItem( getBlockItem(item), ancestors ); -} +protected: + bool m_evalConditions; -inline const NifItem * NifModel::getBlockItem( const QModelIndex & index ) const -{ - return getBlockItem( getItem(index) ); -} -inline const NifItem * NifModel::getBlockItem( const QModelIndex & index, const QString & ancestor ) const -{ - return getBlockItem( getItem(index), ancestor ); -} -inline const NifItem * NifModel::getBlockItem( const QModelIndex & index, const QLatin1String & ancestor ) const -{ - return getBlockItem( getItem(index), ancestor ); -} -inline const NifItem * NifModel::getBlockItem( const QModelIndex & index, const char * ancestor ) const -{ - return getBlockItem( getItem(index), QLatin1String(ancestor) ); -} -inline const NifItem * NifModel::getBlockItem( const QModelIndex & index, const std::initializer_list & ancestors ) const -{ - return getBlockItem( getItem(index), ancestors ); -} -inline const NifItem * NifModel::getBlockItem( const QModelIndex & index, const QStringList & ancestors ) const -{ - return getBlockItem( getItem(index), ancestors ); -} + inline void initEvalFlag( ItemPtr item ) + { + m_evalConditions = m_start && !item->isArray(); + } +}; -#define _NIFMODEL_NONCONST_GETBLOCKITEM_1(arg) const_cast( const_cast(this)->getBlockItem( arg ) ) -#define _NIFMODEL_NONCONST_GETBLOCKITEM_2(arg1, arg2) const_cast( const_cast(this)->getBlockItem( arg1, arg2 ) ) -inline NifItem * NifModel::getBlockItem( qint32 link ) -{ - return _NIFMODEL_NONCONST_GETBLOCKITEM_1( link ); -} -inline NifItem * NifModel::getBlockItem( qint32 link, const QString & ancestor ) -{ - return _NIFMODEL_NONCONST_GETBLOCKITEM_2( link, ancestor ); -} -inline NifItem * NifModel::getBlockItem( qint32 link, const QLatin1String & ancestor ) -{ - return _NIFMODEL_NONCONST_GETBLOCKITEM_2( link, ancestor ); -} -inline NifItem * NifModel::getBlockItem( qint32 link, const char * ancestor ) -{ - return _NIFMODEL_NONCONST_GETBLOCKITEM_2( link, QLatin1String(ancestor) ); -} -inline NifItem * NifModel::getBlockItem( qint32 link, const std::initializer_list & ancestors ) -{ - return _NIFMODEL_NONCONST_GETBLOCKITEM_2( link, ancestors ); -} -inline NifItem * NifModel::getBlockItem( qint32 link, const QStringList & ancestors ) +//! NifField template class (container for NifItems and proxy between a NifModel and its NifItems). +template +class NifFieldTemplate final { - return _NIFMODEL_NONCONST_GETBLOCKITEM_2( link, ancestors ); -} + friend NifModel; + friend NifFieldIterPtr; + friend NifFieldEvalIterPtr; + friend NifField; + friend NifFieldConst; -inline NifItem * NifModel::getBlockItem( const NifItem * item ) -{ - return _NIFMODEL_NONCONST_GETBLOCKITEM_1( item ); -} +private: + ItemPtr m_item; // Only NifItems that pass evalCondition check should be here. Otherwise it must be nullptr. + + inline static const QString EMPTY_QSTRING; + + // Constructors +public: + constexpr NifFieldTemplate() noexcept : m_item( nullptr ) {} +protected: + NifFieldTemplate( ItemPtr item ) noexcept : m_item( item ) {} + +public: + bool isNull() const noexcept + { + return m_item == nullptr; + } + bool isValid() const noexcept + { + return m_item != nullptr; + } + explicit operator bool() const noexcept + { + return isValid(); + } + + operator NifFieldConst() const noexcept + { + return NifFieldConst( m_item ); + } + + bool operator == ( const NifField & other ) const noexcept + { + return m_item == other.m_item; + } + bool operator == ( const NifFieldConst & other ) const noexcept + { + return m_item == other.m_item; + } + bool operator != ( const NifField & other ) const noexcept + { + return m_item != other.m_item; + } + bool operator != ( const NifFieldConst & other ) const noexcept + { + return m_item != other.m_item; + } + +private: + static inline ModelPtr _model( ItemPtr item ) + { + return static_cast( item->model() ); + } + inline ModelPtr _model() const + { + return _model( m_item ); + } + +public: + //! Get the field's NifModel. + ModelPtr model() const + { + return m_item ? _model() : nullptr; + } + + //! Get the field as a NifItem pointer. + ItemPtr item() const noexcept + { + return m_item; + } + + //! Get a model index of the field. + QModelIndex toIndex( int column = 0 ) const + { + return m_item ? _model()->itemToIndex( m_item, column ) : QModelIndex(); + } + + //! Get string representation of the field. + QString repr() const + { + return m_item ? _model()->itemRepr(m_item) : "[NULL]"; + } + QString repr( NifFieldConst cutoffParent ) const + { + return m_item ? _model()->itemRepr(m_item, cutoffParent.item() ) : "[NULL]"; + } + + + // Child fields + + //! Get a child field by its index. + NifFieldTemplate child( int childIndex, bool reportErrors = false ) const + { + return NifFieldTemplate( m_item ? _model()->getItem( m_item, childIndex, reportErrors ) : nullptr ); + } + //! Get a child field by its name. + NifFieldTemplate child( const QString & childName, bool reportErrors = false ) const + { + return NifFieldTemplate( m_item ? _model()->getItem( m_item, childName, reportErrors ) : nullptr ); + } + //! Get a child field by its name. + NifFieldTemplate child( const QLatin1String & name, bool reportErrors = false ) const + { + return NifFieldTemplate( m_item ? _model()->getItem( m_item, name, reportErrors ) : nullptr ); + } + //! Get a child field by its name. + NifFieldTemplate child( const char * name, bool reportErrors = false ) const + { + return child( QLatin1String(name), reportErrors ); + } + + //! Get a child field by its index. Same as field(...) with reportErrors = true. + NifFieldTemplate operator [] ( int childIndex ) const + { + return child( childIndex, true ); + } + //! Get a child field by its name. Same as field(...) with reportErrors = true. + NifFieldTemplate operator [] ( const QString & childName ) const + { + return child( childName, true ); + } + //! Get a child field by its name. Same as field(...) with reportErrors = true. + NifFieldTemplate operator [] ( const QLatin1String & childName ) const + { + return child( childName, true ); + } + //! Get a child field by its name. Same as field(...) with reportErrors = true. + NifFieldTemplate operator [] ( const char * childName ) const + { + return child( QLatin1String(childName), true ); + } + + //! Get the number of child fields. + int childCount() const + { + return m_item ? m_item->childCount() : 0; + } + + //! Get iterator of child fields for for(...) loops. + // The iterator returns only the child fields that pass evalCondition check. + NifFieldIteratorEval iter() const + { + return NifFieldIteratorEval( m_item ); + } + //! Get iterator of child fields for for(...) loops, starting at iStart index. + // The iterator returns only the child fields that pass evalCondition check. + NifFieldIteratorEval iter( int iStart ) const + { + return NifFieldIteratorEval( m_item, iStart ); + } + //! Get iterator of child fields for for(...) loops, starting at iStart index and ending at iLast (inclusive). + // The iterator returns only the child fields that pass evalCondition check. + NifFieldIteratorEval iter( int iStart, int iLast ) const + { + return NifFieldIteratorEval( m_item, iStart, iLast ); + } + + + // Item hierarchy + + //! Get the parent field. + NifFieldTemplate parent() const + { + return NifFieldTemplate( m_item ? m_item->parent() : nullptr ); + } + + //! Check if the field is a direct child of the model's root. + bool isTop() const + { + return m_item && _model()->isTopItem(m_item); + } + + //! Get the top parent (a direct child of the model's root). + NifFieldTemplate topParent() const + { + return NifFieldTemplate( m_item ? _model()->getTopItem( m_item ) : nullptr ); + } + +private: + static inline bool _isDescendantOf( const NifItem * descendant, const NifItem * ancestor ) + { + return descendant && descendant->isDescendantOf(ancestor); + } + +public: + //! Check if the field is testAncestor or its child or a child of a child, etc. + bool isDescendantOf( const NifField & testAncestor ) const + { + return _isDescendantOf( m_item, testAncestor.m_item ); + } + //! Check if the field is testAncestor or its child or a child of a child, etc. + bool isDescendantOf( const NifFieldConst & testAncestor ) const + { + return _isDescendantOf( m_item, testAncestor.m_item ); + } + + //! Check if testDescendant is this field or its child or a child of a child, etc. + bool isAncestorOf( const NifField & testDescendant ) const + { + return _isDescendantOf( testDescendant.m_item, m_item ); + } + //! Check if testDescendant is this field or its child or a child of a child, etc. + bool isAncestorOf( const NifFieldConst & testDescendant ) const + { + return _isDescendantOf( testDescendant.m_item, m_item ); + } + + //! Get the ancestry level of the field relative to testAncestor. + // 0 - this field is testAncestor; 1 - the field is a child of testAncestor; 2 - a child of a child, etc. + // Returns -1 if the field is not a descendant of testAncestor. + int ancestorLevel( const NifField & testAncestor ) const + { + return m_item ? m_item->ancestorLevel( testAncestor.m_item ) : -1; + } + //! Get the ancestry level of the field relative to testAncestor. + // 0 - this field is testAncestor; 1 - the field is a child of testAncestor; 2 - a child of a child, etc. + // Returns -1 if the field is not a descendant of testAncestor. + int ancestorLevel( const NifFieldConst & testAncestor ) const + { + return m_item ? m_item->ancestorLevel( testAncestor.m_item ) : -1; + } + + //! Get the ancestor of the field at level testLevel, + // where levels are: 0 - this field; 1 - the field's parent; 2 - the parent of the parent, etc. + NifFieldTemplate ancestorAt( int testLevel ) const + { + return NifFieldTemplate( m_item ? m_item->ancestorAt( testLevel ) : nullptr ); + } + + //! Check if the field is a NiBlock. + bool isBlock() const + { + return isTop() && _model()->isBlockRow( m_item->row() ); + } + + //! Check if the field is a NiBlock of type testType (w/o inheritance check). + bool isBlockType( const QString & testType ) const; + //! Check if the field is a NiBlock of type testType1 or testType2 (w/o inheritance check). + bool isBlockType( const QString & testType1, const QString & testType2 ) const; + //! Check if the field is a NiBlock of type testType1 or testType2 or testType3 (w/o inheritance check). + bool isBlockType( const QString & testType1, const QString & testType2, const QString & testType3 ) const; + //! Check if the field is a NiBlock of type testType1 or testType2 or testType3 or testType4 (w/o inheritance check). + bool isBlockType( const QString & testType1, const QString & testType2, const QString & testType3, const QString & testType4 ) const; + //! Check if the field is a NiBlock of type testType1 or testType2 or testType3 or testType4 or testType5 (w/o inheritance check). + bool isBlockType( const QString & testType1, const QString & testType2, const QString & testType3, const QString & testType4, const QString & testType5 ) const; + //! Check if the field is a NiBlock of type testType (w/o inheritance check). + bool isBlockType( const QLatin1String & testType ) const; + //! Check if the field is a NiBlock of type testType1 or testType2 (w/o inheritance check). + bool isBlockType( const QLatin1String & testType1, const QLatin1String & testType2 ) const; + //! Check if the field is a NiBlock of type testType1 or testType2 or testType3 (w/o inheritance check). + bool isBlockType( const QLatin1String & testType1, const QLatin1String & testType2, const QLatin1String & testType3 ) const; + //! Check if the field is a NiBlock of type testType1 or testType2 or testType3 or testType4 (w/o inheritance check). + bool isBlockType( const QLatin1String & testType1, const QLatin1String & testType2, const QLatin1String & testType3, const QLatin1String & testType4 ) const; + //! Check if the field is a NiBlock of type testType1 or testType2 or testType3 or testType4 or testType5 (w/o inheritance check). + bool isBlockType( const QLatin1String & testType1, const QLatin1String & testType2, const QLatin1String & testType3, const QLatin1String & testType4, const QLatin1String & testType5 ) const; + //! Check if the field is a NiBlock of type testType (w/o inheritance check). + bool isBlockType( const char * testType ) const; + //! Check if the field is a NiBlock of type testType1 or testType2 (w/o inheritance check). + bool isBlockType( const char * testType1, const char * testType2 ) const; + //! Check if the field is a NiBlock of type testType1 or testType2 or testType3 (w/o inheritance check). + bool isBlockType( const char * testType1, const char * testType2, const char * testType3 ) const; + //! Check if the field is a NiBlock of type testType1 or testType2 or testType3 or testType4 (w/o inheritance check). + bool isBlockType( const char * testType1, const char * testType2, const char * testType3, const char * testType4 ) const; + //! Check if the field is a NiBlock of type testType1 or testType2 or testType3 or testType4 or testType5 (w/o inheritance check). + bool isBlockType( const char * testType1, const char * testType2, const char * testType3, const char * testType4, const char * testType5 ) const; + //! Check if the field is a NiBlock of any of testTypes (w/o inheritance check). + bool isBlockType( const QStringList & testTypes ) const; + +private: + static bool _inherits( ItemPtr block, const QString & testAncestor ) + { + return _model( block )->inherits( block->name(), testAncestor ); + } + static bool _inherits( ItemPtr block, const QLatin1String & testAncestor ) + { + return _model( block )->inherits( block->name(), testAncestor ); + } + static bool _inherits( ItemPtr block, const QStringList & testAncestors ) + { + return _model( block )->inherits( block->name(), testAncestors ); + } + +public: + + //! Check if the field is a NiBlock that inherits testAncestor. + bool inherits( const QString & testAncestor ) const; + //! Check if the field is a NiBlock that inherits testAncestor1 or testAncestor2. + bool inherits( const QString & testAncestor1, const QString & testAncestor2 ) const; + //! Check if the field is a NiBlock that inherits testAncestor1 or testAncestor2 or testAncestor3. + bool inherits( const QString & testAncestor1, const QString & testAncestor2, const QString & testAncestor3 ) const; + //! Check if the field is a NiBlock that inherits testAncestor1 or testAncestor2 or testAncestor3 or testAncestor4. + bool inherits( const QString & testAncestor1, const QString & testAncestor2, const QString & testAncestor3, const QString & testAncestor4 ) const; + //! Check if the field is a NiBlock that inherits testAncestor1 or testAncestor2 or testAncestor3 or testAncestor4 or testAncestor5. + bool inherits( const QString & testAncestor1, const QString & testAncestor2, const QString & testAncestor3, const QString & testAncestor4, const QString & testAncestor5 ) const; + //! Check if the field is a NiBlock that inherits testAncestor. + bool inherits( const QLatin1String & testAncestor ) const; + //! Check if the field is a NiBlock that inherits testAncestor1 or testAncestor2. + bool inherits( const QLatin1String & testAncestor1, const QLatin1String & testAncestor2 ) const; + //! Check if the field is a NiBlock that inherits testAncestor1 or testAncestor2 or testAncestor3. + bool inherits( const QLatin1String & testAncestor1, const QLatin1String & testAncestor2, const QLatin1String & testAncestor3 ) const; + //! Check if the field is a NiBlock that inherits testAncestor1 or testAncestor2 or testAncestor3 or testAncestor4. + bool inherits( const QLatin1String & testAncestor1, const QLatin1String & testAncestor2, const QLatin1String & testAncestor3, const QLatin1String & testAncestor4 ) const; + //! Check if the field is a NiBlock that inherits testAncestor1 or testAncestor2 or testAncestor3 or testAncestor4 or testAncestor5. + bool inherits( const QLatin1String & testAncestor1, const QLatin1String & testAncestor2, const QLatin1String & testAncestor3, const QLatin1String & testAncestor4, const QLatin1String & testAncestor5 ) const; + //! Check if the field is a NiBlock that inherits testAncestor. + bool inherits( const char * testAncestor ) const; + //! Check if the field is a NiBlock that inherits testAncestor1 or testAncestor2. + bool inherits( const char * testAncestor1, const char * testAncestor2 ) const; + //! Check if the field is a NiBlock that inherits testAncestor1 or testAncestor2 or testAncestor3. + bool inherits( const char * testAncestor1, const char * testAncestor2, const char * testAncestor3 ) const; + //! Check if the field is a NiBlock that inherits testAncestor1 or testAncestor2 or testAncestor3 or testAncestor4. + bool inherits( const char * testAncestor1, const char * testAncestor2, const char * testAncestor3, const char * testAncestor4 ) const; + //! Check if the field is a NiBlock that inherits testAncestor1 or testAncestor2 or testAncestor3 or testAncestor4 or testAncestor5. + bool inherits( const char * testAncestor1, const char * testAncestor2, const char * testAncestor3, const char * testAncestor4, const char * testAncestor5 ) const; + //! Check if the field is a NiBlock that inherits any of testAncestors. + bool inherits( const QStringList & testAncestors ) const; + + //! Get the block the field belongs to. + NifFieldTemplate block() const + { + return NifFieldTemplate( m_item ? _model()->getBlockItem( m_item ) : nullptr ); + } + //! Get the block the field belongs to, with a check that it inherits testAncestor. + NifFieldTemplate block( const QString & testAncestor ) const; + //! Get the block the field belongs to, with a check that it inherits testAncestor1 or testAncestor2. + NifFieldTemplate block( const QString & testAncestor1, const QString & testAncestor2 ) const; + //! Get the block the field belongs to, with a check that it inherits testAncestor1 or testAncestor2 or testAncestor3. + NifFieldTemplate block( const QString & testAncestor1, const QString & testAncestor2, const QString & testAncestor3 ) const; + //! Get the block the field belongs to, with a check that it inherits testAncestor1 or testAncestor2 or testAncestor3 or testAncestor4. + NifFieldTemplate block( const QString & testAncestor1, const QString & testAncestor2, const QString & testAncestor3, const QString & testAncestor4 ) const; + //! Get the block the field belongs to, with a check that it inherits testAncestor1 or testAncestor2 or testAncestor3 or testAncestor4 or testAncestor5. + NifFieldTemplate block( const QString & testAncestor1, const QString & testAncestor2, const QString & testAncestor3, const QString & testAncestor4, const QString & testAncestor5 ) const; + //! Get the block the field belongs to, with a check that it inherits testAncestor. + NifFieldTemplate block( const QLatin1String & testAncestor ) const; + //! Get the block the field belongs to, with a check that it inherits testAncestor1 or testAncestor2. + NifFieldTemplate block( const QLatin1String & testAncestor1, const QLatin1String & testAncestor2 ) const; + //! Get the block the field belongs to, with a check that it inherits testAncestor1 or testAncestor2 or testAncestor3. + NifFieldTemplate block( const QLatin1String & testAncestor1, const QLatin1String & testAncestor2, const QLatin1String & testAncestor3 ) const; + //! Get the block the field belongs to, with a check that it inherits testAncestor1 or testAncestor2 or testAncestor3 or testAncestor4. + NifFieldTemplate block( const QLatin1String & testAncestor1, const QLatin1String & testAncestor2, const QLatin1String & testAncestor3, const QLatin1String & testAncestor4 ) const; + //! Get the block the field belongs to, with a check that it inherits testAncestor1 or testAncestor2 or testAncestor3 or testAncestor4 or testAncestor5. + NifFieldTemplate block( const QLatin1String & testAncestor1, const QLatin1String & testAncestor2, const QLatin1String & testAncestor3, const QLatin1String & testAncestor4, const QLatin1String & testAncestor5 ) const; + //! Get the block the field belongs to, with a check that it inherits testAncestor. + NifFieldTemplate block( const char * testAncestor ) const; + //! Get the block the field belongs to, with a check that it inherits testAncestor1 or testAncestor2. + NifFieldTemplate block( const char * testAncestor1, const char * testAncestor2 ) const; + //! Get the block the field belongs to, with a check that it inherits testAncestor1 or testAncestor2 or testAncestor3. + NifFieldTemplate block( const char * testAncestor1, const char * testAncestor2, const char * testAncestor3 ) const; + //! Get the block the field belongs to, with a check that it inherits testAncestor1 or testAncestor2 or testAncestor3 or testAncestor4. + NifFieldTemplate block( const char * testAncestor1, const char * testAncestor2, const char * testAncestor3, const char * testAncestor4 ) const; + //! Get the block the field belongs to, with a check that it inherits testAncestor1 or testAncestor2 or testAncestor3 or testAncestor4 or testAncestor5. + NifFieldTemplate block( const char * testAncestor1, const char * testAncestor2, const char * testAncestor3, const char * testAncestor4, const char * testAncestor5 ) const; + //! Get the block the field belongs to, with a check that it inherits any of testAncestors. + NifFieldTemplate block( const QStringList & testAncestors ) const; + + //! Get link to the field (block number) if it's a NiBlock, otherwise returns -1. + // Same as toBlockNumber(). + qint32 toLink() const + { + if ( isTop() ) { + auto r = m_item->row(); + if ( _model()->isBlockRow( r ) ) + return r - _model()->firstBlockRow(); + } + + return -1; + } + + //! Get block number (link to the field) if it's a NiBlock, otherwise returns -1. + // Same as toLink(). + quint32 toBlockNumber() const + { + return toLink(); + } + + //! Check if the field is the model's header. + bool isHeader() const + { + return m_item && m_item == _model()->getHeaderItem(); + } + + //! Check if the field is the model's footer. + bool isFooter() const + { + return m_item && m_item == _model()->getFooterItem(); + } + + + // NifItem proxy methods + + //! Get the field's row (its index in the parent's children). Returns -1 for null fields. + int row() const + { + return m_item ? m_item->row() : -1; + } + + //! Get the field's name. + const QString & name() const + { + return m_item ? m_item->name() : EMPTY_QSTRING; + } + //! Get the string type of the field data (the "type" attribute in the XML file). + const QString & strType() const + { + return m_item ? m_item->strType() : EMPTY_QSTRING; + } + //! Get the field value type (NifValue::Type). Returns NifValue::tNone for null fields. + NifValue::Type valType() const + { + return m_item ? m_item->valueType() : NifValue::tNone; + } + //! Get the template type of the field data. + const QString & templ() const + { + return m_item ? m_item->templ() : EMPTY_QSTRING; + } + //! Get the argument attribute of the field data. + const QString & arg() const + { + return m_item ? m_item->arg() : EMPTY_QSTRING; + } + //! Get the first array length of the field data. + const QString & arr1() const + { + return m_item ? m_item->arr1() : EMPTY_QSTRING; + } + //! Get the second array length of the field data. + const QString & arr2() const + { + return m_item ? m_item->arr2() : EMPTY_QSTRING; + } + //! Get the condition attribute of the field data. + const QString & cond() const + { + return m_item ? m_item->cond() : EMPTY_QSTRING; + } + //! Get the earliest version attribute of the field data. Returns 0 for null fields. + quint32 ver1() const + { + return m_item ? m_item->ver1() : 0; + } + //! Get the latest version attribute of the field data. Returns 0 for null fields. + quint32 ver2() const + { + return m_item ? m_item->ver2() : 0; + } + //! Get the description text of the field data. + const QString & text() const + { + return m_item ? m_item->text() : EMPTY_QSTRING; + } + + //! Check if the field's name matches testName. + bool hasName( const QString & testName ) const; + //! Check if the field's name matches testName1 or testName2. + bool hasName( const QString & testName1, const QString & testName2 ) const; + //! Check if the field's name matches testName1 or testName2 or testName3. + bool hasName( const QString & testName1, const QString & testName2, const QString & testName3 ) const; + //! Check if the field's name matches testName1 or testName2 or testName3 or testName4. + bool hasName( const QString & testName1, const QString & testName2, const QString & testName3, const QString & testName4 ) const; + //! Check if the field's name matches testName1 or testName2 or testName3 or testName4 or testName5. + bool hasName( const QString & testName1, const QString & testName2, const QString & testName3, const QString & testName4, const QString & testName5 ) const; + //! Check if the field's name matches testName. + bool hasName( const QLatin1String & testName ) const; + //! Check if the field's name matches testName1 or testName2. + bool hasName( const QLatin1String & testName1, const QLatin1String & testName2 ) const; + //! Check if the field's name matches testName1 or testName2 or testName3. + bool hasName( const QLatin1String & testName1, const QLatin1String & testName2, const QLatin1String & testName3 ) const; + //! Check if the field's name matches testName1 or testName2 or testName3 or testName4. + bool hasName( const QLatin1String & testName1, const QLatin1String & testName2, const QLatin1String & testName3, const QLatin1String & testName4 ) const; + //! Check if the field's name matches testName1 or testName2 or testName3 or testName4 or testName5. + bool hasName( const QLatin1String & testName1, const QLatin1String & testName2, const QLatin1String & testName3, const QLatin1String & testName4, const QLatin1String & testName5 ) const; + //! Check if the field's name matches testName. + bool hasName( const char * testName ) const; + //! Check if the field's name matches testName1 or testName2. + bool hasName( const char * testName1, const char * testName2 ) const; + //! Check if the field's name matches testName1 or testName2 or testName3. + bool hasName( const char * testName1, const char * testName2, const char * testName3 ) const; + //! Check if the field's name matches testName1 or testName2 or testName3 or testName4. + bool hasName( const char * testName1, const char * testName2, const char * testName3, const char * testName4 ) const; + //! Check if the field's name matches testName1 or testName2 or testName3 or testName4 or testName5. + bool hasName( const char * testName1, const char * testName2, const char * testName3, const char * testName4, const char * testName5 ) const; + + //! Check if the field's string type matches testType. + bool hasStrType( const QString & testType ) const; + //! Check if the field's string type matches testType1 or testType2. + bool hasStrType( const QString & testType1, const QString & testType2 ) const; + //! Check if the field's string type matches testType1 or testType2 or testType3. + bool hasStrType( const QString & testType1, const QString & testType2, const QString & testType3 ) const; + //! Check if the field's string type matches testType1 or testType2 or testType3 or testType4. + bool hasStrType( const QString & testType1, const QString & testType2, const QString & testType3, const QString & testType4 ) const; + //! Check if the field's string type matches testType1 or testType2 or testType3 or testType4 or testType5. + bool hasStrType( const QString & testType1, const QString & testType2, const QString & testType3, const QString & testType4, const QString & testType5 ) const; + //! Check if the field's string type matches testType. + bool hasStrType( const QLatin1String & testType ) const; + //! Check if the field's string type matches testType1 or testType2. + bool hasStrType( const QLatin1String & testType1, const QLatin1String & testType2 ) const; + //! Check if the field's string type matches testType1 or testType2 or testType3. + bool hasStrType( const QLatin1String & testType1, const QLatin1String & testType2, const QLatin1String & testType3 ) const; + //! Check if the field's string type matches testType1 or testType2 or testType3 or testType4. + bool hasStrType( const QLatin1String & testType1, const QLatin1String & testType2, const QLatin1String & testType3, const QLatin1String & testType4 ) const; + //! Check if the field's string type matches testType1 or testType2 or testType3 or testType4 or testType5. + bool hasStrType( const QLatin1String & testType1, const QLatin1String & testType2, const QLatin1String & testType3, const QLatin1String & testType4, const QLatin1String & testType5 ) const; + //! Check if the field's string type matches testType. + bool hasStrType( const char * testType ) const; + //! Check if the field's string type matches testType1 or testType2. + bool hasStrType( const char * testType1, const char * testType2 ) const; + //! Check if the field's string type matches testType1 or testType2 or testType3. + bool hasStrType( const char * testType1, const char * testType2, const char * testType3 ) const; + //! Check if the field's string type matches testType1 or testType2 or testType3 or testType4. + bool hasStrType( const char * testType1, const char * testType2, const char * testType3, const char * testType4 ) const; + //! Check if the field's string type matches testType1 or testType2 or testType3 or testType4 or testType5. + bool hasStrType( const char * testType1, const char * testType2, const char * testType3, const char * testType4, const char * testType5 ) const; + + //! Check if the field's value type (NifValue::Type) matches testType. + bool hasValType( NifValue::Type testType ) const; + //! Check if the field's value type (NifValue::Type) matches testType1 or testType2. + bool hasValType( NifValue::Type testType1, NifValue::Type testType2 ) const; + //! Check if the field's value type (NifValue::Type) matches testType1 or testType2 or testType3. + bool hasValType( NifValue::Type testType1, NifValue::Type testType2, NifValue::Type testType3 ) const; + //! Check if the field's value type (NifValue::Type) matches testType1 or testType2 or testType3 or testType4. + bool hasValType( NifValue::Type testType1, NifValue::Type testType2, NifValue::Type testType3, NifValue::Type testType4 ) const; + //! Check if the field's value type (NifValue::Type) matches testType1 or testType2 or testType3 or testType4 or testType5. + bool hasValType( NifValue::Type testType1, NifValue::Type testType2, NifValue::Type testType3, NifValue::Type testType4, NifValue::Type testType5 ) const; + + //! Check if the type of the field value is a color type (Color3 or Color4 in xml). + bool isColor() const + { + return m_item && m_item->isColor(); + } + //! Check if the type of the field value is a count. + bool isCount() const + { + return m_item && m_item->isCount(); + } + //! Check if the type of the field value is a flag type (Flags in xml). + bool isFlags() const + { + return m_item && m_item->isFlags(); + } + //! Check if the type of the field value is a float type (Float in xml). + bool isFloat() const + { + return m_item && m_item->isFloat(); + } + //! Check if the type of the field value is of a link type (Ref or Ptr in xml). + bool isLink() const + { + return m_item && m_item->isLink(); + } + //! Check if the type of the field value is a 3x3 matrix type (Matrix33 in xml). + bool isMatrix() const + { + return m_item && m_item->isMatrix(); + } + //! Check if the type of the field value is a 4x4 matrix type (Matrix44 in xml). + bool isMatrix4() const + { + return m_item && m_item->isMatrix4(); + } + //! Check if the type of the field value is a byte matrix. + bool isByteMatrix() const + { + return m_item && m_item->isByteMatrix(); + } + //! Check if the type of the field value is a quaternion type. + bool isQuat() const + { + return m_item && m_item->isQuat(); + } + //! Check if the type of the field value is a string type. + bool isString() const + { + return m_item && m_item->isString(); + } + //! Check if the type of the field value is a Vector 2. + bool isVector2() const + { + return m_item && m_item->isVector2(); + } + //! Check if the type of the field value is a HalfVector2. + bool isHalfVector2() const + { + return m_item && m_item->isHalfVector2(); + } + //! Check if the type of the field value is a Vector 3. + bool isVector3() const + { + return m_item && m_item->isVector3(); + } + //! Check if the type of the field value is a Half Vector3. + bool isHalfVector3() const + { + return m_item && m_item->isHalfVector3(); + } + //! Check if the type of the field value is a Byte Vector3. + bool isByteVector3() const + { + return m_item && m_item->isByteVector3(); + } + //! Check if the type of the field value is a Vector 4. + bool isVector4() const + { + return m_item && m_item->isVector4(); + } + //! Check if the type of the field value is a triangle type. + bool isTriangle() const + { + return m_item && m_item->isTriangle(); + } + //! Check if the type of the field value is a byte array. + bool isByteArray() const + { + return m_item && m_item->isByteArray(); + } + //! Check if the type of the field value is a File Version. + bool isFileVersion() const + { + return m_item && m_item->isFileVersion(); + } + + //! Check if the field is abstract the abstract attribute of the field's data. + bool isAbstract() const + { + return m_item && m_item->isAbstract(); + } + //! Check if the field data is binary. Binary means the data is being treated as one blob. + bool isBinary() const + { + return m_item && m_item->isBinary(); + } + //! Check if the field data is templated. Templated means the type is dynamic. + bool isTemplated() const + { + return m_item && m_item->isTemplated(); + } + //! Check if the field data is a compound. Compound means the data type is a compound block. + bool isCompound() const + { + return m_item && m_item->isCompound(); + } + //! Check if the field data is an array. Array means the data on this row repeats. + bool isArray() const + { + return m_item && m_item->isArray(); + } + //! Check if the field data is a multi-array. Multi-array means the field's children are also arrays. + bool isMultiArray() const + { + return m_item && m_item->isMultiArray(); + } + //! Check if the field data is conditionless. Conditionless means no expression evaluation is necessary. + bool isConditionless() const + { + return m_item && m_item->isConditionless(); + } + //! Does the field data's condition checks only the type of the parent block. + bool hasTypeCondition() const + { + return m_item && m_item->hasTypeCondition(); + } + + // Item value getters/setters + + //! Get the field value. + template + T value() const + { + return m_item ? _model()->get( m_item ) : T(); + } + + //! Set the field value. + template + bool setValue( const T & val ) const + { + return m_item && _model()->set( m_item, val ); + } + + //! Get the child fields' values as a QVector. + template + QVector array() const + { + return m_item ? _model()->getArray( m_item ) : QVector(); + } + + bool updateArraySize() const + { + return m_item && _model()->updateArraySize( m_item ); + } + + template + void setArray( const QVector & array ) const + { + if ( m_item ) + _model()->setArray( m_item, array ); + } + + template + void fillArray( const T & val ) const + { + if ( m_item ) + _model()->fillArray( m_item, val ); + } + + qint32 link() const + { + return m_item ? m_item->getLinkValue() : -1; + } + + NifFieldTemplate linkBlock() const + { + if ( m_item ) { + auto link = m_item->getLinkValue(); + if ( _model()->isValidBlockNumber( link ) ) + return NifFieldTemplate( _model()->root->child( link + _model()->firstBlockRow() ) ); + } + return NifFieldTemplate(); + } + //! Get the link's block, with a check that it inherits testAncestor. + NifFieldTemplate linkBlock( const QString & testAncestor ) const; + //! Get the link's block, with a check that it inherits testAncestor1 or testAncestor2. + NifFieldTemplate linkBlock( const QString & testAncestor1, const QString & testAncestor2 ) const; + //! Get the link's block, with a check that it inherits testAncestor1 or testAncestor2 or testAncestor3. + NifFieldTemplate linkBlock( const QString & testAncestor1, const QString & testAncestor2, const QString & testAncestor3 ) const; + //! Get the link's block, with a check that it inherits testAncestor1 or testAncestor2 or testAncestor3 or testAncestor4. + NifFieldTemplate linkBlock( const QString & testAncestor1, const QString & testAncestor2, const QString & testAncestor3, const QString & testAncestor4 ) const; + //! Get the link's block, with a check that it inherits testAncestor1 or testAncestor2 or testAncestor3 or testAncestor4 or testAncestor5. + NifFieldTemplate linkBlock( const QString & testAncestor1, const QString & testAncestor2, const QString & testAncestor3, const QString & testAncestor4, const QString & testAncestor5 ) const; + //! Get the link's block, with a check that it inherits testAncestor. + NifFieldTemplate linkBlock( const QLatin1String & testAncestor ) const; + //! Get the link's block, with a check that it inherits testAncestor1 or testAncestor2. + NifFieldTemplate linkBlock( const QLatin1String & testAncestor1, const QLatin1String & testAncestor2 ) const; + //! Get the link's block, with a check that it inherits testAncestor1 or testAncestor2 or testAncestor3. + NifFieldTemplate linkBlock( const QLatin1String & testAncestor1, const QLatin1String & testAncestor2, const QLatin1String & testAncestor3 ) const; + //! Get the link's block, with a check that it inherits testAncestor1 or testAncestor2 or testAncestor3 or testAncestor4. + NifFieldTemplate linkBlock( const QLatin1String & testAncestor1, const QLatin1String & testAncestor2, const QLatin1String & testAncestor3, const QLatin1String & testAncestor4 ) const; + //! Get the link's block, with a check that it inherits testAncestor1 or testAncestor2 or testAncestor3 or testAncestor4 or testAncestor5. + NifFieldTemplate linkBlock( const QLatin1String & testAncestor1, const QLatin1String & testAncestor2, const QLatin1String & testAncestor3, const QLatin1String & testAncestor4, const QLatin1String & testAncestor5 ) const; + //! Get the link's block, with a check that it inherits testAncestor. + NifFieldTemplate linkBlock( const char * testAncestor ) const; + //! Get the link's block, with a check that it inherits testAncestor1 or testAncestor2. + NifFieldTemplate linkBlock( const char * testAncestor1, const char * testAncestor2 ) const; + //! Get the link's block, with a check that it inherits testAncestor1 or testAncestor2 or testAncestor3. + NifFieldTemplate linkBlock( const char * testAncestor1, const char * testAncestor2, const char * testAncestor3 ) const; + //! Get the link's block, with a check that it inherits testAncestor1 or testAncestor2 or testAncestor3 or testAncestor4. + NifFieldTemplate linkBlock( const char * testAncestor1, const char * testAncestor2, const char * testAncestor3, const char * testAncestor4 ) const; + //! Get the link's block, with a check that it inherits testAncestor1 or testAncestor2 or testAncestor3 or testAncestor4 or testAncestor5. + NifFieldTemplate linkBlock( const char * testAncestor1, const char * testAncestor2, const char * testAncestor3, const char * testAncestor4, const char * testAncestor5 ) const; + //! Get the link's block, with a check that it inherits any of testAncestors. + NifFieldTemplate linkBlock( const QStringList & testAncestors ) const; + + bool setLink( qint32 link ) const + { + return m_item && _model()->setLink( m_item, link ); + } + + bool clearLink( ) const + { + return setLink( -1 ); + } + + template + bool setLink( const NifFieldTemplate & block ) const + { + if ( !m_item ) + return false; + + if ( block.isNull() ) + return _model()->setLink( m_item, -1 ); + + if ( block._model() != _model() ) { + reportError( __func__, QString( "Item \"%1\" belongs to another model." ).arg( block.repr() ) ); + return false; + } + + auto link = block.toLink(); + if ( link < 0 ) { + reportError( __func__, QString( "Item \"%1\" is not a NiBlock." ).arg( block.repr() ) ); + return false; + } + + return _model()->setLink( m_item, link ); + } + + QVector linkArray() const + { + return m_item ? _model()->getLinkArray( m_item ) : QVector(); + } + + bool setLinkArray( QVector links ) const + { + return m_item && _model()->setLinkArray( m_item, links ); + } + + void reportError( const QString & msg ) const + { + if ( m_item ) + _model()->reportError( m_item, msg ); + } + void reportError( const QString & funcName, const QString & msg ) const + { + if ( m_item ) + _model()->reportError( m_item, funcName, msg ); + } +}; + + +// Inlines + +inline const NifModel * NifModel::fromIndex( const QModelIndex & index ) +{ + return static_cast( index.model() ); // qobject_cast +} + +inline const NifModel * NifModel::fromValidIndex( const QModelIndex & index ) +{ + return index.isValid() ? NifModel::fromIndex( index ) : nullptr; +} + +inline QString NifModel::createRTTIName( const QModelIndex & iBlock ) const +{ + return createRTTIName( getItem(iBlock) ); +} + +inline NifItem * NifModel::getHeaderItem() +{ + return const_cast( const_cast(this)->getHeaderItem() ); +} + +inline QModelIndex NifModel::getHeaderIndex() const +{ + return itemToIndex( getHeaderItem() ); +} + +inline NifItem * NifModel::getFooterItem() +{ + return const_cast( const_cast(this)->getFooterItem() ); +} + +inline QStringList NifModel::allNiBlocks() +{ + QStringList lst; + for ( NifBlockPtr blk : blocks ) { + if ( !blk->abstract ) + lst.append( blk->id ); + } + return lst; +} + +inline bool NifModel::isAncestorOrNiBlock( const QString & name ) const +{ + return blocks.contains( name ); +} + +inline bool NifModel::isNiBlock( const QString & name ) +{ + NifBlockPtr blk = blocks.value( name ); + return blk && !blk->abstract; +} + +inline bool NifModel::isAncestor( const QString & name ) +{ + NifBlockPtr blk = blocks.value( name ); + return blk && blk->abstract; +} + +inline bool NifModel::isCompound( const QString & name ) +{ + return compounds.contains( name ); +} + +inline bool NifModel::isFixedCompound( const QString & name ) +{ + return fixedCompounds.contains( name ); +} + +inline bool NifModel::isVersionSupported( quint32 v ) +{ + return supportedVersions.contains( v ); +} + +inline QList NifModel::getRootLinks() const +{ + return rootLinks; +} + +inline QList NifModel::getChildLinks( int block ) const +{ + return childLinks.value( block ); +} + +inline QList NifModel::getParentLinks( int block ) const +{ + return parentLinks.value( block ); +} + +inline bool NifModel::isLink( const NifItem * item ) const +{ + return item && item->isLink(); +} + +inline bool NifModel::isLink( const QModelIndex & index ) const +{ + return isLink( getItem(index) ); +} + +inline bool NifModel::checkVersion( quint32 since, quint32 until ) const +{ + return BaseModel::checkVersion( version, since, until ); +} + +constexpr inline int NifModel::firstBlockRow() const +{ + return 1; // The fist root's child is always the header +} + +inline int NifModel::lastBlockRow() const +{ + return root->childCount() - 2; // The last root's child is always the footer. +} + +inline bool NifModel::isBlockRow( int row ) const +{ + return ( row >= firstBlockRow() && row <= lastBlockRow() ); +} + +inline int NifModel::getBlockCount() const +{ + return std::max( lastBlockRow() - firstBlockRow() + 1, 0 ); +} + +inline int NifModel::getBlockNumber( const QModelIndex & index ) const +{ + return getBlockNumber( getItem(index) ); +} + +inline bool NifModel::isValidBlockNumber( qint32 blockNum ) const +{ + return blockNum >= 0 && blockNum < getBlockCount(); +} + + +// Block item getters + +inline const NifItem * NifModel::getBlockItem( qint32 link, const QString & ancestor ) const +{ + return _getBlockItem( getBlockItem(link), ancestor ); +} +inline const NifItem * NifModel::getBlockItem( qint32 link, const QLatin1String & ancestor ) const +{ + return _getBlockItem( getBlockItem(link), ancestor ); +} +inline const NifItem * NifModel::getBlockItem( qint32 link, const char * ancestor ) const +{ + return _getBlockItem( getBlockItem(link), QLatin1Literal(ancestor) ); +} +inline const NifItem * NifModel::getBlockItem( qint32 link, const std::initializer_list & ancestors ) const +{ + return _getBlockItem( getBlockItem(link), ancestors ); +} +inline const NifItem * NifModel::getBlockItem( qint32 link, const QStringList & ancestors ) const +{ + return _getBlockItem( getBlockItem(link), ancestors ); +} + +inline const NifItem * NifModel::getBlockItem( const NifItem * item, const QString & ancestor ) const +{ + return _getBlockItem( getBlockItem(item), ancestor ); +} +inline const NifItem * NifModel::getBlockItem( const NifItem * item, const QLatin1String & ancestor ) const +{ + return _getBlockItem( getBlockItem(item), ancestor ); +} +inline const NifItem * NifModel::getBlockItem( const NifItem * item, const char * ancestor ) const +{ + return _getBlockItem( getBlockItem(item), QLatin1String(ancestor) ); +} +inline const NifItem * NifModel::getBlockItem( const NifItem * item, const std::initializer_list & ancestors ) const +{ + return _getBlockItem( getBlockItem(item), ancestors ); +} +inline const NifItem * NifModel::getBlockItem( const NifItem * item, const QStringList & ancestors ) const +{ + return _getBlockItem( getBlockItem(item), ancestors ); +} + +inline const NifItem * NifModel::getBlockItem( const QModelIndex & index ) const +{ + return getBlockItem( getItem(index) ); +} +inline const NifItem * NifModel::getBlockItem( const QModelIndex & index, const QString & ancestor ) const +{ + return getBlockItem( getItem(index), ancestor ); +} +inline const NifItem * NifModel::getBlockItem( const QModelIndex & index, const QLatin1String & ancestor ) const +{ + return getBlockItem( getItem(index), ancestor ); +} +inline const NifItem * NifModel::getBlockItem( const QModelIndex & index, const char * ancestor ) const +{ + return getBlockItem( getItem(index), QLatin1String(ancestor) ); +} +inline const NifItem * NifModel::getBlockItem( const QModelIndex & index, const std::initializer_list & ancestors ) const +{ + return getBlockItem( getItem(index), ancestors ); +} +inline const NifItem * NifModel::getBlockItem( const QModelIndex & index, const QStringList & ancestors ) const +{ + return getBlockItem( getItem(index), ancestors ); +} + +#define _NIFMODEL_NONCONST_GETBLOCKITEM_1(arg) const_cast( const_cast(this)->getBlockItem( arg ) ) +#define _NIFMODEL_NONCONST_GETBLOCKITEM_2(arg1, arg2) const_cast( const_cast(this)->getBlockItem( arg1, arg2 ) ) + +inline NifItem * NifModel::getBlockItem( qint32 link ) +{ + return _NIFMODEL_NONCONST_GETBLOCKITEM_1( link ); +} +inline NifItem * NifModel::getBlockItem( qint32 link, const QString & ancestor ) +{ + return _NIFMODEL_NONCONST_GETBLOCKITEM_2( link, ancestor ); +} +inline NifItem * NifModel::getBlockItem( qint32 link, const QLatin1String & ancestor ) +{ + return _NIFMODEL_NONCONST_GETBLOCKITEM_2( link, ancestor ); +} +inline NifItem * NifModel::getBlockItem( qint32 link, const char * ancestor ) +{ + return _NIFMODEL_NONCONST_GETBLOCKITEM_2( link, QLatin1String(ancestor) ); +} +inline NifItem * NifModel::getBlockItem( qint32 link, const std::initializer_list & ancestors ) +{ + return _NIFMODEL_NONCONST_GETBLOCKITEM_2( link, ancestors ); +} +inline NifItem * NifModel::getBlockItem( qint32 link, const QStringList & ancestors ) +{ + return _NIFMODEL_NONCONST_GETBLOCKITEM_2( link, ancestors ); +} + +inline NifItem * NifModel::getBlockItem( const NifItem * item ) +{ + return _NIFMODEL_NONCONST_GETBLOCKITEM_1( item ); +} inline NifItem * NifModel::getBlockItem( const NifItem * item, const QString & ancestor ) { - return _NIFMODEL_NONCONST_GETBLOCKITEM_2( item, ancestor ); + return _NIFMODEL_NONCONST_GETBLOCKITEM_2( item, ancestor ); +} +inline NifItem * NifModel::getBlockItem( const NifItem * item, const QLatin1String & ancestor ) +{ + return _NIFMODEL_NONCONST_GETBLOCKITEM_2( item, ancestor ); +} +inline NifItem * NifModel::getBlockItem( const NifItem * item, const char * ancestor ) +{ + return _NIFMODEL_NONCONST_GETBLOCKITEM_2( item, QLatin1String(ancestor) ); +} +inline NifItem * NifModel::getBlockItem( const NifItem * item, const std::initializer_list & ancestors ) +{ + return _NIFMODEL_NONCONST_GETBLOCKITEM_2( item, ancestors ); +} +inline NifItem * NifModel::getBlockItem( const NifItem * item, const QStringList & ancestors ) +{ + return _NIFMODEL_NONCONST_GETBLOCKITEM_2( item, ancestors ); +} + +inline NifItem * NifModel::getBlockItem( const QModelIndex & index ) +{ + return _NIFMODEL_NONCONST_GETBLOCKITEM_1( index ); +} +inline NifItem * NifModel::getBlockItem( const QModelIndex & index, const QString & ancestor ) +{ + return _NIFMODEL_NONCONST_GETBLOCKITEM_2( index, ancestor ); +} +inline NifItem * NifModel::getBlockItem( const QModelIndex & index, const QLatin1String & ancestor ) +{ + return _NIFMODEL_NONCONST_GETBLOCKITEM_2( index, ancestor ); +} +inline NifItem * NifModel::getBlockItem( const QModelIndex & index, const char * ancestor ) +{ + return _NIFMODEL_NONCONST_GETBLOCKITEM_2( index, QLatin1String(ancestor) ); +} +inline NifItem * NifModel::getBlockItem( const QModelIndex & index, const std::initializer_list & ancestors ) +{ + return _NIFMODEL_NONCONST_GETBLOCKITEM_2( index, ancestors ); +} +inline NifItem * NifModel::getBlockItem( const QModelIndex & index, const QStringList & ancestors ) +{ + return _NIFMODEL_NONCONST_GETBLOCKITEM_2( index, ancestors ); +} + + +// Block index getters + +#define _NIFMODEL_GETBLOCKINDEX_1(arg) itemToIndex( getBlockItem( arg ) ) +#define _NIFMODEL_GETBLOCKINDEX_2(arg1, arg2) itemToIndex( getBlockItem( arg1, arg2 ) ) + +inline QModelIndex NifModel::getBlockIndex( qint32 link ) const +{ + return _NIFMODEL_GETBLOCKINDEX_1( link ); +} +inline QModelIndex NifModel::getBlockIndex( qint32 link, const QString & ancestor ) const +{ + return _NIFMODEL_GETBLOCKINDEX_2( link, ancestor ); +} +inline QModelIndex NifModel::getBlockIndex( qint32 link, const QLatin1String & ancestor ) const +{ + return _NIFMODEL_GETBLOCKINDEX_2( link, ancestor ); +} +inline QModelIndex NifModel::getBlockIndex( qint32 link, const char * ancestor ) const +{ + return _NIFMODEL_GETBLOCKINDEX_2( link, QLatin1String(ancestor) ); +} +inline QModelIndex NifModel::getBlockIndex( qint32 link, const std::initializer_list & ancestors ) const +{ + return _NIFMODEL_GETBLOCKINDEX_2( link, ancestors ); +} +inline QModelIndex NifModel::getBlockIndex( qint32 link, const QStringList & ancestors ) const +{ + return _NIFMODEL_GETBLOCKINDEX_2( link, ancestors ); +} + +inline QModelIndex NifModel::getBlockIndex( const NifItem * item ) const +{ + return _NIFMODEL_GETBLOCKINDEX_1( item ); +} +inline QModelIndex NifModel::getBlockIndex( const NifItem * item, const QString & ancestor ) const +{ + return _NIFMODEL_GETBLOCKINDEX_2( item, ancestor ); +} +inline QModelIndex NifModel::getBlockIndex( const NifItem * item, const QLatin1String & ancestor ) const +{ + return _NIFMODEL_GETBLOCKINDEX_2( item, ancestor ); +} +inline QModelIndex NifModel::getBlockIndex( const NifItem * item, const char * ancestor ) const +{ + return _NIFMODEL_GETBLOCKINDEX_2( item, QLatin1String(ancestor) ); +} +inline QModelIndex NifModel::getBlockIndex( const NifItem * item, const std::initializer_list & ancestors ) const +{ + return _NIFMODEL_GETBLOCKINDEX_2( item, ancestors ); +} +inline QModelIndex NifModel::getBlockIndex( const NifItem * item, const QStringList & ancestors ) const +{ + return _NIFMODEL_GETBLOCKINDEX_2( item, ancestors ); +} + +inline QModelIndex NifModel::getBlockIndex( const QModelIndex & index ) const +{ + return _NIFMODEL_GETBLOCKINDEX_1( index ); +} +inline QModelIndex NifModel::getBlockIndex( const QModelIndex & index, const QString & ancestor ) const +{ + return _NIFMODEL_GETBLOCKINDEX_2( index, ancestor ); +} +inline QModelIndex NifModel::getBlockIndex( const QModelIndex & index, const QLatin1String & ancestor ) const +{ + return _NIFMODEL_GETBLOCKINDEX_2( index, ancestor ); +} +inline QModelIndex NifModel::getBlockIndex( const QModelIndex & index, const char * ancestor ) const +{ + return _NIFMODEL_GETBLOCKINDEX_2( index, QLatin1String(ancestor) ); +} +inline QModelIndex NifModel::getBlockIndex( const QModelIndex & index, const std::initializer_list & ancestors ) const +{ + return _NIFMODEL_GETBLOCKINDEX_2( index, ancestors ); +} +inline QModelIndex NifModel::getBlockIndex( const QModelIndex & index, const QStringList & ancestors ) const +{ + return _NIFMODEL_GETBLOCKINDEX_2( index, ancestors ); +} + + +// isNiBlock + +inline bool NifModel::isNiBlock( const NifItem * item ) const +{ + return isTopItem( item ) && isBlockRow( item->row() ); +} +inline bool NifModel::isNiBlock( const NifItem * item, const QString & testType ) const +{ + return isNiBlock(item) && item->hasName(testType); +} +inline bool NifModel::isNiBlock( const NifItem * item, const QLatin1String & testType ) const +{ + return isNiBlock(item) && item->hasName(testType); +} +inline bool NifModel::isNiBlock( const NifItem * item, const char * testType ) const +{ + return isNiBlock( item, QLatin1String(testType) ); +} +inline bool NifModel::isNiBlock( const QModelIndex & index ) const +{ + return isNiBlock( getItem(index) ); +} +inline bool NifModel::isNiBlock( const QModelIndex & index, const QString & testType ) const +{ + return isNiBlock( getItem(index), testType ); +} +inline bool NifModel::isNiBlock( const QModelIndex & index, const QLatin1String & testType ) const +{ + return isNiBlock( getItem(index), testType ); +} +inline bool NifModel::isNiBlock( const QModelIndex & index, const char * testType ) const +{ + return isNiBlock( getItem(index), QLatin1String(testType) ); +} +inline bool NifModel::isNiBlock( const QModelIndex & index, const std::initializer_list & testTypes ) const +{ + return isNiBlock( getItem(index), testTypes ); +} +inline bool NifModel::isNiBlock( const QModelIndex & index, const QStringList & testTypes ) const +{ + return isNiBlock( getItem(index), testTypes ); +} + + +// Block inheritance + +inline bool NifModel::inherits( const QString & blockName, const char * ancestor ) const +{ + return inherits( blockName, QLatin1String(ancestor) ); +} +inline bool NifModel::blockInherits( const NifItem * item, const char * ancestor ) const +{ + return blockInherits( item, QLatin1String(ancestor) ); +} +inline bool NifModel::blockInherits( const QModelIndex & index, const QString & ancestor ) const +{ + return blockInherits( getItem(index), ancestor ); +} +inline bool NifModel::blockInherits( const QModelIndex & index, const QLatin1String & ancestor ) const +{ + return blockInherits( getItem(index), ancestor ); +} +inline bool NifModel::blockInherits( const QModelIndex & index, const char * ancestor ) const +{ + return blockInherits( getItem(index), QLatin1String(ancestor) ); +} +inline bool NifModel::blockInherits( const QModelIndex & index, const std::initializer_list & ancestors ) const +{ + return blockInherits( getItem(index), ancestors ); +} +inline bool NifModel::blockInherits( const QModelIndex & index, const QStringList & ancestors ) const +{ + return blockInherits( getItem(index), ancestors ); +} + + +// Item value getters + +template inline T NifModel::get( const NifItem * item ) const +{ + return BaseModel::get( item ); +} +template <> inline QString NifModel::get( const NifItem * item ) const +{ + return resolveString( item ); +} +template inline T NifModel::get( const NifItem * itemParent, int itemIndex ) const +{ + return get( getItem(itemParent, itemIndex) ); +} +template inline T NifModel::get( const NifItem * itemParent, const QString & itemName ) const +{ + return get( getItem(itemParent, itemName) ); +} +template inline T NifModel::get( const NifItem * itemParent, const QLatin1String & itemName ) const +{ + return get( getItem(itemParent, itemName) ); +} +template inline T NifModel::get( const NifItem * itemParent, const char * itemName ) const +{ + return get( getItem(itemParent, QLatin1String(itemName)) ); +} +template inline T NifModel::get( const QModelIndex & index ) const +{ + return get( getItem(index) ); +} +template inline T NifModel::get( const QModelIndex & itemParent, int itemIndex ) const +{ + return get( getItem(itemParent, itemIndex) ); +} +template inline T NifModel::get( const QModelIndex & itemParent, const QString & itemName ) const +{ + return get( getItem(itemParent, itemName) ); +} +template inline T NifModel::get( const QModelIndex & itemParent, const QLatin1String & itemName ) const +{ + return get( getItem(itemParent, itemName) ); +} +template inline T NifModel::get( const QModelIndex & itemParent, const char * itemName ) const +{ + return get( getItem(itemParent, QLatin1String(itemName)) ); +} + + +// Item value setters + +template inline bool NifModel::set( NifItem * item, const T & val ) +{ + return BaseModel::set( item, val ); +} +template <> inline bool NifModel::set( NifItem * item, const QString & val ) +{ + return assignString( item, val ); +} +template inline bool NifModel::set( const NifItem * itemParent, int itemIndex, const T & val ) +{ + return set( getItem(itemParent, itemIndex, true), val ); +} +template inline bool NifModel::set( const NifItem * itemParent, const QString & itemName, const T & val ) +{ + return set( getItem(itemParent, itemName, true), val ); +} +template inline bool NifModel::set( const NifItem * itemParent, const QLatin1String & itemName, const T & val ) +{ + return set( getItem(itemParent, itemName, true), val ); +} +template inline bool NifModel::set( const NifItem * itemParent, const char * itemName, const T & val ) +{ + return set( getItem(itemParent, QLatin1String(itemName), true), val ); +} +template inline bool NifModel::set( const QModelIndex & index, const T & val ) +{ + return set( getItem(index), val ); +} +template inline bool NifModel::set( const QModelIndex & itemParent, int itemIndex, const T & val ) +{ + return set( getItem(itemParent, itemIndex, true), val ); +} +template inline bool NifModel::set( const QModelIndex & itemParent, const QString & itemName, const T & val ) +{ + return set( getItem(itemParent, itemName, true), val ); +} +template inline bool NifModel::set( const QModelIndex & itemParent, const QLatin1String & itemName, const T & val ) +{ + return set( getItem(itemParent, itemName, true), val ); +} +template inline bool NifModel::set( const QModelIndex & itemParent, const char * itemName, const T & val ) +{ + return set( getItem(itemParent, QLatin1String(itemName), true), val ); +} + + +// String resolving ("get ex") + +inline QString NifModel::resolveString( const NifItem * itemParent, int itemIndex ) const +{ + return resolveString( getItem(itemParent, itemIndex) ); +} +inline QString NifModel::resolveString( const NifItem * itemParent, const QString & itemName ) const +{ + return resolveString( getItem(itemParent, itemName) ); +} +inline QString NifModel::resolveString( const NifItem * itemParent, const QLatin1String & itemName ) const +{ + return resolveString( getItem(itemParent, itemName) ); +} +inline QString NifModel::resolveString( const NifItem * itemParent, const char * itemName ) const +{ + return resolveString( getItem(itemParent, QLatin1String(itemName)) ); +} +inline QString NifModel::resolveString( const QModelIndex & index ) const +{ + return resolveString( getItem(index) ); +} +inline QString NifModel::resolveString( const QModelIndex & itemParent, int itemIndex ) const +{ + return resolveString( getItem(itemParent, itemIndex) ); +} +inline QString NifModel::resolveString( const QModelIndex & itemParent, const QString & itemName ) const +{ + return resolveString( getItem(itemParent, itemName) ); +} +inline QString NifModel::resolveString( const QModelIndex & itemParent, const QLatin1String & itemName ) const +{ + return resolveString( getItem(itemParent, itemName) ); +} +inline QString NifModel::resolveString( const QModelIndex & itemParent, const char * itemName ) const +{ + return resolveString( getItem(itemParent, QLatin1String(itemName)) ); +} + + +// String assigning ("set ex") + +inline bool NifModel::assignString( const NifItem * itemParent, int itemIndex, const QString & string, bool replace ) +{ + return assignString( getItem(itemParent, itemIndex, true), string, replace ); } -inline NifItem * NifModel::getBlockItem( const NifItem * item, const QLatin1String & ancestor ) +inline bool NifModel::assignString( const NifItem * itemParent, const QString & itemName, const QString & string, bool replace ) { - return _NIFMODEL_NONCONST_GETBLOCKITEM_2( item, ancestor ); + return assignString( getItem(itemParent, itemName, true), string, replace ); } -inline NifItem * NifModel::getBlockItem( const NifItem * item, const char * ancestor ) +inline bool NifModel::assignString( const NifItem * itemParent, const QLatin1String & itemName, const QString & string, bool replace ) { - return _NIFMODEL_NONCONST_GETBLOCKITEM_2( item, QLatin1String(ancestor) ); + return assignString( getItem(itemParent, itemName, true), string, replace ); } -inline NifItem * NifModel::getBlockItem( const NifItem * item, const std::initializer_list & ancestors ) +inline bool NifModel::assignString( const NifItem * itemParent, const char * itemName, const QString & string, bool replace ) { - return _NIFMODEL_NONCONST_GETBLOCKITEM_2( item, ancestors ); + return assignString( getItem(itemParent, QLatin1String(itemName), true), string, replace ); } -inline NifItem * NifModel::getBlockItem( const NifItem * item, const QStringList & ancestors ) +inline bool NifModel::assignString( const QModelIndex & index, const QString & string, bool replace ) { - return _NIFMODEL_NONCONST_GETBLOCKITEM_2( item, ancestors ); + return assignString( getItem(index), string, replace ); +} +inline bool NifModel::assignString( const QModelIndex & itemParent, int itemIndex, const QString & string, bool replace ) +{ + return assignString( getItem(itemParent, itemIndex, true), string, replace ); +} +inline bool NifModel::assignString( const QModelIndex & itemParent, const QString & itemName, const QString & string, bool replace ) +{ + return assignString( getItem(itemParent, itemName, true), string, replace ); +} +inline bool NifModel::assignString( const QModelIndex & itemParent, const QLatin1String & itemName, const QString & string, bool replace ) +{ + return assignString( getItem(itemParent, itemName, true), string, replace ); +} +inline bool NifModel::assignString( const QModelIndex & itemParent, const char * itemName, const QString & string, bool replace ) +{ + return assignString( getItem(itemParent, QLatin1String(itemName), true), string, replace ); } -inline NifItem * NifModel::getBlockItem( const QModelIndex & index ) + +// Link getters + +inline qint32 NifModel::getLink( const NifItem * item ) const { - return _NIFMODEL_NONCONST_GETBLOCKITEM_1( index ); + return item ? item->getLinkValue() : -1; } -inline NifItem * NifModel::getBlockItem( const QModelIndex & index, const QString & ancestor ) +inline qint32 NifModel::getLink( const NifItem * itemParent, int itemIndex ) const { - return _NIFMODEL_NONCONST_GETBLOCKITEM_2( index, ancestor ); + return getLink( getItem(itemParent, itemIndex) ); } -inline NifItem * NifModel::getBlockItem( const QModelIndex & index, const QLatin1String & ancestor ) +inline qint32 NifModel::getLink( const NifItem * itemParent, const QString & itemName ) const { - return _NIFMODEL_NONCONST_GETBLOCKITEM_2( index, ancestor ); + return getLink( getItem(itemParent, itemName) ); } -inline NifItem * NifModel::getBlockItem( const QModelIndex & index, const char * ancestor ) +inline qint32 NifModel::getLink( const NifItem * itemParent, const QLatin1String & itemName ) const { - return _NIFMODEL_NONCONST_GETBLOCKITEM_2( index, QLatin1String(ancestor) ); + return getLink( getItem(itemParent, itemName) ); } -inline NifItem * NifModel::getBlockItem( const QModelIndex & index, const std::initializer_list & ancestors ) +inline qint32 NifModel::getLink( const NifItem * itemParent, const char * itemName ) const { - return _NIFMODEL_NONCONST_GETBLOCKITEM_2( index, ancestors ); + return getLink( getItem(itemParent, QLatin1String(itemName)) ); } -inline NifItem * NifModel::getBlockItem( const QModelIndex & index, const QStringList & ancestors ) +inline qint32 NifModel::getLink( const QModelIndex & index ) const { - return _NIFMODEL_NONCONST_GETBLOCKITEM_2( index, ancestors ); + return getLink( getItem(index) ); +} +inline qint32 NifModel::getLink( const QModelIndex & itemParent, int itemIndex ) const +{ + return getLink( getItem(itemParent, itemIndex) ); +} +inline qint32 NifModel::getLink( const QModelIndex & itemParent, const QString & itemName ) const +{ + return getLink( getItem(itemParent, itemName) ); +} +inline qint32 NifModel::getLink( const QModelIndex & itemParent, const QLatin1String & itemName ) const +{ + return getLink( getItem(itemParent, itemName) ); +} +inline qint32 NifModel::getLink( const QModelIndex & itemParent, const char * itemName ) const +{ + return getLink( getItem(itemParent, QLatin1String(itemName)) ); } -// Block index getters +// Link setters -#define _NIFMODEL_GETBLOCKINDEX_1(arg) itemToIndex( getBlockItem( arg ) ) -#define _NIFMODEL_GETBLOCKINDEX_2(arg1, arg2) itemToIndex( getBlockItem( arg1, arg2 ) ) +inline bool NifModel::setLink( const NifItem * itemParent, int itemIndex, qint32 link ) +{ + return setLink( getItem(itemParent, itemIndex, true), link ); +} +inline bool NifModel::setLink( const NifItem * itemParent, const QString & itemName, qint32 link ) +{ + return setLink( getItem(itemParent, itemName, true), link ); +} +inline bool NifModel::setLink( const NifItem * itemParent, const QLatin1String & itemName, qint32 link ) +{ + return setLink( getItem(itemParent, itemName, true), link ); +} +inline bool NifModel::setLink( const NifItem * itemParent, const char * itemName, qint32 link ) +{ + return setLink( getItem(itemParent, QLatin1String(itemName), true), link ); +} +inline bool NifModel::setLink( const QModelIndex & index, qint32 link ) +{ + return setLink( getItem(index), link ); +} +inline bool NifModel::setLink( const QModelIndex & itemParent, int itemIndex, qint32 link ) +{ + return setLink( getItem(itemParent, itemIndex, true), link ); +} +inline bool NifModel::setLink( const QModelIndex & itemParent, const QString & itemName, qint32 link ) +{ + return setLink( getItem(itemParent, itemName, true), link ); +} +inline bool NifModel::setLink( const QModelIndex & itemParent, const QLatin1String & itemName, qint32 link ) +{ + return setLink( getItem(itemParent, itemName, true), link ); +} +inline bool NifModel::setLink( const QModelIndex & itemParent, const char * itemName, qint32 link ) +{ + return setLink( getItem(itemParent, QLatin1String(itemName), true), link ); +} -inline QModelIndex NifModel::getBlockIndex( qint32 link ) const + +// Link array getters + +inline QVector NifModel::getLinkArray( const NifItem * arrayParent, int arrayIndex ) const { - return _NIFMODEL_GETBLOCKINDEX_1( link ); + return getLinkArray( getItem(arrayParent, arrayIndex) ); } -inline QModelIndex NifModel::getBlockIndex( qint32 link, const QString & ancestor ) const +inline QVector NifModel::getLinkArray( const NifItem * arrayParent, const QString & arrayName ) const { - return _NIFMODEL_GETBLOCKINDEX_2( link, ancestor ); + return getLinkArray( getItem(arrayParent, arrayName) ); } -inline QModelIndex NifModel::getBlockIndex( qint32 link, const QLatin1String & ancestor ) const +inline QVector NifModel::getLinkArray( const NifItem * arrayParent, const QLatin1String & arrayName ) const { - return _NIFMODEL_GETBLOCKINDEX_2( link, ancestor ); + return getLinkArray( getItem(arrayParent, arrayName) ); } -inline QModelIndex NifModel::getBlockIndex( qint32 link, const char * ancestor ) const +inline QVector NifModel::getLinkArray( const NifItem * arrayParent, const char * arrayName ) const { - return _NIFMODEL_GETBLOCKINDEX_2( link, QLatin1String(ancestor) ); + return getLinkArray( getItem(arrayParent, QLatin1String(arrayName)) ); } -inline QModelIndex NifModel::getBlockIndex( qint32 link, const std::initializer_list & ancestors ) const +inline QVector NifModel::getLinkArray( const QModelIndex & iArray ) const { - return _NIFMODEL_GETBLOCKINDEX_2( link, ancestors ); + return getLinkArray( getItem(iArray) ); } -inline QModelIndex NifModel::getBlockIndex( qint32 link, const QStringList & ancestors ) const +inline QVector NifModel::getLinkArray( const QModelIndex & arrayParent, int arrayIndex ) const { - return _NIFMODEL_GETBLOCKINDEX_2( link, ancestors ); + return getLinkArray( getItem(arrayParent, arrayIndex) ); +} +inline QVector NifModel::getLinkArray( const QModelIndex & arrayParent, const QString & arrayName ) const +{ + return getLinkArray( getItem(arrayParent, arrayName) ); +} +inline QVector NifModel::getLinkArray( const QModelIndex & arrayParent, const QLatin1String & arrayName ) const +{ + return getLinkArray( getItem(arrayParent, arrayName) ); +} +inline QVector NifModel::getLinkArray( const QModelIndex & arrayParent, const char * arrayName ) const +{ + return getLinkArray( getItem(arrayParent, QLatin1String(arrayName)) ); } -inline QModelIndex NifModel::getBlockIndex( const NifItem * item ) const + +// Link array setters + +inline bool NifModel::setLinkArray( const NifItem * arrayParent, int arrayIndex, const QVector & links ) { - return _NIFMODEL_GETBLOCKINDEX_1( item ); + return setLinkArray( getItem(arrayParent, arrayIndex, true), links ); } -inline QModelIndex NifModel::getBlockIndex( const NifItem * item, const QString & ancestor ) const +inline bool NifModel::setLinkArray( const NifItem * arrayParent, const QString & arrayName, const QVector & links ) { - return _NIFMODEL_GETBLOCKINDEX_2( item, ancestor ); + return setLinkArray( getItem(arrayParent, arrayName, true), links ); } -inline QModelIndex NifModel::getBlockIndex( const NifItem * item, const QLatin1String & ancestor ) const +inline bool NifModel::setLinkArray( const NifItem * arrayParent, const QLatin1String & arrayName, const QVector & links ) { - return _NIFMODEL_GETBLOCKINDEX_2( item, ancestor ); + return setLinkArray( getItem(arrayParent, arrayName, true), links ); } -inline QModelIndex NifModel::getBlockIndex( const NifItem * item, const char * ancestor ) const +inline bool NifModel::setLinkArray( const NifItem * arrayParent, const char * arrayName, const QVector & links ) { - return _NIFMODEL_GETBLOCKINDEX_2( item, QLatin1String(ancestor) ); + return setLinkArray( getItem(arrayParent, QLatin1String(arrayName), true), links ); } -inline QModelIndex NifModel::getBlockIndex( const NifItem * item, const std::initializer_list & ancestors ) const +inline bool NifModel::setLinkArray( const QModelIndex & iArray, const QVector & links ) { - return _NIFMODEL_GETBLOCKINDEX_2( item, ancestors ); + return setLinkArray( getItem(iArray), links ); } -inline QModelIndex NifModel::getBlockIndex( const NifItem * item, const QStringList & ancestors ) const +inline bool NifModel::setLinkArray( const QModelIndex & arrayParent, int arrayIndex, const QVector & links ) { - return _NIFMODEL_GETBLOCKINDEX_2( item, ancestors ); + return setLinkArray( getItem(arrayParent, arrayIndex, true), links ); +} +inline bool NifModel::setLinkArray( const QModelIndex & arrayParent, const QString & arrayName, const QVector & links ) +{ + return setLinkArray( getItem(arrayParent, arrayName, true), links ); +} +inline bool NifModel::setLinkArray( const QModelIndex & arrayParent, const QLatin1String & arrayName, const QVector & links ) +{ + return setLinkArray( getItem(arrayParent, arrayName, true), links ); +} +inline bool NifModel::setLinkArray( const QModelIndex & arrayParent, const char * arrayName, const QVector & links ) +{ + return setLinkArray( getItem(arrayParent, QLatin1String(arrayName), true), links ); } -inline QModelIndex NifModel::getBlockIndex( const QModelIndex & index ) const + +// NifModel -> NifFieldTemplate methods + +inline NifFieldConst NifModel::field( const QModelIndex & index, bool reportErrors ) const { - return _NIFMODEL_GETBLOCKINDEX_1( index ); + auto item = getItem( index, reportErrors ); + return NifFieldConst( evalCondition(item) ? item : nullptr ); } -inline QModelIndex NifModel::getBlockIndex( const QModelIndex & index, const QString & ancestor ) const +inline NifField NifModel::field( const QModelIndex & index, bool reportErrors ) { - return _NIFMODEL_GETBLOCKINDEX_2( index, ancestor ); + auto item = getItem( index, reportErrors ); + return NifField( evalCondition(item) ? item : nullptr ); } -inline QModelIndex NifModel::getBlockIndex( const QModelIndex & index, const QLatin1String & ancestor ) const + +inline NifFieldConst NifModel::block( quint32 link ) const { - return _NIFMODEL_GETBLOCKINDEX_2( index, ancestor ); + return NifFieldConst( getBlockItem(link) ); } -inline QModelIndex NifModel::getBlockIndex( const QModelIndex & index, const char * ancestor ) const +inline NifField NifModel::block( quint32 link ) { - return _NIFMODEL_GETBLOCKINDEX_2( index, QLatin1String(ancestor) ); + return NifField( getBlockItem(link) ); } -inline QModelIndex NifModel::getBlockIndex( const QModelIndex & index, const std::initializer_list & ancestors ) const + +inline NifFieldConst NifModel::block( const QModelIndex & index, bool reportErrors ) const { - return _NIFMODEL_GETBLOCKINDEX_2( index, ancestors ); + return field( index, reportErrors ).block(); } -inline QModelIndex NifModel::getBlockIndex( const QModelIndex & index, const QStringList & ancestors ) const +inline NifField NifModel::block( const QModelIndex & index, bool reportErrors ) { - return _NIFMODEL_GETBLOCKINDEX_2( index, ancestors ); + return field( index, reportErrors ).block(); +} + +// TODO: Move to nifmodel.cpp +inline const NifItem * NifModel::findBlockItemByName( const QString & blockName ) const +{ + for ( int i = firstBlockRow(), iLast = lastBlockRow(); i <= iLast; i++ ) { + auto block = root->child(i); + auto nameItem = getItem( block, "Name", false ); + if ( nameItem && get(nameItem) == blockName ) + return block; + } + return nullptr; +} +// TODO: Move to nifmodel.cpp +inline const NifItem * NifModel::findBlockItemByName( const QLatin1String & blockName ) const +{ + for ( int i = firstBlockRow(), iLast = lastBlockRow(); i <= iLast; i++ ) { + auto block = root->child(i); + auto nameItem = getItem( block, "Name", false ); + if ( nameItem && get(nameItem) == blockName ) + return block; + } + return nullptr; } +inline NifFieldConst NifModel::block( const QString & blockName ) const +{ + return NifFieldConst( findBlockItemByName(blockName) ); +} +inline NifField NifModel::block( const QString & blockName ) +{ + return NifField( const_cast( findBlockItemByName(blockName) ) ); +} +inline NifFieldConst NifModel::block( const QLatin1String & blockName ) const +{ + return NifFieldConst( findBlockItemByName(blockName) ); +} +inline NifField NifModel::block( const QLatin1String & blockName ) +{ + return NifField( const_cast( findBlockItemByName(blockName) ) ); +} +inline NifFieldConst NifModel::block( const char * blockName ) const +{ + return block( QLatin1String(blockName) ); +} +inline NifField NifModel::block( const char * blockName ) +{ + return block( QLatin1String(blockName) ); +} -// isNiBlock +inline NifFieldConst NifModel::header() const +{ + return NifFieldConst( getHeaderItem() ); +} +inline NifField NifModel::header() +{ + return NifField( getHeaderItem() ); +} -inline bool NifModel::isNiBlock( const NifItem * item ) const +inline NifFieldConst NifModel::footer() const { - return isTopItem( item ) && isBlockRow( item->row() ); + return NifFieldConst( getFooterItem() ); } -inline bool NifModel::isNiBlock( const NifItem * item, const QString & testType ) const +inline NifField NifModel::footer() +{ + return NifField( getFooterItem() ); +} + +inline NifFieldIteratorSimple NifModel::blockIter() const +{ + return NifFieldIteratorSimple( root, firstBlockRow(), lastBlockRow() ); +} + +inline NifFieldIteratorSimple NifModel::blockIter() +{ + return NifFieldIteratorSimple( root, firstBlockRow(), lastBlockRow() ); +} + + +// NifFieldTemplate - isBlockType() methods (generated) + +template +inline bool NifFieldTemplate::isBlockType( const QString & testType ) const { - return isNiBlock(item) && item->hasName(testType); + return isBlock() && m_item->hasName(testType); } -inline bool NifModel::isNiBlock( const NifItem * item, const QLatin1String & testType ) const +template +inline bool NifFieldTemplate::isBlockType( const QString & testType1, const QString & testType2 ) const { - return isNiBlock(item) && item->hasName(testType); + return isBlock() && ( m_item->hasName(testType1) || m_item->hasName(testType2) ); } -inline bool NifModel::isNiBlock( const NifItem * item, const char * testType ) const +template +inline bool NifFieldTemplate::isBlockType( const QString & testType1, const QString & testType2, const QString & testType3 ) const { - return isNiBlock( item, QLatin1String(testType) ); + return isBlock() && ( m_item->hasName(testType1) || m_item->hasName(testType2) || m_item->hasName(testType3) ); } -inline bool NifModel::isNiBlock( const QModelIndex & index ) const +template +inline bool NifFieldTemplate::isBlockType( const QString & testType1, const QString & testType2, const QString & testType3, const QString & testType4 ) const { - return isNiBlock( getItem(index) ); + return isBlock() && ( m_item->hasName(testType1) || m_item->hasName(testType2) || m_item->hasName(testType3) || m_item->hasName(testType4) ); } -inline bool NifModel::isNiBlock( const QModelIndex & index, const QString & testType ) const +template +inline bool NifFieldTemplate::isBlockType( const QString & testType1, const QString & testType2, const QString & testType3, const QString & testType4, const QString & testType5 ) const { - return isNiBlock( getItem(index), testType ); + return isBlock() && ( m_item->hasName(testType1) || m_item->hasName(testType2) || m_item->hasName(testType3) || m_item->hasName(testType4) || m_item->hasName(testType5) ); } -inline bool NifModel::isNiBlock( const QModelIndex & index, const QLatin1String & testType ) const +template +inline bool NifFieldTemplate::isBlockType( const QLatin1String & testType ) const { - return isNiBlock( getItem(index), testType ); + return isBlock() && m_item->hasName(testType); } -inline bool NifModel::isNiBlock( const QModelIndex & index, const char * testType ) const +template +inline bool NifFieldTemplate::isBlockType( const QLatin1String & testType1, const QLatin1String & testType2 ) const { - return isNiBlock( getItem(index), QLatin1String(testType) ); + return isBlock() && ( m_item->hasName(testType1) || m_item->hasName(testType2) ); } -inline bool NifModel::isNiBlock( const QModelIndex & index, const std::initializer_list & testTypes ) const +template +inline bool NifFieldTemplate::isBlockType( const QLatin1String & testType1, const QLatin1String & testType2, const QLatin1String & testType3 ) const { - return isNiBlock( getItem(index), testTypes ); + return isBlock() && ( m_item->hasName(testType1) || m_item->hasName(testType2) || m_item->hasName(testType3) ); } -inline bool NifModel::isNiBlock( const QModelIndex & index, const QStringList & testTypes ) const +template +inline bool NifFieldTemplate::isBlockType( const QLatin1String & testType1, const QLatin1String & testType2, const QLatin1String & testType3, const QLatin1String & testType4 ) const { - return isNiBlock( getItem(index), testTypes ); + return isBlock() && ( m_item->hasName(testType1) || m_item->hasName(testType2) || m_item->hasName(testType3) || m_item->hasName(testType4) ); } - - -// Block inheritance - -inline bool NifModel::inherits( const QString & blockName, const char * ancestor ) const +template +inline bool NifFieldTemplate::isBlockType( const QLatin1String & testType1, const QLatin1String & testType2, const QLatin1String & testType3, const QLatin1String & testType4, const QLatin1String & testType5 ) const { - return inherits( blockName, QLatin1String(ancestor) ); + return isBlock() && ( m_item->hasName(testType1) || m_item->hasName(testType2) || m_item->hasName(testType3) || m_item->hasName(testType4) || m_item->hasName(testType5) ); } -inline bool NifModel::blockInherits( const NifItem * item, const char * ancestor ) const +template +inline bool NifFieldTemplate::isBlockType( const char * testType ) const { - return blockInherits( item, QLatin1String(ancestor) ); + return isBlockType( QLatin1String(testType) ); } -inline bool NifModel::blockInherits( const QModelIndex & index, const QString & ancestor ) const +template +inline bool NifFieldTemplate::isBlockType( const char * testType1, const char * testType2 ) const { - return blockInherits( getItem(index), ancestor ); + return isBlockType( QLatin1String(testType1), QLatin1String(testType2) ); } -inline bool NifModel::blockInherits( const QModelIndex & index, const QLatin1String & ancestor ) const +template +inline bool NifFieldTemplate::isBlockType( const char * testType1, const char * testType2, const char * testType3 ) const { - return blockInherits( getItem(index), ancestor ); + return isBlockType( QLatin1String(testType1), QLatin1String(testType2), QLatin1String(testType3) ); } -inline bool NifModel::blockInherits( const QModelIndex & index, const char * ancestor ) const +template +inline bool NifFieldTemplate::isBlockType( const char * testType1, const char * testType2, const char * testType3, const char * testType4 ) const { - return blockInherits( getItem(index), QLatin1String(ancestor) ); + return isBlockType( QLatin1String(testType1), QLatin1String(testType2), QLatin1String(testType3), QLatin1String(testType4) ); } -inline bool NifModel::blockInherits( const QModelIndex & index, const std::initializer_list & ancestors ) const +template +inline bool NifFieldTemplate::isBlockType( const char * testType1, const char * testType2, const char * testType3, const char * testType4, const char * testType5 ) const { - return blockInherits( getItem(index), ancestors ); + return isBlockType( QLatin1String(testType1), QLatin1String(testType2), QLatin1String(testType3), QLatin1String(testType4), QLatin1String(testType5) ); } -inline bool NifModel::blockInherits( const QModelIndex & index, const QStringList & ancestors ) const +template +inline bool NifFieldTemplate::isBlockType( const QStringList & testTypes ) const { - return blockInherits( getItem(index), ancestors ); + if ( isBlock() ) { + for ( const QString & name : testTypes ) + if ( m_item->hasName(name) ) + return true; + } + return false; } -// Item value getters +// NifFieldTemplate - inherits() methods (generated) -template inline T NifModel::get( const NifItem * item ) const +template +inline bool NifFieldTemplate::inherits( const QString & testAncestor ) const { - return BaseModel::get( item ); + return isBlock() && _inherits( m_item, testAncestor ); } -template <> inline QString NifModel::get( const NifItem * item ) const +template +inline bool NifFieldTemplate::inherits( const QString & testAncestor1, const QString & testAncestor2 ) const { - return resolveString( item ); + return isBlock() && ( _inherits( m_item, testAncestor1 ) || _inherits( m_item, testAncestor2 ) ); } -template inline T NifModel::get( const NifItem * itemParent, int itemIndex ) const +template +inline bool NifFieldTemplate::inherits( const QString & testAncestor1, const QString & testAncestor2, const QString & testAncestor3 ) const { - return get( getItem(itemParent, itemIndex) ); + return isBlock() && ( _inherits( m_item, testAncestor1 ) || _inherits( m_item, testAncestor2 ) || _inherits( m_item, testAncestor3 ) ); } -template inline T NifModel::get( const NifItem * itemParent, const QString & itemName ) const +template +inline bool NifFieldTemplate::inherits( const QString & testAncestor1, const QString & testAncestor2, const QString & testAncestor3, const QString & testAncestor4 ) const { - return get( getItem(itemParent, itemName) ); + return isBlock() && ( _inherits( m_item, testAncestor1 ) || _inherits( m_item, testAncestor2 ) || _inherits( m_item, testAncestor3 ) || _inherits( m_item, testAncestor4 ) ); } -template inline T NifModel::get( const NifItem * itemParent, const QLatin1String & itemName ) const +template +inline bool NifFieldTemplate::inherits( const QString & testAncestor1, const QString & testAncestor2, const QString & testAncestor3, const QString & testAncestor4, const QString & testAncestor5 ) const { - return get( getItem(itemParent, itemName) ); + return isBlock() && ( _inherits( m_item, testAncestor1 ) || _inherits( m_item, testAncestor2 ) || _inherits( m_item, testAncestor3 ) || _inherits( m_item, testAncestor4 ) || _inherits( m_item, testAncestor5 ) ); } -template inline T NifModel::get( const NifItem * itemParent, const char * itemName ) const +template +inline bool NifFieldTemplate::inherits( const QLatin1String & testAncestor ) const { - return get( getItem(itemParent, QLatin1String(itemName)) ); + return isBlock() && _inherits( m_item, testAncestor ); } -template inline T NifModel::get( const QModelIndex & index ) const +template +inline bool NifFieldTemplate::inherits( const QLatin1String & testAncestor1, const QLatin1String & testAncestor2 ) const { - return get( getItem(index) ); + return isBlock() && ( _inherits( m_item, testAncestor1 ) || _inherits( m_item, testAncestor2 ) ); } -template inline T NifModel::get( const QModelIndex & itemParent, int itemIndex ) const +template +inline bool NifFieldTemplate::inherits( const QLatin1String & testAncestor1, const QLatin1String & testAncestor2, const QLatin1String & testAncestor3 ) const { - return get( getItem(itemParent, itemIndex) ); + return isBlock() && ( _inherits( m_item, testAncestor1 ) || _inherits( m_item, testAncestor2 ) || _inherits( m_item, testAncestor3 ) ); } -template inline T NifModel::get( const QModelIndex & itemParent, const QString & itemName ) const +template +inline bool NifFieldTemplate::inherits( const QLatin1String & testAncestor1, const QLatin1String & testAncestor2, const QLatin1String & testAncestor3, const QLatin1String & testAncestor4 ) const { - return get( getItem(itemParent, itemName) ); + return isBlock() && ( _inherits( m_item, testAncestor1 ) || _inherits( m_item, testAncestor2 ) || _inherits( m_item, testAncestor3 ) || _inherits( m_item, testAncestor4 ) ); } -template inline T NifModel::get( const QModelIndex & itemParent, const QLatin1String & itemName ) const +template +inline bool NifFieldTemplate::inherits( const QLatin1String & testAncestor1, const QLatin1String & testAncestor2, const QLatin1String & testAncestor3, const QLatin1String & testAncestor4, const QLatin1String & testAncestor5 ) const { - return get( getItem(itemParent, itemName) ); + return isBlock() && ( _inherits( m_item, testAncestor1 ) || _inherits( m_item, testAncestor2 ) || _inherits( m_item, testAncestor3 ) || _inherits( m_item, testAncestor4 ) || _inherits( m_item, testAncestor5 ) ); } -template inline T NifModel::get( const QModelIndex & itemParent, const char * itemName ) const +template +inline bool NifFieldTemplate::inherits( const char * testAncestor ) const { - return get( getItem(itemParent, QLatin1String(itemName)) ); + return inherits( QLatin1String(testAncestor) ); } - - -// Item value setters - -template inline bool NifModel::set( NifItem * item, const T & val ) +template +inline bool NifFieldTemplate::inherits( const char * testAncestor1, const char * testAncestor2 ) const { - return BaseModel::set( item, val ); + return inherits( QLatin1String(testAncestor1), QLatin1String(testAncestor2) ); } -template <> inline bool NifModel::set( NifItem * item, const QString & val ) +template +inline bool NifFieldTemplate::inherits( const char * testAncestor1, const char * testAncestor2, const char * testAncestor3 ) const { - return assignString( item, val ); + return inherits( QLatin1String(testAncestor1), QLatin1String(testAncestor2), QLatin1String(testAncestor3) ); } -template inline bool NifModel::set( const NifItem * itemParent, int itemIndex, const T & val ) +template +inline bool NifFieldTemplate::inherits( const char * testAncestor1, const char * testAncestor2, const char * testAncestor3, const char * testAncestor4 ) const { - return set( getItem(itemParent, itemIndex, true), val ); + return inherits( QLatin1String(testAncestor1), QLatin1String(testAncestor2), QLatin1String(testAncestor3), QLatin1String(testAncestor4) ); } -template inline bool NifModel::set( const NifItem * itemParent, const QString & itemName, const T & val ) +template +inline bool NifFieldTemplate::inherits( const char * testAncestor1, const char * testAncestor2, const char * testAncestor3, const char * testAncestor4, const char * testAncestor5 ) const { - return set( getItem(itemParent, itemName, true), val ); + return inherits( QLatin1String(testAncestor1), QLatin1String(testAncestor2), QLatin1String(testAncestor3), QLatin1String(testAncestor4), QLatin1String(testAncestor5) ); } -template inline bool NifModel::set( const NifItem * itemParent, const QLatin1String & itemName, const T & val ) +template +inline bool NifFieldTemplate::inherits( const QStringList & testAncestors ) const { - return set( getItem(itemParent, itemName, true), val ); + return isBlock() && _inherits( m_item, testAncestors ); } -template inline bool NifModel::set( const NifItem * itemParent, const char * itemName, const T & val ) + + +// NifFieldTemplate - block() methods (generated) + +template +inline NifFieldTemplate NifFieldTemplate::block( const QString & testAncestor ) const { - return set( getItem(itemParent, QLatin1String(itemName), true), val ); + if ( m_item ) { + auto block = _model()->getBlockItem( m_item ); + if ( block && _inherits( block, testAncestor ) ) + return NifFieldTemplate( block ); + } + return NifFieldTemplate(); } -template inline bool NifModel::set( const QModelIndex & index, const T & val ) +template +inline NifFieldTemplate NifFieldTemplate::block( const QString & testAncestor1, const QString & testAncestor2 ) const { - return set( getItem(index), val ); + if ( m_item ) { + auto block = _model()->getBlockItem( m_item ); + if ( block && ( _inherits( block, testAncestor1 ) || _inherits( block, testAncestor2 ) ) ) + return NifFieldTemplate( block ); + } + return NifFieldTemplate(); } -template inline bool NifModel::set( const QModelIndex & itemParent, int itemIndex, const T & val ) +template +inline NifFieldTemplate NifFieldTemplate::block( const QString & testAncestor1, const QString & testAncestor2, const QString & testAncestor3 ) const { - return set( getItem(itemParent, itemIndex, true), val ); + if ( m_item ) { + auto block = _model()->getBlockItem( m_item ); + if ( block && ( _inherits( block, testAncestor1 ) || _inherits( block, testAncestor2 ) || _inherits( block, testAncestor3 ) ) ) + return NifFieldTemplate( block ); + } + return NifFieldTemplate(); } -template inline bool NifModel::set( const QModelIndex & itemParent, const QString & itemName, const T & val ) +template +inline NifFieldTemplate NifFieldTemplate::block( const QString & testAncestor1, const QString & testAncestor2, const QString & testAncestor3, const QString & testAncestor4 ) const { - return set( getItem(itemParent, itemName, true), val ); + if ( m_item ) { + auto block = _model()->getBlockItem( m_item ); + if ( block && ( _inherits( block, testAncestor1 ) || _inherits( block, testAncestor2 ) || _inherits( block, testAncestor3 ) || _inherits( block, testAncestor4 ) ) ) + return NifFieldTemplate( block ); + } + return NifFieldTemplate(); } -template inline bool NifModel::set( const QModelIndex & itemParent, const QLatin1String & itemName, const T & val ) +template +inline NifFieldTemplate NifFieldTemplate::block( const QString & testAncestor1, const QString & testAncestor2, const QString & testAncestor3, const QString & testAncestor4, const QString & testAncestor5 ) const { - return set( getItem(itemParent, itemName, true), val ); + if ( m_item ) { + auto block = _model()->getBlockItem( m_item ); + if ( block && ( _inherits( block, testAncestor1 ) || _inherits( block, testAncestor2 ) || _inherits( block, testAncestor3 ) || _inherits( block, testAncestor4 ) || _inherits( block, testAncestor5 ) ) ) + return NifFieldTemplate( block ); + } + return NifFieldTemplate(); } -template inline bool NifModel::set( const QModelIndex & itemParent, const char * itemName, const T & val ) +template +inline NifFieldTemplate NifFieldTemplate::block( const QLatin1String & testAncestor ) const { - return set( getItem(itemParent, QLatin1String(itemName), true), val ); + if ( m_item ) { + auto block = _model()->getBlockItem( m_item ); + if ( block && _inherits( block, testAncestor ) ) + return NifFieldTemplate( block ); + } + return NifFieldTemplate(); } - - -// String resolving ("get ex") - -inline QString NifModel::resolveString( const NifItem * itemParent, int itemIndex ) const +template +inline NifFieldTemplate NifFieldTemplate::block( const QLatin1String & testAncestor1, const QLatin1String & testAncestor2 ) const { - return resolveString( getItem(itemParent, itemIndex) ); + if ( m_item ) { + auto block = _model()->getBlockItem( m_item ); + if ( block && ( _inherits( block, testAncestor1 ) || _inherits( block, testAncestor2 ) ) ) + return NifFieldTemplate( block ); + } + return NifFieldTemplate(); } -inline QString NifModel::resolveString( const NifItem * itemParent, const QString & itemName ) const +template +inline NifFieldTemplate NifFieldTemplate::block( const QLatin1String & testAncestor1, const QLatin1String & testAncestor2, const QLatin1String & testAncestor3 ) const { - return resolveString( getItem(itemParent, itemName) ); + if ( m_item ) { + auto block = _model()->getBlockItem( m_item ); + if ( block && ( _inherits( block, testAncestor1 ) || _inherits( block, testAncestor2 ) || _inherits( block, testAncestor3 ) ) ) + return NifFieldTemplate( block ); + } + return NifFieldTemplate(); } -inline QString NifModel::resolveString( const NifItem * itemParent, const QLatin1String & itemName ) const +template +inline NifFieldTemplate NifFieldTemplate::block( const QLatin1String & testAncestor1, const QLatin1String & testAncestor2, const QLatin1String & testAncestor3, const QLatin1String & testAncestor4 ) const { - return resolveString( getItem(itemParent, itemName) ); + if ( m_item ) { + auto block = _model()->getBlockItem( m_item ); + if ( block && ( _inherits( block, testAncestor1 ) || _inherits( block, testAncestor2 ) || _inherits( block, testAncestor3 ) || _inherits( block, testAncestor4 ) ) ) + return NifFieldTemplate( block ); + } + return NifFieldTemplate(); } -inline QString NifModel::resolveString( const NifItem * itemParent, const char * itemName ) const +template +inline NifFieldTemplate NifFieldTemplate::block( const QLatin1String & testAncestor1, const QLatin1String & testAncestor2, const QLatin1String & testAncestor3, const QLatin1String & testAncestor4, const QLatin1String & testAncestor5 ) const { - return resolveString( getItem(itemParent, QLatin1String(itemName)) ); + if ( m_item ) { + auto block = _model()->getBlockItem( m_item ); + if ( block && ( _inherits( block, testAncestor1 ) || _inherits( block, testAncestor2 ) || _inherits( block, testAncestor3 ) || _inherits( block, testAncestor4 ) || _inherits( block, testAncestor5 ) ) ) + return NifFieldTemplate( block ); + } + return NifFieldTemplate(); } -inline QString NifModel::resolveString( const QModelIndex & index ) const +template +inline NifFieldTemplate NifFieldTemplate::block( const char * testAncestor ) const { - return resolveString( getItem(index) ); + return block( QLatin1String(testAncestor) ); } -inline QString NifModel::resolveString( const QModelIndex & itemParent, int itemIndex ) const +template +inline NifFieldTemplate NifFieldTemplate::block( const char * testAncestor1, const char * testAncestor2 ) const { - return resolveString( getItem(itemParent, itemIndex) ); + return block( QLatin1String(testAncestor1), QLatin1String(testAncestor2) ); } -inline QString NifModel::resolveString( const QModelIndex & itemParent, const QString & itemName ) const +template +inline NifFieldTemplate NifFieldTemplate::block( const char * testAncestor1, const char * testAncestor2, const char * testAncestor3 ) const { - return resolveString( getItem(itemParent, itemName) ); + return block( QLatin1String(testAncestor1), QLatin1String(testAncestor2), QLatin1String(testAncestor3) ); } -inline QString NifModel::resolveString( const QModelIndex & itemParent, const QLatin1String & itemName ) const +template +inline NifFieldTemplate NifFieldTemplate::block( const char * testAncestor1, const char * testAncestor2, const char * testAncestor3, const char * testAncestor4 ) const { - return resolveString( getItem(itemParent, itemName) ); + return block( QLatin1String(testAncestor1), QLatin1String(testAncestor2), QLatin1String(testAncestor3), QLatin1String(testAncestor4) ); } -inline QString NifModel::resolveString( const QModelIndex & itemParent, const char * itemName ) const +template +inline NifFieldTemplate NifFieldTemplate::block( const char * testAncestor1, const char * testAncestor2, const char * testAncestor3, const char * testAncestor4, const char * testAncestor5 ) const { - return resolveString( getItem(itemParent, QLatin1String(itemName)) ); + return block( QLatin1String(testAncestor1), QLatin1String(testAncestor2), QLatin1String(testAncestor3), QLatin1String(testAncestor4), QLatin1String(testAncestor5) ); +} +template +inline NifFieldTemplate NifFieldTemplate::block( const QStringList & testAncestors ) const +{ + if ( m_item ) { + auto block = _model()->getBlockItem( m_item ); + if ( block && _inherits( block, testAncestors ) ) + return NifFieldTemplate( block ); + } + return NifFieldTemplate(); } -// String assigning ("set ex") +// NifFieldTemplate - hasName() methods (generated) -inline bool NifModel::assignString( const NifItem * itemParent, int itemIndex, const QString & string, bool replace ) +template +inline bool NifFieldTemplate::hasName( const QString & testName ) const { - return assignString( getItem(itemParent, itemIndex, true), string, replace ); + return m_item && m_item->hasName(testName); } -inline bool NifModel::assignString( const NifItem * itemParent, const QString & itemName, const QString & string, bool replace ) +template +inline bool NifFieldTemplate::hasName( const QString & testName1, const QString & testName2 ) const { - return assignString( getItem(itemParent, itemName, true), string, replace ); + return m_item && ( m_item->hasName(testName1) || m_item->hasName(testName2) ); } -inline bool NifModel::assignString( const NifItem * itemParent, const QLatin1String & itemName, const QString & string, bool replace ) +template +inline bool NifFieldTemplate::hasName( const QString & testName1, const QString & testName2, const QString & testName3 ) const { - return assignString( getItem(itemParent, itemName, true), string, replace ); + return m_item && ( m_item->hasName(testName1) || m_item->hasName(testName2) || m_item->hasName(testName3) ); } -inline bool NifModel::assignString( const NifItem * itemParent, const char * itemName, const QString & string, bool replace ) +template +inline bool NifFieldTemplate::hasName( const QString & testName1, const QString & testName2, const QString & testName3, const QString & testName4 ) const { - return assignString( getItem(itemParent, QLatin1String(itemName), true), string, replace ); + return m_item && ( m_item->hasName(testName1) || m_item->hasName(testName2) || m_item->hasName(testName3) || m_item->hasName(testName4) ); } -inline bool NifModel::assignString( const QModelIndex & index, const QString & string, bool replace ) +template +inline bool NifFieldTemplate::hasName( const QString & testName1, const QString & testName2, const QString & testName3, const QString & testName4, const QString & testName5 ) const { - return assignString( getItem(index), string, replace ); + return m_item && ( m_item->hasName(testName1) || m_item->hasName(testName2) || m_item->hasName(testName3) || m_item->hasName(testName4) || m_item->hasName(testName5) ); } -inline bool NifModel::assignString( const QModelIndex & itemParent, int itemIndex, const QString & string, bool replace ) +template +inline bool NifFieldTemplate::hasName( const QLatin1String & testName ) const { - return assignString( getItem(itemParent, itemIndex, true), string, replace ); + return m_item && m_item->hasName(testName); } -inline bool NifModel::assignString( const QModelIndex & itemParent, const QString & itemName, const QString & string, bool replace ) +template +inline bool NifFieldTemplate::hasName( const QLatin1String & testName1, const QLatin1String & testName2 ) const { - return assignString( getItem(itemParent, itemName, true), string, replace ); + return m_item && ( m_item->hasName(testName1) || m_item->hasName(testName2) ); } -inline bool NifModel::assignString( const QModelIndex & itemParent, const QLatin1String & itemName, const QString & string, bool replace ) +template +inline bool NifFieldTemplate::hasName( const QLatin1String & testName1, const QLatin1String & testName2, const QLatin1String & testName3 ) const { - return assignString( getItem(itemParent, itemName, true), string, replace ); + return m_item && ( m_item->hasName(testName1) || m_item->hasName(testName2) || m_item->hasName(testName3) ); } -inline bool NifModel::assignString( const QModelIndex & itemParent, const char * itemName, const QString & string, bool replace ) +template +inline bool NifFieldTemplate::hasName( const QLatin1String & testName1, const QLatin1String & testName2, const QLatin1String & testName3, const QLatin1String & testName4 ) const { - return assignString( getItem(itemParent, QLatin1String(itemName), true), string, replace ); + return m_item && ( m_item->hasName(testName1) || m_item->hasName(testName2) || m_item->hasName(testName3) || m_item->hasName(testName4) ); } - - -// Link getters - -inline qint32 NifModel::getLink( const NifItem * item ) const +template +inline bool NifFieldTemplate::hasName( const QLatin1String & testName1, const QLatin1String & testName2, const QLatin1String & testName3, const QLatin1String & testName4, const QLatin1String & testName5 ) const { - return item ? item->getLinkValue() : -1; + return m_item && ( m_item->hasName(testName1) || m_item->hasName(testName2) || m_item->hasName(testName3) || m_item->hasName(testName4) || m_item->hasName(testName5) ); } -inline qint32 NifModel::getLink( const NifItem * itemParent, int itemIndex ) const +template +inline bool NifFieldTemplate::hasName( const char * testName ) const { - return getLink( getItem(itemParent, itemIndex) ); + return hasName( QLatin1String(testName) ); } -inline qint32 NifModel::getLink( const NifItem * itemParent, const QString & itemName ) const +template +inline bool NifFieldTemplate::hasName( const char * testName1, const char * testName2 ) const { - return getLink( getItem(itemParent, itemName) ); + return hasName( QLatin1String(testName1), QLatin1String(testName2) ); } -inline qint32 NifModel::getLink( const NifItem * itemParent, const QLatin1String & itemName ) const +template +inline bool NifFieldTemplate::hasName( const char * testName1, const char * testName2, const char * testName3 ) const { - return getLink( getItem(itemParent, itemName) ); + return hasName( QLatin1String(testName1), QLatin1String(testName2), QLatin1String(testName3) ); } -inline qint32 NifModel::getLink( const NifItem * itemParent, const char * itemName ) const +template +inline bool NifFieldTemplate::hasName( const char * testName1, const char * testName2, const char * testName3, const char * testName4 ) const { - return getLink( getItem(itemParent, QLatin1String(itemName)) ); + return hasName( QLatin1String(testName1), QLatin1String(testName2), QLatin1String(testName3), QLatin1String(testName4) ); } -inline qint32 NifModel::getLink( const QModelIndex & index ) const +template +inline bool NifFieldTemplate::hasName( const char * testName1, const char * testName2, const char * testName3, const char * testName4, const char * testName5 ) const { - return getLink( getItem(index) ); + return hasName( QLatin1String(testName1), QLatin1String(testName2), QLatin1String(testName3), QLatin1String(testName4), QLatin1String(testName5) ); } -inline qint32 NifModel::getLink( const QModelIndex & itemParent, int itemIndex ) const + + +// NifFieldTemplate - hasStrType() methods (generated) + +template +inline bool NifFieldTemplate::hasStrType( const QString & testType ) const { - return getLink( getItem(itemParent, itemIndex) ); + return m_item && m_item->hasStrType(testType); } -inline qint32 NifModel::getLink( const QModelIndex & itemParent, const QString & itemName ) const +template +inline bool NifFieldTemplate::hasStrType( const QString & testType1, const QString & testType2 ) const { - return getLink( getItem(itemParent, itemName) ); + return m_item && ( m_item->hasStrType(testType1) || m_item->hasStrType(testType2) ); } -inline qint32 NifModel::getLink( const QModelIndex & itemParent, const QLatin1String & itemName ) const +template +inline bool NifFieldTemplate::hasStrType( const QString & testType1, const QString & testType2, const QString & testType3 ) const { - return getLink( getItem(itemParent, itemName) ); + return m_item && ( m_item->hasStrType(testType1) || m_item->hasStrType(testType2) || m_item->hasStrType(testType3) ); } -inline qint32 NifModel::getLink( const QModelIndex & itemParent, const char * itemName ) const +template +inline bool NifFieldTemplate::hasStrType( const QString & testType1, const QString & testType2, const QString & testType3, const QString & testType4 ) const { - return getLink( getItem(itemParent, QLatin1String(itemName)) ); + return m_item && ( m_item->hasStrType(testType1) || m_item->hasStrType(testType2) || m_item->hasStrType(testType3) || m_item->hasStrType(testType4) ); } - - -// Link setters - -inline bool NifModel::setLink( const NifItem * itemParent, int itemIndex, qint32 link ) +template +inline bool NifFieldTemplate::hasStrType( const QString & testType1, const QString & testType2, const QString & testType3, const QString & testType4, const QString & testType5 ) const { - return setLink( getItem(itemParent, itemIndex, true), link ); + return m_item && ( m_item->hasStrType(testType1) || m_item->hasStrType(testType2) || m_item->hasStrType(testType3) || m_item->hasStrType(testType4) || m_item->hasStrType(testType5) ); } -inline bool NifModel::setLink( const NifItem * itemParent, const QString & itemName, qint32 link ) +template +inline bool NifFieldTemplate::hasStrType( const QLatin1String & testType ) const { - return setLink( getItem(itemParent, itemName, true), link ); + return m_item && m_item->hasStrType(testType); } -inline bool NifModel::setLink( const NifItem * itemParent, const QLatin1String & itemName, qint32 link ) +template +inline bool NifFieldTemplate::hasStrType( const QLatin1String & testType1, const QLatin1String & testType2 ) const { - return setLink( getItem(itemParent, itemName, true), link ); + return m_item && ( m_item->hasStrType(testType1) || m_item->hasStrType(testType2) ); } -inline bool NifModel::setLink( const NifItem * itemParent, const char * itemName, qint32 link ) +template +inline bool NifFieldTemplate::hasStrType( const QLatin1String & testType1, const QLatin1String & testType2, const QLatin1String & testType3 ) const { - return setLink( getItem(itemParent, QLatin1String(itemName), true), link ); + return m_item && ( m_item->hasStrType(testType1) || m_item->hasStrType(testType2) || m_item->hasStrType(testType3) ); } -inline bool NifModel::setLink( const QModelIndex & index, qint32 link ) +template +inline bool NifFieldTemplate::hasStrType( const QLatin1String & testType1, const QLatin1String & testType2, const QLatin1String & testType3, const QLatin1String & testType4 ) const { - return setLink( getItem(index), link ); + return m_item && ( m_item->hasStrType(testType1) || m_item->hasStrType(testType2) || m_item->hasStrType(testType3) || m_item->hasStrType(testType4) ); } -inline bool NifModel::setLink( const QModelIndex & itemParent, int itemIndex, qint32 link ) +template +inline bool NifFieldTemplate::hasStrType( const QLatin1String & testType1, const QLatin1String & testType2, const QLatin1String & testType3, const QLatin1String & testType4, const QLatin1String & testType5 ) const { - return setLink( getItem(itemParent, itemIndex, true), link ); + return m_item && ( m_item->hasStrType(testType1) || m_item->hasStrType(testType2) || m_item->hasStrType(testType3) || m_item->hasStrType(testType4) || m_item->hasStrType(testType5) ); } -inline bool NifModel::setLink( const QModelIndex & itemParent, const QString & itemName, qint32 link ) +template +inline bool NifFieldTemplate::hasStrType( const char * testType ) const { - return setLink( getItem(itemParent, itemName, true), link ); + return hasStrType( QLatin1String(testType) ); } -inline bool NifModel::setLink( const QModelIndex & itemParent, const QLatin1String & itemName, qint32 link ) +template +inline bool NifFieldTemplate::hasStrType( const char * testType1, const char * testType2 ) const { - return setLink( getItem(itemParent, itemName, true), link ); + return hasStrType( QLatin1String(testType1), QLatin1String(testType2) ); } -inline bool NifModel::setLink( const QModelIndex & itemParent, const char * itemName, qint32 link ) +template +inline bool NifFieldTemplate::hasStrType( const char * testType1, const char * testType2, const char * testType3 ) const { - return setLink( getItem(itemParent, QLatin1String(itemName), true), link ); + return hasStrType( QLatin1String(testType1), QLatin1String(testType2), QLatin1String(testType3) ); +} +template +inline bool NifFieldTemplate::hasStrType( const char * testType1, const char * testType2, const char * testType3, const char * testType4 ) const +{ + return hasStrType( QLatin1String(testType1), QLatin1String(testType2), QLatin1String(testType3), QLatin1String(testType4) ); +} +template +inline bool NifFieldTemplate::hasStrType( const char * testType1, const char * testType2, const char * testType3, const char * testType4, const char * testType5 ) const +{ + return hasStrType( QLatin1String(testType1), QLatin1String(testType2), QLatin1String(testType3), QLatin1String(testType4), QLatin1String(testType5) ); } -// Link array getters +// NifFieldTemplate - hasValType() methods (generated) -inline QVector NifModel::getLinkArray( const NifItem * arrayParent, int arrayIndex ) const +template +inline bool NifFieldTemplate::hasValType( NifValue::Type testType ) const { - return getLinkArray( getItem(arrayParent, arrayIndex) ); + return m_item && m_item->valueType() == testType; } -inline QVector NifModel::getLinkArray( const NifItem * arrayParent, const QString & arrayName ) const +template +inline bool NifFieldTemplate::hasValType( NifValue::Type testType1, NifValue::Type testType2 ) const { - return getLinkArray( getItem(arrayParent, arrayName) ); + return m_item && ( m_item->valueType() == testType1 || m_item->valueType() == testType2 ); } -inline QVector NifModel::getLinkArray( const NifItem * arrayParent, const QLatin1String & arrayName ) const +template +inline bool NifFieldTemplate::hasValType( NifValue::Type testType1, NifValue::Type testType2, NifValue::Type testType3 ) const { - return getLinkArray( getItem(arrayParent, arrayName) ); + return m_item && ( m_item->valueType() == testType1 || m_item->valueType() == testType2 || m_item->valueType() == testType3 ); } -inline QVector NifModel::getLinkArray( const NifItem * arrayParent, const char * arrayName ) const +template +inline bool NifFieldTemplate::hasValType( NifValue::Type testType1, NifValue::Type testType2, NifValue::Type testType3, NifValue::Type testType4 ) const { - return getLinkArray( getItem(arrayParent, QLatin1String(arrayName)) ); + return m_item && ( m_item->valueType() == testType1 || m_item->valueType() == testType2 || m_item->valueType() == testType3 || m_item->valueType() == testType4 ); } -inline QVector NifModel::getLinkArray( const QModelIndex & iArray ) const +template +inline bool NifFieldTemplate::hasValType( NifValue::Type testType1, NifValue::Type testType2, NifValue::Type testType3, NifValue::Type testType4, NifValue::Type testType5 ) const { - return getLinkArray( getItem(iArray) ); + return m_item && ( m_item->valueType() == testType1 || m_item->valueType() == testType2 || m_item->valueType() == testType3 || m_item->valueType() == testType4 || m_item->valueType() == testType5 ); } -inline QVector NifModel::getLinkArray( const QModelIndex & arrayParent, int arrayIndex ) const + + +// NifFieldTemplate - getLinkBlock() methods (generated) + +template +inline NifFieldTemplate NifFieldTemplate::linkBlock( const QString & testAncestor ) const { - return getLinkArray( getItem(arrayParent, arrayIndex) ); + if ( m_item ) { + auto link = m_item->getLinkValue(); + auto block = _model()->getBlockItem( link ); + if ( block && _inherits( block, testAncestor ) ) + return NifFieldTemplate( block ); + } + return NifFieldTemplate(); } -inline QVector NifModel::getLinkArray( const QModelIndex & arrayParent, const QString & arrayName ) const +template +inline NifFieldTemplate NifFieldTemplate::linkBlock( const QString & testAncestor1, const QString & testAncestor2 ) const { - return getLinkArray( getItem(arrayParent, arrayName) ); + if ( m_item ) { + auto link = m_item->getLinkValue(); + auto block = _model()->getBlockItem( link ); + if ( block && ( _inherits( block, testAncestor1 ) || _inherits( block, testAncestor2 ) ) ) + return NifFieldTemplate( block ); + } + return NifFieldTemplate(); } -inline QVector NifModel::getLinkArray( const QModelIndex & arrayParent, const QLatin1String & arrayName ) const +template +inline NifFieldTemplate NifFieldTemplate::linkBlock( const QString & testAncestor1, const QString & testAncestor2, const QString & testAncestor3 ) const { - return getLinkArray( getItem(arrayParent, arrayName) ); + if ( m_item ) { + auto link = m_item->getLinkValue(); + auto block = _model()->getBlockItem( link ); + if ( block && ( _inherits( block, testAncestor1 ) || _inherits( block, testAncestor2 ) || _inherits( block, testAncestor3 ) ) ) + return NifFieldTemplate( block ); + } + return NifFieldTemplate(); } -inline QVector NifModel::getLinkArray( const QModelIndex & arrayParent, const char * arrayName ) const +template +inline NifFieldTemplate NifFieldTemplate::linkBlock( const QString & testAncestor1, const QString & testAncestor2, const QString & testAncestor3, const QString & testAncestor4 ) const { - return getLinkArray( getItem(arrayParent, QLatin1String(arrayName)) ); + if ( m_item ) { + auto link = m_item->getLinkValue(); + auto block = _model()->getBlockItem( link ); + if ( block && ( _inherits( block, testAncestor1 ) || _inherits( block, testAncestor2 ) || _inherits( block, testAncestor3 ) || _inherits( block, testAncestor4 ) ) ) + return NifFieldTemplate( block ); + } + return NifFieldTemplate(); } - - -// Link array setters - -inline bool NifModel::setLinkArray( const NifItem * arrayParent, int arrayIndex, const QVector & links ) +template +inline NifFieldTemplate NifFieldTemplate::linkBlock( const QString & testAncestor1, const QString & testAncestor2, const QString & testAncestor3, const QString & testAncestor4, const QString & testAncestor5 ) const { - return setLinkArray( getItem(arrayParent, arrayIndex, true), links ); + if ( m_item ) { + auto link = m_item->getLinkValue(); + auto block = _model()->getBlockItem( link ); + if ( block && ( _inherits( block, testAncestor1 ) || _inherits( block, testAncestor2 ) || _inherits( block, testAncestor3 ) || _inherits( block, testAncestor4 ) || _inherits( block, testAncestor5 ) ) ) + return NifFieldTemplate( block ); + } + return NifFieldTemplate(); } -inline bool NifModel::setLinkArray( const NifItem * arrayParent, const QString & arrayName, const QVector & links ) +template +inline NifFieldTemplate NifFieldTemplate::linkBlock( const QLatin1String & testAncestor ) const { - return setLinkArray( getItem(arrayParent, arrayName, true), links ); + if ( m_item ) { + auto link = m_item->getLinkValue(); + auto block = _model()->getBlockItem( link ); + if ( block && _inherits( block, testAncestor ) ) + return NifFieldTemplate( block ); + } + return NifFieldTemplate(); } -inline bool NifModel::setLinkArray( const NifItem * arrayParent, const QLatin1String & arrayName, const QVector & links ) +template +inline NifFieldTemplate NifFieldTemplate::linkBlock( const QLatin1String & testAncestor1, const QLatin1String & testAncestor2 ) const { - return setLinkArray( getItem(arrayParent, arrayName, true), links ); + if ( m_item ) { + auto link = m_item->getLinkValue(); + auto block = _model()->getBlockItem( link ); + if ( block && ( _inherits( block, testAncestor1 ) || _inherits( block, testAncestor2 ) ) ) + return NifFieldTemplate( block ); + } + return NifFieldTemplate(); } -inline bool NifModel::setLinkArray( const NifItem * arrayParent, const char * arrayName, const QVector & links ) +template +inline NifFieldTemplate NifFieldTemplate::linkBlock( const QLatin1String & testAncestor1, const QLatin1String & testAncestor2, const QLatin1String & testAncestor3 ) const { - return setLinkArray( getItem(arrayParent, QLatin1String(arrayName), true), links ); + if ( m_item ) { + auto link = m_item->getLinkValue(); + auto block = _model()->getBlockItem( link ); + if ( block && ( _inherits( block, testAncestor1 ) || _inherits( block, testAncestor2 ) || _inherits( block, testAncestor3 ) ) ) + return NifFieldTemplate( block ); + } + return NifFieldTemplate(); } -inline bool NifModel::setLinkArray( const QModelIndex & iArray, const QVector & links ) +template +inline NifFieldTemplate NifFieldTemplate::linkBlock( const QLatin1String & testAncestor1, const QLatin1String & testAncestor2, const QLatin1String & testAncestor3, const QLatin1String & testAncestor4 ) const { - return setLinkArray( getItem(iArray), links ); + if ( m_item ) { + auto link = m_item->getLinkValue(); + auto block = _model()->getBlockItem( link ); + if ( block && ( _inherits( block, testAncestor1 ) || _inherits( block, testAncestor2 ) || _inherits( block, testAncestor3 ) || _inherits( block, testAncestor4 ) ) ) + return NifFieldTemplate( block ); + } + return NifFieldTemplate(); } -inline bool NifModel::setLinkArray( const QModelIndex & arrayParent, int arrayIndex, const QVector & links ) +template +inline NifFieldTemplate NifFieldTemplate::linkBlock( const QLatin1String & testAncestor1, const QLatin1String & testAncestor2, const QLatin1String & testAncestor3, const QLatin1String & testAncestor4, const QLatin1String & testAncestor5 ) const { - return setLinkArray( getItem(arrayParent, arrayIndex, true), links ); + if ( m_item ) { + auto link = m_item->getLinkValue(); + auto block = _model()->getBlockItem( link ); + if ( block && ( _inherits( block, testAncestor1 ) || _inherits( block, testAncestor2 ) || _inherits( block, testAncestor3 ) || _inherits( block, testAncestor4 ) || _inherits( block, testAncestor5 ) ) ) + return NifFieldTemplate( block ); + } + return NifFieldTemplate(); } -inline bool NifModel::setLinkArray( const QModelIndex & arrayParent, const QString & arrayName, const QVector & links ) +template +inline NifFieldTemplate NifFieldTemplate::linkBlock( const char * testAncestor ) const { - return setLinkArray( getItem(arrayParent, arrayName, true), links ); + return linkBlock( QLatin1String(testAncestor) ); } -inline bool NifModel::setLinkArray( const QModelIndex & arrayParent, const QLatin1String & arrayName, const QVector & links ) +template +inline NifFieldTemplate NifFieldTemplate::linkBlock( const char * testAncestor1, const char * testAncestor2 ) const { - return setLinkArray( getItem(arrayParent, arrayName, true), links ); + return linkBlock( QLatin1String(testAncestor1), QLatin1String(testAncestor2) ); } -inline bool NifModel::setLinkArray( const QModelIndex & arrayParent, const char * arrayName, const QVector & links ) +template +inline NifFieldTemplate NifFieldTemplate::linkBlock( const char * testAncestor1, const char * testAncestor2, const char * testAncestor3 ) const { - return setLinkArray( getItem(arrayParent, QLatin1String(arrayName), true), links ); + return linkBlock( QLatin1String(testAncestor1), QLatin1String(testAncestor2), QLatin1String(testAncestor3) ); +} +template +inline NifFieldTemplate NifFieldTemplate::linkBlock( const char * testAncestor1, const char * testAncestor2, const char * testAncestor3, const char * testAncestor4 ) const +{ + return linkBlock( QLatin1String(testAncestor1), QLatin1String(testAncestor2), QLatin1String(testAncestor3), QLatin1String(testAncestor4) ); +} +template +inline NifFieldTemplate NifFieldTemplate::linkBlock( const char * testAncestor1, const char * testAncestor2, const char * testAncestor3, const char * testAncestor4, const char * testAncestor5 ) const +{ + return linkBlock( QLatin1String(testAncestor1), QLatin1String(testAncestor2), QLatin1String(testAncestor3), QLatin1String(testAncestor4), QLatin1String(testAncestor5) ); +} +template +inline NifFieldTemplate NifFieldTemplate::linkBlock( const QStringList & testAncestors ) const +{ + if ( m_item ) { + auto link = m_item->getLinkValue(); + auto block = _model()->getBlockItem( link ); + if ( block && _inherits( block, testAncestors ) ) + return NifFieldTemplate( block ); + } + return NifFieldTemplate(); } #endif diff --git a/src/model/nifproxymodel.cpp b/src/model/nifproxymodel.cpp index 1cb736213..d2c607a25 100644 --- a/src/model/nifproxymodel.cpp +++ b/src/model/nifproxymodel.cpp @@ -54,7 +54,7 @@ class NifProxyItem qDeleteAll( childItems ); } - NifProxyItem * getLink( int link ) + NifProxyItem * getLink( int link ) const { for ( NifProxyItem * item : childItems ) { if ( item->block() == link ) @@ -63,39 +63,17 @@ class NifProxyItem return nullptr; } - int rowLink( int link ) - { - int row = 0; - for ( NifProxyItem * item : childItems ) { - if ( item->block() == link ) - return row; - - row++; - } - return -1; - } - NifProxyItem * addLink( int link ) { - NifProxyItem * child = getLink( link ); - - if ( child ) { - return child; - } else { - child = new NifProxyItem( link, this ); - childItems.append( child ); - return child; - } + NifProxyItem * child = new NifProxyItem( link, this ); + childItems.append( child ); + return child; } - void delLink( int link ) + void delAt( int i ) { - NifProxyItem * child = getLink( link ); - - if ( child ) { - childItems.removeAll( child ); - delete child; - } + delete childItems[i]; + childItems.removeAt( i ); } NifProxyItem * parent() const @@ -103,12 +81,24 @@ class NifProxyItem return parentItem; } + bool hasParentLink( int link ) const + { + NifProxyItem * parent = parentItem; + while ( parent && parent->parentItem ) { + if ( parent->block() == link ) + return true; + parent = parent->parentItem; + } + + return false; + } + NifProxyItem * child( int row ) { return childItems.value( row ); } - int childCount() + int childCount() const { return childItems.count(); } @@ -132,28 +122,6 @@ class NifProxyItem return blockNumber; } - QList parentBlocks() const - { - QList parents; - NifProxyItem * parent = parentItem; - - while ( parent && parent->parentItem ) { - parents.append( parent->blockNumber ); - parent = parent->parentItem; - } - - return parents; - } - - QList childBlocks() const - { - QList blocks; - for ( NifProxyItem * item : childItems ) { - blocks.append( item->block() ); - } - return blocks; - } - NifProxyItem * findItem( int b, bool scanParents = true ) { if ( blockNumber == b ) @@ -249,10 +217,12 @@ void NifProxyModel::reset() void NifProxyModel::updateRoot( bool fast ) { - if ( !( nif && nif->getBlockCount() > 0 ) ) { + QModelIndex rootIndex; + + if ( !nif || nif->getBlockCount() <= 0 ) { if ( root->childCount() > 0 ) { if ( !fast ) - beginRemoveRows( QModelIndex(), 0, root->childCount() - 1 ); + beginRemoveRows( rootIndex, 0, root->childCount() - 1 ); root->killChildren(); @@ -265,93 +235,62 @@ void NifProxyModel::updateRoot( bool fast ) //qDebug() << "proxy update top level"; - // Make a copy to iterate over - auto items = root->childItems; - for ( NifProxyItem * item : items ) { - if ( !nif->getRootLinks().contains( item->block() ) ) { - int at = root->rowLink( item->block() ); - - if ( !fast ) - beginRemoveRows( QModelIndex(), at, at ); - - root->delLink( item->block() ); - - if ( !fast ) - endRemoveRows(); - } - } - - for ( const auto l : nif->getRootLinks() ) { - NifProxyItem * item = root->getLink( l ); - - if ( !item ) { - if ( !fast ) - beginInsertRows( QModelIndex(), root->childCount(), root->childCount() ); - - item = root->addLink( l ); - - if ( !fast ) - endInsertRows(); - } - - updateItem( item, fast ); - } + updateItem( root, rootIndex, nif->getRootLinks(), QList(), fast ); } -void NifProxyModel::updateItem( NifProxyItem * item, bool fast ) +void NifProxyModel::updateItem( NifProxyItem * item, const QModelIndex & index, const QList & goodChildLinks, const QList & goodParentLinks, bool fast ) { - QModelIndex index( createIndex( item->row(), 0, item ) ); - - QList parents( item->parentBlocks() ); - - for ( const auto l : item->childBlocks() ) { - if ( !( nif->getChildLinks( item->block() ).contains( l ) || nif->getParentLinks( item->block() ).contains( l ) ) ) { - int at = item->rowLink( l ); - - if ( !fast ) - beginRemoveRows( index, at, at ); - - item->delLink( l ); - - if ( !fast ) - endRemoveRows(); + // Clear bad links + for ( int i = item->childCount() - 1; i >= 0; i-- ) { + auto link = item->child(i)->block(); + if ( ( goodChildLinks.contains( link ) || goodParentLinks.contains( link ) ) && !item->hasParentLink( link ) ) + continue; + + if ( fast ) { + item->delAt( i ); + } else { + beginRemoveRows( index, i, i ); + item->delAt( i ); + endRemoveRows(); } } - for ( const auto l : nif->getChildLinks( item->block() ) ) { - NifProxyItem * child = item->getLink( l ); - if ( !child ) { - int at = item->childCount(); + auto addChildLink = [ this, item, index, fast ]( int link ) -> NifProxyItem * { + NifProxyItem * child; - if ( !fast ) - beginInsertRows( index, at, at ); + if ( fast ) { + child = item->addLink( link ); + } else { + int at = item->childCount(); + beginInsertRows( index, at, at ); + child = item->addLink( link ); + endInsertRows(); + } - child = item->addLink( l ); + return child; + }; - if ( !fast ) - endInsertRows(); + // Add good child links + for ( auto link : goodChildLinks ) { + if ( item->hasParentLink( link ) ) { + auto block1 = nif->block( item->block() ); + auto block2 = nif->block( link ); + nif->reportError( tr("Infinite recursive link construct detected: %1 -> %2.").arg( block1.repr(), block2.repr() ) ); + continue; } - if ( !parents.contains( child->block() ) ) { - updateItem( child, fast ); - } else { - Message::append( tr( "Warnings were generated while reading NIF file." ), - tr( "infinite recursive link construct detected %1 -> %2" ).arg( item->block() ).arg( child->block() ) - ); - } + NifProxyItem * child = item->getLink( link ); + if ( !child ) + child = addChildLink( link ); + updateItem( child, createIndex( child->row(), 0, child ), nif->getChildLinks( link ), nif->getParentLinks( link ), fast ); } - for ( const auto l : nif->getParentLinks( item->block() ) ) { - if ( !item->getLink( l ) ) { - int at = item->childCount(); - if ( !fast ) - beginInsertRows( index, at, at ); - - item->addLink( l ); - - if ( !fast ) - endInsertRows(); - } + // Add good parent links + for ( auto link : goodParentLinks ) { + if ( item->hasParentLink( link ) ) + continue; + if ( !item->getLink( link ) ) + addChildLink( link ); } } diff --git a/src/model/nifproxymodel.h b/src/model/nifproxymodel.h index eda1be448..4bbe6a4dc 100644 --- a/src/model/nifproxymodel.h +++ b/src/model/nifproxymodel.h @@ -88,7 +88,7 @@ protected slots: QList mapFrom( const QModelIndex & index ) const; void updateRoot( bool fast ); - void updateItem( NifProxyItem * item, bool fast ); + void updateItem( NifProxyItem * item, const QModelIndex & index, const QList & goodChildLinks, const QList & goodParentLinks, bool fast ); NifModel * nif; diff --git a/src/nifskope.cpp b/src/nifskope.cpp index c79f629ce..d77510012 100644 --- a/src/nifskope.cpp +++ b/src/nifskope.cpp @@ -36,7 +36,6 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "glview.h" #include "message.h" #include "spellbook.h" -#include "version.h" #include "gl/glscene.h" #include "model/kfmmodel.h" #include "model/nifmodel.h" @@ -47,6 +46,7 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "ui/widgets/inspect.h" #include "ui/about_dialog.h" #include "ui/settingsdialog.h" +#include "ui/UiUtils.h" #include #include @@ -148,24 +148,19 @@ NifSkope::NifSkope() if ( !options ) options = new SettingsDialog; - // Migrate settings from older versions of NifSkope - migrateSettings(); - // Update Settings struct from registry updateSettings(); // Create models /* ********************** */ - nif = new NifModel( this ); + nif = new NifModel( this, BaseModel::MSG_USER ); proxy = new NifProxyModel( this ); proxy->setModel( nif ); nifEmpty = new NifModel( this ); proxyEmpty = new NifProxyModel( this ); - nif->setMessageMode( BaseModel::MSG_USER ); - // Setup QUndoStack nif->undoStack = new QUndoStack( this ); @@ -174,11 +169,11 @@ NifSkope::NifSkope() // Setup Window Modified on data change connect( nif, &NifModel::dataChanged, [this]( const QModelIndex &, const QModelIndex & ) { // Only if UI is enabled (prevents asterisk from flashing during save/load) - if ( !windowTitle().isEmpty() && isEnabled() ) + if ( !currentFile.isEmpty() && isEnabled() ) setWindowModified( true ); } ); - kfm = new KfmModel( this ); + kfm = new KfmModel( this, BaseModel::MSG_USER ); kfmEmpty = new KfmModel( this ); book = SpellBookPtr( new SpellBook( nif, QModelIndex(), this, SLOT( select( const QModelIndex & ) ) ) ); @@ -217,6 +212,7 @@ NifSkope::NifSkope() header->header()->moveSection( 1, 2 ); header->header()->resizeSection( NifModel::NameCol, 135 ); header->header()->resizeSection( NifModel::ValueCol, 250 ); + resetHeaderSelection(); // KFM kfmtree = ui->kfmtree; @@ -256,6 +252,8 @@ NifSkope::NifSkope() ogl->setObjectName( "OGL1" ); ogl->setNif( nif ); ogl->installEventFilter( this ); + ogl->setViewMode( GLView::ViewFront ); + ogl->center(); // Create InspectView /* ********************** */ @@ -326,6 +324,8 @@ NifSkope::NifSkope() connect( options, &SettingsDialog::localeChanged, this, &NifSkope::sltLocaleChanged ); connect( qApp, &QApplication::lastWindowClosed, this, &NifSkope::exitRequested ); + + updateWindowTitle(); } void NifSkope::exitRequested() @@ -345,21 +345,17 @@ NifSkope::~NifSkope() delete ui; } -void NifSkope::swapModels() +void NifSkope::updateWindowTitle() { - // Swap out the models with empty versions while loading the file - // This is so that the views do not update while loading the file - if ( tree->model() == nif ) { - list->setModel( proxyEmpty ); - tree->setModel( nifEmpty ); - header->setModel( nifEmpty ); - kfmtree->setModel( kfmEmpty ); - } else { - list->setModel( proxy ); - tree->setModel( nif ); - header->setModel( nif ); - kfmtree->setModel( kfm ); + if ( nif ) { + QString nifName = nif->getFileInfo().fileName(); + if ( !nifName.isEmpty() ) { + UIUtils::setWindowTitle( this, nifName + QStringLiteral("[*]"), UIUtils::applicationDisplayName ); + return; + } } + + UIUtils::setWindowTitle( this, UIUtils::applicationDisplayName ); } void NifSkope::updateSettings() @@ -380,7 +376,6 @@ SettingsDialog * NifSkope::getOptions() } - void NifSkope::closeEvent( QCloseEvent * e ) { saveUi(); @@ -391,7 +386,6 @@ void NifSkope::closeEvent( QCloseEvent * e ) e->ignore(); } - void NifSkope::select( const QModelIndex & index ) { if ( selecting ) @@ -505,11 +499,12 @@ void NifSkope::select( const QModelIndex & index ) void NifSkope::setListMode() { - QModelIndex idx = list->currentIndex(); - QAction * a = gListMode->checkedAction(); + bool bListMode = isInListMode(); - if ( !a || a == aList ) { + if ( bListMode ) { if ( list->model() != nif ) { + QModelIndex iOldSelection = list->currentIndex(); + // switch to list view QHeaderView * head = list->header(); int s0 = head->sectionSize( head->logicalIndex( 0 ) ); @@ -517,22 +512,16 @@ void NifSkope::setListMode() list->setModel( nif ); list->setItemsExpandable( false ); list->setRootIsDecorated( false ); - list->setCurrentIndex( proxy->mapTo( idx ) ); - list->setColumnHidden( NifModel::NameCol, false ); - list->setColumnHidden( NifModel::TypeCol, true ); - list->setColumnHidden( NifModel::ValueCol, false ); - list->setColumnHidden( NifModel::ArgCol, true ); - list->setColumnHidden( NifModel::Arr1Col, true ); - list->setColumnHidden( NifModel::Arr2Col, true ); - list->setColumnHidden( NifModel::CondCol, true ); - list->setColumnHidden( NifModel::Ver1Col, true ); - list->setColumnHidden( NifModel::Ver2Col, true ); - list->setColumnHidden( NifModel::VerCondCol, true ); + list->setCurrentIndex( proxy->mapTo( iOldSelection ) ); + for ( int c = 0; c < NifModel::NumColumns; c++ ) + list->setColumnHidden( c, c != NifModel::NameCol && c != NifModel::ValueCol ); head->resizeSection( 0, s0 ); head->resizeSection( 1, s1 ); } } else { if ( list->model() != proxy ) { + QModelIndex iOldSelection = list->currentIndex(); + // switch to hierarchy view QHeaderView * head = list->header(); int s0 = head->sectionSize( head->logicalIndex( 0 ) ); @@ -540,7 +529,7 @@ void NifSkope::setListMode() list->setModel( proxy ); list->setItemsExpandable( true ); list->setRootIsDecorated( true ); - QModelIndex pidx = proxy->mapFrom( idx, QModelIndex() ); + QModelIndex pidx = proxy->mapFrom( iOldSelection, QModelIndex() ); list->setCurrentIndex( pidx ); // proxy model has only two columns (see columnCount in nifproxymodel.h) list->setColumnHidden( 0, false ); @@ -549,6 +538,15 @@ void NifSkope::setListMode() head->resizeSection( 1, s1 ); } } + + ui->bExpandAllList->setHidden( bListMode ); + ui->bCollapseAllList->setHidden( bListMode ); + + select( nif->getHeaderIndex() ); + + // Expand the top level of Block List tree + if ( !bListMode ) + list->expandToDepth(0); } // 'Recent Files' Helpers @@ -608,7 +606,7 @@ void NifSkope::updateAllRecentFileActions() } } -QString NifSkope::getCurrentFile() const +const QString & NifSkope::getCurrentFile() const { return currentFile; } @@ -619,7 +617,7 @@ void NifSkope::setCurrentFile( const QString & filename ) nif->refreshFileInfo( currentFile ); - setWindowFilePath( currentFile ); + updateWindowTitle(); // Avoid adding files opened from BSAs to Recent Files QFileInfo file( currentFile ); @@ -1164,132 +1162,14 @@ void NifSkope::sltLocaleChanged() //ui->retranslateUi( this ); } - -void NifSkope::migrateSettings() const +bool NifSkope::isInListMode() const { - // Load current NifSkope settings - QSettings settings; - // Load pre-1.2 NifSkope settings - QSettings cfg1_1( "NifTools", "NifSkope" ); - // Load NifSkope 1.2 settings - QSettings cfg1_2( "NifTools", "NifSkope 1.2" ); - - // Current version strings - QString curVer = NIFSKOPE_VERSION; - QString curQtVer = QT_VERSION_STR; - QString curDisplayVer = NifSkopeVersion::rawToDisplay( NIFSKOPE_VERSION, true ); - - // New Install, no need to migrate anything - if ( !settings.value( "Version" ).isValid() && !cfg1_1.value( "version" ).isValid() ) { - // QSettings constructor creates an empty folder, so clear it. - cfg1_1.clear(); - - // Set version values - settings.setValue( "Version", curVer ); - settings.setValue( "Qt Version", curQtVer ); - settings.setValue( "Display Version", curDisplayVer ); - - return; - } - - QString prevVer = curVer; - QString prevQtVer = settings.value( "Qt Version" ).toString(); - QString prevDisplayVer = settings.value( "Display Version" ).toString(); - - // Set full granularity for version comparisons - NifSkopeVersion::setNumParts( 7 ); - - // Test migration lambda - // Note: Sets value of prevVer - auto testMigration = [&prevVer]( QSettings & migrateFrom, const char * migrateTo ) { - if ( migrateFrom.value( "version" ).isValid() && !migrateFrom.value( "migrated" ).isValid() ) { - prevVer = migrateFrom.value( "version" ).toString(); - - NifSkopeVersion tmp( prevVer ); - if ( tmp < migrateTo ) - return true; - } - return false; - }; - - // Migrate lambda - // Using a QHash of registry keys (stored in version.h), migrates from one version to another. - auto migrate = []( QSettings & migrateFrom, QSettings & migrateTo, const QHash & migration ) { - QHash::const_iterator i; - for ( i = migration.begin(); i != migration.end(); ++i ) { - QVariant val = migrateFrom.value( i.key() ); - - if ( val.isValid() ) { - migrateTo.setValue( i.value(), val ); - } - } - - migrateFrom.setValue( "migrated", true ); - }; - - // NOTE: These set `prevVer` and must come before setting `oldVersion` - bool migrateFrom1_1 = testMigration( cfg1_1, "1.2.0" ); - bool migrateFrom1_2 = testMigration( cfg1_2, "2.0" ); - - if ( !migrateFrom1_1 && !migrateFrom1_2 ) { - prevVer = settings.value( "Version" ).toString(); - } - - NifSkopeVersion oldVersion( prevVer ); - NifSkopeVersion newVersion( curVer ); - - // Check NifSkope Version - // Assure full granularity here - NifSkopeVersion::setNumParts( 7 ); - if ( oldVersion != newVersion ) { - - // Migrate from 1.1.x to 1.2 - if ( migrateFrom1_1 ) { - qDebug() << "Migrating from 1.1 to 1.2"; - migrate( cfg1_1, cfg1_2, migrateTo1_2 ); - } - - // Migrate from 1.2.x to 2.0 - if ( migrateFrom1_2 ) { - qDebug() << "Migrating from 1.2 to 2.0"; - migrate( cfg1_2, settings, migrateTo2_0 ); - } - - // Set new Version - settings.setValue( "Version", curVer ); - - if ( prevDisplayVer != curDisplayVer ) - settings.setValue( "Display Version", curDisplayVer ); - - // Migrate to new Settings - if ( oldVersion <= NifSkopeVersion( "2.0.dev1" ) ) { - qDebug() << "Migrating to new Settings"; - - // Remove old keys - - settings.remove( "FSEngine" ); - settings.remove( "Render Settings" ); - settings.remove( "Settings/Language" ); - settings.remove( "Settings/Startup Version" ); - } - } - -#ifdef QT_NO_DEBUG - // Check Qt Version - if ( curQtVer != prevQtVer ) { - // Check all keys and delete all QByteArrays - // to prevent portability problems between Qt versions - QStringList keys = settings.allKeys(); - - for ( const auto& key : keys ) { - if ( settings.value( key ).type() == QVariant::ByteArray ) { - qDebug() << "Removing Qt version-specific settings" << key - << "while migrating settings from previous version"; - settings.remove( key ); - } - } + return gListMode->checkedAction() == aList; +} - settings.setValue( "Qt Version", curQtVer ); +void NifSkope::forceQuickResize() +{ + if ( isResizing && resizeTimer->isActive() ) { + resizeTimer->start( 10 ); } -#endif } diff --git a/src/nifskope.h b/src/nifskope.h index 0c90e058d..ae0882dce 100644 --- a/src/nifskope.h +++ b/src/nifskope.h @@ -82,6 +82,7 @@ class QStringList; class QTimer; class QTreeView; class QUdpSocket; +class QSlider; namespace nstheme { @@ -117,8 +118,10 @@ class NifSkope final : public QMainWindow //! Restore NifSkope UI settings. void restoreUi(); + void updateWindowTitle(); + //! Returns path of currently open file - QString getCurrentFile() const; + const QString & getCurrentFile() const; /*! Create and initialize a new NifSkope application window. * @@ -171,8 +174,6 @@ public slots: void openArchiveFile( const QModelIndex & ); void openArchiveFileString( BSA *, const QString & ); - void enableUi(); - void updateSettings(); //! Select a NIF index @@ -245,12 +246,6 @@ protected slots: //! Override the view font void overrideViewFont(); - /*! Sets Import/Export menus - * - * @see importex/importex.cpp - */ - void fillImportExportMenus(); - void updateImportExportMenu(const QMenu* menu); //! Perform Import or Export void sltImport( QAction* action ); void sltExport( QAction* action ); @@ -264,6 +259,12 @@ protected slots: //! Called after window resizing has stopped void resizeDone(); + void setLodSliderEnabled( bool enabled ); + void onLodSliderChange( int newLodLevel ); + + void hideAnimToolbar(); + void showAnimToolbar(); + protected: void closeEvent( QCloseEvent * e ) override final; //void resizeEvent( QResizeEvent * event ) override final; @@ -294,11 +295,18 @@ protected slots: void updateRecentArchiveActions(); void updateRecentArchiveFileActions(); + /*! Sets Import/Export menus + * + * @see importex/importex.cpp + */ + void fillImportExportMenus(); + void updateImportExportMenu(const QMenu* menu); //! Currently selected NiBlock index in the list or tree view QModelIndex currentNifIndex() const; - - //! Disconnect and reconnect the models to the views - void swapModels(); + + void updateFileMenus(); + + void resetHeaderSelection(); QMenu * lightingWidget(); QWidget * filePathWidget( QWidget * ); @@ -307,18 +315,15 @@ protected slots: //! Load the theme void loadTheme(); - //! Sync the theme actions in the UI - void setThemeActions(); //! Set the toolbar size void setToolbarSize(); //! Set the theme void setTheme( nstheme::WindowTheme theme ); - //! Migrate settings from older versions of NifSkope. - void migrateSettings() const; + //! Checks if Block List is shown as a list, not tree. + bool isInListMode() const; - //! All QActions in the UI - QSet allActions; + void forceQuickResize(); nstheme::WindowTheme theme = nstheme::ThemeDark; nstheme::ToolbarSize toolbarSize = nstheme::ToolbarLarge; @@ -326,8 +331,6 @@ protected slots: QString currentFile; BSA * currentArchive = nullptr; - QByteArray filehash; - //! Stores the NIF file in memory. NifModel * nif; //! A hierarchical proxy for the NIF file. @@ -374,7 +377,6 @@ protected slots: QAction * animGroupsAction; bool selecting = false; - bool initialShowEvent = true; QProgressBar * progress = nullptr; @@ -386,8 +388,6 @@ protected slots: QDockWidget * dInsp; QDockWidget * dBrowser; - QToolBar * tool; - QAction * aSanitize; QAction * undoAction; @@ -403,17 +403,19 @@ protected slots: QAction * aCondition; QAction * aRCondition; - QAction * aSelectFont; - QMenu * mExport; QMenu * mImport; + QAction * mSpells = nullptr; + QAction * aRecentFilesSeparator; QAction * recentFileActs[NumRecentFiles]; QAction * recentArchiveActs[NumRecentFiles]; QAction * recentArchiveFileActs[NumRecentFiles]; + QSlider * lodSlider; + bool isResizing; QTimer * resizeTimer; QImage viewBuffer; @@ -436,6 +438,10 @@ protected slots: QStandardItemModel * emptyModel; QMenu * mRecentArchiveFiles; + + QActionGroup * viewActions = nullptr; +protected slots: + void updateCurrentViewAction(); }; diff --git a/src/nifskope_ui.cpp b/src/nifskope_ui.cpp index bd6eb5a08..239053432 100644 --- a/src/nifskope_ui.cpp +++ b/src/nifskope_ui.cpp @@ -36,7 +36,6 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "glview.h" #include "message.h" #include "spellbook.h" -#include "version.h" #include "gl/glscene.h" #include "model/kfmmodel.h" #include "model/nifmodel.h" @@ -121,15 +120,25 @@ NifSkope * NifSkope::createWindow( const QString & fname ) { NifSkope * skope = new NifSkope; skope->setAttribute( Qt::WA_DeleteOnClose ); - skope->restoreUi(); + + // Prevent the main window from opening in the minimized state. + // This is a fix for this scenario: + // If you minimize a NifSkope window and then double click on a .nif file in Windows Explorer or any other file manager, + // the file is opened in a new window that is also minimized and requires an additional click to bring it to the front. + skope->setWindowState( skope->windowState() & ~Qt::WindowMinimized ); + skope->loadTheme(); + skope->updateFileMenus(); skope->show(); + skope->restoreUi(); skope->raise(); if ( !fname.isEmpty() ) { skope->loadFile( fname ); } + skope->forceQuickResize(); + return skope; } @@ -140,27 +149,16 @@ void NifSkope::initActions() aHierarchy = ui->aHierarchy; aCondition = ui->aCondition; aRCondition = ui->aRCondition; - aSelectFont = ui->aSelectFont; - - // Build all actions list - allActions = QSet::fromList( - ui->tFile->actions() - << ui->mRender->actions() - << ui->tRender->actions() - << ui->tAnim->actions() - ); // Undo/Redo undoAction = nif->undoStack->createUndoAction( this, tr( "&Undo" ) ); undoAction->setShortcut( QKeySequence::Undo ); undoAction->setObjectName( "aUndo" ); undoAction->setIcon( QIcon( ":btn/undo" ) ); - allActions << undoAction; redoAction = nif->undoStack->createRedoAction( this, tr( "&Redo" ) ); redoAction->setShortcut( QKeySequence::Redo ); redoAction->setObjectName( "aRedo" ); redoAction->setIcon( QIcon( ":btn/redo" ) ); - allActions << redoAction; // TODO: Back/Forward button in Block List //idxForwardAction = indexStack->createRedoAction( this ); @@ -186,8 +184,6 @@ void NifSkope::initActions() connect( ui->aSave, &QAction::triggered, this, &NifSkope::save ); connect( ui->aSaveAs, &QAction::triggered, this, &NifSkope::saveAsDlg ); - ui->aReload->setDisabled(true); - // TODO: Assure Actions and Scene state are synced // Set Data for Actions to pass onto Scene when clicking /* @@ -295,9 +291,9 @@ void NifSkope::initActions() connect( aCondition, &QAction::toggled, tree, &NifTreeView::setRowHiding ); connect( aCondition, &QAction::toggled, kfmtree, &NifTreeView::setRowHiding ); - connect( ui->aAboutNifSkope, &QAction::triggered, []() { - auto aboutDialog = new AboutDialog(); - aboutDialog->show(); + connect( ui->aAboutNifSkope, &QAction::triggered, [this]() { + auto aboutDialog = new AboutDialog( this ); + aboutDialog->open( true ); } ); connect( ui->aAboutQt, &QAction::triggered, qApp, &QApplication::aboutQt ); @@ -352,14 +348,7 @@ void NifSkope::initActions() connect( ogl, &GLView::clicked, this, &NifSkope::select ); connect( ogl, &GLView::sceneTimeChanged, inspect, &InspectView::updateTime ); connect( ogl, &GLView::paintUpdate, inspect, &InspectView::refresh ); - connect( ogl, &GLView::viewpointChanged, [this]() { - ui->aViewTop->setChecked( false ); - ui->aViewFront->setChecked( false ); - ui->aViewLeft->setChecked( false ); - ui->aViewUser->setChecked( false ); - - ogl->setOrientation( GLView::ViewDefault, false ); - } ); + connect( ogl, &GLView::viewModeChanged, this, &NifSkope::updateCurrentViewAction ); connect( graphicsView, &GLGraphicsView::customContextMenuRequested, this, &NifSkope::contextMenu ); @@ -409,6 +398,8 @@ void NifSkope::initDockWidgets() ui->tView->addAction(dKfm->toggleViewAction()); ui->tView->addAction(dRefr->toggleViewAction()); + ui->mHelp->insertAction( ui->mHelp->actions().at(0), dRefr->toggleViewAction() ); + // Set Inspect widget dInsp->setWidget( inspect ); @@ -418,9 +409,6 @@ void NifSkope::initDockWidgets() void NifSkope::initMenu() { - // Disable without NIF loaded - ui->mRender->setEnabled( false ); - // Populate Toolbars menu with all enabled toolbars for ( QObject * o : children() ) { QToolBar * tb = qobject_cast(o); @@ -430,8 +418,8 @@ void NifSkope::initMenu() } } - // Insert SpellBook class before Help - ui->menubar->insertMenu( ui->menubar->actions().at( 3 ), book.get() ); + // Insert SpellBook class before Options + mSpells = ui->menubar->insertMenu( ui->menubar->actions().at( 3 ), book.get() ); // Insert Import/Export menus mExport = ui->menuExport; @@ -532,8 +520,7 @@ void NifSkope::initMenu() void NifSkope::initToolBars() { - // Disable without NIF loaded - ui->tRender->setEnabled( false ); + // Add right click menu to tRender toolbar that duplicates all of the toolbar buttons. ui->tRender->setContextMenuPolicy( Qt::ActionsContextMenu ); // Status Bar @@ -546,13 +533,15 @@ void NifSkope::initToolBars() // Render - QActionGroup * grpView = new QActionGroup( this ); - grpView->addAction( ui->aViewTop ); - grpView->addAction( ui->aViewFront ); - grpView->addAction( ui->aViewLeft ); - grpView->addAction( ui->aViewWalk ); - grpView->setExclusive( true ); + viewActions = new QActionGroup( this ); + viewActions->addAction( ui->aViewTop ); + viewActions->addAction( ui->aViewFront ); + viewActions->addAction( ui->aViewLeft ); + viewActions->addAction( ui->aViewWalk ); + viewActions->addAction( ui->aViewUser ); + viewActions->setExclusive( true ); + updateCurrentViewAction(); // Animate connect( ui->aAnimate, &QAction::toggled, ui->tAnim, &QToolBar::setVisible ); @@ -604,63 +593,40 @@ void NifSkope::initToolBars() ui->tAnim->addWidget( animSlider ); animGroupsAction = ui->tAnim->addWidget( animGroups ); - connect( ogl, &GLView::sequencesDisabled, ui->tAnim, &QToolBar::hide ); + hideAnimToolbar(); + + connect( ogl, &GLView::sequencesDisabled, this, &NifSkope::hideAnimToolbar ); connect( ogl, &GLView::sequenceStopped, ui->aAnimPlay, &QAction::toggle ); connect( ogl, &GLView::sequenceChanged, [this]( const QString & seqname ) { animGroups->setCurrentIndex( ogl->getScene()->animGroups.indexOf( seqname ) ); } ); - connect( ogl, &GLView::sequencesUpdated, [this]() { - ui->tAnim->show(); - - animGroups->clear(); - animGroups->addItems( ogl->getScene()->animGroups ); - animGroups->setCurrentIndex( ogl->getScene()->animGroups.indexOf( ogl->getScene()->animGroup ) ); - - if ( animGroups->count() == 0 ) { - animGroupsAction->setVisible( false ); - ui->aAnimSwitch->setVisible( false ); - } else { - ui->aAnimSwitch->setVisible( animGroups->count() != 1 ); - animGroupsAction->setVisible( true ); - animGroups->adjustSize(); - } - } ); + connect( ogl, &GLView::sequencesUpdated, this, &NifSkope::showAnimToolbar ); connect ( ogl->scene, &Scene::disableSave, [this]() { + ui->aSaveMenu->setDisabled(true); ui->aSave->setDisabled(true); ui->aSaveAs->setDisabled(true); ui->aReload->setDisabled(true); } ); // LOD Toolbar - QToolBar * tLOD = ui->tLOD; - - //QSettings settings; - //int lodLevel = settings.value( "GLView/LOD Level", 0 ).toInt(); - //settings.setValue( "GLView/LOD Level", lodLevel ); - - QSlider * lodSlider = new QSlider( Qt::Horizontal ); + lodSlider = new QSlider( Qt::Horizontal ); lodSlider->setFocusPolicy( Qt::StrongFocus ); lodSlider->setTickPosition( QSlider::TicksBelow ); lodSlider->setTickInterval( 1 ); lodSlider->setSingleStep( 1 ); lodSlider->setMinimum( 0 ); - lodSlider->setMaximum( 3 ); - lodSlider->setValue(0); - tLOD->addWidget( lodSlider ); - tLOD->setEnabled( false ); - tLOD->setVisible( false ); + ui->tLOD->addWidget( lodSlider ); + + setLodSliderEnabled( false ); - connect( lodSlider, &QSlider::valueChanged, ogl->getScene(), &Scene::updateLodLevel ); - connect( lodSlider, &QSlider::valueChanged, ogl, &GLView::updateGL ); - connect( nif, &NifModel::lodSliderChanged, [tLOD]( bool enabled ) { tLOD->setEnabled( enabled ); tLOD->setVisible( enabled ); } ); + connect( nif, &NifModel::lodSliderChanged, this, &NifSkope::setLodSliderEnabled ); + connect( lodSlider, &QSlider::valueChanged, this, &NifSkope::onLodSliderChange ); } void NifSkope::initConnections() { - connect( nif, &NifModel::beginUpdateHeader, this, &NifSkope::enableUi ); - connect( this, &NifSkope::beginLoading, this, &NifSkope::onLoadBegin ); connect( this, &NifSkope::beginSave, this, &NifSkope::onSaveBegin ); @@ -774,16 +740,23 @@ void NifSkope::openDlg() void NifSkope::onLoadBegin() { - // Disconnect the models from the views - swapModels(); - + setEnabled( false ); ogl->setUpdatesEnabled( false ); ogl->setEnabled( false ); - setEnabled( false ); - ui->tAnim->setEnabled( false ); - ui->tLOD->setEnabled( false ); - ui->tLOD->setVisible( false ); + // Swap out the models with empty versions while loading the file. + // This is so that the views do not update. + if ( isInListMode() ) + list->setModel( proxyEmpty ); + else + list->setModel( nifEmpty ); + tree->setModel( nifEmpty ); + header->setModel( nifEmpty ); + kfmtree->setModel( kfmEmpty ); + + animGroups->clear(); + hideAnimToolbar(); + setLodSliderEnabled( false ); progress->setVisible( true ); progress->reset(); @@ -793,39 +766,8 @@ void NifSkope::onLoadComplete( bool success, QString & fname ) { QApplication::restoreOverrideCursor(); - updateImportExportMenu(mExport); - updateImportExportMenu(mImport); - - // Reconnect the models to the views - swapModels(); - // Set List vs Tree - setListMode(); - - // Re-enable window - ogl->setUpdatesEnabled( true ); - ogl->setEnabled( true ); - setEnabled( true ); // IMPORTANT! - - ui->aSave->setDisabled(false); - ui->aSaveAs->setDisabled(false); - ui->aReload->setDisabled(false); - int timeout = 2500; - if ( success ) { - // Scroll panel back to top - tree->scrollTo( nif->index( 0, 0 ) ); - - select( nif->getHeaderIndex() ); - - header->setRootIndex( nif->getHeaderIndex() ); - // Refresh the header rows - header->updateConditions( nif->getHeaderIndex().child( 0, 0 ), nif->getHeaderIndex().child( 20, 0 ) ); - - ogl->setOrientation( GLView::ViewFront ); - - enableUi(); - - } else { + if ( !success ) { // File failed to load Message::append( this, NifModel::tr( readFail ), NifModel::tr( readFailFinal ).arg( fname ), QMessageBox::Critical ); @@ -839,20 +781,28 @@ void NifSkope::onLoadComplete( bool success, QString & fname ) // Reset currentFile.clear(); - setWindowFilePath( "" ); + updateWindowTitle(); progress->reset(); } + updateFileMenus(); + + // Reconnect the models to the views + tree->setModel( nif ); + setListMode(); + header->setModel( nif ); + resetHeaderSelection(); + kfmtree->setModel( kfm ); + // Mark window as unmodified setWindowModified( false ); nif->undoStack->clear(); indexStack->clear(); - // Center the model on load - ogl->center(); - - // Expand the top level of Block List tree - ui->list->expandToDepth(0); + // Re-enable window + ogl->setUpdatesEnabled( true ); + ogl->setEnabled( true ); + setEnabled( true ); // IMPORTANT! // Hide Progress Bar QTimer::singleShot( timeout, progress, SLOT( hide() ) ); @@ -882,12 +832,13 @@ void NifSkope::onSaveComplete( bool success, QString & fname ) setEnabled( true ); if ( success ) { - // Update if Save As results in filename change - setWindowFilePath( fname ); + updateWindowTitle(); // Mark window as unmodified nif->undoStack->setClean(); setWindowModified( false ); } + + updateFileMenus(); } bool NifSkope::saveConfirm() @@ -912,38 +863,20 @@ bool NifSkope::saveConfirm() return true; } -void NifSkope::enableUi() -{ - // Re-enable toolbars, actions, and menus - ui->aSaveMenu->setEnabled( true ); - ui->aSave->setEnabled( true ); - ui->aSaveAs->setEnabled( true ); - ui->aReload->setEnabled( true ); - ui->aHeader->setEnabled( true ); - - ui->mRender->setEnabled( true ); - ui->tAnim->setEnabled( true ); - animGroups->clear(); - - - ui->tRender->setEnabled( true ); - - // We only need to enable the UI once, disconnect - disconnect( nif, &NifModel::beginUpdateHeader, this, &NifSkope::enableUi ); -} +const int WINDOW_STATE_VER = 0x073; void NifSkope::saveUi() const { QSettings settings; // TODO: saveState takes a version number which can be incremented between releases if necessary - settings.setValue( "Window State"_uip, saveState( 0x073 ) ); + settings.setValue( "Window State"_uip, saveState( WINDOW_STATE_VER ) ); settings.setValue( "Window Geometry"_uip, saveGeometry() ); settings.setValue( "Theme", theme ); settings.setValue( "File/Auto Sanitize", aSanitize->isChecked() ); - settings.setValue( "List Mode"_uip, (gListMode->checkedAction() == aList ? "list" : "hierarchy") ); + settings.setValue( "List Mode"_uip, (isInListMode() ? "list" : "hierarchy") ); settings.setValue( "Show Non-applicable Rows"_uip, aCondition->isChecked() ); settings.setValue( "List Header"_uip, list->header()->saveState() ); @@ -952,9 +885,6 @@ void NifSkope::saveUi() const settings.setValue( "Kfmtree Header"_uip, kfmtree->header()->saveState() ); settings.setValue( "GLView/Enable Animations", ui->aAnimate->isChecked() ); - //settings.setValue( "GLView/Play Animation", ui->aAnimPlay->isChecked() ); - //settings.setValue( "GLView/Loop Animation", ui->aAnimLoop->isChecked() ); - //settings.setValue( "GLView/Switch Animation", ui->aAnimSwitch->isChecked() ); settings.setValue( "GLView/Perspective", ui->aViewPerspective->isChecked() ); } @@ -963,9 +893,21 @@ void NifSkope::restoreUi() { QSettings settings; restoreGeometry( settings.value( "Window Geometry"_uip ).toByteArray() ); - restoreState( settings.value( "Window State"_uip ).toByteArray(), 0x073 ); - aSanitize->setChecked( settings.value( "File/Auto Sanitize", true ).toBool() ); + // Here goes a workaround for this Qt 5 bug: + // The positions/sizes of docked widgets of a main window are not restored properly if they were saved while the window was maximized in the previous session. + // Workaround: if the window is supposed to be maximized, let all the events caused by restoreGeometry to be proccessed before calling restoreState. + // References: + // https://bugreports.qt.io/browse/QTBUG-46620 + // https://bugreports.qt.io/browse/QTBUG-16252 + // https://stackoverflow.com/questions/44005852/qdockwidgetrestoregeometry-not-working-correctly-when-qmainwindow-is-maximized + // QTBUG-46620 above labeled with "Fix Version/s: 6.3.0 Alpha", so this hack could be not needed after migrating to Qt 6. + if ( isMaximized() ) + QApplication::processEvents(); + + restoreState( settings.value( "Window State"_uip ).toByteArray(), WINDOW_STATE_VER ); + + aSanitize->setChecked( settings.value( "File/Auto Sanitize", false ).toBool() ); if ( settings.value( "List Mode"_uip, "hierarchy" ).toString() == "list" ) aList->setChecked( true ); @@ -981,6 +923,7 @@ void NifSkope::restoreUi() header->header()->restoreState( settings.value( "Header Header"_uip ).toByteArray() ); kfmtree->header()->restoreState( settings.value( "Kfmtree Header"_uip ).toByteArray() ); + // Hide advanced metadata loaded from nif.xml as it's not useful or necessary for editing auto hideSections = []( NifTreeView * tree, bool hidden ) { tree->header()->setSectionHidden( NifModel::ArgCol, hidden ); tree->header()->setSectionHidden( NifModel::Arr1Col, hidden ); @@ -991,20 +934,11 @@ void NifSkope::restoreUi() tree->header()->setSectionHidden( NifModel::VerCondCol, hidden ); }; - // Hide advanced metadata loaded from nif.xml as it's not useful or necessary for editing - if ( settings.value( "Settings/Nif/Hide metadata columns", true ).toBool() ) { - hideSections( tree, true ); - hideSections( header, true ); - } else { - // Unhide here, or header()->restoreState() will keep them perpetually hidden - hideSections( tree, false ); - hideSections( header, false ); - } + bool bHideMetadatColumns = settings.value( "Settings/Nif/Hide metadata columns", true ).toBool(); + hideSections( tree, bHideMetadatColumns ); + hideSections( header, bHideMetadatColumns ); ui->aAnimate->setChecked( settings.value( "GLView/Enable Animations", true ).toBool() ); - //ui->aAnimPlay->setChecked( settings.value( "GLView/Play Animation", true ).toBool() ); - //ui->aAnimLoop->setChecked( settings.value( "GLView/Loop Animation", true ).toBool() ); - //ui->aAnimSwitch->setChecked( settings.value( "GLView/Switch Animation", true ).toBool() ); auto isPersp = settings.value( "GLView/Perspective", true ).toBool(); ui->aViewPerspective->setChecked( isPersp ); @@ -1020,6 +954,26 @@ void NifSkope::restoreUi() tabifyDockWidget( ui->InspectDock, ui->KfmDock ); } +void NifSkope::updateFileMenus() +{ + ui->aSaveMenu->setEnabled( true ); + ui->aSave->setEnabled( true ); + ui->aSaveAs->setEnabled( true ); + ui->aReload->setEnabled( !getCurrentFile().isEmpty() ); + + updateImportExportMenu(mExport); + updateImportExportMenu(mImport); +} + +void NifSkope::resetHeaderSelection() +{ + auto headerIndex = nif->getHeaderIndex(); + header->setRootIndex( headerIndex ); + int iLastChild = std::max( nif->rowCount( headerIndex ) - 1, 0 ); + header->updateConditions( headerIndex.child( 0, 0 ), headerIndex.child( iLastChild, 0 ) ); + header->autoExpandBlock( headerIndex ); +} + void NifSkope::setViewFont( const QFont & font ) { list->setFont( font ); @@ -1052,7 +1006,6 @@ void NifSkope::loadTheme() toolbarSize = ToolbarSize( settings.value( "Settings/Theme/Large Icons", ToolbarLarge ).toBool() ); - //setThemeActions(); setToolbarSize(); switch ( theme ) @@ -1162,22 +1115,6 @@ void NifSkope::loadTheme() qApp->setStyleSheet( styleData ); } -void NifSkope::setThemeActions() -{ - // Map of QAction object names to QRC alias - QMap names = { - //{"aTextures", "textures"} - }; - - QString themeString = (theme == ThemeDark) ? "dark" : "light"; - for ( auto a : allActions ) { - auto obj = a->objectName(); - if ( names.contains( obj ) ) { - a->setIcon( QIcon( QString(":btn/%1/%2").arg(themeString).arg(names[obj]) ) ); - } - } -} - void NifSkope::setToolbarSize() { QSize size = {18, 18}; @@ -1238,10 +1175,65 @@ void NifSkope::resizeDone() ogl->setUpdatesEnabled( true ); ogl->setDisabled( false ); ogl->getScene()->animate = true; - ogl->update(); - ogl->resizeGL( centralWidget()->width(), centralWidget()->height() ); + auto viewSize = graphicsView->size(); + ogl->resize( viewSize.width(), viewSize.height() ); + ogl->resizeGL( viewSize.width(), viewSize.height() ); } +void NifSkope::setLodSliderEnabled( bool enabled ) +{ + auto tLOD = ui->tLOD; + + if ( enabled ) { + Scene * scene = ogl->getScene(); + if ( !scene ) { // Just in case + enabled = false; + } else if ( !tLOD->isEnabled() ) { + lodSlider->setMaximum( scene->maxLodLevel() ); + lodSlider->setValue( scene->lodLevel ); + } + } + + tLOD->setEnabled( enabled ); + tLOD->setVisible( enabled ); +} + +void NifSkope::onLodSliderChange( int newLodLevel ) +{ + if ( ui->tLOD->isEnabled() ) { + Scene * scene = ogl->getScene(); + if ( scene ) { + scene->updateLodLevel( newLodLevel ); + ogl->updateGL(); + } + } +} + +void NifSkope::hideAnimToolbar() +{ + ogl->resetAnimation(); + ui->tAnim->setEnabled( false ); + ui->tAnim->hide(); +} + +void NifSkope::showAnimToolbar() +{ + ui->tAnim->setEnabled( true ); + ui->tAnim->show(); + + animGroups->clear(); + animGroups->addItems( ogl->getScene()->animGroups ); + animGroups->setCurrentIndex( ogl->getScene()->animGroups.indexOf( ogl->getScene()->animGroup ) ); + + if ( animGroups->count() == 0 ) { + animGroupsAction->setVisible( false ); + ui->aAnimSwitch->setVisible( false ); + } else { + ui->aAnimSwitch->setVisible( animGroups->count() != 1 ); + animGroupsAction->setVisible( true ); + animGroups->adjustSize(); + } +} bool NifSkope::eventFilter( QObject * o, QEvent * e ) { @@ -1422,31 +1414,26 @@ void NifSkope::on_aHeader_triggered() select( nif->getHeaderIndex() ); } - -void NifSkope::on_tRender_actionTriggered( QAction * action ) +void NifSkope::on_tRender_actionTriggered( [[maybe_unused]] QAction * action ) { - Q_UNUSED( action ); } void NifSkope::on_aViewTop_triggered( bool checked ) { - if ( checked ) { - ogl->setOrientation( GLView::ViewTop ); - } + if ( checked ) + ogl->setViewMode( GLView::ViewTop ); } void NifSkope::on_aViewFront_triggered( bool checked ) { - if ( checked ) { - ogl->setOrientation( GLView::ViewFront ); - } + if ( checked ) + ogl->setViewMode( GLView::ViewFront ); } void NifSkope::on_aViewLeft_triggered( bool checked ) { - if ( checked ) { - ogl->setOrientation( GLView::ViewLeft ); - } + if ( checked ) + ogl->setViewMode( GLView::ViewLeft ); } void NifSkope::on_aViewCenter_triggered() @@ -1454,10 +1441,9 @@ void NifSkope::on_aViewCenter_triggered() ogl->center(); } -void NifSkope::on_aViewFlip_triggered( bool checked ) +void NifSkope::on_aViewFlip_triggered( [[maybe_unused]] bool checked ) { - Q_UNUSED( checked ); - ogl->flipOrientation(); + ogl->flipView(); } void NifSkope::on_aViewPerspective_toggled( bool checked ) @@ -1467,26 +1453,19 @@ void NifSkope::on_aViewPerspective_toggled( bool checked ) void NifSkope::on_aViewWalk_triggered( bool checked ) { - if ( checked ) { - ogl->setOrientation( GLView::ViewWalk ); - } + if ( checked ) + ogl->setViewMode( GLView::ViewWalk ); } - -void NifSkope::on_aViewUserSave_triggered( bool checked ) +void NifSkope::on_aViewUserSave_triggered( [[maybe_unused]] bool checked ) { - Q_UNUSED( checked ); ogl->saveUserView(); - ui->aViewUser->setChecked( true ); } - void NifSkope::on_aViewUser_toggled( bool checked ) { - if ( checked ) { - ogl->setOrientation( GLView::ViewUser, false ); + if ( checked ) ogl->loadUserView(); - } } void NifSkope::on_aSettings_triggered() @@ -1502,3 +1481,34 @@ void NifSkope::on_mTheme_triggered( QAction * action ) setTheme( newTheme ); } + +void NifSkope::updateCurrentViewAction() +{ + QAction * pSelectedView = nullptr; + + switch( ogl->view ) { + case GLView::ViewFront: + pSelectedView = ui->aViewFront; + break; + case GLView::ViewLeft: + pSelectedView = ui->aViewLeft; + break; + case GLView::ViewTop: + pSelectedView = ui->aViewTop; + break; + case GLView::ViewWalk: + pSelectedView = ui->aViewWalk; + break; + case GLView::ViewUser: + pSelectedView = ui->aViewUser; + break; + } + + if ( pSelectedView ) { + pSelectedView->setChecked( true ); + } else if ( viewActions ) { + pSelectedView = viewActions->checkedAction(); + if ( pSelectedView ) + pSelectedView->setChecked( false ); + } +} diff --git a/src/spells/blocks.cpp b/src/spells/blocks.cpp index 9163041a9..0252da93b 100644 --- a/src/spells/blocks.cpp +++ b/src/spells/blocks.cpp @@ -346,6 +346,63 @@ void deserializeStrings( NifModel * nif, const QModelIndex & iBlock, const QStri setStringsArray( nif, nif->getIndex( iBlock, "Material Data" ), strings, "Material Name" ); } +static void trySetLink( NifField linkField, NifFieldConst newLinkBlock ) +{ + if ( linkField && newLinkBlock.inherits( linkField.templ() ) ) { + linkField.setLink( newLinkBlock ); + } +} + +static void tryAddLinkToArray( NifField arrayRoot, NifFieldConst blockToAdd, const QString & purgeOtherLinksOfType = QString() ) +{ + if ( !arrayRoot ) + return; + + // TODO: verify that blockToAdd interits arrayRoot.templ() + + auto linkToAdd = blockToAdd.toLink(); + Q_ASSERT( linkToAdd >= 0 ); + if ( linkToAdd < 0 ) + return; + + auto sizeField = arrayRoot.parent()[QString("Num ") + arrayRoot.name()]; + if ( !sizeField ) + return; + + int oldSize = sizeField.value(); + + bool doArrayUpdate = false; + bool doAddLink = true; + QVector newLinks; + newLinks.reserve( std::max( arrayRoot.childCount(), oldSize ) + 1 ); + for ( auto arrayEntry : arrayRoot.iter() ) { + auto link = arrayEntry.link(); + if ( link == linkToAdd ) { + doAddLink = false; + } else if ( !purgeOtherLinksOfType.isEmpty() && arrayEntry.linkBlock( purgeOtherLinksOfType ).isValid() ) { + doArrayUpdate = true; + continue; + } + newLinks.append( link ); + } + if ( doAddLink ) { + newLinks.append( linkToAdd ); + doArrayUpdate = true; + } + int newSize = newLinks.count(); + if ( newSize != oldSize ) { + // Pad newLinks with -1 up to oldSize if necessary + for ( ; newSize < oldSize; newSize++ ) newLinks.append( -1 ); + doArrayUpdate = true; + } + + if ( doArrayUpdate ) { + sizeField.setValue( newSize ); + arrayRoot.updateArraySize(); + arrayRoot.setLinkArray( newLinks ); + } +} + //! Add a link to the specified block to a link array /*! * @param nif The model @@ -407,52 +464,108 @@ void delLink( NifModel * nif, const QModelIndex & iParent, QString array, int li } -//! Link one block to another -/*! -* @param nif The model -* @param index The block to link to (becomes parent) -* @param iBlock The block to link (becomes child) -*/ -void blockLink( NifModel * nif, const QModelIndex & index, const QModelIndex & iBlock ) +static void linkBlocks( NifField parentField, NifField childBlock ) { - if ( nif->isLink( index ) && nif->blockInherits( iBlock, nif->itemTempl( index ) ) ) { - nif->setLink( index, nif->getBlockNumber( iBlock ) ); + if ( parentField.isLink() ) { + trySetLink( parentField, childBlock ); } - if ( nif->blockInherits( index, "NiNode" ) && nif->blockInherits( iBlock, "NiAVObject" ) ) { - addLink( nif, index, "Children", nif->getBlockNumber( iBlock ) ); + auto parentBlock = parentField.block(); - if ( nif->blockInherits( iBlock, "NiDynamicEffect" ) ) { - addLink( nif, index, "Effects", nif->getBlockNumber( iBlock ) ); + if ( parentBlock.inherits("NiNode") && childBlock.inherits("NiAVObject") ) { + tryAddLinkToArray( parentBlock["Children"], childBlock ); + + auto effectsRoot = parentBlock.child("Effects"); + if ( effectsRoot && childBlock.inherits( effectsRoot.templ() ) ) { + tryAddLinkToArray( effectsRoot, childBlock ); } - } else if ( nif->blockInherits( index, "NiAVObject" ) && nif->blockInherits( iBlock, "NiProperty" ) ) { - if ( !addLink( nif, index, "Properties", nif->getBlockNumber( iBlock ) ) ) { - // Absent in Bethesda 20.2.0.7 stream version > 34 - if ( nif->inherits( nif->itemName( iBlock ), "BSShaderProperty" ) ) { - nif->setLink( index, "Shader Property", nif->getBlockNumber( iBlock ) ); - } else if ( nif->itemName( iBlock ) == "NiAlphaProperty" ) { - nif->setLink( index, "Alpha Property", nif->getBlockNumber( iBlock ) ); + } + + if ( parentBlock.inherits("NiAVObject") ) { + if ( childBlock.inherits("NiProperty") ) { + auto propertiesRoot = parentBlock.child("Properties"); + if ( propertiesRoot ) { + QString oldShaderType = "BSShaderLightingProperty"; + QString replacePropType = childBlock.inherits(oldShaderType) ? oldShaderType : childBlock.name(); + tryAddLinkToArray( propertiesRoot, childBlock, replacePropType ); + } else { + trySetLink( parentBlock.child("Shader Property"), childBlock ); + trySetLink( parentBlock.child("Alpha Property"), childBlock ); } + + } else if ( childBlock.inherits("NiExtraData") ) { + tryAddLinkToArray( parentBlock.child("Extra Data List"), childBlock ); + + } else if ( childBlock.inherits("NiCollisionObject") ) { + parentBlock.child("Collision Object").setLink( childBlock ); + + } else if ( childBlock.inherits("NiSkinInstance") ) { + parentBlock.child("Skin").setLink( childBlock ); } - } else if ( nif->blockInherits( index, "NiAVObject" ) && nif->blockInherits( iBlock, "NiExtraData" ) ) { - addLink( nif, index, "Extra Data List", nif->getBlockNumber( iBlock ) ); - } else if ( nif->blockInherits( index, "NiObjectNET" ) && nif->blockInherits( iBlock, "NiTimeController" ) ) { - if ( nif->getLink( index, "Controller" ) > 0 ) { - blockLink( nif, nif->getBlockIndex( nif->getLink( index, "Controller" ) ), iBlock ); - } else { - nif->setLink( index, "Controller", nif->getBlockNumber( iBlock ) ); - nif->setLink( iBlock, "Target", nif->getBlockNumber( index ) ); + } + + if ( parentBlock.inherits("NiObjectNET") && childBlock.inherits("NiTimeController") ) { + auto ctrlField = parentBlock.child("Controller"); + if ( ctrlField ) { + auto ctrlBlock = ctrlField.linkBlock(); + if ( ctrlBlock && ctrlBlock != childBlock ) { + linkBlocks( ctrlBlock, childBlock ); + } else { + ctrlField.setLink( childBlock ); + childBlock["Target"].setLink( parentBlock ); + } } - } else if ( nif->blockInherits( index, "NiTimeController" ) && nif->blockInherits( iBlock, "NiTimeController" ) ) { - if ( nif->getLink( index, "Next Controller" ) > 0 ) { - blockLink( nif, nif->getBlockIndex( nif->getLink( index, "Next Controller" ) ), iBlock ); - } else { - nif->setLink( index, "Next Controller", nif->getBlockNumber( iBlock ) ); - nif->setLink( iBlock, "Target", nif->getLink( index, "Target" ) ); + } + + if ( parentBlock.inherits("NiTimeController") && childBlock.inherits("NiTimeController") ) { + auto nextCtrlField = parentBlock["Next Controller"]; + if ( nextCtrlField ) { + auto ctrlBlock = nextCtrlField.linkBlock(); + if ( ctrlBlock && ctrlBlock != childBlock ) { + linkBlocks( ctrlBlock, childBlock ); + } else { + nextCtrlField.setLink( childBlock ); + childBlock["Target"].setLink( parentBlock["Target"].link() ); + } } - } else if ( nif->blockInherits( index, "NiAVObject" ) && nif->blockInherits( iBlock, "NiCollisionObject" ) ) { - nif->setLink( index, "Collision Object", nif->getBlockNumber( iBlock ) ); } + + if ( parentBlock.inherits("NiGeometry") ) { + trySetLink( parentBlock.child("Data"), childBlock ); + trySetLink( parentBlock.child("Skin Instance"), childBlock ); + } + + if ( parentBlock.inherits("NiSkinInstance") ) { + trySetLink( parentBlock.child("Data"), childBlock ); + trySetLink( parentBlock.child("Skin Partition"), childBlock ); + } + + if ( parentBlock.inherits("BSShaderProperty") ) { + trySetLink( parentBlock.child("Texture Set"), childBlock ); + } + + if ( parentBlock.inherits("bhkNiCollisionObject") ) { + trySetLink( parentBlock["Body"], childBlock ); + } + + if ( parentBlock.inherits("bhkWorldObject", "bhkShape") ) { + trySetLink( parentBlock.child("Shape"), childBlock ); + } + + if ( parentBlock.inherits("bhkCompressedMeshShape") ) { + trySetLink( parentBlock["Data"], childBlock ); + } +} + +//! Link one block to another +/*! +* @param nif The model +* @param index The block to link to (becomes parent) +* @param iBlock The block to link (becomes child) +*/ +void blockLink( NifModel * nif, const QModelIndex & index, const QModelIndex & iBlock ) +{ + linkBlocks( nif->field( index ), nif->field( iBlock) ); } //! Helper function for branch paste diff --git a/src/spells/bounds.cpp b/src/spells/bounds.cpp index bb85ca22e..636f774ec 100644 --- a/src/spells/bounds.cpp +++ b/src/spells/bounds.cpp @@ -58,6 +58,7 @@ class spEditBounds final : public Spell } edit->show(); + edit->activateWindow(); return index; } }; diff --git a/src/spells/flags.cpp b/src/spells/flags.cpp index 73c302c87..a31f66ffb 100644 --- a/src/spells/flags.cpp +++ b/src/spells/flags.cpp @@ -1,4 +1,6 @@ #include "spellbook.h" +#include "ui/UiUtils.h" +#include "ui/ToolDialog.h" #include #include @@ -971,14 +973,20 @@ class spEditVertexDesc final : public spEditFlags bool isApplicable( const NifModel * nif, const QModelIndex & index ) override final { - return nif->blockInherits( index.parent(), "BSTriShape" ) && nif->itemName( index ) == "Vertex Desc"; + auto field = nif->field( index ); + if ( field.hasStrType("BSVertexDesc") ) { + auto block = field.block(); + if ( block.inherits("NiSkinPartition") ) { + return ( field.parent() == block ); // Ignore Vert Desc fields in Partitions + } else { + return true; + } + } + return false; } QModelIndex cast( NifModel * nif, const QModelIndex & index ) override final { - auto desc = nif->get( index ); - bool dynamic = nif->blockInherits( index.parent(), "BSDynamicTriShape" ); - QStringList flagNames { Spell::tr( "Vertex" ), // VA_POSITION = 0x0, Spell::tr( "UVs" ), // VA_TEXCOORD0 = 0x1, @@ -993,18 +1001,67 @@ class spEditVertexDesc final : public spEditFlags Spell::tr( "Full Precision" ) // 1 << 10 }; - QDialog dlg; + auto editedField = nif->field( index ); + auto flagVals = editedField.value(); + auto editedBlock = editedField.block(); + + bool isDynamic = false; + bool lockSkinning = false; + NifField skinPartBlock; + QVector shapeBlocks; + if ( editedField.hasName("Vertex Desc") && editedField.parent() == editedBlock ) { + auto getShapeSkinPartition = [nif]( NifField shape, bool shapeIsDynamic ) -> NifField { + if ( nif->getBSVersion() == 100 ) { + if ( shapeIsDynamic || shape.child("Vertex Desc").value().HasFlag(VertexAttribute::VA_SKINNING) ) { + return shape.child("Skin").linkBlock().child("Skin Partition").linkBlock("NiSkinPartition"); + } + } + + return NifField(); + }; + + if ( editedBlock.inherits("BSTriShape") ) { + shapeBlocks << editedBlock; + isDynamic = editedBlock.inherits("BSDynamicTriShape"); + skinPartBlock = getShapeSkinPartition( editedBlock, isDynamic ); + if ( skinPartBlock || isDynamic ) + lockSkinning = true; + } else if ( editedBlock.inherits("NiSkinPartition") ) { + skinPartBlock = editedBlock; + lockSkinning = true; + for ( auto b : nif->blockIter() ) { + if ( b.inherits("BSTriShape") ) { + bool blockIsDynamic = b.inherits("BSDynamicTriShape"); + if ( getShapeSkinPartition( b, blockIsDynamic ) == editedBlock ) { + shapeBlocks << b; + if ( blockIsDynamic ) + isDynamic = true; + } + } + } + } + } + + QDialog dlg( qApp->activeWindow() ); + ToolDialog::setDialogFlagsAndModality( &dlg, 0 ); + UIUtils::setWindowTitle( &dlg, Spell::tr("Vertex Flags") ); QVBoxLayout * vbox = new QVBoxLayout; dlg.setLayout( vbox ); QList chkBoxes; + chkBoxes.reserve( flagNames.count() ); int x = 0; - for ( const QString& flagName : flagNames ) { + for ( const QString & flagName : flagNames ) { chkBoxes << dlgCheck( vbox, flagName ); - chkBoxes.last()->setChecked( desc.HasFlag( VertexAttribute(x) ) ); - // Hide unused attributes - if ( x == 2 || x == 7 || x == 9 ) - chkBoxes.last()->setHidden( true ); + if ( x == 6 && lockSkinning ) { + chkBoxes.last()->setChecked( true ); + chkBoxes.last()->setEnabled( false ); + } else { + chkBoxes.last()->setChecked( flagVals.HasFlag( VertexAttribute(x) ) ); + // Hide unused attributes + if ( x == 2 || x == 7 || x == 9 ) + chkBoxes.last()->setHidden( true ); + } x++; } @@ -1014,30 +1071,49 @@ class spEditVertexDesc final : public spEditFlags x = 0; for ( QCheckBox * chk : chkBoxes ) { if ( chk->isChecked() ) - desc.SetFlag( VertexAttribute(x) ); + flagVals.SetFlag( VertexAttribute(x) ); else - desc.RemoveFlag( VertexAttribute(x) ); + flagVals.RemoveFlag( VertexAttribute(x) ); x++; } // Make sure sizes and offsets in rest of vertexDesc are updated from flags - desc.ResetAttributeOffsets( nif->getBSVersion() ); - if ( dynamic ) - desc.MakeDynamic(); - - if ( nif->set( index, desc ) ) { - auto iDataSize = nif->getIndex( index.parent(), "Data Size" ); - auto numVerts = nif->get( index.parent(), "Num Vertices" ); - auto numTris = nif->get( index.parent(), "Num Triangles" ); - - if ( iDataSize.isValid() ) - nif->set( iDataSize, desc.GetVertexSize() * numVerts + 6 * numTris ); + flagVals.ResetAttributeOffsets( nif->getBSVersion() ); + if ( isDynamic ) + flagVals.MakeDynamic(); + + if ( editedField.setValue( flagVals ) ) { + uint vertexDataSize = flagVals.GetVertexSize(); + + for ( auto shape : shapeBlocks ) { + auto shapeFlagField = shape.child("Vertex Desc"); + if ( shapeFlagField != editedField ) + shapeFlagField.setValue( flagVals ); + uint dataSize; + if ( !skinPartBlock ) { + dataSize = vertexDataSize * shape["Num Vertices"].value() + sizeof(Triangle) * shape["Num Triangles"].value(); + } else { + dataSize = 0; + } + shape["Data Size"].setValue( dataSize ); + shape.child("Vertex Data").updateArraySize(); + } - nif->updateArraySize( index.parent(), "Vertex Data" ); + if ( skinPartBlock ) { + auto partFlagField = skinPartBlock["Vertex Desc"]; + if ( partFlagField != editedField ) + partFlagField.setValue( flagVals ); + auto vertexDataField = skinPartBlock.child("Vertex Data"); + skinPartBlock["Vertex Size"].setValue( vertexDataSize ); + skinPartBlock["Data Size"].setValue( vertexDataSize * vertexDataField.childCount() ); + vertexDataField.updateArraySize(); + + for ( auto partEntry : skinPartBlock.child("Partitions").iter() ) + partEntry["Vertex Desc"].setValue( flagVals ); + } } - } - + return index; } diff --git a/src/spells/havok.cpp b/src/spells/havok.cpp index d23ff8a42..abb8203be 100644 --- a/src/spells/havok.cpp +++ b/src/spells/havok.cpp @@ -445,15 +445,9 @@ class spPackHavokStrips final : public Spell if ( iData.isValid() ) { QVector vrts = nif->getArray( iData, "Vertices" ); - QVector tris; + QVector tris = triangulateStrips( nif, nif->getIndex( iData, "Points" ) ); QVector nrms; - QModelIndex iPoints = nif->getIndex( iData, "Points" ); - - for ( int x = 0; x < nif->rowCount( iPoints ); x++ ) { - tris += triangulate( nif->getArray( iPoints.child( x, 0 ) ) ); - } - QMutableVectorIterator it( tris ); while ( it.hasNext() ) { diff --git a/src/spells/light.cpp b/src/spells/light.cpp index 2918a5646..846923b5e 100644 --- a/src/spells/light.cpp +++ b/src/spells/light.cpp @@ -99,6 +99,7 @@ class spLightEdit final : public Spell le->add( new NifFloatEdit( nif, nif->getIndex( iLight, "Exponent" ), 0, 128 ) ); le->popLayout(); le->show(); + le->activateWindow(); return index; } diff --git a/src/spells/materialedit.cpp b/src/spells/materialedit.cpp index ae92bb81f..936c03ab8 100644 --- a/src/spells/materialedit.cpp +++ b/src/spells/materialedit.cpp @@ -130,6 +130,7 @@ class spMaterialEdit final : public Spell me->add( new NifFloatSlider( nif, nif->getIndex( iMaterial, "Glossiness" ), 0.0, 100.0 ) ); me->setWindowModality( Qt::ApplicationModal ); me->show(); + me->activateWindow(); return index; } diff --git a/src/spells/mesh.cpp b/src/spells/mesh.cpp index 5d9cda56b..173318843 100644 --- a/src/spells/mesh.cpp +++ b/src/spells/mesh.cpp @@ -759,22 +759,52 @@ bool spUpdateTrianglesFromSkin::isApplicable( const NifModel * nif, const QModel QModelIndex spUpdateTrianglesFromSkin::cast( NifModel * nif, const QModelIndex & index ) { - auto iData = nif->getBlockIndex( nif->getLink( index, "Data" ) ); - auto iSkin = nif->getBlockIndex( nif->getLink( index, "Skin Instance" ) ); - auto iSkinPart = nif->getBlockIndex( nif->getLink( iSkin, "Skin Partition" ) ); - if ( !iSkinPart.isValid() || !iData.isValid() ) + auto block = nif->block(index); + auto dataBlock = block.child("Data").linkBlock(); + auto skinBlock = block.child("Skin Instance").linkBlock(); + auto skinPartBlock = skinBlock.child("Skin Partition").linkBlock(); + if ( !skinPartBlock || !dataBlock ) return QModelIndex(); - QVector tris; - auto iParts = nif->getIndex( iSkinPart, "Partitions" ); - for ( int i = 0; i < nif->rowCount( iParts ) && iParts.isValid(); i++ ) - tris << SkinPartition( nif, iParts.child( i, 0 ) ).getRemappedTriangles(); + QVector combinedTris; + bool success = true; + for ( auto partEntry : skinPartBlock["Partitions"].iter() ) { + auto partTrisRoot = partEntry.child("Triangles"); + int nPartTris = partTrisRoot.childCount(); + if ( nPartTris <= 0 ) + continue; + combinedTris.reserve( combinedTris.count() + nPartTris ); + + auto vertexMap = partEntry.child("Vertex Map").array(); + int nMappedVertices = vertexMap.count(); + + for ( auto triEntry : partTrisRoot.iter() ) { + Triangle t = triEntry.value(); + + if ( nMappedVertices > 0 ) { + for ( TriVertexIndex & tv : t.v ) { + if ( tv < nMappedVertices ) { + tv = vertexMap[tv]; + } else { + triEntry.reportError( tr("Invalid vertex index %1.").arg(tv) ); + success = false; + } + } + } + + combinedTris << t; + } + } + if ( !success ) + return QModelIndex(); - nif->set( iData, "Has Triangles", true ); - nif->set( iData, "Num Triangles", tris.size() ); - nif->set( iData, "Num Triangle Points", tris.size() * 3 ); - nif->updateArraySize( iData, "Triangles" ); - nif->setArray( iData, "Triangles", tris ); + int nTris = combinedTris.count(); + dataBlock["Has Triangles"].setValue( true ); + dataBlock["Num Triangles"].setValue( nTris ); + dataBlock["Num Triangle Points"].setValue( nTris * 3 ); + auto triField = dataBlock["Triangles"]; + triField.updateArraySize(); + triField.setArray( combinedTris ); return index; } diff --git a/src/spells/normals.cpp b/src/spells/normals.cpp index b7d0aac59..5bf17217a 100644 --- a/src/spells/normals.cpp +++ b/src/spells/normals.cpp @@ -82,12 +82,7 @@ class spFaceNormals final : public Spell QModelIndex iPoints = nif->getIndex( iData, "Points" ); if ( iPoints.isValid() ) { - QVector > strips; - - for ( int r = 0; r < nif->rowCount( iPoints ); r++ ) - strips.append( nif->getArray( iPoints.child( r, 0 ) ) ); - - triangles = triangulate( strips ); + triangles = triangulateStrips( nif, iPoints ); } else { triangles = nif->getArray( iData, "Triangles" ); } diff --git a/src/spells/sanitize.cpp b/src/spells/sanitize.cpp index 8ef491136..0b9081482 100644 --- a/src/spells/sanitize.cpp +++ b/src/spells/sanitize.cpp @@ -156,24 +156,29 @@ class spAdjustTextureSources final : public Spell QModelIndex cast( NifModel * nif, const QModelIndex & ) override final { - for ( int i = 0; i < nif->getBlockCount(); i++ ) { - QModelIndex iTexSrc = nif->getBlockIndex( i, "NiSourceTexture" ); - - if ( iTexSrc.isValid() ) { - QModelIndex iFileName = nif->getIndex( iTexSrc, "File Name" ); - - if ( iFileName.isValid() ) // adjust file path - nif->set( iFileName, nif->get( iFileName ).replace( "/", "\\" ) ); + for ( auto block : nif->blockIter() ) { + if ( !block.inherits("NiSourceTexture") ) + continue; - if ( nif->checkVersion( 0x14000005, 0x14000005 ) ) { - // adjust format options (oblivion only) - nif->set( iTexSrc, "Pixel Layout", 6 ); - nif->set( iTexSrc, "Use Mipmaps", 1 ); - nif->set( iTexSrc, "Alpha Format", 3 ); - nif->set( iTexSrc, "Unknown Byte", 1 ); - nif->set( iTexSrc, "Unknown Byte 2", 1 ); - } + // adjust file path + auto pathField = block.child("File Name"); + if ( pathField ) + pathField.setValue( pathField.value().replace( "/", "\\" ) ); + + // Some of the sanitized fields below disappeared from NiSourceTexture in nif.xml somewhere between 2008 and 2018 (Unknown Byte, Unknown Byte 2). + // For others, NifSkope itself sets values other than the "forced" ones below. + // And anyway, this code has not be doing anything for years because of the changes in nif.xml, until 2023 when "field not found" warnings were introduced. + // So let's ditch it. + /* + // adjust format options (oblivion only) + if ( nif->checkVersion( 0x14000005, 0x14000005 ) ) { + nif->set( iTexSrc, "Pixel Layout", 6 ); + nif->set( iTexSrc, "Use Mipmaps", 1 ); + nif->set( iTexSrc, "Alpha Format", 3 ); + nif->set( iTexSrc, "Unknown Byte", 1 ); + nif->set( iTexSrc, "Unknown Byte 2", 1 ); } + */ } return QModelIndex(); diff --git a/src/spells/skeleton.cpp b/src/spells/skeleton.cpp index ff6fb0a05..cf92e565e 100644 --- a/src/spells/skeleton.cpp +++ b/src/spells/skeleton.cpp @@ -439,22 +439,7 @@ class spSkinPartition final : public Spell if ( iShapeType == "NiTriShape" ) { triangles = nif->getArray( iData, "Triangles" ).toList(); } else if ( iShapeType == "NiTriStrips" ) { - // triangulate first (code copied from strippify.cpp) - QVector > strips; - QModelIndex iPoints = nif->getIndex( iData, "Points" ); - - for ( int s = 0; s < nif->rowCount( iPoints ); s++ ) { - QVector strip; - QModelIndex iStrip = iPoints.child( s, 0 ); - - for ( int p = 0; p < nif->rowCount( iStrip ); p++ ) { - strip.append( nif->get( iStrip.child( p, 0 ) ) ); - } - - strips.append( strip ); - } - - triangles = triangulate( strips ).toList(); + triangles = triangulateStrips( nif, nif->getIndex( iData, "Points" ) ).toList(); } QMap trimap; @@ -499,23 +484,8 @@ class spSkinPartition final : public Spell if ( hasFaces && numStrips == 0 ) { partTriangles = nif->getArray( iPart, "Triangles" ); - } else if ( numStrips != 0 ) { - // triangulate first (code copied from strippify.cpp) - QVector > strips; - QModelIndex iPoints = nif->getIndex( iPart, "Strips" ); - - for ( int s = 0; s < nif->rowCount( iPoints ); s++ ) { - QVector strip; - QModelIndex iStrip = iPoints.child( s, 0 ); - - for ( int p = 0; p < nif->rowCount( iStrip ); p++ ) { - strip.append( nif->get( iStrip.child( p, 0 ) ) ); - } - - strips.append( strip ); - } - - partTriangles = triangulate( strips ); + } else if ( numStrips > 0 ) { + partTriangles = triangulateStrips( nif, nif->getIndex( iPart, "Strips" ) ); } for ( int j = 0; j < partTriangles.count(); ++j ) { @@ -857,13 +827,13 @@ class spSkinPartition final : public Spell } // stripify the triangles - QVector > strips; + QVector strips; int numTriangles = 0; if ( make_strips == true ) { - strips = stripify( triangles ); + strips = stripifyTriangles( triangles ); - for ( const QVector& strip : strips ) { + for ( const TriStrip & strip : strips ) { numTriangles += strip.count() - 2; } } else { diff --git a/src/spells/strippify.cpp b/src/spells/strippify.cpp index ff544dff0..32ed62ac1 100644 --- a/src/spells/strippify.cpp +++ b/src/spells/strippify.cpp @@ -66,13 +66,13 @@ class spStrippify final : public Spell //qDebug() << "num triangles" << triangles.count() << "skipped" << skip; - QVector > strips = stripify( triangles, true ); + auto strips = stripifyTriangles( triangles, true ); if ( strips.count() <= 0 ) return idx; uint numTriangles = 0; - for ( const QVector& strip : strips ) { + for ( const auto & strip : strips ) { numTriangles += strip.count() - 2; } @@ -251,24 +251,12 @@ class spTriangulate final : public Spell if ( !iStripData.isValid() ) return idx; - QVector > strips; - QModelIndex iPoints = nif->getIndex( iStripData, "Points" ); if ( !iPoints.isValid() ) return idx; - for ( int s = 0; s < nif->rowCount( iPoints ); s++ ) { - QVector strip; - QModelIndex iStrip = iPoints.child( s, 0 ); - - for ( int p = 0; p < nif->rowCount( iStrip ); p++ ) - strip.append( nif->get( iStrip.child( p, 0 ) ) ); - - strips.append( strip ); - } - - QVector triangles = triangulate( strips ); + QVector triangles = triangulateStrips( nif, iPoints ); nif->insertNiBlock( "NiTriShapeData", nif->getBlockNumber( idx ) + 1 ); QModelIndex iTriData = nif->getBlockIndex( nif->getBlockNumber( idx ) + 1, "NiTriShapeData" ); diff --git a/src/spells/tangentspace.cpp b/src/spells/tangentspace.cpp index a4951c057..81d187a0b 100644 --- a/src/spells/tangentspace.cpp +++ b/src/spells/tangentspace.cpp @@ -99,12 +99,7 @@ QModelIndex spTangentSpace::cast( NifModel * nif, const QModelIndex & iBlock ) QModelIndex iPoints = nif->getIndex( iData, "Points" ); if ( iPoints.isValid() ) { - QVector > strips; - - for ( int r = 0; r < nif->rowCount( iPoints ); r++ ) - strips.append( nif->getArray( iPoints.child( r, 0 ) ) ); - - triangles = triangulate( strips ); + triangles = triangulateStrips( nif, iPoints ); } else if ( nif->getBSVersion() < 100 ) { triangles = nif->getArray( iData, "Triangles" ); } else if ( nif->getBSVersion() >= 100 ) { diff --git a/src/spells/texture.cpp b/src/spells/texture.cpp index 15a004122..3f8dd27a1 100644 --- a/src/spells/texture.cpp +++ b/src/spells/texture.cpp @@ -279,42 +279,62 @@ class spEditTexCoords final : public Spell REGISTER_SPELL( spEditTexCoords ) -//! Add a texture to the specified texture slot -/*! - * \param nif The model - * \param index The index of the mesh - * \param name The name of the texture slot - */ -QModelIndex addTexture( NifModel * nif, const QModelIndex & index, const QString & name ) + +QModelIndex NiTexturingProperty_addTexture( NifModel * nif, const QModelIndex & iTexProperty, const QModelIndex & iOldSrcTexBlock, const QString & texName ) { - QModelIndex iTexProp = nif->getBlockIndex( index, "NiTexturingProperty" ); + auto propBlock = nif->field( iTexProperty ).block("NiTexturingProperty"); + if ( !propBlock ) + return QModelIndex(); - if ( !iTexProp.isValid() ) - return index; + // Set up NiTexturingProperty + auto texCountField = propBlock["Texture Count"]; + if ( texCountField.value() < 7 ) + texCountField.setValue(7); - if ( nif->get( iTexProp, "Texture Count" ) < 7 ) - nif->set( iTexProp, "Texture Count", 7 ); + propBlock[QString("Has ") + texName].setValue(true); - nif->set( iTexProp, QString( "Has %1" ).arg( name ), 1 ); - QPersistentModelIndex iTex = nif->getIndex( iTexProp, name ); + auto descEntry = propBlock[texName]; + if ( !descEntry ) + return QModelIndex(); - if ( !iTex.isValid() ) - return index; + descEntry.child("Clamp Mode").setValue(3); + descEntry.child("Filter Mode").setValue(3); + descEntry.child("PS2 K").setValue(-75); + descEntry.child("Unknown Short 1").setValue(257); + + // Set up NiSourceTexture for the texture + NifField texBlock = nif->field( iOldSrcTexBlock ).block("NiSourceTexture"); + if ( !texBlock ) { + texBlock = nif->block( nif->insertNiBlock( "NiSourceTexture", propBlock.toBlockNumber() + 1 ) ); + if ( !texBlock ) + return QModelIndex(); + } + descEntry["Source"].setLink( texBlock ); - nif->set( iTex, "Clamp Mode", 3 ); - nif->set( iTex, "Filter Mode", 3 ); - nif->set( iTex, "PS2 K", -75 ); - nif->set( iTex, "Unknown1", 257 ); + texBlock["Use External"].setValue(1); + texBlock["Is Static"].setValue(1); - QModelIndex iSrcTex = nif->insertNiBlock( "NiSourceTexture", nif->getBlockNumber( iTexProp ) + 1 ); - nif->setLink( iTex, "Source", nif->getBlockNumber( iSrcTex ) ); + auto prefsField = texBlock["Format Prefs"]; + prefsField["Pixel Layout"].setValue( ( nif->checkVersion( 0x14000005, 0x14000005 ) && descEntry.hasName("Base Texture") ) ? 6 : 5 ); + prefsField["Use Mipmaps"].setValue(2); + prefsField["Alpha Format"].setValue(3); - nif->set( iSrcTex, "Pixel Layout", ( nif->getVersion() == "20.0.0.5" && name == "Base Texture" ? 6 : 5 ) ); - nif->set( iSrcTex, "Use Mipmaps", 2 ); - nif->set( iSrcTex, "Alpha Format", 3 ); - nif->set( iSrcTex, "Unknown Byte", 1 ); - nif->set( iSrcTex, "Is Static", 1 ); - nif->set( iSrcTex, "Use External", 1 ); + // texBlock["Unknown Byte"].setValue(1); // No idea what the "Unknown Byte" from 2008 is now. + + return texBlock.toIndex(); +} + +//! Add a texture to the specified texture slot +/*! + * \param nif The model + * \param index The index of the mesh + * \param name The name of the texture slot + */ +static QModelIndex addTexture( NifModel * nif, const QModelIndex & index, const QString & name ) +{ + QModelIndex iSrcTex = NiTexturingProperty_addTexture( nif, index, QModelIndex(), name ); + if ( !iSrcTex.isValid() ) + return index; spChooseTexture * chooser = new spChooseTexture(); return chooser->cast( nif, iSrcTex ); @@ -549,7 +569,7 @@ class spTextureTemplate final : public Spell dlg.setLayout( lay ); FileSelector * file = new FileSelector( FileSelector::SaveFile, "File", QBoxLayout::RightToLeft ); - file->setFilter( { "", "PNG (*.png)", "BMP (*.bmp)" } ); + file->setFilter( { "PNG (*.png)", "BMP (*.bmp)" } ); lay->addWidget( file, 0, 0, 1, 2 ); lay->addWidget( new QLabel( "Size" ), 1, 0 ); @@ -664,12 +684,7 @@ class spTextureTemplate final : public Spell QModelIndex iPoints = nif->getIndex( iData, "Points" ); if ( iPoints.isValid() ) { - QVector > strips; - - for ( int r = 0; r < nif->rowCount( iPoints ); r++ ) - strips.append( nif->getArray( iPoints.child( r, 0 ) ) ); - - tri = triangulate( strips ); + tri = triangulateStrips( nif, iPoints ); } else if ( nif->getBSVersion() < 100 ) { tri = nif->getArray( nif->getIndex( getData( nif, index ), "Triangles" ) ); } diff --git a/src/spells/texture.h b/src/spells/texture.h index accc57930..8927e727d 100644 --- a/src/spells/texture.h +++ b/src/spells/texture.h @@ -38,4 +38,13 @@ protected slots: void listFromNif(); }; +/*! Add a texture to the specified texture slot in a NiTexturingProperty block +* \param nif The model +* \param iTexProperty The index of the NiTexturingProperty (or its child) +* \param iOldSrcTexBlock The index of the NiSourceTexture to reuse (optional) +* \param texName The name of the texture slot ("Base Texture", "Detail Texture"...) +* \return The index of the result NiSourceTexture +*/ +QModelIndex NiTexturingProperty_addTexture( NifModel * nif, const QModelIndex & iTexProperty, const QModelIndex & iOldSrcTexBlock, const QString & texName ); + #endif diff --git a/src/spells/transform.cpp b/src/spells/transform.cpp index a3a4ef903..6455970d5 100644 --- a/src/spells/transform.cpp +++ b/src/spells/transform.cpp @@ -381,6 +381,7 @@ class spEditTransformation final : public Spell } edit->show(); + edit->activateWindow(); return index; } }; diff --git a/src/ui/ToolDialog.cpp b/src/ui/ToolDialog.cpp new file mode 100644 index 000000000..7015d57e8 --- /dev/null +++ b/src/ui/ToolDialog.cpp @@ -0,0 +1,578 @@ +#include "ToolDialog.h" + +#include +#include +#include + +#include "UiUtils.h" + +const QString SETTING_WIDTH( "DialogWidth" ); +const QString SETTING_HEIGHT( "DialogHeight" ); + +const int PUSH_BUTTON_WIDTH_PADDING = 8; +const int PUSH_BUTTON_HEIGHT_PADDING = 0; + + +ToolDialog::ToolDialog( QWidget * parent, const QString & title, ToolDialogFlagsType flags, int startWidth, int startHeight ) + : QWidget( parent ? parent->window() : nullptr, 0 ), toolDialogFlags( flags ), startWidth( startWidth ), startHeight( startHeight ) +{ + UIUtils::setWindowTitle( this, title ); +} + +void ToolDialog::open( bool autoDeleteOnClose ) +{ + setAttribute( Qt::WA_DeleteOnClose, autoDeleteOnClose ); + + // Finalize main button layout + if ( !defaultButton && mainButtons.count() == 1 ) + defaultButton = mainButtons[0]; + if ( defaultButton ) + defaultButton->setDefault( true ); + + if ( mainButtonLayout ) { + int newButtonWidth = 100, newButtonHeight = 0; + for ( QPushButton * button : mainButtons ) { + auto testSize = button->sizeHint(); + auto testWidth = testSize.width() + PUSH_BUTTON_WIDTH_PADDING; + if ( testWidth > newButtonWidth ) + newButtonWidth = testWidth; + auto testHeight = testSize.height() + PUSH_BUTTON_HEIGHT_PADDING; + if ( testHeight > newButtonHeight ) + newButtonHeight = testHeight; + } + for ( auto button : mainButtons ) + button->setFixedSize( newButtonWidth, newButtonHeight ); + + // Add small margin at the top to visually split the main buttons from the rest of widgets + QMargins margins = mainButtonLayout->contentsMargins(); + margins.setTop( margins.top() + 4 ); + mainButtonLayout->setContentsMargins( margins ); + } + + mainButtons.clear(); // Don't need it anymore + + // Window flags and modality + setDialogFlagsAndModality( this, toolDialogFlags ); + + // Setting sizes + QSize szHint = sizeHint(); + int minWidth = szHint.width(), minHeight = szHint.height(); + if ( startWidth < minWidth ) + startWidth = minWidth; + if ( startHeight < minHeight ) + startHeight = minHeight; + + // The setMinimum...(...) calls below at first glance are redundant because the size of a window can't get smaller than sizeHint() anyway, + // but those setMinimum... poke something within Qt (at least in Qt5) that properly centers the window on show(). + if ( hasToolDialogFlag( Flags::HResize ) ) + setMinimumWidth( minWidth ); + else + setFixedWidth( startWidth ); + if ( hasToolDialogFlag( Flags::VResize ) ) + setMinimumHeight( minHeight ); + else + setFixedHeight( startHeight ); + + auto getCustomDimension = [this]( int minDim, int startDim, Flags resizeFlag, const QString & settingName ) { + if ( hasToolDialogFlag( resizeFlag ) && hasSettings() ) { + int savedDim = settingsIntValue( settingName, 0 ); + if ( savedDim >= minDim ) + return savedDim; + } + return startDim; + }; + + int customWidth = getCustomDimension( minWidth, startWidth, Flags::HResize, SETTING_WIDTH ); + int customHeight = getCustomDimension( minHeight, startHeight, Flags::VResize, SETTING_HEIGHT ); + if ( customWidth > minWidth || customHeight > minHeight ) + resize( customWidth, customHeight ); + + if ( hasToolDialogFlag( Flags::Resize ) ) { + sizeGrip = new QSizeGrip( this ); + sizeGrip->resize( sizeGrip->sizeHint() ); + positionSizeGrip(); + } + + // Show and activate me + showDialog( this ); +} + +void ToolDialog::setDialogFlagsAndModality( QWidget * dialog, ToolDialogFlagsType flags ) +{ + Qt::WindowModality winModality = Qt::WindowModality::WindowModal; + if ( ( flags & Flags::ApplicationBlocking ) || !dialog->parentWidget() ) + winModality = Qt::WindowModality::ApplicationModal; + else if ( flags & Flags::NonBlocking ) + winModality = Qt::WindowModality::NonModal; + + Qt::WindowFlags winFlags = Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowCloseButtonHint; + if ( winModality == Qt::WindowModality::NonModal ) + winFlags |= Qt::Tool; + else + winFlags |= Qt::Dialog | Qt::WindowSystemMenuHint; + if ( ( flags & Flags::Resize ) == 0 ) // If the dialog is not resizable... + winFlags |= Qt::MSWindowsFixedSizeDialogHint; + + dialog->setWindowFlags( winFlags ); + dialog->setWindowModality( winModality ); +} + +void ToolDialog::showDialog( QWidget * dialog ) +{ + dialog->show(); + if ( dialog->windowModality() == Qt::WindowModality::NonModal ) + dialog->activateWindow(); +} + +void ToolDialog::closeEvent( QCloseEvent * event ) +{ + QSize sz = size(); + QWidget::closeEvent( event ); + if ( event->isAccepted() && hasSettings() ) { + auto saveDimension = [this]( int dim, Flags resizeFlag, const QString & settingName ) { + if ( hasToolDialogFlag( resizeFlag ) ) + setSettingsIntValue( settingName, dim ); + else + settings.remove( settingsKeyPath( settingName ) ); + }; + + saveDimension( sz.width(), Flags::HResize, SETTING_WIDTH ); + saveDimension( sz.height(), Flags::VResize, SETTING_HEIGHT ); + } +} + +void ToolDialog::keyPressEvent( QKeyEvent * event ) +{ + // Must implement closing the dialog on Esc and pressing the default button on Enter + if ( event->matches( QKeySequence::Cancel ) ) { + close(); + return; + } else if ( defaultButton && ( event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return ) ) { + if ( defaultButton->isEnabled() ) + defaultButton->click(); + return; + } + + event->ignore(); +} + +void ToolDialog::resizeEvent( [[maybe_unused]] QResizeEvent * event ) +{ + positionSizeGrip(); +} + +void ToolDialog::positionSizeGrip() +{ + if ( sizeGrip ) { + if ( isRightToLeft() ) + sizeGrip->move( rect().bottomLeft() - sizeGrip->rect().bottomLeft() ); + else + sizeGrip->move( rect().bottomRight() - sizeGrip->rect().bottomRight() ); + sizeGrip->raise(); + } +} + +void ToolDialog::setSettingsFolder( const QString & settingsFolder ) +{ + if ( settingsFolder.isEmpty() ) { + settingsPath.clear(); + } else { + settingsPath = QStringLiteral("Dialogs/") % settingsFolder % QStringLiteral("/"); + } +} + + +// Layout helpers + +static inline void attachLayoutToWidget( QLayout * childLayout, QWidget * parentWidget ) +{ + if ( parentWidget ) + parentWidget->setLayout( childLayout ); +} + +static inline void attachLayoutToLayout( QLayout * childLayout, QBoxLayout * parentLayout ) +{ + if ( parentLayout ) + parentLayout->addLayout( childLayout ); +} + +static inline void attachLayoutToLayout( QLayout * childLayout, QGridLayout * parentLayout, int parentRow, int parentColumn ) +{ + if ( parentLayout ) + parentLayout->addLayout( childLayout, parentRow, parentColumn ); +} + +static inline void attachLayoutToLayout( QLayout * childLayout, QGridLayout * parentLayout, int parentRow, int parentColumn, int parentSpan ) +{ + if ( parentLayout ) + parentLayout->addLayout( childLayout, parentRow, parentColumn, 1, parentSpan ); +} + +QHBoxLayout * ToolDialog::addHBoxLayout( QWidget * attachTo ) +{ + QHBoxLayout * layout = addHBoxLayout(); + attachLayoutToWidget( layout, attachTo ); + return layout; +} + +QHBoxLayout * ToolDialog::addHBoxLayout( QBoxLayout * attachTo ) +{ + QHBoxLayout * layout = addHBoxLayout(); + attachLayoutToLayout( layout, attachTo ); + return layout; +} + +QHBoxLayout * ToolDialog::addHBoxLayout( QGridLayout * attachTo, int attachColumn ) +{ + QHBoxLayout * layout = addHBoxLayout(); + attachLayoutToLayout( layout, attachTo, gridLayoutRow, attachColumn ); + return layout; +} + +QHBoxLayout * ToolDialog::addHBoxLayout( QGridLayout * attachTo, int attachColumn, int attachSpan ) +{ + QHBoxLayout * layout = addHBoxLayout(); + attachLayoutToLayout( layout, attachTo, gridLayoutRow, attachColumn, attachSpan ); + return layout; +} + +QVBoxLayout * ToolDialog::addVBoxLayout( QWidget * attachTo ) +{ + QVBoxLayout * layout = addVBoxLayout(); + attachLayoutToWidget( layout, attachTo ); + return layout; +} + +QVBoxLayout * ToolDialog::addVBoxLayout( QBoxLayout * attachTo ) +{ + QVBoxLayout * layout = addVBoxLayout(); + attachLayoutToLayout( layout, attachTo ); + return layout; +} + +QVBoxLayout * ToolDialog::addVBoxLayout( QGridLayout * attachTo, int attachColumn ) +{ + QVBoxLayout * layout = addVBoxLayout(); + attachLayoutToLayout( layout, attachTo, gridLayoutRow, attachColumn ); + return layout; +} + +QVBoxLayout * ToolDialog::addVBoxLayout( QGridLayout * attachTo, int attachColumn, int attachSpan ) +{ + QVBoxLayout * layout = addVBoxLayout(); + attachLayoutToLayout( layout, attachTo, gridLayoutRow, attachColumn, attachSpan ); + return layout; +} + +QGridLayout * ToolDialog::addGridLayout() +{ + gridLayoutRow = 0; + return new QGridLayout(); +} + +QGridLayout * ToolDialog::addGridLayout( QWidget * attachTo ) +{ + QGridLayout * layout = addGridLayout(); + attachLayoutToWidget( layout, attachTo ); + return layout; +} + +QGridLayout * ToolDialog::addGridLayout( QBoxLayout * attachTo ) +{ + QGridLayout * layout = addGridLayout(); + attachLayoutToLayout( layout, attachTo ); + return layout; +} + +static inline void attachWidgetToLayout( QWidget * widget, QBoxLayout * layout ) +{ + if ( layout ) + layout->addWidget( widget ); +} + +static inline void attachWidgetToLayout( QWidget * widget, QGridLayout * layout, int layoutRow, int layoutColumn ) +{ + if ( layout ) + layout->addWidget( widget, layoutRow, layoutColumn ); +} + +static inline void attachWidgetToLayout( QWidget * widget, QGridLayout * layout, int layoutRow, int layoutColumn, int layoutSpan ) +{ + if ( layout ) + layout->addWidget( widget, layoutRow, layoutColumn, 1, layoutSpan ); +} + + +// Label helpers + +QLabel * ToolDialog::addLabel( const QString & text, bool fixedSize ) +{ + QLabel * label = new QLabel( text ); + if ( fixedSize ) + lockWidgetSize( label ); + return label; +} + +QLabel * ToolDialog::addLabel( QBoxLayout * parentLayout, const QString & text, bool fixedSize ) +{ + QLabel * label = addLabel( text, fixedSize ); + attachWidgetToLayout( label, parentLayout ); + return label; +} + +QLabel * ToolDialog::addLabel( QGridLayout * parentLayout, int layoutColumn, const QString & text, bool fixedSize ) +{ + QLabel * label = addLabel( text, fixedSize ); + attachWidgetToLayout( label, parentLayout, gridLayoutRow, layoutColumn ); + return label; +} + +QLabel * ToolDialog::addLabel( QGridLayout * parentLayout, int layoutColumn, int layoutSpan, const QString & text, bool fixedSize ) +{ + QLabel * label = addLabel( text, fixedSize ); + attachWidgetToLayout( label, parentLayout, gridLayoutRow, layoutColumn, layoutSpan ); + return label; +} + + +// Push button helpers + +QPushButton * ToolDialog::addPushButton( const QString & text ) +{ + auto button = new QPushButton( text ); + button->setAutoDefault( false ); + return button; +} + +QPushButton * ToolDialog::addPushButton( QBoxLayout * parentLayout, const QString & text ) +{ + QPushButton * button = addPushButton( text ); + attachWidgetToLayout( button, parentLayout ); + return button; +} + +QPushButton * ToolDialog::addPushButton( QGridLayout * parentLayout, int layoutColumn, const QString & text ) +{ + QPushButton * button = addPushButton( text ); + attachWidgetToLayout( button, parentLayout, gridLayoutRow, layoutColumn ); + return button; +} + +QPushButton * ToolDialog::addPushButton( QGridLayout * parentLayout, int layoutColumn, int layoutSpan, const QString & text ) +{ + QPushButton * button = addPushButton( text ); + attachWidgetToLayout( button, parentLayout, gridLayoutRow, layoutColumn, layoutSpan ); + return button; +} + +void ToolDialog::lockPushButtonSize( QPushButton * button ) +{ + QSize szHint = button->sizeHint(); + button->setFixedSize( szHint.width() + PUSH_BUTTON_WIDTH_PADDING, szHint.height() + PUSH_BUTTON_HEIGHT_PADDING ); +} + +QHBoxLayout * ToolDialog::beginMainButtonLayout() +{ + if ( !mainButtonLayout ) + mainButtonLayout = addHBoxLayout(); + return mainButtonLayout; +} + +QHBoxLayout * ToolDialog::beginMainButtonLayout( QBoxLayout * attachTo ) +{ + if ( !mainButtonLayout ) + mainButtonLayout = addHBoxLayout( attachTo ); + return mainButtonLayout; +} + +QHBoxLayout * ToolDialog::beginMainButtonLayout( QGridLayout * attachTo ) +{ + if ( !mainButtonLayout ) + mainButtonLayout = addHBoxLayout( attachTo, 0, -1 ); + return mainButtonLayout; +} + +QPushButton * ToolDialog::addMainButton( const QString & text, bool isDefaultButton ) +{ + if ( mainButtonLayout && mainButtons.count() == 0) + mainButtonLayout->addStretch( 1 ); + + QPushButton * button = addPushButton( mainButtonLayout, text ); + if ( isDefaultButton ) + defaultButton = button; + mainButtons.append( button ); + return button; +} + +QPushButton * ToolDialog::addCloseButton( const QString & text ) +{ + QPushButton * button = addMainButton( text, false ); + connect( button, &QPushButton::clicked, this, &QWidget::close ); + return button; +} + + +// Check box helpers + +QCheckBox * ToolDialog::addCheckBox( const QString & text ) +{ + return new QCheckBox( text ); +} + +QCheckBox * ToolDialog::addCheckBox( QBoxLayout * parentLayout, const QString & text ) +{ + QCheckBox * checkBox = addCheckBox( text ); + attachWidgetToLayout( checkBox, parentLayout ); + return checkBox; +} + +QCheckBox * ToolDialog::addCheckBox( QGridLayout * parentLayout, int layoutColumn, const QString & text ) +{ + QCheckBox * checkBox = addCheckBox( text ); + attachWidgetToLayout( checkBox, parentLayout, gridLayoutRow, layoutColumn ); + return checkBox; +} + +QCheckBox * ToolDialog::addCheckBox( QGridLayout * parentLayout, int layoutColumn, int layoutSpan, const QString & text ) +{ + QCheckBox * checkBox = addCheckBox( text ); + attachWidgetToLayout( checkBox, parentLayout, gridLayoutRow, layoutColumn, layoutSpan ); + return checkBox; +} + + +// Radio button helpers + +QButtonGroup * ToolDialog::beginRadioGroup() +{ + radioGroup = new QButtonGroup( this ); + radioGroup->setExclusive( true ); + return radioGroup; +} + +QRadioButton * ToolDialog::addRadioButton( const QString & text, int groupId ) +{ + QRadioButton * button = new QRadioButton( text ); + if ( radioGroup ) + radioGroup->addButton( button, groupId ); + return button; +} + +QRadioButton * ToolDialog::addRadioButton( QBoxLayout * parentLayout, const QString & text, int groupId ) +{ + QRadioButton * button = addRadioButton( text, groupId ); + attachWidgetToLayout( button, parentLayout ); + return button; +} + +QRadioButton * ToolDialog::addRadioButton( QGridLayout * parentLayout, int layoutColumn, const QString & text, int groupId ) +{ + QRadioButton * button = addRadioButton( text, groupId ); + attachWidgetToLayout( button, parentLayout, gridLayoutRow, layoutColumn ); + return button; +} + +QRadioButton * ToolDialog::addRadioButton( QGridLayout * parentLayout, int layoutColumn, int layoutSpan, const QString & text, int groupId ) +{ + QRadioButton * button = addRadioButton( text, groupId ); + attachWidgetToLayout( button, parentLayout, gridLayoutRow, layoutColumn, layoutSpan ); + return button; +} + + +// Group box helpers + +QGroupBox * ToolDialog::addGroupBox( const QString & text, QLayout * innerLayout ) +{ + auto groupBox = new QGroupBox( text ); + if ( innerLayout ) + groupBox->setLayout( innerLayout ); + return groupBox; +} + +QGroupBox * ToolDialog::addGroupBox( QBoxLayout * parentLayout, const QString & text, QLayout * innerLayout ) +{ + QGroupBox * groupBox = addGroupBox( text, innerLayout ); + attachWidgetToLayout( groupBox, parentLayout ); + return groupBox; +} + +QGroupBox * ToolDialog::addGroupBox( QGridLayout * parentLayout, int layoutColumn, const QString & text, QLayout * innerLayout ) +{ + QGroupBox * groupBox = addGroupBox( text, innerLayout ); + attachWidgetToLayout( groupBox, parentLayout, gridLayoutRow, layoutColumn ); + return groupBox; +} + +QGroupBox * ToolDialog::addGroupBox( QGridLayout * parentLayout, int layoutColumn, int layoutSpan, const QString & text, QLayout * innerLayout ) +{ + QGroupBox * groupBox = addGroupBox( text, innerLayout ); + attachWidgetToLayout( groupBox, parentLayout, gridLayoutRow, layoutColumn, layoutSpan ); + return groupBox; +} + + +// Spin box helpers + +QSpinBox * ToolDialog::addSpinBox( int minVal, int maxVal, int initVal, int step ) +{ + auto spinBox = new QSpinBox; + spinBox->setRange( minVal, maxVal ); + spinBox->setValue( initVal ); + spinBox->setSingleStep( step ); + + return spinBox; +} + +QSpinBox * ToolDialog::addSpinBox( QBoxLayout * parentLayout, int minVal, int maxVal, int initVal, int step ) +{ + QSpinBox * spinBox = addSpinBox( minVal, maxVal, initVal, step ); + attachWidgetToLayout( spinBox, parentLayout ); + return spinBox; +} + +QSpinBox * ToolDialog::addSpinBox( QGridLayout * parentLayout, int layoutColumn, int minVal, int maxVal, int initVal, int step ) +{ + QSpinBox * spinBox = addSpinBox( minVal, maxVal, initVal, step ); + attachWidgetToLayout( spinBox, parentLayout, gridLayoutRow, layoutColumn ); + return spinBox; +} + +QSpinBox * ToolDialog::addSpinBox( QGridLayout * parentLayout, int layoutColumn, int layoutSpan, int minVal, int maxVal, int initVal, int step ) +{ + QSpinBox * spinBox = addSpinBox( minVal, maxVal, initVal, step ); + attachWidgetToLayout( spinBox, parentLayout, gridLayoutRow, layoutColumn, layoutSpan ); + return spinBox; +} + +QDoubleSpinBox * ToolDialog::addDoubleSpinBox( double minVal, double maxVal, double initVal, int decimalDigits, double step ) +{ + auto spinBox = new QDoubleSpinBox; + spinBox->setDecimals( decimalDigits ); + spinBox->setRange( minVal, maxVal ); + spinBox->setValue( initVal ); + spinBox->setSingleStep( step ); + + return spinBox; +} + +QDoubleSpinBox * ToolDialog::addDoubleSpinBox( QBoxLayout * parentLayout, double minVal, double maxVal, double initVal, int decimalDigits, double step ) +{ + QDoubleSpinBox * spinBox = addDoubleSpinBox( minVal, maxVal, initVal, decimalDigits, step ); + attachWidgetToLayout( spinBox, parentLayout ); + return spinBox; +} + +QDoubleSpinBox * ToolDialog::addDoubleSpinBox( QGridLayout * parentLayout, int layoutColumn, double minVal, double maxVal, double initVal, int decimalDigits, double step ) +{ + QDoubleSpinBox * spinBox = addDoubleSpinBox( minVal, maxVal, initVal, decimalDigits, step ); + attachWidgetToLayout( spinBox, parentLayout, gridLayoutRow, layoutColumn ); + return spinBox; +} + +QDoubleSpinBox * ToolDialog::addDoubleSpinBox( QGridLayout * parentLayout, int layoutColumn, int layoutSpan, double minVal, double maxVal, double initVal, int decimalDigits, double step ) +{ + QDoubleSpinBox * spinBox = addDoubleSpinBox( minVal, maxVal, initVal, decimalDigits, step ); + attachWidgetToLayout( spinBox, parentLayout, gridLayoutRow, layoutColumn, layoutSpan ); + return spinBox; +} diff --git a/src/ui/ToolDialog.h b/src/ui/ToolDialog.h new file mode 100644 index 000000000..6261283e9 --- /dev/null +++ b/src/ui/ToolDialog.h @@ -0,0 +1,191 @@ +#ifndef TOOLDIALOG_H +#define TOOLDIALOG_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using ToolDialogFlagsType = unsigned int; + +// Base class for tool dialogs (edits, spells, abouts...). +// Streamlines managing dialog flags and modality, child layouts and widgets, tool's settings, etc. +class ToolDialog : public QWidget +{ +public: + enum Flags : ToolDialogFlagsType + { + HResize = 0x01, // Allow horizontal resizing of the dialog + VResize = 0x02, // Allow vertical resizing of the dialog + Resize = HResize | VResize, // Allow both horizontal and vertical resizing of the dialog + + NonBlocking = 0x04, // Do NOT block the parent window or the whole application + ApplicationBlocking = 0x08, // Block the whole application (all other windows) + + }; + + ToolDialog( QWidget * parent, const QString & title, ToolDialogFlagsType flags, int startWidth = 0, int startHeight = 0 ); + + void open( bool autoDeleteOnClose ); + + bool hasToolDialogFlag( Flags flag ) const { return toolDialogFlags & flag; } + +protected: + void closeEvent( QCloseEvent * event ) override; + void keyPressEvent( QKeyEvent * event ) override; + void resizeEvent( QResizeEvent * event ) override; + +private: + ToolDialogFlagsType toolDialogFlags; + int startWidth; + int startHeight; + QSizeGrip * sizeGrip = nullptr; + + void positionSizeGrip(); + + + // Settings helpers +public: + bool hasSettings() const { return !settingsPath.isEmpty(); } + + void setSettingsFolder( const QString & settingsFolder ); + + QString settingsKeyPath( const QString & key ) const + { return hasSettings() ? ( settingsPath + key ) : key; } + + QVariant settingsValue( const QString & key, const QVariant & defaultValue = QVariant() ) + { return settings.value( settingsKeyPath( key ), defaultValue ); } + int settingsIntValue( const QString & key, int defaultValue = 0 ) + { return settingsValue( key, defaultValue ).toInt(); } + QString settingsStrValue( const QString & key, const QString & defaultValue = QString() ) + { return settingsValue( key, defaultValue ).toString(); } + + void setSettingsValue( const QString & key, const QVariant & value ) + { settings.setValue( settingsKeyPath( key ), value ); } + void setSettingsIntValue( const QString & key, int value ) + { setSettingsValue( key, value ); } + void setSettingsStrValue( const QString & key, const QString & value ) + { setSettingsValue( key, value ); } + +private: + QSettings settings; + QString settingsPath; + + + // Layout helpers +public: + QHBoxLayout * addHBoxLayout() { return new QHBoxLayout(); } + QHBoxLayout * addHBoxLayout( QWidget * attachTo ); + QHBoxLayout * addHBoxLayout( QBoxLayout * attachTo ); + QHBoxLayout * addHBoxLayout( QGridLayout * attachTo, int attachColumn ); + QHBoxLayout * addHBoxLayout( QGridLayout * attachTo, int attachColumn, int attachSpan ); + + QVBoxLayout * addVBoxLayout() { return new QVBoxLayout(); } + QVBoxLayout * addVBoxLayout( QWidget * attachTo ); + QVBoxLayout * addVBoxLayout( QBoxLayout * attachTo ); + QVBoxLayout * addVBoxLayout( QGridLayout * attachTo, int attachColumn ); + QVBoxLayout * addVBoxLayout( QGridLayout * attachTo, int attachColumn, int attachSpan ); + + void addStretch( QBoxLayout * layout, int stretch = 0 ) { layout->addStretch( stretch ); } + + QGridLayout * addGridLayout(); + QGridLayout * addGridLayout( QWidget * attachTo ); + QGridLayout * addGridLayout( QBoxLayout * attachTo ); + + void beginGridRow( const QGridLayout * grid ) { gridLayoutRow = grid->rowCount(); } + +private: + int gridLayoutRow = 0; + + + // Dialog helpers (static) +public: + static void setDialogFlagsAndModality( QWidget * dialog, ToolDialogFlagsType flags ); + static void showDialog( QWidget * dialog ); + + + // Widget helpers +public: + void lockWidgetSize( QWidget * widget ) { widget->setSizePolicy( QSizePolicy::Fixed, QSizePolicy::Fixed ); } + + + // Label helpers +public: + QLabel * addLabel( const QString & text, bool fixedSize = false ); + QLabel * addLabel( QBoxLayout * parentLayout, const QString & text, bool fixedSize = false ); + QLabel * addLabel( QGridLayout * parentLayout, int layoutColumn, const QString & text, bool fixedSize = false ); + QLabel * addLabel( QGridLayout * parentLayout, int layoutColumn, int layoutSpan, const QString & text, bool fixedSize = false ); + + + // Push button helpers +public: + QPushButton * addPushButton( const QString & text ); + QPushButton * addPushButton( QBoxLayout * parentLayout, const QString & text ); + QPushButton * addPushButton( QGridLayout * parentLayout, int layoutColumn, const QString & text ); + QPushButton * addPushButton( QGridLayout * parentLayout, int layoutColumn, int layoutSpan, const QString & text ); + + void lockPushButtonSize( QPushButton * button ); + + QHBoxLayout * beginMainButtonLayout(); + QHBoxLayout * beginMainButtonLayout( QBoxLayout * attachTo ); + QHBoxLayout * beginMainButtonLayout( QGridLayout * attachTo ); + QPushButton * addMainButton( const QString & text, bool isDefaultButton ); + QPushButton * addCloseButton( const QString & text ); + +private: + QHBoxLayout * mainButtonLayout = nullptr; + QVector mainButtons; + QPushButton * defaultButton = nullptr; + + + // Check box helpers +public: + QCheckBox * addCheckBox( const QString & text ); + QCheckBox * addCheckBox( QBoxLayout * parentLayout, const QString & text ); + QCheckBox * addCheckBox( QGridLayout * parentLayout, int layoutColumn, const QString & text ); + QCheckBox * addCheckBox( QGridLayout * parentLayout, int layoutColumn, int layoutSpan, const QString & text ); + + + // Radio button helpers +public: + QButtonGroup * beginRadioGroup(); + + QRadioButton * addRadioButton( const QString & text, int groupId = -1 ); + QRadioButton * addRadioButton( QBoxLayout * parentLayout, const QString & text, int groupId = -1 ); + QRadioButton * addRadioButton( QGridLayout * parentLayout, int layoutColumn, const QString & text, int groupId = -1 ); + QRadioButton * addRadioButton( QGridLayout * parentLayout, int layoutColumn, int layoutSpan, const QString & text, int groupId = -1 ); + +private: + QButtonGroup * radioGroup = nullptr; + + + // Group box helpers +public: + QGroupBox * addGroupBox( const QString & text, QLayout * innerLayout = nullptr ); + QGroupBox * addGroupBox( QBoxLayout * parentLayout, const QString & text, QLayout * innerLayout = nullptr ); + QGroupBox * addGroupBox( QGridLayout * parentLayout, int layoutColumn, const QString & text, QLayout * innerLayout = nullptr ); + QGroupBox * addGroupBox( QGridLayout * parentLayout, int layoutColumn, int layoutSpan, const QString & text, QLayout * innerLayout = nullptr ); + + + // Spin box helpers +public: + QSpinBox * addSpinBox( int minVal, int maxVal, int initVal, int step = 1 ); + QSpinBox * addSpinBox( QBoxLayout * parentLayout, int minVal, int maxVal, int initVal, int step = 1 ); + QSpinBox * addSpinBox( QGridLayout * parentLayout, int layoutColumn, int minVal, int maxVal, int initVal, int step = 1 ); + QSpinBox * addSpinBox( QGridLayout * parentLayout, int layoutColumn, int layoutSpan, int minVal, int maxVal, int initVal, int step = 1 ); + + QDoubleSpinBox * addDoubleSpinBox( double minVal, double maxVal, double initVal, int decimalDigits, double step = 1.0 ); + QDoubleSpinBox * addDoubleSpinBox( QBoxLayout * parentLayout, double minVal, double maxVal, double initVal, int decimalDigits, double step = 1.0 ); + QDoubleSpinBox * addDoubleSpinBox( QGridLayout * parentLayout, int layoutColumn, double minVal, double maxVal, double initVal, int decimalDigits, double step = 1.0 ); + QDoubleSpinBox * addDoubleSpinBox( QGridLayout * parentLayout, int layoutColumn, int layoutSpan, double minVal, double maxVal, double initVal, int decimalDigits, double step = 1.0 ); +}; + +#endif // TOOLDIALOG_H diff --git a/src/ui/UiUtils.cpp b/src/ui/UiUtils.cpp new file mode 100644 index 000000000..fe211ab59 --- /dev/null +++ b/src/ui/UiUtils.cpp @@ -0,0 +1,93 @@ +#include "UiUtils.h" + +#include +#include +#include +#include +#include + +#ifdef Q_OS_WIN32 +#define WIN32_LEAN_AND_MEAN +#include "windows.h" +#endif + +QString UIUtils::applicationDisplayName; +const QString UIUtils::windowTitleSeparator(" - "); + +void UIUtils::setWindowTitle( QWidget * windowWidget, const QString * titlePart1, const QString * titlePart2, const QString * titlePart3 ) +{ + const QString * goodParts[3]; + int nGoodParts = 0; + auto addGoodPart = [&goodParts, &nGoodParts]( const QString * part ) { + if ( part && !part->isEmpty() ) { + goodParts[nGoodParts] = part; + nGoodParts++; + } + }; + addGoodPart( titlePart1 ); + addGoodPart( titlePart2 ); + addGoodPart( titlePart3 ); + + switch( nGoodParts ) { + case 3: + windowWidget->setWindowTitle( *goodParts[0] % windowTitleSeparator % *goodParts[1] % windowTitleSeparator % *goodParts[2] ); + break; + case 2: + windowWidget->setWindowTitle( *goodParts[0] % windowTitleSeparator % *goodParts[1] ); + break; + case 1: + windowWidget->setWindowTitle( *goodParts[0] ); + break; + default: + windowWidget->setWindowTitle( applicationDisplayName ); + break; + } +} + + +double UIUtils::widgetUIScaleFactor( QWidget * widget ) +{ + QWidget * window = widget->window(); + if ( window ) { + QWindow * winHandle = window->windowHandle(); + if ( winHandle ) { + double scaleFactor = winHandle->devicePixelRatio(); + if ( scaleFactor > 0.0 ) + return scaleFactor; + + } else { + // winHandle is null, so the window has not been created yet + + // Let's try the parent window of this window first... + QWidget * parentWindow = window->parentWidget(); + if ( parentWindow ) + return widgetUIScaleFactor( parentWindow ); + + // Last resort: the scale factor of the primary (default) screen. + QScreen * screen = QGuiApplication::primaryScreen(); + if ( screen ) { + double scaleFactor = screen->devicePixelRatio(); + if ( scaleFactor > 0.0 ) + return scaleFactor; + } + + } + } + + return 1.0; +} + +QSize UIUtils::widgetRealSize( QWidget * widget ) +{ +#ifdef Q_OS_WIN32 + RECT wrect; + if ( GetClientRect( (HWND) widget->winId(), &wrect ) ) + return QSize( wrect.right, wrect.bottom ); +#endif + + double scaleFactor = widgetUIScaleFactor( widget ); + if ( scaleFactor != 1.0 ) + return QSize( qRound( widget->width() * scaleFactor ), qRound( widget->height() * scaleFactor ) ); + + return widget->size(); +} diff --git a/src/ui/UiUtils.h b/src/ui/UiUtils.h new file mode 100644 index 000000000..7c6fe1ae4 --- /dev/null +++ b/src/ui/UiUtils.h @@ -0,0 +1,37 @@ +#ifndef UIUTILS_H +#define UIUTILS_H + +#include + +//! +class UIUtils +{ + // Application title +public: + static QString applicationDisplayName; + + static const QString windowTitleSeparator; + +private: + static void setWindowTitle( QWidget * windowWidget, const QString * titlePart1, const QString * titlePart2, const QString * titlePart3 ); +public: + static void setWindowTitle( QWidget * windowWidget ) + { setWindowTitle( windowWidget, nullptr, nullptr, nullptr ); } + static void setWindowTitle( QWidget * windowWidget, const QString & title ) + { setWindowTitle( windowWidget, &title, nullptr, nullptr ); } + static void setWindowTitle( QWidget * windowWidget, const QString & title1, const QString & title2 ) + { setWindowTitle( windowWidget, &title1, &title2, nullptr ); } + static void setWindowTitle( QWidget * windowWidget, const QString & title1, const QString & title2, const QString & title3 ) + { setWindowTitle( windowWidget, &title1, &title2, &title3 ); } + + + // System UI scale +public: + static double widgetUIScaleFactor( QWidget * widget ); + + //! Returns the real size of a QWidget (what's called the client rectangle in Windows) in pixels. + // This is a workaround for QWidget::size(), QWidget::width(), etc. returning real pixels divided by the system UI scale, with all the rounding errors accompanying that. + static QSize widgetRealSize( QWidget * widget ); +}; + +#endif // UIUTILS_H diff --git a/src/ui/about_dialog.cpp b/src/ui/about_dialog.cpp index 06c58aac9..a8a19dce0 100644 --- a/src/ui/about_dialog.cpp +++ b/src/ui/about_dialog.cpp @@ -1,22 +1,19 @@ #include "about_dialog.h" +#include "version.h" -AboutDialog::AboutDialog( QWidget * parent ) - : QDialog( parent ) -{ - ui.setupUi( this ); +#include - setAttribute( Qt::WA_DeleteOnClose ); -#ifdef NIFSKOPE_REVISION - this->setWindowTitle( tr( "About NifSkope %1 (revision %2)" ).arg( NIFSKOPE_VERSION, NIFSKOPE_REVISION ) ); -#else - this->setWindowTitle( tr( "About NifSkope %1" ).arg( NIFSKOPE_VERSION ) ); -#endif +AboutDialog::AboutDialog( QWidget * parent ) + : ToolDialog( parent, + tr( "About %1 (revision %2)" ).arg( APP_NAME_FULL, APP_GIT_BUILD ), + ToolDialog::ApplicationBlocking, 650, 400 ) +{ QString text = tr( R"rhtml(

NifSkope is a tool for opening and editing the NetImmerse file format (NIF).

NifSkope is free software available under a BSD license. - The source is available via GitHub

+ The source is available via GitHub.

The most recent version of NifSkope can be downloaded from the official GitHub release page.

@@ -81,5 +78,34 @@ AboutDialog::AboutDialog( QWidget * parent ) )rhtml" ); - ui.text->setText( text ); + QVBoxLayout * mainLayout = addVBoxLayout( this ); + + QHBoxLayout * infoLayout = addHBoxLayout( mainLayout ); + + QLabel * iconLabel = addLabel( QString(), true ); + infoLayout->addWidget( iconLabel, 0, Qt::AlignLeft | Qt::AlignTop ); + iconLabel->setScaledContents( true ); + iconLabel->setPixmap( QPixmap( ":/res/nifskope.png" ) ); + + QLabel * textLabel = addLabel( text ); + textLabel->setAlignment( Qt::AlignLeft | Qt::AlignTop ); + textLabel->setTextFormat( Qt::TextFormat::RichText ); + textLabel->setWordWrap( true ); + textLabel->setScaledContents( false ); + textLabel->setOpenExternalLinks( true ); + textLabel->setTextInteractionFlags( Qt::TextInteractionFlag::TextBrowserInteraction ); + const int TEXT_MARGIN_HORZ = 8; + const int TEXT_MARGIN_VERT = 6; + textLabel->setContentsMargins( TEXT_MARGIN_HORZ, TEXT_MARGIN_VERT, TEXT_MARGIN_HORZ, 0 ); + + QScrollArea * scrollWidget = new QScrollArea; + infoLayout->addWidget( scrollWidget, 1 ); + scrollWidget->setFrameShape( QFrame::Shape::StyledPanel ); + scrollWidget->setFrameShadow( QFrame::Plain ); + scrollWidget->setBackgroundRole( QPalette::Base ); // White background (at least in Windows theme) + scrollWidget->setWidget( textLabel ); + scrollWidget->setWidgetResizable( true ); + + beginMainButtonLayout( mainLayout ); + addCloseButton( tr("OK") ); } diff --git a/src/ui/about_dialog.h b/src/ui/about_dialog.h index bb859813c..00b6bcdde 100644 --- a/src/ui/about_dialog.h +++ b/src/ui/about_dialog.h @@ -1,20 +1,15 @@ #ifndef ABOUT_H #define ABOUT_H -#include "ui_about_dialog.h" +#include "ToolDialog.h" -#include - -class AboutDialog : public QDialog +class AboutDialog : public ToolDialog { Q_OBJECT public: - AboutDialog( QWidget * parent = nullptr ); - -private: - Ui::AboutDialog ui; + AboutDialog( QWidget * parent ); }; diff --git a/src/ui/about_dialog.ui b/src/ui/about_dialog.ui deleted file mode 100644 index 9514f5d3a..000000000 --- a/src/ui/about_dialog.ui +++ /dev/null @@ -1,172 +0,0 @@ - - - AboutDialog - - - Qt::ApplicationModal - - - - 0 - 0 - 600 - 400 - - - - - 0 - 0 - - - - - 600 - 400 - - - - - 600 - 400 - - - - Dialog - - - true - - - - - 10 - 360 - 581 - 31 - - - - - 0 - 0 - - - - Qt::Horizontal - - - QDialogButtonBox::Ok - - - - - - 10 - 10 - 121 - 121 - - - - - - - :/res/nifskope.png - - - - - - 130 - 10 - 464 - 351 - - - - QFrame::NoFrame - - - true - - - - - 0 - 0 - 464 - 351 - - - - - - - - 450 - 1000 - - - - - - - Qt::RichText - - - false - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - true - - - true - - - - - - - - - - - - - buttonBox - accepted() - AboutDialog - accept() - - - 248 - 254 - - - 157 - 274 - - - - - buttonBox - rejected() - AboutDialog - reject() - - - 316 - 260 - - - 286 - 274 - - - - - diff --git a/src/ui/nifskope.ui b/src/ui/nifskope.ui index 09105d763..610de232d 100644 --- a/src/ui/nifskope.ui +++ b/src/ui/nifskope.ui @@ -92,9 +92,6 @@ - - false - Animation @@ -182,7 +179,7 @@ 0 0 1280 - 21 + 22
@@ -313,15 +310,14 @@ Help - - + @@ -1050,9 +1046,6 @@ background: qradialgradient(spread:pad, cx:0.5, cy:0.5, radius:0.5, fx:0.5, fy:0 - - false - Header @@ -1085,9 +1078,6 @@ background: qradialgradient(spread:pad, cx:0.5, cy:0.5, radius:0.5, fx:0.5, fy:0 true - - true - :/btn/cubeFront16:/btn/cubeFront16 @@ -1139,10 +1129,10 @@ background: qradialgradient(spread:pad, cx:0.5, cy:0.5, radius:0.5, fx:0.5, fy:0 Load View - Load View + Load Saved View - Load View + Load Saved View F8 @@ -1647,10 +1637,10 @@ background: qradialgradient(spread:pad, cx:0.5, cy:0.5, radius:0.5, fx:0.5, fy:0 :/btn/bulb:/btn/bulb - + Lighting Options - + Lighting Options Lighting Options diff --git a/src/ui/settingsdialog.cpp b/src/ui/settingsdialog.cpp index 39c14f92f..7e4ee353c 100644 --- a/src/ui/settingsdialog.cpp +++ b/src/ui/settingsdialog.cpp @@ -12,7 +12,7 @@ //! @file settingsdialog.cpp SettingsDialog SettingsDialog::SettingsDialog( QWidget * parent ) : - QDialog( parent ), + QDialog( parent, Qt::Tool ), ui( new Ui::SettingsDialog ) { ui->setupUi( this ); @@ -21,8 +21,6 @@ SettingsDialog::SettingsDialog( QWidget * parent ) : categories = ui->categoryList; setWindowTitle( tr( "Settings" ) ); - setWindowFlags( Qt::Tool ); - setWindowModality( Qt::WindowModality::ApplicationModal ); installEventFilter( this ); content->addWidget( new SettingsGeneral( this ) ); diff --git a/src/ui/settingsdialog.ui b/src/ui/settingsdialog.ui index b84a6597c..bed9a81de 100644 --- a/src/ui/settingsdialog.ui +++ b/src/ui/settingsdialog.ui @@ -2,6 +2,9 @@ SettingsDialog + + Qt::ApplicationModal + 0 diff --git a/src/ui/widgets/fileselect.cpp b/src/ui/widgets/fileselect.cpp index 0c122bbc9..327f05fbf 100644 --- a/src/ui/widgets/fileselect.cpp +++ b/src/ui/widgets/fileselect.cpp @@ -56,7 +56,7 @@ CompletionAction::CompletionAction( QObject * parent ) : QAction( "Completion of { QSettings cfg; setCheckable( true ); - setChecked( cfg.value( "completion of file names", false ).toBool() ); + setChecked( cfg.value( "completion of file names", true ).toBool() ); connect( this, &CompletionAction::toggled, this, &CompletionAction::sltToggled ); } @@ -85,6 +85,7 @@ FileSelector::FileSelector( Modes mode, const QString & buttonText, QBoxLayout:: action = new QAction( this ); action->setText( buttonText ); + action->setIconText( buttonText ); // To support browse buttons with names like "..." connect( action, &QAction::triggered, this, &FileSelector::browse ); if ( !keySeq.isEmpty() ) { @@ -95,10 +96,11 @@ FileSelector::FileSelector( Modes mode, const QString & buttonText, QBoxLayout:: QToolButton * button = new QToolButton( this ); button->setDefaultAction( action ); + button->setFixedHeight( line->sizeHint().height() + 2 ); // Without the "+ 2" the actual height of the button is smaller than line->sizeHint().height() lay->addWidget( line ); lay->addWidget( button ); - + // setFocusProxy( line ); line->installEventFilter( this ); @@ -114,7 +116,6 @@ FileSelector::FileSelector( Modes mode, const QString & buttonText, QBoxLayout:: connect( timer, &QTimer::timeout, this, &FileSelector::rstState ); } - void FileSelector::setCompletionEnabled( bool x ) { if ( x && !dirmdl ) { @@ -203,31 +204,69 @@ QStringList FileSelector::filter() const return fltr; } +QString getFilterFromFilePath( const QStringList & filters, const QString & path ) +{ + QString defaultResult; + + if ( filters.count() <= 1 ) // No choice anyway... + return defaultResult; + + if ( path.isEmpty() ) + return defaultResult; + + QFileInfo finfo( path ); + if ( finfo.isDir() ) + return defaultResult; + + QString fext = finfo.suffix(); + if ( fext.isEmpty() ) + return defaultResult; + + // Look for a filter with "*." in its last pair of () brackets + QString lookupExt = QStringLiteral("*.") + fext; + for ( const QString & filterEntry : filters ) { + int iExtStart = filterEntry.lastIndexOf( QStringLiteral("(") ); + if ( iExtStart < 0 ) + continue; + iExtStart++; + + int iExtEnd = filterEntry.lastIndexOf( QStringLiteral(")") ); + if ( iExtEnd <= iExtStart ) + continue; + + QStringList filterExtensions = filterEntry.mid( iExtStart, iExtEnd - iExtStart ).split( QStringLiteral(" ") ); + for ( const auto & filterExt : filterExtensions ) { + if ( filterExt.compare( lookupExt, Qt::CaseInsensitive ) == 0 ) + return filterEntry; + } + } + + return defaultResult; +} + void FileSelector::browse() { - QString x; + QString newPath; + QString curPath = file(); + QString startFilter; switch ( Mode ) { case Folder: - x = QFileDialog::getExistingDirectory( this, tr( "Choose a folder" ), file() ); + newPath = QFileDialog::getExistingDirectory( this, tr( "Choose a folder" ), curPath ); break; case LoadFile: // Qt uses ;; as separator if multiple types are available - { - QStringList allfltr = fltr; - x = QFileDialog::getOpenFileName( this, tr( "Choose a file" ), file(), allfltr.join( ";;" ) ); - } + startFilter = getFilterFromFilePath( fltr, curPath ); + newPath = QFileDialog::getOpenFileName( this, tr( "Choose a file" ), curPath, fltr.join( ";;" ), startFilter.isEmpty() ? nullptr : &startFilter ); break; case SaveFile: - { - QStringList saveFltr = fltr; saveFltr.removeAt( 0 ); // Remove "All Files" - x = QFileDialog::getSaveFileName( this, tr( "Choose a file" ), file(), saveFltr.join( ";;" ) ); - } + startFilter = getFilterFromFilePath( fltr, curPath ); + newPath = QFileDialog::getSaveFileName( this, tr( "Choose a file" ), curPath, fltr.join( ";;" ), startFilter.isEmpty() ? nullptr : &startFilter ); break; } - if ( !x.isEmpty() ) { - line->setText( x ); + if ( !newPath.isEmpty() ) { + line->setText( newPath ); activate(); } } diff --git a/src/ui/widgets/inspect.cpp b/src/ui/widgets/inspect.cpp index a793bf8a7..9154a98c7 100644 --- a/src/ui/widgets/inspect.cpp +++ b/src/ui/widgets/inspect.cpp @@ -344,7 +344,7 @@ void InspectView::updateSelection( const QModelIndex & select ) if ( !scene || !nif ) { clear(); } else { - selection = select; + selection = nif->getBlockIndex( select ); Node * node = scene->getNode( nif, selection ); if ( !node ) { diff --git a/src/ui/widgets/nifview.cpp b/src/ui/widgets/nifview.cpp index 61e55e087..1ea61e9a8 100644 --- a/src/ui/widgets/nifview.cpp +++ b/src/ui/widgets/nifview.cpp @@ -33,7 +33,7 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "nifview.h" #include "spellbook.h" -#include "model/basemodel.h" +#include "model/nifmodel.h" #include "model/nifproxymodel.h" #include "model/undocommands.h" @@ -60,15 +60,16 @@ NifTreeView::~NifTreeView() void NifTreeView::setModel( QAbstractItemModel * model ) { - if ( nif ) - disconnect( nif, &BaseModel::dataChanged, this, &NifTreeView::updateConditions ); + if ( baseModel ) + disconnect( baseModel, &BaseModel::dataChanged, this, &NifTreeView::updateConditions ); - nif = qobject_cast( model ); + baseModel = qobject_cast( model ); + nifModel = qobject_cast( model ); QTreeView::setModel( model ); - if ( nif ) - connect( nif, &BaseModel::dataChanged, this, &NifTreeView::updateConditions ); + if ( baseModel ) + connect( baseModel, &BaseModel::dataChanged, this, &NifTreeView::updateConditions ); } void NifTreeView::setRootIndex( const QModelIndex & index ) @@ -93,8 +94,8 @@ void NifTreeView::setRowHiding( bool show ) doRowHiding = !show; - if ( nif ) - connect( nif, &BaseModel::dataChanged, this, &NifTreeView::updateConditions ); + if ( baseModel ) + connect( baseModel, &BaseModel::dataChanged, this, &NifTreeView::updateConditions ); // refresh updateConditionRecurse( rootIndex() ); @@ -110,12 +111,20 @@ bool NifTreeView::isRowHidden( int r, const QModelIndex & index ) const bool NifTreeView::isRowHidden( const NifItem * rowItem ) const { - if ( rowItem && nif ) { - if ( doRowHiding || rowItem->hasTypeCondition() ) { - if ( !nif->evalCondition( rowItem ) ) + if ( baseModel ) { + if ( !rowItem ) + return true; + + // Do full condition check if any of the below is true: + // - "Show Non-applicable Row" option is off; + // - the NifItem's condition only checks the type of the block it belongs to (basically it's a check of file version); + // - the NifItem belongs to the header of NifModel (it's not different from a file version check too). + // Otherwise do only file version check. + if ( doRowHiding || rowItem->hasTypeCondition() || ( nifModel && rowItem->isDescendantOf(nifModel->getHeaderItem()) ) ) { + if ( !baseModel->evalCondition( rowItem ) ) return true; } else { - if ( !nif->evalVersion( rowItem ) ) + if ( !baseModel->evalVersion( rowItem ) ) return true; } } @@ -170,7 +179,7 @@ void NifTreeView::pasteTo( const QModelIndex iDest, const NifValue & srcValue ) if ( iDest.column() != NifModel::ValueCol ) return; - NifItem * item = nif->getItem( iDest ); + NifItem * item = baseModel->getItem( iDest ); if ( !item || item->valueType() != srcValue.type() ) return; @@ -178,13 +187,13 @@ void NifTreeView::pasteTo( const QModelIndex iDest, const NifValue & srcValue ) switch ( item->valueType() ) { case NifValue::tByte: - item->set( srcValue.get( nif, nullptr ) ); + item->set( srcValue.get( baseModel, nullptr ) ); break; case NifValue::tWord: case NifValue::tShort: case NifValue::tFlags: case NifValue::tBlockTypeIndex: - item->set( srcValue.get( nif, nullptr ) ); + item->set( srcValue.get( baseModel, nullptr ) ); break; case NifValue::tStringOffset: case NifValue::tInt: @@ -193,41 +202,41 @@ void NifTreeView::pasteTo( const QModelIndex iDest, const NifValue & srcValue ) case NifValue::tStringIndex: case NifValue::tUpLink: case NifValue::tLink: - item->set( srcValue.get( nif, nullptr ) ); + item->set( srcValue.get( baseModel, nullptr ) ); break; case NifValue::tVector2: case NifValue::tHalfVector2: - item->set( srcValue.get( nif, nullptr ) ); + item->set( srcValue.get( baseModel, nullptr ) ); break; case NifValue::tVector3: case NifValue::tByteVector3: case NifValue::tHalfVector3: - item->set( srcValue.get( nif, nullptr ) ); + item->set( srcValue.get( baseModel, nullptr ) ); break; case NifValue::tVector4: - item->set( srcValue.get( nif, nullptr ) ); + item->set( srcValue.get( baseModel, nullptr ) ); break; case NifValue::tFloat: case NifValue::tHfloat: case NifValue::tNormbyte: - item->set( srcValue.get( nif, nullptr ) ); + item->set( srcValue.get( baseModel, nullptr ) ); break; case NifValue::tColor3: - item->set( srcValue.get( nif, nullptr ) ); + item->set( srcValue.get( baseModel, nullptr ) ); break; case NifValue::tColor4: case NifValue::tByteColor4: - item->set( srcValue.get( nif, nullptr ) ); + item->set( srcValue.get( baseModel, nullptr ) ); break; case NifValue::tQuat: case NifValue::tQuatXYZW: - item->set( srcValue.get( nif, nullptr ) ); + item->set( srcValue.get( baseModel, nullptr ) ); break; case NifValue::tMatrix: - item->set( srcValue.get( nif, nullptr ) ); + item->set( srcValue.get( baseModel, nullptr ) ); break; case NifValue::tMatrix4: - item->set( srcValue.get( nif, nullptr ) ); + item->set( srcValue.get( baseModel, nullptr ) ); break; case NifValue::tString: case NifValue::tSizedString: @@ -236,16 +245,15 @@ void NifTreeView::pasteTo( const QModelIndex iDest, const NifValue & srcValue ) case NifValue::tHeaderString: case NifValue::tLineString: case NifValue::tChar8String: - item->set( srcValue.get( nif, nullptr ) ); + item->set( srcValue.get( baseModel, nullptr ) ); break; default: // Return and do not push to Undo Stack return; } - auto n = static_cast(nif); - if ( n ) - n->undoStack->push( new ChangeValueCommand( iDest, item->value(), srcValue, valueType, n ) ); + if ( nifModel ) + nifModel->undoStack->push( new ChangeValueCommand( iDest, item->value(), srcValue, valueType, nifModel ) ); } void NifTreeView::paste() @@ -264,20 +272,20 @@ void NifTreeView::pasteArray() Q_ASSERT( values.size() == 1 ); auto root = values.at( 0 ); - auto cnt = nif->rowCount( root ); + auto cnt = baseModel->rowCount( root ); ChangeValueCommand::createTransaction(); - nif->setState( BaseModel::Processing ); + baseModel->setState( BaseModel::Processing ); for ( int i = 0; i < cnt && i < valueClipboard->getValues().size(); i++ ) { auto iDest = root.child( i, NifModel::ValueCol ); auto srcValue = valueClipboard->getValues().at( iDest.row() ); pasteTo( iDest, srcValue ); } - nif->restoreState(); + baseModel->restoreState(); if ( cnt > 0 ) - emit nif->dataChanged( root.child( 0, NifModel::ValueCol ), root.child( cnt - 1, NifModel::ValueCol ) ); + emit baseModel->dataChanged( root.child( 0, NifModel::ValueCol ), root.child( cnt - 1, NifModel::ValueCol ) ); } void NifTreeView::drawBranches( QPainter * painter, const QRect & rect, const QModelIndex & index ) const @@ -288,7 +296,7 @@ void NifTreeView::drawBranches( QPainter * painter, const QRect & rect, const QM void NifTreeView::updateConditions( const QModelIndex & topLeft, const QModelIndex & bottomRight ) { - if ( nif->getState() != BaseModel::Default ) + if ( baseModel->getState() != BaseModel::Default ) return; Q_UNUSED( bottomRight ); @@ -298,7 +306,7 @@ void NifTreeView::updateConditions( const QModelIndex & topLeft, const QModelInd void NifTreeView::updateConditionRecurse( const QModelIndex & index ) { - if ( nif->getState() != BaseModel::Default ) + if ( baseModel->getState() != BaseModel::Default ) return; NifItem * item = static_cast(index.internalPointer()); @@ -337,11 +345,7 @@ QModelIndexList NifTreeView::valueIndexList( const QModelIndexList & rows ) cons void NifTreeView::keyPressEvent( QKeyEvent * e ) { - NifModel * nif = nullptr; - NifProxyModel * proxy = nullptr; - - nif = static_cast(model()); - if ( model()->inherits( "NifModel" ) && nif ) { + if ( nifModel ) { // Determine if a block or branch has been copied bool hasBlockCopied = false; if ( e->matches( QKeySequence::Copy ) || e->matches( QKeySequence::Paste ) ) { @@ -363,7 +367,7 @@ void NifTreeView::keyPressEvent( QKeyEvent * e ) auto firstRow = selectedRows.at( 0 ); auto firstValue = valueColumns.at( 0 ); - auto firstRowType = nif->getValue( firstRow ).type(); + auto firstRowType = nifModel->getValue( firstRow ).type(); if ( e->matches( QKeySequence::Copy ) ) { copy(); @@ -374,14 +378,14 @@ void NifTreeView::keyPressEvent( QKeyEvent * e ) && (valueClipboard->getValue().isValid() || valueClipboard->getValues().size() > 0) && !hasBlockCopied ) { // Do row paste if there is no block/branch copied and the NifValue is valid - if ( valueColumns.size() == 1 && nif->rowCount( firstRow ) > 0 ) { + if ( valueColumns.size() == 1 && nifModel->rowCount( firstRow ) > 0 ) { pasteArray(); } else if ( valueClipboard->getValue().isValid() ) { paste(); } return; } else if ( valueColumns.size() == 1 - && firstRow.parent().isValid() && nif->isArray( firstRow.parent() ) + && firstRow.parent().isValid() && nifModel->isArray( firstRow.parent() ) && (firstRowType == NifValue::tUpLink || firstRowType == NifValue::tLink) ) { // Link Array Sorting auto parent = firstRow.parent(); @@ -392,7 +396,7 @@ void NifTreeView::keyPressEvent( QKeyEvent * e ) MOVE_DOWN = 1 } moveDir = MOVE_NONE; - if ( e->key() == Qt::Key_Down && e->modifiers() == Qt::CTRL && row < nif->rowCount( parent ) - 1 ) + if ( e->key() == Qt::Key_Down && e->modifiers() == Qt::CTRL && row < nifModel->rowCount( parent ) - 1 ) moveDir = MOVE_DOWN; else if ( e->key() == Qt::Key_Up && e->modifiers() == Qt::CTRL && row > 0 ) moveDir = MOVE_UP; @@ -401,17 +405,17 @@ void NifTreeView::keyPressEvent( QKeyEvent * e ) // Swap the rows row = row + moveDir; QModelIndex newValue = firstRow.sibling( row, NifModel::ValueCol ); - QVariant v = nif->data( firstValue, Qt::EditRole ); - nif->setData( firstValue, nif->data( newValue, Qt::EditRole ) ); - nif->setData( newValue, v ); + QVariant v = nifModel->data( firstValue, Qt::EditRole ); + nifModel->setData( firstValue, nifModel->data( newValue, Qt::EditRole ) ); + nifModel->setData( newValue, v ); // Change the selected row selectionModel()->select( parent.child( row, 0 ), QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows ); // Add row swap to undo ChangeValueCommand::createTransaction(); - nif->undoStack->push( new ChangeValueCommand( firstValue, nif->getValue( newValue ), nif->getValue( firstValue ), "Link", nif ) ); - nif->undoStack->push( new ChangeValueCommand( newValue, nif->getValue( firstValue ), nif->getValue( newValue ), "Link", nif ) ); + nifModel->undoStack->push( new ChangeValueCommand( firstValue, nifModel->getValue( newValue ), nifModel->getValue( firstValue ), "Link", nifModel ) ); + nifModel->undoStack->push( new ChangeValueCommand( newValue, nifModel->getValue( firstValue ), nifModel->getValue( newValue ), "Link", nifModel ) ); } } } @@ -425,8 +429,10 @@ void NifTreeView::keyPressEvent( QKeyEvent * e ) // TODO: Value clipboard does not get cleared when using the context menu. valueClipboard->clear(); - if ( model()->inherits( "NifModel" ) ) { - nif = static_cast( model() ); + NifModel * nif = nullptr; + NifProxyModel * proxy = nullptr; + if ( nifModel ) { + nif = nifModel; oldidx = currentIndex(); } else if ( model()->inherits( "NifProxyModel" ) ) { proxy = static_cast( model() ); @@ -505,7 +511,7 @@ void NifTreeView::currentChanged( const QModelIndex & current, const QModelIndex { QTreeView::currentChanged( current, last ); - if ( nif ) + if ( baseModel ) updateConditionRecurse( current ); autoExpanded = false; @@ -517,45 +523,53 @@ void NifTreeView::currentChanged( const QModelIndex & current, const QModelIndex void NifTreeView::autoExpandBlock( const QModelIndex & blockIndex ) { - auto mdl = qobject_cast( nif ); - if ( !mdl ) + if ( !nifModel ) return; - auto block = mdl->getItem( blockIndex, false ); - if ( !mdl->isNiBlock(block) ) + auto field = nifModel->field(blockIndex, false); + if ( !field.isTop() ) return; - if ( mdl->inherits(block->name(), "NiTransformInterpolator") || mdl->inherits(block->name(), "NiBSplineTransformInterpolator") ) { - // Auto-Expand NiQuatTransform - autoExpandItem( mdl->getItem(block, "Transform") ); - } else if ( mdl->inherits(block->name(), "NiNode") ) { - // Auto-Expand Children array - autoExpandItem( mdl->getItem(block, "Children") ); - } else if ( mdl->inherits(block->name(), "NiSkinPartition") || mdl->inherits(block->name(), "BSDismemberSkinInstance") ) { - // Auto-Expand skin partitions array - autoExpandItem( mdl->getItem(block, "Partitions") ); - } else { - // Auto-Expand final arrays/compounds - for( int i = block->childCount() - 1; i >= 0; i-- ) { - auto c = block->child(i); - if ( c && !isRowHidden(c) ) { - autoExpandItem( c ); - break; - } + if ( field.isHeader() ) { + // Auto-Expand BS Header + autoExpandItem( field.child("BS Header") ); + return; + + } else if ( field.isBlock() ) { + if ( field.inherits("NiTransformInterpolator", "NiBSplineTransformInterpolator") ) { + // Auto-Expand NiQuatTransform + autoExpandItem( field.child("Transform") ); + return; + } + if ( field.inherits("NiNode") ) { + // Auto-Expand Children array + autoExpandItem( field.child("Children") ); + return; + } + if ( field.inherits("NiSkinPartition", "BSDismemberSkinInstance") ) { + // Auto-Expand skin partitions array + autoExpandItem( field.child("Partitions") ); + return; + } + } + + // Auto-Expand final arrays/compounds + for( int i = field.childCount() - 1; i >= 0; i-- ) { + auto f = field.child(i); + if ( f && !isRowHidden(f.item()) ) { + autoExpandItem( f ); + break; } } } -void NifTreeView::autoExpandItem( const NifItem * item ) +void NifTreeView::autoExpandItem( NifFieldConst field ) { - if ( !item ) - return; - - auto nChildren = item->childCount(); + auto nChildren = field.childCount(); if ( nChildren > 0 && nChildren < 100 ) { autoExpanded = true; blockMouseSelection = true; - expand( nif->itemToIndex(item) ); + expand( field.toIndex() ); } } diff --git a/src/ui/widgets/nifview.h b/src/ui/widgets/nifview.h index d8c11b3f7..34a5496bd 100644 --- a/src/ui/widgets/nifview.h +++ b/src/ui/widgets/nifview.h @@ -104,8 +104,10 @@ protected slots: QStyleOptionViewItem viewOptions() const override final; +public: void autoExpandBlock( const QModelIndex & blockIndex ); - void autoExpandItem( const NifItem * item ); +protected: + void autoExpandItem( NifFieldConst field ); bool doRowHiding = true; bool autoExpanded = false; @@ -121,7 +123,8 @@ protected slots: // because QTreeView treats this as if you'd click on the expanded/collapsed item and drag the cursor to another item. bool blockMouseSelection = false; - class BaseModel * nif = nullptr; + class BaseModel * baseModel = nullptr; + NifModel * nifModel = nullptr; //! Row Copy void copy(); diff --git a/src/ui/widgets/uvedit.cpp b/src/ui/widgets/uvedit.cpp index bd906352e..e2b904321 100644 --- a/src/ui/widgets/uvedit.cpp +++ b/src/ui/widgets/uvedit.cpp @@ -39,6 +39,7 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "gl/gltools.h" #include "model/nifmodel.h" #include "ui/settingsdialog.h" +#include "ui/UiUtils.h" #include "lib/nvtristripwrapper.h" @@ -74,6 +75,7 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #define MAXSCALE 10.0 #define MAXTRANS 10.0 +const QString GEOMETRY_SETTING("UI/UV Editor Geometry"); UVWidget * UVWidget::createEditor( NifModel * nif, const QModelIndex & idx ) { @@ -86,7 +88,11 @@ UVWidget * UVWidget::createEditor( NifModel * nif, const QModelIndex & idx ) return nullptr; } - uvw->showMaximized(); + QSettings settings; + if ( uvw->restoreGeometry( settings.value( GEOMETRY_SETTING ).toByteArray() ) ) + uvw->show(); + else + uvw->showMaximized(); return uvw; } @@ -258,7 +264,7 @@ void UVWidget::paintGL() glPushMatrix(); glLoadIdentity(); - setupViewport( width(), height() ); + setupViewport(); glMatrixMode( GL_MODELVIEW ); glPushMatrix(); @@ -476,12 +482,13 @@ void UVWidget::drawTexCoords() glPopMatrix(); } -void UVWidget::setupViewport( int width, int height ) +void UVWidget::setupViewport() { glMatrixMode( GL_PROJECTION ); glLoadIdentity(); - glViewport( 0, 0, width, height ); + auto vportSize = UIUtils::widgetRealSize( this ); + glViewport( 0, 0, vportSize.width(), vportSize.height() ); glOrtho( glViewRect[0], glViewRect[1], glViewRect[2], glViewRect[3], -10.0, +10.0 ); } @@ -787,6 +794,15 @@ void UVWidget::keyReleaseEvent( QKeyEvent * e ) } } +void UVWidget::closeEvent( QCloseEvent * event ) +{ + QGLWidget::closeEvent( event ); + if ( event->isAccepted() ) { + QSettings settings; + settings.setValue( GEOMETRY_SETTING, saveGeometry() ); + } +} + bool UVWidget::setNifData( NifModel * nifModel, const QModelIndex & nifIndex ) { if ( nif ) { @@ -803,10 +819,7 @@ bool UVWidget::setNifData( NifModel * nifModel, const QModelIndex & nifIndex ) iShape = nifIndex; isDataOnSkin = false; - auto newTitle = tr("UV Editor"); - if (nif) - newTitle += tr(" - ") + nif->getFileInfo().fileName(); - setWindowTitle(newTitle); + UIUtils::setWindowTitle( this, tr("UV Editor"), nif ? nif->getFileInfo().fileName() : QString(), UIUtils::applicationDisplayName ); game = Game::GameManager::get_game(nif->getVersionNumber(), nif->getUserVersion(), nif->getBSVersion()); @@ -977,9 +990,7 @@ bool UVWidget::setTexCoords() if ( !iPoints.isValid() ) return false; - for ( int r = 0; r < nif->rowCount( iPoints ); r++ ) { - tris += triangulate( nif->getArray( iPoints.child( r, 0 ) ) ); - } + tris = triangulateStrips( nif, iPoints ); } else if ( nif->blockInherits( iShape, "BSTriShape" ) ) { if ( !isDataOnSkin ) { tris = nif->getArray( iShape, "Triangles" ); @@ -1309,113 +1320,84 @@ class UVWScaleCommand final : public QUndoCommand float scaleX, scaleY; }; -void UVWidget::scaleSelection() -{ - ScalingDialog * scaleDialog = new ScalingDialog( this ); - - if ( scaleDialog->exec() == QDialog::Accepted ) { - // order does not matter here, since we scale around the center - // don't perform identity transforms - if ( !( scaleDialog->getXScale() == 1.0 && scaleDialog->getYScale() == 1.0 ) ) { - undoStack->push( new UVWScaleCommand( this, scaleDialog->getXScale(), scaleDialog->getYScale() ) ); - } - - if ( !( scaleDialog->getXMove() == 0.0 && scaleDialog->getYMove() == 0.0 ) ) { - undoStack->push( new UVWMoveCommand( this, scaleDialog->getXMove(), scaleDialog->getYMove() ) ); - } - } -} - -ScalingDialog::ScalingDialog( QWidget * parent ) : QDialog( parent ) -{ - grid = new QGridLayout; - setLayout( grid ); - int currentRow = 0; - - grid->addWidget( new QLabel( tr( "Enter scaling factors" ) ), currentRow, 0, 1, -1 ); - currentRow++; - - grid->addWidget( new QLabel( "X: " ), currentRow, 0, 1, 1 ); - spinXScale = new QDoubleSpinBox; - spinXScale->setValue( 1.0 ); - spinXScale->setRange( -MAXSCALE, MAXSCALE ); - grid->addWidget( spinXScale, currentRow, 1, 1, 1 ); - - grid->addWidget( new QLabel( "Y: " ), currentRow, 2, 1, 1 ); - spinYScale = new QDoubleSpinBox; - spinYScale->setValue( 1.0 ); - spinYScale->setRange( -MAXSCALE, MAXSCALE ); - grid->addWidget( spinYScale, currentRow, 3, 1, 1 ); - currentRow++; - - uniform = new QCheckBox; - connect( uniform, &QCheckBox::toggled, this, &ScalingDialog::setUniform ); - uniform->setChecked( true ); - grid->addWidget( uniform, currentRow, 0, 1, 1 ); - grid->addWidget( new QLabel( tr( "Uniform scaling" ) ), currentRow, 1, 1, -1 ); - currentRow++; - - grid->addWidget( new QLabel( tr( "Enter translation amounts" ) ), currentRow, 0, 1, -1 ); - currentRow++; - - grid->addWidget( new QLabel( "X: " ), currentRow, 0, 1, 1 ); - spinXMove = new QDoubleSpinBox; - spinXMove->setValue( 0.0 ); - spinXMove->setRange( -MAXTRANS, MAXTRANS ); - grid->addWidget( spinXMove, currentRow, 1, 1, 1 ); - - grid->addWidget( new QLabel( "Y: " ), currentRow, 2, 1, 1 ); - spinYMove = new QDoubleSpinBox; - spinYMove->setValue( 0.0 ); - spinYMove->setRange( -MAXTRANS, MAXTRANS ); - grid->addWidget( spinYMove, currentRow, 3, 1, 1 ); - currentRow++; - - QPushButton * ok = new QPushButton( tr( "OK" ) ); - grid->addWidget( ok, currentRow, 0, 1, 2 ); - connect( ok, &QPushButton::clicked, this, &ScalingDialog::accept ); - - QPushButton * cancel = new QPushButton( tr( "Cancel" ) ); - grid->addWidget( cancel, currentRow, 2, 1, 2 ); - connect( cancel, &QPushButton::clicked, this, &ScalingDialog::reject ); -} - -float ScalingDialog::getXScale() +UVScaleMoveDialog::UVScaleMoveDialog( UVWidget * parent ) + : ToolDialog( parent, tr("Scale and Translate Selected"), 0 ), + editor( parent ) { - return spinXScale->value(); + QGridLayout * mainLayout = addGridLayout( this ); + + addLabel( mainLayout, 0, -1, tr("Scale:") ); + + beginGridRow( mainLayout ); + auto createScaleBox = [this, mainLayout]( const QString & labelText, int column ) { + addLabel( mainLayout, column, labelText, true ); + return addDoubleSpinBox( mainLayout, column + 1, -MAXSCALE, MAXSCALE, 1.0, 6 ); + }; + boxScaleX = createScaleBox( "X: ", 0 ); + boxScaleY = createScaleBox( "Y: ", 2 ); + + beginGridRow( mainLayout ); + QCheckBox * uniformScaleBox = addCheckBox( mainLayout, 0, -1, tr("Uniform scaling") ); + connect( uniformScaleBox, &QCheckBox::toggled, this, &UVScaleMoveDialog::setUniform ); + uniformScaleBox->setChecked( true ); + + beginGridRow( mainLayout ); + addLabel( mainLayout, 0, -1, tr("Translation:") ); + + beginGridRow( mainLayout ); + auto createTranslationBox = [this, mainLayout]( const QString & labelText, int column ) { + addLabel( mainLayout, column, labelText, true ); + return addDoubleSpinBox( mainLayout, column + 1, -MAXTRANS, MAXTRANS, 0.0, 6 ); + }; + boxMoveX = createTranslationBox( "X: ", 0 ); + boxMoveY = createTranslationBox( "Y: ", 2 ); + + beginGridRow( mainLayout ); + beginMainButtonLayout( mainLayout ); + QPushButton * btnOK = addMainButton( tr("OK"), true ); + connect( btnOK, &QPushButton::clicked, this, &UVScaleMoveDialog::onOkClicked ); + addCloseButton( tr("Cancel") ); } -float ScalingDialog::getYScale() -{ - return spinYScale->value(); -} - -void ScalingDialog::setUniform( bool status ) +void UVScaleMoveDialog::setUniform( bool status ) { // Cast QDoubleSpinBox slot auto dsbValueChanged = static_cast(&QDoubleSpinBox::valueChanged); if ( status == true ) { - connect( spinXScale, dsbValueChanged, spinYScale, &QDoubleSpinBox::setValue ); - spinYScale->setEnabled( false ); - spinYScale->setValue( spinXScale->value() ); + connect( boxScaleX, dsbValueChanged, boxScaleY, &QDoubleSpinBox::setValue ); + boxScaleY->setEnabled( false ); + boxScaleY->setValue( boxScaleX->value() ); } else { - disconnect( spinXScale, dsbValueChanged, spinYScale, &QDoubleSpinBox::setValue ); - spinYScale->setEnabled( true ); + disconnect( boxScaleX, dsbValueChanged, boxScaleY, &QDoubleSpinBox::setValue ); + boxScaleY->setEnabled( true ); } } -// 1 unit corresponds to 2 grid squares -float ScalingDialog::getXMove() +void UVScaleMoveDialog::onOkClicked() { - return spinXMove->value() / 2.0; + // order does not matter here, since we scale around the center + // don't perform identity transforms + double scaleX = boxScaleX->value(); + double scaleY = boxScaleY->value(); + if ( scaleX != 1.0 || scaleY != 1.0 ) + editor->undoStack->push( new UVWScaleCommand( editor, scaleX, scaleY ) ); + + double moveX = boxMoveX->value(); + double moveY = boxMoveY->value(); + if ( moveX != 0.0 || moveY != 0.0 ) + editor->undoStack->push( new UVWMoveCommand( editor, moveX, moveY ) ); + + close(); } -float ScalingDialog::getYMove() +void UVWidget::scaleSelection() { - return spinYMove->value() / 2.0; + auto scaleDialog = new UVScaleMoveDialog( this ); + scaleDialog->open( true ); } + //! A class to perform rotation of UV coordinates class UVWRotateCommand final : public QUndoCommand { @@ -1504,14 +1486,35 @@ class UVWRotateCommand final : public QUndoCommand float rotation; }; -void UVWidget::rotateSelection() +UVRotateDialog::UVRotateDialog( UVWidget * parent ) + : ToolDialog( parent, tr("Rotate Selected"), 0 ), + editor( parent ) { - bool ok; - float rotateFactor = QInputDialog::getDouble( this, "NifSkope", tr( "Enter rotation angle" ), 0.0, -360.0, 360.0, 2, &ok ); + QVBoxLayout * mainLayout = addVBoxLayout( this ); - if ( ok ) { - undoStack->push( new UVWRotateCommand( this, rotateFactor ) ); - } + QHBoxLayout * inputLayout = addHBoxLayout( mainLayout ); + addLabel( inputLayout, tr("Rotation angle:"), true ); + boxAngle = addDoubleSpinBox( inputLayout, -360.0, 360.0, 0.0, 2 ); + + beginMainButtonLayout( mainLayout ); + QPushButton * btnOK = addMainButton( tr("OK"), true ); + connect( btnOK, &QPushButton::clicked, this, &UVRotateDialog::onOkClicked ); + addCloseButton( tr("Cancel") ); +} + +void UVRotateDialog::onOkClicked() +{ + double angle = boxAngle->value(); + if ( angle != 0.0 ) + editor->undoStack->push( new UVWRotateCommand( editor, angle ) ); + + close(); +} + +void UVWidget::rotateSelection() +{ + auto rotateDialog = new UVRotateDialog( this ); + rotateDialog->open( true ); } void UVWidget::getTexSlots() diff --git a/src/ui/widgets/uvedit.h b/src/ui/widgets/uvedit.h index e108cc204..59e2f5d2a 100644 --- a/src/ui/widgets/uvedit.h +++ b/src/ui/widgets/uvedit.h @@ -42,6 +42,7 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include +#include "ui/ToolDialog.h" class NifModel; class TexCache; @@ -108,6 +109,8 @@ class UVWidget final : public QGLWidget void keyPressEvent( QKeyEvent * ) override final; void keyReleaseEvent( QKeyEvent * ) override final; + void closeEvent( QCloseEvent * event ) override final; + public slots: //! Does the selection contain this vertex? bool isSelected( int index ); @@ -180,7 +183,7 @@ protected slots: void drawTexCoords(); - void setupViewport( int width, int height ); + void setupViewport(); void updateViewRect( int width, int height ); bool bindTexture( const QString & filename, const Game::GameMode game ); bool bindTexture( const QModelIndex & iSource, const Game::GameMode game ); @@ -234,6 +237,9 @@ protected slots: QUndoStack * undoStack; + friend class UVScaleMoveDialog; + friend class UVRotateDialog; + friend class UVWSelectCommand; friend class UVWMoveCommand; friend class UVWScaleCommand; @@ -252,34 +258,41 @@ protected slots: }; //! Dialog for getting scaling factors -class ScalingDialog final : public QDialog +class UVScaleMoveDialog final : public ToolDialog { Q_OBJECT public: - ScalingDialog( QWidget * parent = nullptr ); + UVScaleMoveDialog( UVWidget * parent ); -protected: - /* - QVBoxLayout * vbox; - QHBoxLayout * hbox; - QHBoxLayout * btnbox; - */ - QGridLayout * grid; - QDoubleSpinBox * spinXScale; - QDoubleSpinBox * spinYScale; - QCheckBox * uniform; - QDoubleSpinBox * spinXMove; - QDoubleSpinBox * spinYMove; +protected slots: + void setUniform( bool status ); + void onOkClicked(); -public slots: - float getXScale(); - float getYScale(); - float getXMove(); - float getYMove(); +private: + UVWidget * editor; + + QDoubleSpinBox * boxScaleX = nullptr; + QDoubleSpinBox * boxScaleY = nullptr; + QDoubleSpinBox * boxMoveX = nullptr; + QDoubleSpinBox * boxMoveY = nullptr; +}; + +//! Dialog for rotation angle +class UVRotateDialog final : public ToolDialog +{ + Q_OBJECT + +public: + UVRotateDialog( UVWidget * parent ); protected slots: - void setUniform( bool status ); + void onOkClicked(); + +private: + UVWidget * editor; + + QDoubleSpinBox * boxAngle = nullptr; }; #endif diff --git a/src/version.cpp b/src/version.cpp deleted file mode 100644 index 80c2bfed2..000000000 --- a/src/version.cpp +++ /dev/null @@ -1,514 +0,0 @@ -/***** BEGIN LICENSE BLOCK ***** - -BSD License - -Copyright (c) 2005-2015, NIF File Format Library and Tools -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: -1. Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. -2. Redistributions in binary form must reproduce the above copyright -notice, this list of conditions and the following disclaimer in the -documentation and/or other materials provided with the distribution. -3. The name of the NIF File Format Library and Tools project may not be -used to endorse or promote products derived from this software -without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR -IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES -OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT -NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -***** END LICENCE BLOCK *****/ - -#include "version.h" - -#include "QDebug" - - -//! @file version.cpp %NifSkope version number management - -// Number of version parts (Default = 3) -int NifSkopeVersion::numParts = 3; - -NifSkopeVersion::NifSkopeVersion( const QString & ver ) - : rawVersion( ver ), displayVersion( rawToDisplay( ver, true ) ) -{ - -} - -NifSkopeVersion::NifSkopeVersion( const NifSkopeVersion & other ) - : rawVersion( other.rawVersion ), displayVersion( other.displayVersion ) -{ - -} - -NifSkopeVersion::NifSkopeVersion() -{ - -} - -NifSkopeVersion::~NifSkopeVersion() -{ - -} - -QList NifSkopeVersion::parts() const -{ - return versionParts( rawVersion, numParts ); -} - -QString NifSkopeVersion::majMin() const -{ - return rawToMajMin( rawVersion ); -} - -int NifSkopeVersion::hex() const -{ - return hexVersion( versionParts( rawVersion ) ); -} - - -void NifSkopeVersion::setNumParts( int num ) -{ - if ( num < 8 && num > 1 ) - numParts = num; -} - -int NifSkopeVersion::hexVersion( QString ver ) -{ - return hexVersion( versionParts( ver ) ); -} - -int NifSkopeVersion::hexVersion( QList majMinRev ) -{ - // This can only work up to but not including 32-bit values - // i.e. in this case a 4th value ( n << 24 ) is possible - // but I've decided to leave it as 3 parts only, to reflect - // Qt's own QT_VERSION_CHECK macro. - // - // Granularity past MAJ.MIN.REV will require a NifSkopeVersion object - - return (majMinRev[0] << 16) | (majMinRev[1] << 8) | (majMinRev[2]); -} - - -int NifSkopeVersion::compare( const QString & ver1, const QString & ver2, int parts ) -{ - int oldNumParts = numParts; - - // Set granularity - NifSkopeVersion::setNumParts( parts ); - - NifSkopeVersion v1( ver1 ); - NifSkopeVersion v2( ver2 ); - - int ret = 0; - - if ( v1 < v2 ) - ret = -1; - else if ( v1 == v2 ) - ret = 0; - else - ret = 1; - - // Reset granularity - NifSkopeVersion::setNumParts( oldNumParts ); - - return ret; -} - -int NifSkopeVersion::compare( const QString & ver1, const QString & ver2 ) -{ - return compare( ver1, ver2, numParts ); -} - - -bool NifSkopeVersion::compareGreater( const QString & ver1, const QString & ver2, int parts ) -{ - int oldNumParts = numParts; - - // Set granularity - NifSkopeVersion::setNumParts( parts ); - - NifSkopeVersion v1( ver1 ); - NifSkopeVersion v2( ver2 ); - - bool result = v1 > v2; - - // Reset granularity - NifSkopeVersion::setNumParts( oldNumParts ); - - return result; -} - -bool NifSkopeVersion::compareGreater( const QString & ver1, const QString & ver2 ) -{ - return compareGreater( ver1, ver2, numParts ); -} - - -bool NifSkopeVersion::compareLess( const QString & ver1, const QString & ver2, int parts ) -{ - int oldNumParts = numParts; - - // Set granularity - NifSkopeVersion::setNumParts( parts ); - - NifSkopeVersion v1( ver1 ); - NifSkopeVersion v2( ver2 ); - - bool result = v1 < v2; - - // Reset granularity - NifSkopeVersion::setNumParts( oldNumParts ); - - return result; -} - -bool NifSkopeVersion::compareLess( const QString & ver1, const QString & ver2 ) -{ - return compareLess( ver1, ver2, numParts ); -} - - -QString NifSkopeVersion::rawToDisplay( const QString & ver, bool showStage /* = false */, bool showDev /* = false */ ) -{ - // Explicitly call all parts (i.e. 7) - QList verList = versionParts( ver, 7 ); - - if ( verList.isEmpty() ) - return QString(); - - QString verString = QString( "%1.%2.%3" ).arg( verList[0] ).arg( verList[1] ).arg( verList[2] ); - QString stage, dev; - - QList stages { "Dev", "Alpha", "Beta", "RC", "" }; - QList devs { "Dev", "Post" }; - - if ( showStage ) { - stage = stages.value( verList.value( 3, 4 ), "" ); - int stageVer = verList.value( 4, -1 ); - - if ( stage == "Dev" && verList[2] == 0 ) - verString = QString( "%1.%2" ).arg( verList[0] ).arg( verList[1] ); - - if ( !stage.isEmpty() && stageVer > 0 ) { - // Append Stage - verString = QString( "%1 %2 %3" ).arg( verString ).arg( stage ).arg( stageVer ); - - if ( showDev ) { - dev = devs.value( verList.value( 5, -1 ), "" ); - int devVer = verList.value( 6, -1 ); - - if ( !dev.isEmpty() && devVer > 0 ) { - // Append Dev - verString = QString( "%1 %2 %3" ).arg( verString ).arg( dev ).arg( devVer ); - } - } - } - } - - return verString; -} - - -QString NifSkopeVersion::rawToMajMin( const QString & ver ) -{ - QList parts = versionParts( ver, 2 ); - - return QString( "%1.%2" ).arg( parts[0] ).arg( parts[1] ); -} - - -QList NifSkopeVersion::versionParts( const QString & ver, int parts /* = 7 */ ) -{ - QList verList; - - if ( formatVersion( ver, verList, parts ) ) - return verList; - - return QList(); -} - - -bool NifSkopeVersion::formatVersion( const QString & ver, QList & verNums, int parts ) -{ - QString v = ver; - - if ( ver.isEmpty() ) - v = "0.0.0"; - - if ( parts > 7 || parts < 2 ) - parts = 3; - - QStringList verParts = v.split( "." ); - QList rawVersionNums; - - // The version string must at least have one period e.g. "1.2" - if ( verParts.count() < 2 ) - verParts = QStringList( { "0", "0", "0" } ); - - QString major = verParts[0]; - QString minor = verParts[1]; - // Deal with missing .0, e.g. "1.2" instead of "1.2.0" - QString rev = verParts.value( 2, "0" ); - - bool majIsNum; - bool minIsNum; - - int majNum = major.toInt( &majIsNum ); - int minNum = minor.toInt( &minIsNum ); - - if ( !majIsNum || !minIsNum ) - return false; - - // Combine Version parts - rawVersionNums.append( { majNum, minNum } ); - - // Confirm fourth part begins with "dev" or "post" - // and not "a", "b", or "rc" (an invalid naming scheme) - // dev = 0, post = 1 - QString dev = verParts.value( 3, "" ); - QString devVer = "-1"; - int devCode = -1; - - if ( dev.startsWith( "a" ) || dev.startsWith( "b" ) || dev.startsWith( "rc" ) ) { - // Invalid version format - // Valid: 1.2.0a1.dev1 - // Invalid: 1.2.0.a1.dev1 - // Remove dot here to compensate - rev += dev; - // Reassign dev to 4th value - dev = verParts.value( 4, "" ); - } - - if ( dev.startsWith( "dev" ) || dev.startsWith( "post" ) ) { - // Normal developmental suffix - if ( dev.startsWith( "dev" ) ) { - devCode = 0; - devVer = dev.mid( 3 ); - } else { - devCode = 1; - devVer = dev.mid( 4 ); - } - } - - int devNum = devVer.toInt(); - - // Stage Code - // 0 = dev (pre-alpha) - // 1 = alpha - // 2 = beta - // 3 = rc - // 4 = final - int stageCode = 4; - QString stageVer = "-1"; - - // Check if Revision has an a, b, rc appended - bool isFinal; - rev.toInt( &isFinal ); - if ( !isFinal && rev >= 0 ) { - // Pre-Release Build - QStringList revParts; // "0a1" -> ("0", "1") - QString revPart; // "0a1" -> "0" - QString stagePart; // "0a1" -> "a" - QString stageVerPart; // "0a1" -> "1" - - if ( rev.startsWith( "dev" ) ) { - // Pre-Alpha Dev - // e.g. 1.2.dev1, not 1.2.0dev1, not 1.2.0.dev1 - stagePart = "dev"; - stageVer = rev.mid( 3 ); - stageCode = 0; - // Unset rev - rev = "0"; - } else if ( rev.length() > 1 ) { - // Pre-Release - if ( rev.contains( "a" ) ) { - stagePart = "a"; - stageCode = 1; - } else if ( rev.contains( "b" ) ) { - stagePart = "b"; - stageCode = 2; - } else if ( rev.contains( "rc" ) ) { - stagePart = "rc"; - stageCode = 3; - } - - // Splitting at stagePart gives you a list with the rev and stageVer - // e.g. "0a1" becomes ("0", "1") - revParts = rev.split( stagePart, QString::SkipEmptyParts ); - // This is the revision number without e.g. "a1" appended - revPart = revParts.value( 0, "0" ); - // This is the version of the Alpha/Beta/RC - stageVerPart = revParts.value( 1, "1" ); - - // Set Revision and Stage Version - rev = revPart; - stageVer = stageVerPart; - } - } - - int stageNum = stageVer.toInt(); - - // Append Revision part - bool revIsNum; - int revNum = rev.toInt( &revIsNum ); - - if ( !revIsNum ) - return false; - - rawVersionNums.append( { revNum, stageCode, stageNum, devCode, devNum } ); - verNums = rawVersionNums.mid( 0, parts ); - - return true; -} - - -// Using std::list here removes the need for the for loop as it -// has its own operator overloads already. Yet best not rely on it. - -bool NifSkopeVersion::operator<(const NifSkopeVersion & other) const -{ - QList verParts1, verParts2; - - verParts1 = versionParts( rawVersion, numParts ); - verParts2 = versionParts( other.rawVersion, numParts ); - - for ( int i = 0; i < numParts; i++ ) { - if ( verParts1[i] == verParts2[i] ) - continue; - - return verParts1[i] < verParts2[i]; - } - - return false; -} - -bool NifSkopeVersion::operator<=(const NifSkopeVersion & other) const -{ - QList verParts1, verParts2; - - verParts1 = versionParts( rawVersion, numParts ); - verParts2 = versionParts( other.rawVersion, numParts ); - - for ( int i = 0; i < numParts; i++ ) { - if ( verParts1[i] == verParts2[i] ) - continue; - - return verParts1[i] < verParts2[i]; - } - - return true; -} - -bool NifSkopeVersion::operator==(const NifSkopeVersion & other) const -{ - QList verParts1, verParts2; - - verParts1 = versionParts( rawVersion, numParts ); - verParts2 = versionParts( other.rawVersion, numParts ); - - for ( int i = 0; i < numParts; i++ ) { - if ( verParts1[i] != verParts2[i] ) - return false; - } - - return true; -} - -bool NifSkopeVersion::operator!=(const NifSkopeVersion & other) const -{ - QList verParts1, verParts2; - - verParts1 = versionParts( rawVersion, numParts ); - verParts2 = versionParts( other.rawVersion, numParts ); - - for ( int i = 0; i < numParts; i++ ) { - if ( verParts1[i] != verParts2[i] ) - return true; - } - - return false; -} - -bool NifSkopeVersion::operator>( const NifSkopeVersion & other ) const -{ - QList verParts1, verParts2; - - verParts1 = versionParts( rawVersion, numParts ); - verParts2 = versionParts( other.rawVersion, numParts ); - - for ( int i = 0; i < numParts; i++ ) { - if ( verParts1[i] == verParts2[i] ) - continue; - - return verParts1[i] > verParts2[i]; - } - - return false; -} - -bool NifSkopeVersion::operator>=(const NifSkopeVersion & other) const -{ - QList verParts1, verParts2; - - verParts1 = versionParts( rawVersion, numParts ); - verParts2 = versionParts( other.rawVersion, numParts ); - - for ( int i = 0; i < numParts; i++ ) { - if ( verParts1[i] == verParts2[i] ) - continue; - - return verParts1[i] > verParts2[i]; - } - - return true; -} - -bool NifSkopeVersion::operator<(const QString & other) const -{ - return operator<( NifSkopeVersion( other ) ); -} - -bool NifSkopeVersion::operator<=(const QString & other) const -{ - return operator<=( NifSkopeVersion( other ) ); -} - -bool NifSkopeVersion::operator==(const QString & other) const -{ - return operator==( NifSkopeVersion( other ) ); -} - -bool NifSkopeVersion::operator!=(const QString & other) const -{ - return operator!=( NifSkopeVersion( other ) ); -} - -bool NifSkopeVersion::operator>( const QString & other ) const -{ - return operator>( NifSkopeVersion( other ) ); -} - -bool NifSkopeVersion::operator>=(const QString & other) const -{ - return operator>=( NifSkopeVersion( other ) ); -} - - -QDebug operator<<( QDebug dbg, const NifSkopeVersion & ver ) -{ - ver.setNumParts( 7 ); - return dbg.space() << ver.rawVersion << ver.displayVersion << ver.parts(); -} diff --git a/src/version.h b/src/version.h index a9cd5acc1..92d659bfd 100644 --- a/src/version.h +++ b/src/version.h @@ -1,288 +1,20 @@ -/***** BEGIN LICENSE BLOCK ***** - -BSD License - -Copyright (c) 2005-2015, NIF File Format Library and Tools -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: -1. Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. -2. Redistributions in binary form must reproduce the above copyright -notice, this list of conditions and the following disclaimer in the -documentation and/or other materials provided with the distribution. -3. The name of the NIF File Format Library and Tools project may not be -used to endorse or promote products derived from this software -without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR -IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES -OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT -NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -***** END LICENCE BLOCK *****/ - -#ifndef VERSION_H -#define VERSION_H - -#include -#include -#include -#include - - -/*! - * @file version.h NifSkopeVersion - * @author jonwd7 - * @date 2014-06-06 - * - * @see Github: https://github.com/niftools/nifskope/issues/61 - */ - - -// Forward declarations -class QDebug; - - -//! Define current NifSkope version as hex int -#define NIFSKOPE_VERSION_HEX NifSkopeVersion::hexVersion( NIFSKOPE_VERSION ) - - -/*! Encapsulates application version strings into comparable objects and provides static convenience - * functions for raw strings. - * - * For comparison purposes, such as for migrating QSettings between versions, - * or removing deprecated QSettings. - * - * For formatting purposes, such as display strings for window titles. - */ -class NifSkopeVersion final -{ - -public: - NifSkopeVersion( const QString & ver ); - NifSkopeVersion( const NifSkopeVersion & other ); - ~NifSkopeVersion(); - - //! Raw string as stored in VERSION file or registry - const QString rawVersion; - //! Display string formatted for readability - const QString displayVersion; - - //! Instance version of NifSkopeVersion::versionParts() - QList parts() const; - //! Instance version of NifSkopeVersion::rawToMajMin() - QString majMin() const; - //! Instance version of NifSkopeVersion::hexVersion() - int hex() const; - - /*! Compare two strings beyond MAJ.MIN.REV granularity - * - * Max of 7 parts: - * 0 = Major - * 1 = Minor - * 2 = Revision - * 3 = Stage Code (Alpha, Beta, RC, Final) - * 4 = Stage Version - * 5 = Dev Code (dev, post) - * 6 = Dev Version - * - * @param parts Number of parts - * @return void - * - * @note This sets NifSkopeVersion::numParts (static) for *all* NifSkopeVersion objects. - * @code - * // Default numParts = 3 - * NifSkopeVersion a( "1.2.0" ); - * NifSkopeVersion b( "1.2.0a1" ); - * - * qDebug() << (a > b); // False - * - * NifSkopeVersion::setNumParts( 5 ); - * - * qDebug() << (a > b); // True - * @endcode - */ - static void setNumParts( int num ); - - /*! Integer representation of version string - * represented in hex e.g. `"1.2.1" -> 0x010201 -> 66049` - */ - static int hexVersion( const QString ); - static int hexVersion( const QList ); - - /*! Compare two strings beyond MAJ.MIN.REV granularity - * - * @param[in] ver1 Version 1 - * @param[in] ver2 Version 2 - * @param parts Number of parts to compare - * @return `{-1, 0, 1}` meaning: `{ver1 < ver2, ver1 == ver2, ver1 > ver2}` - */ - static int compare( const QString & ver1, const QString & ver2, int parts ); - static int compare( const QString & ver1, const QString & ver2 ); - - /*! Compare two strings beyond MAJ.MIN.REV granularity - * - * @param[in] ver1 Version 1 - * @param[in] ver2 Version 2 - * @param parts Number of parts to compare - * @return True if `ver1 > ver2` - */ - static bool compareGreater( const QString & ver1, const QString & ver2, int parts ); - static bool compareGreater( const QString & ver1, const QString & ver2 ); - - /*! Compare two strings beyond MAJ.MIN.REV granularity - * - * @param[in] ver1 Version 1 - * @param[in] ver2 Version 2 - * @param parts Number of parts to compare - * @return True if `ver1 < ver2` - */ - static bool compareLess( const QString & ver1, const QString & ver2, int parts ); - static bool compareLess( const QString & ver1, const QString & ver2 ); - - /*! Version string for display - * - * @param[in] ver Version string - * @param showStage (false) Whether to show release stage e.g. "1.2.0 Alpha 1" - * @param showDev (false) Whether to show dev release e.g. "1.2.0 Alpha 1 Dev 1" - * @return Display string for `ver`, optionally formatted with stage/dev information - */ - static QString rawToDisplay( const QString & ver, bool showStage = false, bool showDev = false ); - - /*! Version string for MAJ.MIN format - * - * @param[in] ver Version string - * @return `ver` formatted to MAJ.MIN format, e.g. 1.2 - */ - static QString rawToMajMin( const QString & ver ); - - /*! Version parts for any version string - * - * @param[in] ver Version string - * @param parts (7) Number of parts to return - * @return Version parts of length `parts` - */ - static QList versionParts( const QString & ver, int parts = 7 ); - - /*! Pass by reference a QStringList for the version parts. - * - * Some examples of version strings: - * - * Valid - * "1.0.0", - * "1.0.1a1", - * "1.0.2b1.dev1", - * "1.1.dev1", - * "1.1.12a1", - * "1.1.12.post1" - * - * Invalid, but compensated for - * "1.1.3.a1.dev2", - * "1.2.0a.dev1", - * "1.3.0a", - * "1.4.0rc" - * - * @param[in] ver Version string - * @param[out] verNums Version string encoded as a list of integers - * @param parts (3) Number of parts to return - * @return True if valid string, false if invalid string - */ - static bool formatVersion( const QString & ver, QList & verNums, int parts = 3 ); - - // Compare two NifSkopeVersions - bool operator<(const NifSkopeVersion & other) const; - bool operator<=(const NifSkopeVersion & other) const; - bool operator==(const NifSkopeVersion & other) const; - bool operator!=(const NifSkopeVersion & other) const; - bool operator>(const NifSkopeVersion & other) const; - bool operator>=(const NifSkopeVersion & other) const; - - // Compare a NifSkopeVersion to a QString - bool operator<(const QString & other) const; - bool operator<=(const QString & other) const; - bool operator==(const QString & other) const; - bool operator!=(const QString & other) const; - bool operator>(const QString & other) const; - bool operator>=(const QString & other) const; - -protected: - NifSkopeVersion(); - - //! Number of version parts (Default = 3) \note Global across all instances. - static int numParts; -}; - -/*! QDebug operator for NifSkopeVersion - * - * Prints rawVersion, displayVersion, parts() - * e.g. "1.2.0a1.dev19" "1.2.0 Alpha 1" (1, 2, 0, 1, 1, 0, 19) - */ -QDebug operator<<(QDebug dbg, const NifSkopeVersion & ver); - - -static const QHash migrateTo1_2 = { - { "Export Settings/export_culling", "Export Settings/Export Culling" }, - { "auto sanitize", "File/Auto Sanitize" }, - { "last load", "File/Last Load" }, { "last save", "File/Last Save" }, - { "fsengine/archives", "FSEngine/Archives" }, - { "enable animations", "GLView/Enable Animations" }, { "LOD/LOD Level", "GLView/LOD Level" }, - { "loop animation", "GLView/Loop Animation" }, { "perspective", "GLView/Perspective" }, - { "play animation", "GLView/Play Animation" }, { "switch animation", "GLView/Switch Animation" }, - { "view action", "GLView/View Action" }, - { "JPEG/Quality", "JPEG/Quality" }, - { "Render Settings/Anti Aliasing", "Render Settings/Anti Aliasing" }, { "Render Settings/Background", "Render Settings/Background" }, - { "Render Settings/Cull Expression", "Render Settings/Cull Expression" }, { "Render Settings/Cull Nodes By Name", "Render Settings/Cull Nodes By Name" }, - { "Render Settings/Cull Non Textured", "Render Settings/Cull Non Textured" }, { "Render Settings/Draw Axes", "Render Settings/Draw Axes" }, - { "Render Settings/Draw Collision Geometry", "Render Settings/Draw Collision Geometry" }, { "Render Settings/Draw Constraints", "Render Settings/Draw Constraints" }, - { "Render Settings/Draw Furniture Markers", "Render Settings/Draw Furniture Markers" }, { "Render Settings/Draw Nodes", "Render Settings/Draw Nodes" }, - { "Render Settings/Enable Shaders", "Render Settings/Enable Shaders" }, { "Render Settings/Foreground", "Render Settings/Foreground" }, - { "Render Settings/Highlight", "Render Settings/Highlight" }, - { "Render Settings/Show Hidden Objects", "Render Settings/Show Hidden Objects" }, { "Render Settings/Show Stats", "Render Settings/Show Stats" }, - { "Render Settings/Texture Alternatives", "Render Settings/Texture Alternatives" }, { "Render Settings/Texture Folders", "Render Settings/Texture Folders" }, - { "Render Settings/Texturing", "Render Settings/Texturing" }, { "Render Settings/Up Axis", "Render Settings/Up Axis" }, - { "Settings/Language", "Settings/Language" }, { "Settings/Startup Version", "Settings/Startup Version" }, - { "hide condition zero", "UI/Hide Mismatched Rows" }, { "realtime condition updating", "UI/Realtime Condition Updating" }, - { "XML Checker/Directory", "XML Checker/Directory" }, { "XML Checker/Recursive", "XML Checker/Recursive" }, - { "XML Checker/Threads", "XML Checker/Threads" }, { "XML Checker/check kf", "XML Checker/Check KF" }, - { "XML Checker/check kfm", "XML Checker/Check KFM" }, { "XML Checker/check nif", "XML Checker/Check NIF" }, - { "XML Checker/report errors only", "XML Checker/Report Errors Only" }, - { "import-export/3ds/File Name", "Import-Export/3DS/File Name" }, { "import-export/obj/File Name", "Import-Export/OBJ/File Name" }, - { "spells/Block/Remove By Id/match expression", "Spells/Block/Remove By Id/Match Expression" }, - { "last texture path", "Spells/Texture/Choose/Last Texture Path" }, - { "spells/Texture/Export Template/Antialias", "Spells/Texture/Export Template/Antialias" }, - { "spells/Texture/Export Template/File Name", "Spells/Texture/Export Template/File Name" }, - { "spells/Texture/Export Template/Image Size", "Spells/Texture/Export Template/Image Size" }, - { "spells/Texture/Export Template/Wire Color", "Spells/Texture/Export Template/Wire Color" }, - { "spells/Texture/Export Template/Wrap Mode", "Spells/Texture/Export Template/Wrap Mode" }, - { "version", "Version" } -}; - -static const QHash migrateTo2_0 = { - { "Export Settings/Export Culling", "Export Settings/Export Culling" }, - { "File/Recent File List", "File/Recent File List" }, - { "File/Auto Sanitize", "File/Auto Sanitize" }, - { "File/Last Load", "File/Last Load" }, { "File/Last Save", "File/Last Save" }, - { "FSEngine/Archives", "FSEngine/Archives" }, - { "Render Settings/Anti Aliasing", "Render Settings/Anti Aliasing" }, - { "Render Settings/Texturing", "Render Settings/Texturing" }, - { "Render Settings/Enable Shaders", "Render Settings/Enable Shaders" }, - { "Render Settings/Background", "Render Settings/Background" }, - { "Render Settings/Foreground", "Render Settings/Foreground" }, - { "Render Settings/Highlight", "Render Settings/Highlight" }, - { "Render Settings/Texture Alternatives", "Render Settings/Texture Alternatives" }, - { "Render Settings/Texture Folders", "Render Settings/Texture Folders" }, - { "Render Settings/Up Axis", "Render Settings/Up Axis" }, - { "Settings/Language", "Settings/Language" }, { "Settings/Startup Version", "Settings/Startup Version" }, -}; - - -#endif +// DO NOT EDIT BY HAND. Generated by ../version.py. + +#ifndef VERSION_H +#define VERSION_H + +#define APP_VER_MAJOR 2 +#define APP_VER_MINOR 0 +#define APP_VER_REVISION 9 +#define APP_VER_BUILD 4 + +#define APP_VER_SHORT "2.0.9.4" +#define APP_VER_FULL "2.0 Dev 9d (Gavrant)" + +#define APP_GIT_BUILD "d5fe49c" + +#define APP_COMPANY "NifTools" +#define APP_NAME "NifSkope" +#define APP_NAME_FULL "NifSkope 2.0 Dev 9d (Gavrant)" + +#endif // VERSION_H diff --git a/version.py b/version.py new file mode 100644 index 000000000..ee79ae521 --- /dev/null +++ b/version.py @@ -0,0 +1,108 @@ +import os.path as os_path + +APP_VER_MAJOR = 2 +APP_VER_MINOR = 0 +APP_VER_REVISION = 9 +APP_VER_BUILD = 4 + +APP_VER_SHORT = f"{APP_VER_MAJOR}.{APP_VER_MINOR}.{APP_VER_REVISION}.{APP_VER_BUILD}" + +build_suffix = "" +if APP_VER_BUILD > 0: + if APP_VER_BUILD > 26: + raise Exception(f"The build value ({APP_VER_BUILD}) is too high") + build_suffix = chr(97 + APP_VER_BUILD - 1) # 'a' + (APP_VER_BUILD - 1) +APP_VER_FULL = f"{APP_VER_MAJOR}.{APP_VER_MINOR} Dev {APP_VER_REVISION}{build_suffix} (Gavrant)" + +APP_NAME = "NifSkope" +APP_COMPANY = "NifTools" + + +# Read git build hash +def get_git_hash_path(git_head_path): + with open(git_head_path, "r") as f: + ref_parts = f.readline().split(' ', 1) + if len(ref_parts) == 2 and ref_parts[0] == "ref:": + return ref_parts[1].strip() + else: + raise Exception(f"Failed to read '{git_head_path}'") + +git_hash_path = get_git_hash_path(".git/HEAD") + +def get_git_build(git_hash_path): + GIT_BUILD_LENGTH = 7 + with open(git_hash_path, "r") as f: + hash_line = f.readline().strip() + if len(hash_line) >= GIT_BUILD_LENGTH: + # Single component, hopefully the commit hash + # Fetch first seven characters (abbreviated hash) + return hash_line[:GIT_BUILD_LENGTH] + else: + raise Exception(f"Failed to read hash from '{git_hash_path}'") + +APP_GIT_BUILD = get_git_build(".git/" + git_hash_path) + + +# Generate src/version.h +script_path = os_path.realpath(__file__) +header_path = os_path.realpath("src/version.h") +with open(header_path, "w") as header: + def header_string(s): + outs = "" + for c in s: + if c in ("\"", "\\", "'", "?"): + outs += "\\" + c + elif c >= " " and c <= "~": + outs += c + else: + chcode = ord(c) + if chcode == 0x0a: + outs += "\\n" + elif chcode == 0x0d: + outs += "\\r" + elif chcode == 0x09: + outs += "\\t" + else: + raise Exception(f"Unsupported character in string '{s}'") + return "\"" + outs + "\"" + + header_dir = os_path.split(header_path)[0] + header_script_path = os_path.relpath(script_path, start=header_dir).replace("\\", "/") + header.write(f"// DO NOT EDIT BY HAND. Generated by {header_script_path}.\n") + header.write("\n") + header.write( "#ifndef VERSION_H\n") + header.write( "#define VERSION_H\n") + header.write( "\n") + header.write(f"#define APP_VER_MAJOR {APP_VER_MAJOR}\n") + header.write(f"#define APP_VER_MINOR {APP_VER_MINOR}\n") + header.write(f"#define APP_VER_REVISION {APP_VER_REVISION}\n") + header.write(f"#define APP_VER_BUILD {APP_VER_BUILD}\n") + header.write( "\n") + header.write(f"#define APP_VER_SHORT {header_string(APP_VER_SHORT)}\n") + header.write(f"#define APP_VER_FULL {header_string(APP_VER_FULL)}\n") + header.write( "\n") + header.write(f"#define APP_GIT_BUILD {header_string(APP_GIT_BUILD)}\n") + header.write( "\n") + header.write(f"#define APP_COMPANY {header_string(APP_COMPANY)}\n") + header.write(f"#define APP_NAME {header_string(APP_NAME)}\n") + header.write(f"#define APP_NAME_FULL {header_string(APP_NAME + " " + APP_VER_FULL)}\n") + header.write( "\n") + header.write( "#endif // VERSION_H\n") + + +# Update text files +def replace_text(in_path, out_path, replacements): + with open(in_path, "r") as inf: + in_lines = inf.readlines() + # Force Unix newline ("\n") on write because it's the standard for text files in this repo + with open(out_path, "w", newline = "\n") as outf: + for inline in in_lines: + outline = inline + for replace_what, replace_with in replacements: + outline = outline.replace(replace_what, replace_with) + outf.write(outline) + +replace_text("build/README.md.in", "README.md", [("@VERSION@", APP_VER_FULL), ]) + + +print("Done")