From b376b094671d33a99cbebb888b78e24e711e0126 Mon Sep 17 00:00:00 2001 From: fo76utils <87907510+fo76utils@users.noreply.github.com> Date: Thu, 16 May 2024 14:11:28 +0200 Subject: [PATCH 01/15] Minor changes --- lib/stb_image.h | 5 +++-- src/ui/nifskope.ui | 5 ++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/stb_image.h b/lib/stb_image.h index 8d9e35c7..5271419f 100644 --- a/lib/stb_image.h +++ b/lib/stb_image.h @@ -5079,8 +5079,9 @@ static void stbi__de_iphone(stbi__png *z) static int stbi__parse_png_file(stbi__png *z, int scan, int req_comp) { stbi_uc palette[1024], pal_img_n=0; - stbi_uc has_trans=0, tc[4]={0}; - stbi__uint16 tc16[3]; + stbi_uc has_trans=0; + stbi__uint16 tc16[8]={0}; + stbi_uc *tc=(stbi_uc*)tc16; stbi__uint32 ioff=0, idata_limit=0, i, pal_len=0; int first=1,k,interlace=0, color=0, is_iphone=0; stbi__context *s = z->s; diff --git a/src/ui/nifskope.ui b/src/ui/nifskope.ui index db50ae49..15749e97 100644 --- a/src/ui/nifskope.ui +++ b/src/ui/nifskope.ui @@ -1044,7 +1044,10 @@ background: qradialgradient(spread:pad, cx:0.5, cy:0.5, radius:0.5, fx:0.5, fy:0 - Close Archives + Close Resources + + + Close Resource Archives and Clear Cache of Resource File List Alt+Q From 482aefc38cde915f3c4e7e7b6e0752c3be6eaf53 Mon Sep 17 00:00:00 2001 From: fo76utils <87907510+fo76utils@users.noreply.github.com> Date: Sun, 19 May 2024 18:13:25 +0200 Subject: [PATCH 02/15] Starfield material support improvements and fixes --- CHANGELOG.md | 1 + build/nif.xml | 2 + lib/libfo76utils | 2 +- res/shaders/stf_default.frag | 112 ++++++++++++++++++++++------------- src/gl/renderer.cpp | 81 +++++++++++++++++-------- src/gl/renderer.h | 15 ++++- src/model/nifextfiles.cpp | 2 +- 7 files changed, 146 insertions(+), 69 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6af9f752..9c02f519 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ == CHANGELOG == * The screenshot dialog now saves the image format and path selected as settings. +* Added limited support for rendering Starfield flipbooks and layered emissivity, and for the use vertex color as tint material setting. * Fixed bugs in rendering ordered nodes. * Fixed the axes not being correctly drawn depending on the OpenGL settings from the last shape. * Fixes in the lighting only and textures disabled rendering modes. diff --git a/build/nif.xml b/build/nif.xml index 4faaf45d..e74b2a3c 100644 --- a/build/nif.xml +++ b/build/nif.xml @@ -6664,6 +6664,8 @@ diff --git a/lib/libfo76utils b/lib/libfo76utils index eb24bd7d..860225af 160000 --- a/lib/libfo76utils +++ b/lib/libfo76utils @@ -1 +1 @@ -Subproject commit eb24bd7d072d1a31631fabd389ade03063596a6a +Subproject commit 860225af0120c0650b084d96ae73842757044646 diff --git a/res/shaders/stf_default.frag b/res/shaders/stf_default.frag index 19370a3f..ffb5abdf 100644 --- a/res/shaders/stf_default.frag +++ b/res/shaders/stf_default.frag @@ -16,7 +16,12 @@ struct TextureSet { struct Material { vec4 color; - bool colorModeLerp; + // bit 0 = color override mode (0: multiply, 1: lerp) + // bit 1 = use vertex color as tint (1: yes) + // bits 2 to 8 = flipbook columns (0: not a flipbook) + // bits 9 to 15 = flipbook rows (0: not a flipbook) + // bits 16 to 29 = flipbook frame + int flags; TextureSet textureSet; }; @@ -43,14 +48,12 @@ struct LayeredEmissivityComponent { int firstLayerIndex; vec4 firstLayerTint; int firstLayerMaskIndex; - bool secondLayerActive; - int secondLayerIndex; + int secondLayerIndex; // -1 if the layer is not active vec4 secondLayerTint; int secondLayerMaskIndex; int firstBlenderIndex; int firstBlenderMode; - bool thirdLayerActive; - int thirdLayerIndex; + int thirdLayerIndex; // -1 if the layer is not active vec4 thirdLayerTint; int thirdLayerMaskIndex; int secondBlenderIndex; @@ -356,16 +359,19 @@ vec2 parallaxMapping(int n, vec3 V, vec2 offset, float parallaxScale, float maxL return mix( currentTextureCoords, prevTCoords, weight ); } -void getLayer(int n, inout vec4 baseMap, inout vec3 normalMap, inout vec3 pbrMap) +void getLayer(int n, vec2 offset, inout vec4 baseMap, inout vec3 normalMap, inout vec3 pbrMap) { - vec2 offset = getTexCoord(lm.layers[n].uvStream); // _height.dds if ( lm.layers[n].material.textureSet.textures[6] >= 1 ) offset = parallaxMapping( lm.layers[n].material.textureSet.textures[6], normalize( ViewDir_norm * btnMatrix_norm ), offset, parallaxOcclusionSettings.y, parallaxOcclusionSettings.x ); // _color.dds if ( lm.layers[n].material.textureSet.textures[0] != 0 ) baseMap.rgb = getLayerTexture(n, 0, offset).rgb; - baseMap.rgb *= lm.layers[n].material.color.rgb; + if ( n == 0 || lm.layers[n].material.textureSet.textures[0] != 0 ) { + baseMap.rgb *= lm.layers[n].material.color.rgb; + if ( (lm.layers[n].material.flags & 2) != 0 ) + baseMap.rgb *= C.rgb; + } // _normal.dds if ( lm.layers[n].material.textureSet.textures[1] != 0 ) { normalMap.rg = getLayerTexture(n, 1, offset).rg; @@ -394,23 +400,36 @@ void main(void) vec3 normal = vec3(0.0, 0.0, 1.0); vec3 pbrMap = vec3(0.75, 0.0, 1.0); // roughness, metalness, AO float alpha = 1.0; + vec3 emissive = vec3(0.0); - if ( lm.layersEnabled[0] ) { - if ( lm.decalSettings.isDecal && lm.layers[0].material.textureSet.textures[0] == 0 ) - discard; - getLayer( 0, baseMap, normal, pbrMap ); - } - for (int i = 1; i < 4; i++) { - if ( lm.layersEnabled[i] ) { + for (int i = 0; i < 4; i++) { + if ( !lm.layersEnabled[i] ) + break; + + vec2 offset = getTexCoord( lm.layers[i].uvStream ); + if ( (lm.layers[i].material.flags & 0xFFFC) != 0 ) { + // flipbook + int w = ( lm.layers[i].material.flags >> 2 ) & 0x7F; + int h = ( lm.layers[i].material.flags >> 9 ) & 0x7F; + int n = lm.layers[i].material.flags >> 16; + offset.x = ( fract( offset.x ) + float( n % w ) ) / float( w ); + offset.y = ( fract( offset.y ) + float( n / w ) ) / float( h ); + } + + if ( i == 0 ) { + if ( lm.decalSettings.isDecal && lm.layers[0].material.textureSet.textures[0] == 0 ) + discard; + getLayer( 0, offset, baseMap, normal, pbrMap ); + } else { vec4 layerBaseMap = baseMap; vec3 layerNormal = normal; vec3 layerPBRMap = pbrMap; - getLayer(i, layerBaseMap, layerNormal, layerPBRMap); + getLayer( i, offset, layerBaseMap, layerNormal, layerPBRMap ); + float layerMask = getBlenderMask(i - 1); switch ( lm.blenders[i - 1].blendMode) { case 0: // Linear baseMap.rgb = mix(baseMap.rgb, layerBaseMap.rgb, layerMask); - baseMap.a *= layerBaseMap.a; normal = normalize(mix(normal, layerNormal, layerMask)); pbrMap = mix(pbrMap, layerPBRMap, layerMask); break; @@ -420,6 +439,36 @@ void main(void) break; } } + + if ( lm.layers[i].material.textureSet.textures[2] != 0 ) { + // _opacity.dds + if ( lm.isEffect && lm.hasOpacityComponent && i == lm.opacity.firstLayerIndex ) { + baseMap.a *= getLayerTexture( i, 2, offset ).r; + } else if ( lm.alphaSettings.hasOpacity && i == lm.alphaSettings.opacitySourceLayer ) { + if ( (lm.layers[i].material.flags & 0xFFFC) == 0 ) + baseMap.a *= getLayerTexture( i, 2, getTexCoord(lm.alphaSettings.opacityUVstream) ).r; + else + baseMap.a *= getLayerTexture( i, 2, offset ).r; + alpha = lm.layers[i].material.color.a; + } + } + + if ( lm.layers[i].material.textureSet.textures[7] != 0 ) { + // _emissive.dds + // TODO: layered emissivity masks + vec4 tmp = vec4(0.0); + if ( lm.emissiveSettings.isEnabled && i == lm.emissiveSettings.emissiveSourceLayer ) + tmp = lm.emissiveSettings.emissiveTint; + else if ( lm.layeredEmissivity.isEnabled && i == lm.layeredEmissivity.firstLayerIndex ) + tmp = lm.layeredEmissivity.firstLayerTint; + else if ( lm.layeredEmissivity.isEnabled && i == lm.layeredEmissivity.secondLayerIndex ) + tmp = lm.layeredEmissivity.secondLayerTint; + else if ( lm.layeredEmissivity.isEnabled && i == lm.layeredEmissivity.thirdLayerIndex ) + tmp = lm.layeredEmissivity.thirdLayerTint; + else + continue; + emissive += getLayerTexture( i, 7, offset ).rgb * tmp.rgb * tmp.a; + } } normal = normalize( btnMatrix_norm * normal ); @@ -439,21 +488,10 @@ void main(void) vec3 reflectedWS = vec3(reflMatrix * (gl_ModelViewMatrixInverse * vec4(R, 0.0))); vec3 normalWS = vec3(reflMatrix * (gl_ModelViewMatrixInverse * vec4(normal, 0.0))); - if ( lm.alphaSettings.hasOpacity && lm.alphaSettings.opacitySourceLayer < 4 && lm.layersEnabled[lm.alphaSettings.opacitySourceLayer] ) { - int n = lm.alphaSettings.opacitySourceLayer; - if ( lm.alphaSettings.useVertexColor ) - baseMap.a = C[lm.alphaSettings.vertexColorChannel]; - if ( lm.layers[n].material.textureSet.textures[2] != 0 ) - baseMap.a *= getLayerTexture(n, 2, getTexCoord(lm.alphaSettings.opacityUVstream)).r; - alpha = lm.layers[n].material.color.a; - } + if ( lm.alphaSettings.hasOpacity && lm.alphaSettings.useVertexColor ) + alpha *= C[lm.alphaSettings.vertexColorChannel]; if ( lm.isEffect ) { - if ( lm.hasOpacityComponent ) { - int n = lm.opacity.firstLayerIndex; - if ( n < 4 && lm.layersEnabled[n] && lm.layers[n].material.textureSet.textures[2] != 0 ) - baseMap.a = getLayerTexture(n, 2, getTexCoord(lm.layers[n].uvStream)).r; - } if ( lm.effectSettings.useFallOff || lm.effectSettings.useRGBFallOff ) { float startAngle = cos(radians(lm.effectSettings.falloffStartAngle)); float stopAngle = cos(radians(lm.effectSettings.falloffStopAngle)); @@ -481,17 +519,11 @@ void main(void) vec4 color; vec3 albedo = baseMap.rgb; - // TODO: layered emissivity - vec3 emissive = vec3(0.0); + // emissive intensity (FIXME: this is probably incorrect) if ( lm.emissiveSettings.isEnabled ) { - int n = lm.emissiveSettings.emissiveSourceLayer; - if ( n <= 3 ) { - if ( lm.layersEnabled[n] && lm.layers[n].material.textureSet.textures[7] != 0 ) { - emissive = getLayerTexture(n, 7, getTexCoord(lm.layers[n].uvStream)).rgb; - emissive *= lm.emissiveSettings.emissiveTint.rgb; - emissive *= lm.emissiveSettings.emissiveTint.a * exp2(lm.emissiveSettings.exposureOffset * 0.5); - } - } + emissive *= exp2( lm.emissiveSettings.exposureOffset * 0.5 ); + } else if ( lm.layeredEmissivity.isEnabled ) { + emissive *= exp2( lm.layeredEmissivity.exposureOffset * 0.5 ); } vec3 f0 = mix(vec3(0.04), albedo, pbrMap.g); diff --git a/src/gl/renderer.cpp b/src/gl/renderer.cpp index 917b7a09..8a9076bc 100644 --- a/src/gl/renderer.cpp +++ b/src/gl/renderer.cpp @@ -53,6 +53,7 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include +#include //! @file renderer.cpp Renderer and child classes implementation @@ -283,7 +284,7 @@ bool Renderer::Shader::load( const QString & filepath ) Renderer::Program::Program( const QString & n, QOpenGLFunctions * fn ) : f( fn ), name( n ), id( 0 ) { - uniformLocationsSF.resize( 2048, std::pair< std::uint32_t, int >(0, -1) ); + uniformLocationsSF.resize( 2048 ); id = f->glCreateProgram(); } @@ -646,17 +647,43 @@ bool Renderer::Program::uniSamplerBlank( UniformType var, int & texunit ) return true; } +inline Renderer::Program::UniformLocationMapItem::UniformLocationMapItem() + : fmt( nullptr ), args( 0 ), l( -1 ) +{ +} + +inline Renderer::Program::UniformLocationMapItem::UniformLocationMapItem( const char *s, int arg1, int arg2 ) + : fmt( s ), args( std::uint32_t( ( arg1 & 0xFFFF) | ( arg2 << 16 ) ) ), l( -1 ) +{ +} + +inline bool Renderer::Program::UniformLocationMapItem::operator==( const UniformLocationMapItem & r ) const +{ + return ( fmt == r.fmt && args == r.args ); +} + int Renderer::Program::uniLocation( const char * fmt, int arg1, int arg2 ) { + UniformLocationMapItem key( fmt, arg1, arg2 ); + + std::uint32_t h = 0xFFFFFFFFU; + hashFunctionCRC32C< std::uint64_t >( h, reinterpret_cast< std::uintptr_t >( fmt ) ); + hashFunctionCRC32C< std::uint32_t >( h, key.args ); + + size_t hashMask = uniformLocationsSF.size() - 1; + size_t i = h & hashMask; + for ( ; uniformLocationsSF[i].fmt; i = (i + 1) & hashMask ) { + if ( uniformLocationsSF[i] == key ) + return uniformLocationsSF[i].l; + } + char varNameBuf[256]; char * sp = varNameBuf; char * endp = sp + 254; - std::uint32_t h = 0; while ( sp < endp ) [[likely]] { char c = *( fmt++ ); if ( (unsigned char) c > (unsigned char) '%' ) [[likely]] { *( sp++ ) = c; - hashFunctionCRC32C< unsigned char >( h, (unsigned char) c ); continue; } if ( !c ) @@ -669,7 +696,6 @@ int Renderer::Program::uniLocation( const char * fmt, int arg1, int arg2 ) if ( n >= 10 ) { c = char( (n / 10) & 15 ) | '0'; *( sp++ ) = c; - hashFunctionCRC32C< unsigned char >( h, (unsigned char) c ); n = n % 10; } c = char( n & 15 ) | '0'; @@ -678,23 +704,13 @@ int Renderer::Program::uniLocation( const char * fmt, int arg1, int arg2 ) } } *( sp++ ) = c; - hashFunctionCRC32C< unsigned char >( h, (unsigned char) c ); } *sp = '\0'; - size_t hashMask = uniformLocationsSF.size() - 1; - size_t i = h & hashMask; - for ( ; uniformLocationsSF[i].first; i = (i + 1) & hashMask) { - if ( uniformLocationsSF[i].first == h ) - return uniformLocationsSF[i].second; - } - int l = f->glGetUniformLocation( id, varNameBuf ); - uniformLocationsSF[i] = std::pair< std::uint32_t, int >(h, l); -#if 0 - std::fprintf(stderr, "Hash 0x%08X: uniform '%s' location %d\n", (unsigned int) h, varNameBuf, l); -#endif - if ( l < 0 ) + key.l = f->glGetUniformLocation( id, varNameBuf ); + uniformLocationsSF[i] = key; + if ( key.l < 0 ) qWarning() << "Warning: uniform '" << varNameBuf << "' not found"; - return l; + return key.l; } void Renderer::Program::uni1b_l( int l, bool x ) @@ -777,6 +793,22 @@ bool Renderer::Program::uniSampler_l( BSShaderLightingProperty * bsprop, int & t return false; } +static int setFlipbookParameters( const CE2Material::Material & m ) +{ + int flipbookColumns = std::min< int >( m.flipbookColumns, 127 ); + int flipbookRows = std::min< int >( m.flipbookRows, 127 ); + int flipbookFrames = flipbookColumns * flipbookRows; + if ( flipbookFrames < 2 ) + return 0; + double flipbookFrame = double( std::chrono::duration_cast< std::chrono::milliseconds >( std::chrono::steady_clock::now().time_since_epoch() ).count() ); + // FIXME: this is limited to looping at 30 frames per second + flipbookFrame = flipbookFrame * 0.03 / double( flipbookFrames ); + flipbookFrame = flipbookFrame - std::floor( flipbookFrame ); + int materialFlags = ( flipbookColumns << 2 ) | ( flipbookRows << 9 ); + materialFlags = materialFlags | ( std::min< int >( int( flipbookFrame * double( flipbookFrames ) ), flipbookFrames - 1 ) << 16 ); + return materialFlags; +} + bool Renderer::setupProgramSF( Program * prog, Shape * mesh ) { auto nif = NifModel::fromValidIndex( mesh->index() ); @@ -856,14 +888,12 @@ bool Renderer::setupProgramSF( Program * prog, Shape * mesh ) prog->uni1i_l( prog->uniLocation("lm.layeredEmissivity.firstLayerIndex"), mat->layeredEmissiveSettings->layer1Index ); prog->uni4c_l( prog->uniLocation("lm.layeredEmissivity.firstLayerTint"), mat->layeredEmissiveSettings->layer1Tint ); prog->uni1i_l( prog->uniLocation("lm.layeredEmissivity.firstLayerMaskIndex"), mat->layeredEmissiveSettings->layer1MaskIndex ); - prog->uni1b_l( prog->uniLocation("lm.layeredEmissivity.secondLayerActive"), mat->layeredEmissiveSettings->layer2Active ); - prog->uni1i_l( prog->uniLocation("lm.layeredEmissivity.secondLayerIndex"), mat->layeredEmissiveSettings->layer2Index ); + prog->uni1i_l( prog->uniLocation("lm.layeredEmissivity.secondLayerIndex"), ( mat->layeredEmissiveSettings->layer2Active ? int(mat->layeredEmissiveSettings->layer2Index) : -1 ) ); prog->uni4c_l( prog->uniLocation("lm.layeredEmissivity.secondLayerTint"), mat->layeredEmissiveSettings->layer2Tint ); prog->uni1i_l( prog->uniLocation("lm.layeredEmissivity.secondLayerMaskIndex"), mat->layeredEmissiveSettings->layer2MaskIndex ); prog->uni1i_l( prog->uniLocation("lm.layeredEmissivity.firstBlenderIndex"), mat->layeredEmissiveSettings->blender1Index ); prog->uni1i_l( prog->uniLocation("lm.layeredEmissivity.firstBlenderMode"), mat->layeredEmissiveSettings->blender1Mode ); - prog->uni1b_l( prog->uniLocation("lm.layeredEmissivity.thirdLayerActive"), mat->layeredEmissiveSettings->layer3Active ); - prog->uni1i_l( prog->uniLocation("lm.layeredEmissivity.thirdLayerIndex"), mat->layeredEmissiveSettings->layer3Index ); + prog->uni1i_l( prog->uniLocation("lm.layeredEmissivity.thirdLayerIndex"), ( mat->layeredEmissiveSettings->layer3Active ? int(mat->layeredEmissiveSettings->layer3Index) : -1 ) ); prog->uni4c_l( prog->uniLocation("lm.layeredEmissivity.thirdLayerTint"), mat->layeredEmissiveSettings->layer3Tint ); prog->uni1i_l( prog->uniLocation("lm.layeredEmissivity.thirdLayerMaskIndex"), mat->layeredEmissiveSettings->layer3MaskIndex ); prog->uni1i_l( prog->uniLocation("lm.layeredEmissivity.secondBlenderIndex"), mat->layeredEmissiveSettings->blender2Index ); @@ -986,10 +1016,13 @@ bool Renderer::setupProgramSF( Program * prog, Shape * mesh ) blender = mat->blenders[i - 1]; if ( layer->material ) { prog->uni4f_l( prog->uniLocation("lm.layers[%d].material.color", i), layer->material->color, true ); - prog->uni1b_l( prog->uniLocation("lm.layers[%d].material.colorModeLerp", i), bool(layer->material->colorMode) ); + int materialFlags = layer->material->colorModeFlags & 3; + if ( layer->material->flipbookFlags & 1 ) [[unlikely]] + materialFlags = materialFlags | setFlipbookParameters( *(layer->material) ); + prog->uni1i_l( prog->uniLocation("lm.layers[%d].material.flags", i), materialFlags ); } else { prog->uni4f_l( prog->uniLocation("lm.layers[%d].material.color", i), FloatVector4(1.0f) ); - prog->uni1b_l( prog->uniLocation("lm.layers[%d].material.colorModeLerp", i), false ); + prog->uni1i_l( prog->uniLocation("lm.layers[%d].material.flags", i), 0 ); } if ( layer->material && layer->material->textureSet ) { const CE2Material::TextureSet * textureSet = layer->material->textureSet; diff --git a/src/gl/renderer.h b/src/gl/renderer.h index 36950695..4f12f5fc 100644 --- a/src/gl/renderer.h +++ b/src/gl/renderer.h @@ -349,9 +349,17 @@ public slots: } }; int uniformLocations[NUM_UNIFORM_TYPES]; - // CRC32C(name), location - std::vector< std::pair< std::uint32_t, int > > uniformLocationsSF; - +private: + struct UniformLocationMapItem { + const char * fmt; + std::uint32_t args; + int l; + inline UniformLocationMapItem(); + inline UniformLocationMapItem( const char *s, int arg1, int arg2 ); + inline bool operator==( const UniformLocationMapItem & r ) const; + }; + std::vector< UniformLocationMapItem > uniformLocationsSF; +public: void setUniformLocations(); void uni1f( UniformType var, float x ); @@ -365,6 +373,7 @@ public slots: int & texunit, const QString & alternate, uint clamp, const QString & forced = {} ); bool uniSamplerBlank( UniformType var, int & texunit ); + // fmt must be a string constant, // only supports at most two %d format integers in the range 0 to 99 int uniLocation( const char * fmt, int arg1 = 0, int arg2 = 0 ); void uni1b_l( int l, bool x ); diff --git a/src/model/nifextfiles.cpp b/src/model/nifextfiles.cpp index df21de7d..c8a43307 100644 --- a/src/model/nifextfiles.cpp +++ b/src/model/nifextfiles.cpp @@ -120,7 +120,7 @@ void NifModel::loadSFMaterial( NifItem * parent, const void * o ) if ( o ) { name = material->name; color = material->color; - colorMode = material->colorMode; + colorMode = material->colorModeFlags; isFlipbook = bool(material->flipbookFlags & 1); textureSet = material->textureSet; } From 2f117862b5078e040f54f4bae1f1e4ca8aa6e247 Mon Sep 17 00:00:00 2001 From: fo76utils <87907510+fo76utils@users.noreply.github.com> Date: Mon, 20 May 2024 15:03:20 +0200 Subject: [PATCH 03/15] Do not render Starfield materials with 'Invisible' shader model --- res/shaders/stf_default.frag | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/res/shaders/stf_default.frag b/res/shaders/stf_default.frag index ffb5abdf..2c9dae93 100644 --- a/res/shaders/stf_default.frag +++ b/res/shaders/stf_default.frag @@ -197,6 +197,7 @@ struct TerrainSettingsComponent { }; struct LayeredMaterial { + // shader model IDs are defined in lib/libfo76utils/src/mat_dump.cpp int shaderModel; bool isEffect; bool isTwoSided; @@ -395,6 +396,8 @@ void main(void) fragColor = solidColor; return; } + if ( lm.shaderModel == 45 ) // "Invisible" + discard; vec4 baseMap = vec4(1.0); vec3 normal = vec3(0.0, 0.0, 1.0); @@ -412,8 +415,7 @@ void main(void) int w = ( lm.layers[i].material.flags >> 2 ) & 0x7F; int h = ( lm.layers[i].material.flags >> 9 ) & 0x7F; int n = lm.layers[i].material.flags >> 16; - offset.x = ( fract( offset.x ) + float( n % w ) ) / float( w ); - offset.y = ( fract( offset.y ) + float( n / w ) ) / float( h ); + offset = ( fract( offset ) + vec2( float(n % w), float(n / w) ) ) / vec2( float(w), float(h) ); } if ( i == 0 ) { From ddff2548b149bd6419eba24c45e9e55eb1634bdb Mon Sep 17 00:00:00 2001 From: fo76utils <87907510+fo76utils@users.noreply.github.com> Date: Mon, 20 May 2024 15:03:53 +0200 Subject: [PATCH 04/15] Fixed the hidden flag being ignored on Starfield shapes --- src/gl/BSMesh.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/gl/BSMesh.cpp b/src/gl/BSMesh.cpp index 7722ef5c..2fa26f6a 100644 --- a/src/gl/BSMesh.cpp +++ b/src/gl/BSMesh.cpp @@ -17,11 +17,16 @@ BSMesh::BSMesh(Scene* s, const QModelIndex& iBlock) : Shape(s, iBlock) void BSMesh::transformShapes() { + // TODO: implement this +#if 0 + if ( isHidden() ) + return; +#endif } void BSMesh::drawShapes( NodeList * secondPass ) { - if ( !scene->hasOption(Scene::ShowMarkers) && name.contains("EditorMarker") ) + if ( isHidden() || ( !scene->hasOption(Scene::ShowMarkers) && name.contains("EditorMarker") ) ) return; // Draw translucent meshes in second pass From 9f12ce8bce2a273c047e2d535c249c64b5cd00a1 Mon Sep 17 00:00:00 2001 From: fo76utils <87907510+fo76utils@users.noreply.github.com> Date: Mon, 20 May 2024 15:04:53 +0200 Subject: [PATCH 05/15] Renderer: implemented flipbook frame rate, optimization --- src/gl/renderer.cpp | 68 +++++++++++++++++++++++++++++++++------------ src/gl/renderer.h | 12 +++++--- 2 files changed, 59 insertions(+), 21 deletions(-) diff --git a/src/gl/renderer.cpp b/src/gl/renderer.cpp index 8a9076bc..c24b4314 100644 --- a/src/gl/renderer.cpp +++ b/src/gl/renderer.cpp @@ -284,7 +284,9 @@ bool Renderer::Shader::load( const QString & filepath ) Renderer::Program::Program( const QString & n, QOpenGLFunctions * fn ) : f( fn ), name( n ), id( 0 ) { - uniformLocationsSF.resize( 2048 ); + uniLocationsMap = new UniformLocationMapItem[512]; + uniLocationsMapMask = 511; + uniLocationsMapSize = 0; id = f->glCreateProgram(); } @@ -292,6 +294,7 @@ Renderer::Program::~Program() { if ( id ) f->glDeleteShader( id ); + delete[] uniLocationsMap; } bool Renderer::Program::load( const QString & filepath, Renderer * renderer ) @@ -662,20 +665,20 @@ inline bool Renderer::Program::UniformLocationMapItem::operator==( const Uniform return ( fmt == r.fmt && args == r.args ); } -int Renderer::Program::uniLocation( const char * fmt, int arg1, int arg2 ) +inline std::uint32_t Renderer::Program::UniformLocationMapItem::hashFunction() const { - UniformLocationMapItem key( fmt, arg1, arg2 ); - std::uint32_t h = 0xFFFFFFFFU; + // note: this requires fmt to point to a string literal hashFunctionCRC32C< std::uint64_t >( h, reinterpret_cast< std::uintptr_t >( fmt ) ); - hashFunctionCRC32C< std::uint32_t >( h, key.args ); + hashFunctionCRC32C< std::uint32_t >( h, args ); + return h; +} - size_t hashMask = uniformLocationsSF.size() - 1; - size_t i = h & hashMask; - for ( ; uniformLocationsSF[i].fmt; i = (i + 1) & hashMask ) { - if ( uniformLocationsSF[i] == key ) - return uniformLocationsSF[i].l; - } +int Renderer::Program::storeUniformLocation( const UniformLocationMapItem & o, size_t i ) +{ + const char * fmt = o.fmt; + int arg1 = int( o.args & 0xFFFF ); + int arg2 = int( o.args >> 16 ); char varNameBuf[256]; char * sp = varNameBuf; @@ -706,11 +709,42 @@ int Renderer::Program::uniLocation( const char * fmt, int arg1, int arg2 ) *( sp++ ) = c; } *sp = '\0'; - key.l = f->glGetUniformLocation( id, varNameBuf ); - uniformLocationsSF[i] = key; - if ( key.l < 0 ) + int l = f->glGetUniformLocation( id, varNameBuf ); + uniLocationsMap[i] = o; + uniLocationsMap[i].l = l; + if ( l < 0 ) qWarning() << "Warning: uniform '" << varNameBuf << "' not found"; - return key.l; + + uniLocationsMapSize++; + if ( ( uniLocationsMapSize * size_t(3) ) > ( uniLocationsMapMask * size_t(2) ) ) { + unsigned int m = ( uniLocationsMapMask << 1 ) | 0xFFU; + UniformLocationMapItem * tmpBuf = new UniformLocationMapItem[m + 1U]; + for ( size_t j = 0; j <= uniLocationsMapMask; j++ ) { + size_t k = uniLocationsMap[j].hashFunction() & m; + while ( tmpBuf[k].fmt ) + k = ( k + 1 ) & m; + tmpBuf[k] = uniLocationsMap[j]; + } + delete[] uniLocationsMap; + uniLocationsMap = tmpBuf; + uniLocationsMapMask = m; + } + + return l; +} + +int Renderer::Program::uniLocation( const char * fmt, int arg1, int arg2 ) +{ + UniformLocationMapItem key( fmt, arg1, arg2 ); + + size_t hashMask = uniLocationsMapMask; + size_t i = key.hashFunction() & hashMask; + for ( ; uniLocationsMap[i].fmt; i = (i + 1) & hashMask ) { + if ( uniLocationsMap[i] == key ) + return uniLocationsMap[i].l; + } + + return storeUniformLocation( key, i ); } void Renderer::Program::uni1b_l( int l, bool x ) @@ -800,9 +834,9 @@ static int setFlipbookParameters( const CE2Material::Material & m ) int flipbookFrames = flipbookColumns * flipbookRows; if ( flipbookFrames < 2 ) return 0; + float flipbookFPMS = std::min( std::max( m.flipbookFPS, 1.0f ), 100.0f ) * 0.001f; double flipbookFrame = double( std::chrono::duration_cast< std::chrono::milliseconds >( std::chrono::steady_clock::now().time_since_epoch() ).count() ); - // FIXME: this is limited to looping at 30 frames per second - flipbookFrame = flipbookFrame * 0.03 / double( flipbookFrames ); + flipbookFrame = flipbookFrame * flipbookFPMS / double( flipbookFrames ); flipbookFrame = flipbookFrame - std::floor( flipbookFrame ); int materialFlags = ( flipbookColumns << 2 ) | ( flipbookRows << 9 ); materialFlags = materialFlags | ( std::min< int >( int( flipbookFrame * double( flipbookFrames ) ), flipbookFrames - 1 ) << 16 ); diff --git a/src/gl/renderer.h b/src/gl/renderer.h index 4f12f5fc..ebc15a92 100644 --- a/src/gl/renderer.h +++ b/src/gl/renderer.h @@ -350,15 +350,19 @@ public slots: int uniformLocations[NUM_UNIFORM_TYPES]; private: - struct UniformLocationMapItem { + struct UniformLocationMapItem { const char * fmt; std::uint32_t args; int l; inline UniformLocationMapItem(); inline UniformLocationMapItem( const char *s, int arg1, int arg2 ); inline bool operator==( const UniformLocationMapItem & r ) const; + inline std::uint32_t hashFunction() const; }; - std::vector< UniformLocationMapItem > uniformLocationsSF; + UniformLocationMapItem * uniLocationsMap; + unsigned int uniLocationsMapMask; + unsigned int uniLocationsMapSize; + int storeUniformLocation( const UniformLocationMapItem & o, size_t i ); public: void setUniformLocations(); @@ -373,8 +377,8 @@ public slots: int & texunit, const QString & alternate, uint clamp, const QString & forced = {} ); bool uniSamplerBlank( UniformType var, int & texunit ); - // fmt must be a string constant, - // only supports at most two %d format integers in the range 0 to 99 + // fmt must be a string literal, with at most two %d format + // integer arguments in the range 0 to 99 int uniLocation( const char * fmt, int arg1 = 0, int arg2 = 0 ); void uni1b_l( int l, bool x ); void uni1i_l( int l, int x ); From a02afe30bcf4981c9aba423b349a34311dd8d690 Mon Sep 17 00:00:00 2001 From: fo76utils <87907510+fo76utils@users.noreply.github.com> Date: Mon, 20 May 2024 15:06:05 +0200 Subject: [PATCH 06/15] GLView: update display while M is pressed (workaround for SF flipbooks) --- src/glview.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/glview.cpp b/src/glview.cpp index 0a42d2b9..a7492598 100644 --- a/src/glview.cpp +++ b/src/glview.cpp @@ -1460,6 +1460,9 @@ void GLView::advanceGears() rotate( mouseRot[0], mouseRot[1], mouseRot[2] ); mouseRot = Vector3(); } + + // update display without movement + if ( kbd[ Qt::Key_M ] ) update(); } @@ -1774,6 +1777,7 @@ void GLView::keyPressEvent( QKeyEvent * event ) //case Qt::Key_F: case Qt::Key_Q: case Qt::Key_E: + case Qt::Key_M: case Qt::Key_Space: kbd[event->key()] = true; break; @@ -1808,6 +1812,7 @@ void GLView::keyReleaseEvent( QKeyEvent * event ) //case Qt::Key_F: case Qt::Key_Q: case Qt::Key_E: + case Qt::Key_M: case Qt::Key_Space: kbd[event->key()] = false; break; From 7cbba0dd8a3470fc8b2498879920b65ff9bc2581 Mon Sep 17 00:00:00 2001 From: fo76utils <87907510+fo76utils@users.noreply.github.com> Date: Mon, 20 May 2024 15:09:45 +0200 Subject: [PATCH 07/15] Updated documentation --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c02f519..79ba73aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,15 @@ == CHANGELOG == +#### NifSkope-2.0.dev9-20240520 + * The screenshot dialog now saves the image format and path selected as settings. -* Added limited support for rendering Starfield flipbooks and layered emissivity, and for the use vertex color as tint material setting. +* Added limited support for rendering Starfield flipbooks and layered emissivity, and for the use vertex color as tint material setting. Flipbooks are currently animated only during movement, or while the M key is pressed. * Fixed bugs in rendering ordered nodes. * Fixed the axes not being correctly drawn depending on the OpenGL settings from the last shape. * Fixes in the lighting only and textures disabled rendering modes. +* Fixed the hidden flag being ignored on Starfield shapes. * Optimizations in the archive manager. +* Improvements and fixes to Starfield material support. **Note:** loading .mat files from archives is currently disabled for materials that also exist in CDB format. Base game materials can only be replaced with loose .mat files. #### NifSkope-2.0.dev9-20240505 From e6f5d5fd03c136240427a4f5ac93929aa24e1672 Mon Sep 17 00:00:00 2001 From: fo76utils <87907510+fo76utils@users.noreply.github.com> Date: Mon, 20 May 2024 23:19:38 +0200 Subject: [PATCH 08/15] Changed flipbook texture coordinate calculation to reduce seams --- res/shaders/stf_default.frag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/shaders/stf_default.frag b/res/shaders/stf_default.frag index 2c9dae93..a670d91d 100644 --- a/res/shaders/stf_default.frag +++ b/res/shaders/stf_default.frag @@ -415,7 +415,7 @@ void main(void) int w = ( lm.layers[i].material.flags >> 2 ) & 0x7F; int h = ( lm.layers[i].material.flags >> 9 ) & 0x7F; int n = lm.layers[i].material.flags >> 16; - offset = ( fract( offset ) + vec2( float(n % w), float(n / w) ) ) / vec2( float(w), float(h) ); + offset = ( offset + vec2( float(n % w), float(n / w) ) ) / vec2( float(w), float(h) ); } if ( i == 0 ) { From 9af2590fd6052debd3ef93d25c55dc42ababfba2 Mon Sep 17 00:00:00 2001 From: fo76utils <87907510+fo76utils@users.noreply.github.com> Date: Tue, 21 May 2024 12:32:15 +0200 Subject: [PATCH 09/15] Fixed bugs in CE2MaterialDB::loadMaterial() --- CHANGELOG.md | 2 +- lib/libfo76utils | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79ba73aa..2bef9f21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ == CHANGELOG == -#### NifSkope-2.0.dev9-20240520 +#### NifSkope-2.0.dev9-20240521 * The screenshot dialog now saves the image format and path selected as settings. * Added limited support for rendering Starfield flipbooks and layered emissivity, and for the use vertex color as tint material setting. Flipbooks are currently animated only during movement, or while the M key is pressed. diff --git a/lib/libfo76utils b/lib/libfo76utils index 860225af..596c16e9 160000 --- a/lib/libfo76utils +++ b/lib/libfo76utils @@ -1 +1 @@ -Subproject commit 860225af0120c0650b084d96ae73842757044646 +Subproject commit 596c16e945d7c164c3912e71f15801f9fca34370 From 318bf89c841af11ff349c1cf160eaf3204fa4046 Mon Sep 17 00:00:00 2001 From: fo76utils <87907510+fo76utils@users.noreply.github.com> Date: Thu, 23 May 2024 16:22:48 +0200 Subject: [PATCH 10/15] UVWidget: limited Starfield support, various fixes and improvements --- CHANGELOG.md | 4 + src/spells/texture.cpp | 2 + src/ui/widgets/uvedit.cpp | 185 +++++++++++++++++++++++++++++++++----- src/ui/widgets/uvedit.h | 7 +- 4 files changed, 174 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bef9f21..c763e26f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ == CHANGELOG == +* Added limited (read only) Starfield support to the UV editor. +* Implemented texture slot selection in the UV editor for Fallout 3 and newer games. +* Fixed textures not being rendered in the UV editor. + #### NifSkope-2.0.dev9-20240521 * The screenshot dialog now saves the image format and path selected as settings. diff --git a/src/spells/texture.cpp b/src/spells/texture.cpp index c0b5ef01..54bb141c 100644 --- a/src/spells/texture.cpp +++ b/src/spells/texture.cpp @@ -268,6 +268,8 @@ class spEditTexCoords final : public Spell bool isApplicable( const NifModel * nif, const QModelIndex & index ) override final { + if ( nif->getBSVersion() >= 170 && nif->blockInherits( index, "BSGeometry" ) ) + return true; auto iUVs = getUV( nif, index ); auto iTriData = nif->getIndex( index, "Num Triangles" ); return (iUVs.isValid() || iTriData.isValid()) diff --git a/src/ui/widgets/uvedit.cpp b/src/ui/widgets/uvedit.cpp index df7105f8..28475046 100644 --- a/src/ui/widgets/uvedit.cpp +++ b/src/ui/widgets/uvedit.cpp @@ -41,6 +41,7 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "ui/settingsdialog.h" #include "lib/nvtristripwrapper.h" +#include "io/MeshFile.h" #include // QUndoCommand Inherited #include @@ -226,8 +227,8 @@ void UVWidget::initializeGL() qglClearColor( cfg.background ); - if ( !texfile.isEmpty() ) - bindTexture( texfile, game ); + if ( currentTexSlot < texfiles.size() && !texfiles[currentTexSlot].isEmpty() ) + bindTexture( texfiles[currentTexSlot], game ); else bindTexture( texsource, game ); @@ -282,8 +283,8 @@ void UVWidget::paintGL() else glDisable( GL_BLEND ); - if ( !texfile.isEmpty() ) - bindTexture( texfile, game ); + if ( currentTexSlot < texfiles.size() && !texfiles[currentTexSlot].isEmpty() ) + bindTexture( texfiles[currentTexSlot], game ); else bindTexture( texsource, game ); @@ -787,6 +788,89 @@ void UVWidget::keyReleaseEvent( QKeyEvent * e ) } } +void UVWidget::setTexturePaths( NifModel * nif, QModelIndex iTexProp ) +{ + if ( !iTexProp.isValid() ) + return; + + const QString & blockType = nif->getItem( iTexProp )->name(); + + if ( !( blockType == "BSLightingShaderProperty" || blockType == "BSEffectShaderProperty" ) ) + return; + + QModelIndex iTexPropData; + if ( nif->getBSVersion() >= 151 ) + iTexPropData = nif->getIndex( iTexProp, "Material" ); + if ( iTexPropData.isValid() ) { + // using external material file + if ( nif->getBSVersion() < 170 ) { + // Fallout 76 + for ( int texSlot = 0; texSlot <= 9; texSlot++ ) { + while ( texSlot >= texfiles.size() ) + texfiles.append( QString() ); + texfiles[texSlot] = TexCache::find( nif->get( iTexPropData, QString( "Texture %1" ).arg( texSlot ) ), game ); + } + } else { + // Starfield + std::string matPath = Game::GameManager::get_full_path( nif->get( iTexProp, "Name" ), "materials/", ".mat" ); + if ( matPath.empty() ) + return; + CE2MaterialDB * sfMaterials = Game::GameManager::materials( game ); + if ( !sfMaterials ) + return; + const CE2Material * matData = sfMaterials->loadMaterial( matPath ); + if ( !matData ) + return; + for ( size_t i = 0; i < CE2Material::maxLayers; i++ ) { + if ( !( matData->layerMask & ( 1U << i ) ) ) + continue; + if ( !( matData->layers[i] && matData->layers[i]->material && matData->layers[i]->material->textureSet ) ) + continue; + const CE2Material::TextureSet * txtSet = matData->layers[i]->material->textureSet; + for ( int texSlot = 0; texSlot < CE2Material::TextureSet::maxTexturePaths; texSlot++ ) { + if ( !( txtSet->texturePathMask & ( 1U << (unsigned int) texSlot ) ) ) + continue; + while ( texSlot >= texfiles.size() ) + texfiles.append( QString() ); + if ( !txtSet->texturePaths[texSlot]->empty() ) { + texfiles[texSlot] = TexCache::find( QString::fromStdString( *(txtSet->texturePaths[texSlot]) ), game ); + if ( !texfiles[texSlot].isEmpty() ) + return; + } + } + } + } + return; + } + + iTexPropData = nif->getIndex( iTexProp, "Shader Property Data" ); + if ( !iTexPropData.isValid() ) + return; + if ( blockType == "BSLightingShaderProperty" ) { + QModelIndex iTexSource = nif->getBlockIndex( nif->getLink( iTexPropData, "Texture Set" ) ); + + if ( iTexSource.isValid() ) { + QModelIndex iTextures = nif->getIndex( iTexSource, "Textures" ); + if ( iTextures.isValid() ) { + for ( int texSlot = 0; texSlot <= 9; texSlot++ ) { + while ( texSlot >= texfiles.size() ) + texfiles.append( QString() ); + texfiles[texSlot] = TexCache::find( nif->get( QModelIndex_child( iTextures, texSlot ) ), game ); + } + } + } + } else { + for ( int texSlot = 0; texSlot <= 1; texSlot++ ) { + QModelIndex iTexturePath = nif->getIndex( iTexPropData, ( texSlot == 0 ? "Source Texture" : "Normal Texture" ) ); + if ( !iTexturePath.isValid() ) + continue; + while ( texSlot >= texfiles.size() ) + texfiles.append( QString() ); + texfiles[texSlot] = TexCache::find( nif->get( iTexturePath ), game ); + } + } +} + bool UVWidget::setNifData( NifModel * nifModel, const QModelIndex & nifIndex ) { if ( nif ) { @@ -818,14 +902,14 @@ bool UVWidget::setNifData( NifModel * nifModel, const QModelIndex & nifIndex ) coordSetSelect = new QMenu( tr( "Select Coordinate Set" ) ); addAction( coordSetSelect->menuAction() ); connect( coordSetSelect, &QMenu::aboutToShow, this, &UVWidget::getCoordSets ); + } - texSlotGroup = new QActionGroup( this ); - connect( texSlotGroup, &QActionGroup::triggered, this, &UVWidget::selectTexSlot ); + texSlotGroup = new QActionGroup( this ); + connect( texSlotGroup, &QActionGroup::triggered, this, &UVWidget::selectTexSlot ); - menuTexSelect = new QMenu( tr( "Select Texture Slot" ) ); - addAction( menuTexSelect->menuAction() ); - connect( menuTexSelect, &QMenu::aboutToShow, this, &UVWidget::getTexSlots ); - } + menuTexSelect = new QMenu( tr( "Select Texture Slot" ) ); + addAction( menuTexSelect->menuAction() ); + connect( menuTexSelect, &QMenu::aboutToShow, this, &UVWidget::getTexSlots ); if ( nif ) { connect( nif, &NifModel::modelReset, this, &UVWidget::close ); @@ -884,8 +968,39 @@ bool UVWidget::setNifData( NifModel * nifModel, const QModelIndex & nifIndex ) if ( !setTexCoords() ) return false; + } else if ( nif->getBSVersion() >= 170 && nif->blockInherits( iShape, "BSGeometry" ) ) { + auto meshes = nif->getIndex( iShape, "Meshes" ); + if ( !meshes.isValid() ) + return false; + + for ( int i = 0; i <= 3; i++ ) { + auto mesh = QModelIndex_child( meshes, i ); + if ( !mesh.isValid() ) + continue; + auto hasMesh = nif->getIndex( mesh, "Has Mesh" ); + if ( !hasMesh.isValid() || nif->get( hasMesh ) == 0 ) + continue; + mesh = nif->getIndex( mesh, "Mesh" ); + if ( !mesh.isValid() ) + continue; + QString meshPath( nif->get( mesh, "Mesh Path" ) ); + if ( meshPath.isEmpty() ) + continue; + MeshFile meshFile( meshPath ); + if ( meshFile.isValid() && meshFile.coords.size() > 0 && meshFile.triangles.size() > 0 ) { + for ( qsizetype j = 0; j < meshFile.coords.size(); j++ ) + texcoords << Vector2( meshFile.coords[j][0], meshFile.coords[j][1] ); + if ( !setTexCoords( &(meshFile.triangles) ) ) + return false; + break; + } + } + + // Fake index so that isValid() checks do not fail + iTexCoords = iShape; } + texfiles.clear(); auto props = nif->getLinkArray( iShape, "Properties" ); props << nif->getLink( iShape, "Shader Property" ); for ( const auto l : props ) @@ -918,7 +1033,6 @@ bool UVWidget::setNifData( NifModel * nifModel, const QModelIndex & nifIndex ) QModelIndex iTexSource = nif->getBlockIndex( nif->getLink( iTexProp, "Image" ) ); if ( iTexSource.isValid() ) { - //texfile = TexCache::find( nif->get( iTexSource, "File Name" ) ); texsource = iTexSource; return true; } @@ -926,8 +1040,13 @@ bool UVWidget::setNifData( NifModel * nifModel, const QModelIndex & nifIndex ) // TODO: use the BSShaderTextureSet iTexProp = nif->getBlockIndex( l, "BSShaderPPLightingProperty" ); - if ( !iTexProp.isValid() ) + if ( !iTexProp.isValid() ) { iTexProp = nif->getBlockIndex( l, "BSLightingShaderProperty" ); + if ( iTexProp.isValid() ) { + setTexturePaths( nif, iTexProp ); + iTexProp = QModelIndex(); + } + } if ( iTexProp.isValid() ) { QModelIndex iTexSource = nif->getBlockIndex( nif->getLink( iTexProp, "Texture Set" ) ); @@ -939,7 +1058,8 @@ bool UVWidget::setNifData( NifModel * nifModel, const QModelIndex & nifIndex ) QModelIndex iTextures = nif->getIndex( iTexSource, "Textures" ); if ( iTextures.isValid() ) { - texfile = TexCache::find( nif->get( QModelIndex_child( iTextures ) ), game ); + for ( int i = 0; i <= 1; i++ ) + texfiles.append( TexCache::find( nif->get( QModelIndex_child( iTextures, i ) ), game ) ); return true; } } @@ -947,12 +1067,8 @@ bool UVWidget::setNifData( NifModel * nifModel, const QModelIndex & nifIndex ) iTexProp = nif->getBlockIndex( l, "BSEffectShaderProperty" ); if ( iTexProp.isValid() ) { - QString texture = nif->get( iTexProp, "Source Texture" ); - - if ( !texture.isEmpty() ) { - texfile = TexCache::find( texture, game ); - return true; - } + setTexturePaths( nif, iTexProp ); + return true; } } } @@ -962,7 +1078,7 @@ bool UVWidget::setNifData( NifModel * nifModel, const QModelIndex & nifIndex ) return true; } -bool UVWidget::setTexCoords() +bool UVWidget::setTexCoords( const QVector * triangles ) { if ( nif->blockInherits( iShape, "NiTriBasedGeom" ) ) texcoords = nif->getArray( iTexCoords ); @@ -989,7 +1105,8 @@ bool UVWidget::setTexCoords() tris << nif->getArray( nif->index( i, 0, partIdx ), "Triangles" ); } } - + } else if ( triangles ) { + tris = *triangles; } if ( tris.isEmpty() ) @@ -1519,6 +1636,22 @@ void UVWidget::getTexSlots() menuTexSelect->clear(); validTexs.clear(); + if ( texfiles.size() > 0 ) { + for ( const QString& name : texfiles ) { + if ( name.isEmpty() || validTexs.indexOf( name ) >= 0 ) + continue; + validTexs << name; + QAction * temp = new QAction( name, this ); + menuTexSelect->addAction( temp ); + texSlotGroup->addAction( temp ); + temp->setCheckable( true ); + + if ( currentTexSlot < texfiles.size() && name == texfiles[currentTexSlot] ) + temp->setChecked( true ); + } + return; + } + auto props = nif->getLinkArray( iShape, "Properties" ); props << nif->getLink( iShape, "Shader Property" ); for ( const auto l : props ) @@ -1548,6 +1681,14 @@ void UVWidget::getTexSlots() void UVWidget::selectTexSlot() { QString selected = texSlotGroup->checkedAction()->text(); + + if ( texfiles.size() > 0 ) { + currentTexSlot = texfiles.indexOf( selected ); + if ( currentTexSlot < 0 ) + currentTexSlot = 0; + return; + } + currentTexSlot = texnames.indexOf( selected ); auto props = nif->getLinkArray( iShape, "Properties" ); @@ -1580,7 +1721,7 @@ void UVWidget::getCoordSets() coordSetSelect->clear(); quint8 numUvSets = (nif->get( iShapeData, "Data Flags" ) & 0x3F) - | (nif->get( iShapeData, "BS Data Flags" ) & 0x1); + | (nif->get( iShapeData, "BS Data Flags" ) & 0x1); for ( int i = 0; i < numUvSets; i++ ) { QAction * temp; diff --git a/src/ui/widgets/uvedit.h b/src/ui/widgets/uvedit.h index 1ff99e45..7f6eea07 100644 --- a/src/ui/widgets/uvedit.h +++ b/src/ui/widgets/uvedit.h @@ -69,6 +69,9 @@ class UVWidget final : public QGLWidget UVWidget( QWidget * parent = nullptr ); ~UVWidget(); + // for BSLightingShaderProperty and BSEffectShaderProperty + void setTexturePaths( NifModel * nif, QModelIndex iTexProp ); + public: //! Creates the UV editor widget static UVWidget * createEditor( NifModel * nif, const QModelIndex & index ); @@ -175,7 +178,7 @@ protected slots: QSize sHint; TexCache * textures; - QString texfile; + QStringList texfiles; QModelIndex texsource; void drawTexCoords(); @@ -212,7 +215,7 @@ protected slots: //! Texture slot currently being operated on int currentTexSlot = 0; //! Read texcoords from the nif - bool setTexCoords(); + bool setTexCoords( const QVector * triangles = nullptr ); //! Coordinate set currently in use int currentCoordSet = 0; From 99d9abfe6446eac85c7caa8a1e3ba1c433a30e08 Mon Sep 17 00:00:00 2001 From: fo76utils <87907510+fo76utils@users.noreply.github.com> Date: Thu, 23 May 2024 22:21:29 +0200 Subject: [PATCH 11/15] Minor fixes to rendering Starfield effect materials --- CHANGELOG.md | 1 + res/shaders/stf_default.frag | 7 +++++-- src/gl/renderer.cpp | 2 +- src/ui/widgets/uvedit.cpp | 4 ++-- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c763e26f..8fefc2e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ * Added limited (read only) Starfield support to the UV editor. * Implemented texture slot selection in the UV editor for Fallout 3 and newer games. * Fixed textures not being rendered in the UV editor. +* Minor Starfield rendering fixes. #### NifSkope-2.0.dev9-20240521 diff --git a/res/shaders/stf_default.frag b/res/shaders/stf_default.frag index a670d91d..2d24a145 100644 --- a/res/shaders/stf_default.frag +++ b/res/shaders/stf_default.frag @@ -400,6 +400,8 @@ void main(void) discard; vec4 baseMap = vec4(1.0); + if ( lm.isEffect && ( lm.effectSettings.emissiveOnlyEffect || lm.effectSettings.emissiveOnlyAutomaticallyApplied ) ) + baseMap.rgb = vec3(0.0); vec3 normal = vec3(0.0, 0.0, 1.0); vec3 pbrMap = vec3(0.75, 0.0, 1.0); // roughness, metalness, AO float alpha = 1.0; @@ -444,8 +446,9 @@ void main(void) if ( lm.layers[i].material.textureSet.textures[2] != 0 ) { // _opacity.dds - if ( lm.isEffect && lm.hasOpacityComponent && i == lm.opacity.firstLayerIndex ) { - baseMap.a *= getLayerTexture( i, 2, offset ).r; + if ( lm.isEffect ) { + if ( i == (lm.hasOpacityComponent ? lm.opacity.firstLayerIndex : 0) ) + baseMap.a *= getLayerTexture( i, 2, offset ).r; } else if ( lm.alphaSettings.hasOpacity && i == lm.alphaSettings.opacitySourceLayer ) { if ( (lm.layers[i].material.flags & 0xFFFC) == 0 ) baseMap.a *= getLayerTexture( i, 2, getTexCoord(lm.alphaSettings.opacityUVstream) ).r; diff --git a/src/gl/renderer.cpp b/src/gl/renderer.cpp index c24b4314..31bdc6c7 100644 --- a/src/gl/renderer.cpp +++ b/src/gl/renderer.cpp @@ -1205,7 +1205,7 @@ bool Renderer::setupProgramSF( Program * prog, Shape * mesh ) if ( isEffect ) { glEnable( GL_BLEND ); if ( mat->effectSettings->flags & (CE2Material::EffectFlag_EmissiveOnly | CE2Material::EffectFlag_EmissiveOnlyAuto) ) - glBlendFunc( GL_ONE, GL_ONE ); + glBlendFunc( GL_SRC_ALPHA, GL_ONE ); else glBlendFunc( blendMapS[mat->effectSettings->blendMode], blendMapD[mat->effectSettings->blendMode] ); } else if ( (mat->flags & CE2Material::Flag_IsDecal) && (mat->flags & CE2Material::Flag_AlphaBlending) ) { diff --git a/src/ui/widgets/uvedit.cpp b/src/ui/widgets/uvedit.cpp index 28475046..0c1b67b2 100644 --- a/src/ui/widgets/uvedit.cpp +++ b/src/ui/widgets/uvedit.cpp @@ -806,7 +806,7 @@ void UVWidget::setTexturePaths( NifModel * nif, QModelIndex iTexProp ) if ( nif->getBSVersion() < 170 ) { // Fallout 76 for ( int texSlot = 0; texSlot <= 9; texSlot++ ) { - while ( texSlot >= texfiles.size() ) + if ( texSlot >= texfiles.size() ) texfiles.append( QString() ); texfiles[texSlot] = TexCache::find( nif->get( iTexPropData, QString( "Texture %1" ).arg( texSlot ) ), game ); } @@ -853,7 +853,7 @@ void UVWidget::setTexturePaths( NifModel * nif, QModelIndex iTexProp ) QModelIndex iTextures = nif->getIndex( iTexSource, "Textures" ); if ( iTextures.isValid() ) { for ( int texSlot = 0; texSlot <= 9; texSlot++ ) { - while ( texSlot >= texfiles.size() ) + if ( texSlot >= texfiles.size() ) texfiles.append( QString() ); texfiles[texSlot] = TexCache::find( nif->get( QModelIndex_child( iTextures, texSlot ) ), game ); } From c70728c3534043b80426e9a9eb756dd36e472652 Mon Sep 17 00:00:00 2001 From: fo76utils <87907510+fo76utils@users.noreply.github.com> Date: Thu, 23 May 2024 22:46:18 +0200 Subject: [PATCH 12/15] UVWidget: fixed bug in getting Starfield texture set --- src/ui/widgets/uvedit.cpp | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/ui/widgets/uvedit.cpp b/src/ui/widgets/uvedit.cpp index 0c1b67b2..a37051c6 100644 --- a/src/ui/widgets/uvedit.cpp +++ b/src/ui/widgets/uvedit.cpp @@ -827,17 +827,16 @@ void UVWidget::setTexturePaths( NifModel * nif, QModelIndex iTexProp ) if ( !( matData->layers[i] && matData->layers[i]->material && matData->layers[i]->material->textureSet ) ) continue; const CE2Material::TextureSet * txtSet = matData->layers[i]->material->textureSet; - for ( int texSlot = 0; texSlot < CE2Material::TextureSet::maxTexturePaths; texSlot++ ) { - if ( !( txtSet->texturePathMask & ( 1U << (unsigned int) texSlot ) ) ) - continue; - while ( texSlot >= texfiles.size() ) + std::uint32_t texPathMask = txtSet->texturePathMask; + if ( !texPathMask ) + continue; + for ( int texSlot = 0; texPathMask && texSlot < CE2Material::TextureSet::maxTexturePaths; texSlot++, texPathMask = texPathMask >> 1 ) { + if ( texSlot >= texfiles.size() ) texfiles.append( QString() ); - if ( !txtSet->texturePaths[texSlot]->empty() ) { + if ( ( texPathMask & 1 ) && !txtSet->texturePaths[texSlot]->empty() ) texfiles[texSlot] = TexCache::find( QString::fromStdString( *(txtSet->texturePaths[texSlot]) ), game ); - if ( !texfiles[texSlot].isEmpty() ) - return; - } } + break; } } return; From 7379c15382643384353cbf178146832fe6ac2162 Mon Sep 17 00:00:00 2001 From: fo76utils <87907510+fo76utils@users.noreply.github.com> Date: Sat, 25 May 2024 14:13:04 +0200 Subject: [PATCH 13/15] Improved calculation of Starfield emissive intensity --- res/shaders/stf_default.frag | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/res/shaders/stf_default.frag b/res/shaders/stf_default.frag index 2d24a145..8094d2b2 100644 --- a/res/shaders/stf_default.frag +++ b/res/shaders/stf_default.frag @@ -252,6 +252,20 @@ mat3 btnMatrix_norm = mat3( normalize( btnMatrix[0] ), normalize( btnMatrix[1] ) #define FLT_EPSILON 1.192092896e-07F // smallest such that 1.0 + FLT_EPSILON != 1.0 +float emissiveIntensity( bool useAdaptive, bool adaptiveLimits, vec4 luminanceParams ) +{ + float l = luminanceParams[0]; // luminousEmittance + + if ( useAdaptive ) { + l = length( A.rgb ) * 46.188 + length( D.rgb ) * 184.752; + l = l * exp2( luminanceParams[1] ); // exposureOffset + if ( adaptiveLimits ) // maxOffsetEmittance, minOffsetEmittance + l = clamp( l, luminanceParams[3], luminanceParams[2] ); + } + + return l * 0.0025; +} + vec3 LightingFuncGGX_REF(float NdotL, float LdotR, float NdotV, float roughness) { float alpha = roughness * roughness; @@ -524,11 +538,11 @@ void main(void) vec4 color; vec3 albedo = baseMap.rgb; - // emissive intensity (FIXME: this is probably incorrect) + // emissive intensity if ( lm.emissiveSettings.isEnabled ) { - emissive *= exp2( lm.emissiveSettings.exposureOffset * 0.5 ); + emissive *= emissiveIntensity( lm.emissiveSettings.adaptiveEmittance, lm.emissiveSettings.enableAdaptiveLimits, vec4(lm.emissiveSettings.luminousEmittance, lm.emissiveSettings.exposureOffset, lm.emissiveSettings.maxOffsetEmittance, lm.emissiveSettings.minOffsetEmittance) ); } else if ( lm.layeredEmissivity.isEnabled ) { - emissive *= exp2( lm.layeredEmissivity.exposureOffset * 0.5 ); + emissive *= emissiveIntensity( lm.layeredEmissivity.adaptiveEmittance, lm.layeredEmissivity.enableAdaptiveLimits, vec4(lm.layeredEmissivity.luminousEmittance, lm.layeredEmissivity.exposureOffset, lm.layeredEmissivity.maxOffsetEmittance, lm.layeredEmissivity.minOffsetEmittance) ); } vec3 f0 = mix(vec3(0.04), albedo, pbrMap.g); @@ -558,7 +572,7 @@ void main(void) refl *= ambient; ambient *= textureLod(CubeMap2, normalWS, 0.0).rgb; } else { - ambient /= 12.5; + ambient *= 0.08; refl = ambient; } vec3 f = mix(f0, vec3(1.0), envLUT.r); From 912668d123d60e260ae0fcb72f2cfb01a58a4795 Mon Sep 17 00:00:00 2001 From: fo76utils <87907510+fo76utils@users.noreply.github.com> Date: Sat, 25 May 2024 16:43:10 +0200 Subject: [PATCH 14/15] Added Starfield layered emissivity component to NifModel --- CHANGELOG.md | 3 ++- build/nif.xml | 28 ++++++++++++++++++++++++++++ res/shaders/stf_default.frag | 4 ++-- src/model/nifextfiles.cpp | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 66 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fefc2e4..a4f29c5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,8 @@ * Added limited (read only) Starfield support to the UV editor. * Implemented texture slot selection in the UV editor for Fallout 3 and newer games. * Fixed textures not being rendered in the UV editor. -* Minor Starfield rendering fixes. +* Layered emissivity settings are now shown in Starfield BSLightingShaderProperty blocks. +* Starfield rendering fixes. #### NifSkope-2.0.dev9-20240521 diff --git a/build/nif.xml b/build/nif.xml index e74b2a3c..75823405 100644 --- a/build/nif.xml +++ b/build/nif.xml @@ -6878,6 +6878,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -6945,6 +6971,8 @@ + + diff --git a/res/shaders/stf_default.frag b/res/shaders/stf_default.frag index 8094d2b2..329a980b 100644 --- a/res/shaders/stf_default.frag +++ b/res/shaders/stf_default.frag @@ -257,8 +257,8 @@ float emissiveIntensity( bool useAdaptive, bool adaptiveLimits, vec4 luminancePa float l = luminanceParams[0]; // luminousEmittance if ( useAdaptive ) { - l = length( A.rgb ) * 46.188 + length( D.rgb ) * 184.752; - l = l * exp2( luminanceParams[1] ); // exposureOffset + l = dot( A.rgb * 80.0 + D.rgb * 320.0, vec3(0.2126, 0.7152, 0.0722) ); + l = l * exp2( luminanceParams[1] * 0.5 ); // exposureOffset if ( adaptiveLimits ) // maxOffsetEmittance, minOffsetEmittance l = clamp( l, luminanceParams[3], luminanceParams[2] ); } diff --git a/src/model/nifextfiles.cpp b/src/model/nifextfiles.cpp index c8a43307..a9cd291b 100644 --- a/src/model/nifextfiles.cpp +++ b/src/model/nifextfiles.cpp @@ -430,6 +430,40 @@ void NifModel::loadSFMaterial( const QModelIndex & parent, const void *matPtr, i setValue( o, "Max Offset Emittance", sp->maxOffset ); setValue( o, "Min Offset Emittance", sp->minOffset ); } + bool layeredEmissivity = false; + if ( material ) + layeredEmissivity = bool( material->flags & CE2Material::Flag_LayeredEmissivity ); + setValue( m, "Layered Emissivity", layeredEmissivity ); + if ( layeredEmissivity && ( o = getItem( itemToIndex(m), "Layered Emissivity Settings" ) ) != nullptr ) { + const CE2Material::LayeredEmissiveSettings * sp = material->layeredEmissiveSettings; + setValue( o, "First Layer Index", sp->layer1Index ); + setValue( o, "First Layer Tint", ByteColor4( sp->layer1Tint ) ); + setValue( o, "First Layer Mask Source", sp->layer1MaskIndex ); + setValue( o, "Second Layer Active", sp->layer2Active ); + if ( sp->layer2Active ) { + setValue( o, "Second Layer Index", sp->layer2Index ); + setValue( o, "Second Layer Tint", ByteColor4( sp->layer2Tint ) ); + setValue( o, "Second Layer Mask Source", sp->layer2MaskIndex ); + setValue( o, "First Blender Index", sp->blender1Index ); + setValue( o, "First Blender Mode", sp->blender1Mode ); + } + setValue( o, "Third Layer Active", sp->layer3Active ); + if ( sp->layer3Active ) { + setValue( o, "Third Layer Index", sp->layer3Index ); + setValue( o, "Third Layer Tint", ByteColor4( sp->layer3Tint ) ); + setValue( o, "Third Layer Mask Source", sp->layer3MaskIndex ); + setValue( o, "Second Blender Index", sp->blender2Index ); + setValue( o, "Second Blender Mode", sp->blender2Mode ); + } + setValue( o, "Emissive Clip Threshold", sp->clipThreshold ); + setValue( o, "Adaptive Emittance", sp->adaptiveEmittance ); + setValue( o, "Luminous Emittance", sp->luminousEmittance ); + setValue( o, "Exposure Offset", sp->exposureOffset ); + setValue( o, "Enable Adaptive Limits", sp->enableAdaptiveLimits ); + setValue( o, "Max Offset Emittance", sp->maxOffset ); + setValue( o, "Min Offset Emittance", sp->minOffset ); + setValue( o, "Ignores Fog", sp->ignoresFog ); + } bool isTranslucent = false; if ( material ) isTranslucent = bool( material->flags & CE2Material::Flag_Translucency ); From ba14cdaefc5750b1aea3771d333f5cb4e75a3782 Mon Sep 17 00:00:00 2001 From: fo76utils <87907510+fo76utils@users.noreply.github.com> Date: Sat, 25 May 2024 17:17:50 +0200 Subject: [PATCH 15/15] Implemented spPruneRedundantTriangles for BSTriShape geometry --- CHANGELOG.md | 1 + src/spells/mesh.cpp | 13 ++++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4f29c5a..04463077 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ * Added limited (read only) Starfield support to the UV editor. * Implemented texture slot selection in the UV editor for Fallout 3 and newer games. * Fixed textures not being rendered in the UV editor. +* Implemented the Prune Triangles mesh spell for games using BSTriShape geometry. * Layered emissivity settings are now shown in Starfield BSLightingShaderProperty blocks. * Starfield rendering fixes. diff --git a/src/spells/mesh.cpp b/src/spells/mesh.cpp index dcff9cc5..ba0b0d41 100644 --- a/src/spells/mesh.cpp +++ b/src/spells/mesh.cpp @@ -397,13 +397,19 @@ class spPruneRedundantTriangles final : public Spell bool isApplicable( const NifModel * nif, const QModelIndex & index ) override final { + if ( nif->blockInherits( index, "BSTriShape" ) && nif->getIndex( index, "Triangles" ).isValid() ) + return true; return getTriShapeData( nif, index ).isValid(); } QModelIndex cast( NifModel * nif, const QModelIndex & index ) override final { - QModelIndex iData = getTriShapeData( nif, index ); - + QModelIndex iData; + bool isBSTriShape = nif->blockInherits( index, "BSTriShape" ); + if ( !isBSTriShape ) + iData = getTriShapeData( nif, index ); + else + iData = index; QList tris = nif->getArray( iData, "Triangles" ).toList(); int cnt = 0; @@ -447,7 +453,8 @@ class spPruneRedundantTriangles final : public Spell if ( cnt > 0 ) { Message::info( nullptr, Spell::tr( "Removed %1 triangles" ).arg( cnt ) ); nif->set( iData, "Num Triangles", tris.count() ); - nif->set( iData, "Num Triangle Points", tris.count() * 3 ); + if ( !isBSTriShape ) + nif->set( iData, "Num Triangle Points", tris.count() * 3 ); nif->updateArraySize( iData, "Triangles" ); nif->setArray( iData, "Triangles", tris.toVector() ); }