diff --git a/components/nifosg/nifloader.cpp b/components/nifosg/nifloader.cpp index 23fb9f7439..28019f177d 100644 --- a/components/nifosg/nifloader.cpp +++ b/components/nifosg/nifloader.cpp @@ -200,33 +200,6 @@ namespace } } } - - void handleExtraData(const std::string& data, osg::Group* node) - { - YAML::Node root = YAML::Load(data); - - for (const auto& it : root["shader"]) - { - std::string key = it.first.as(); - - if (key == "soft_effect" && NifOsg::Loader::getSoftEffectEnabled()) - { - SceneUtil::SoftEffectConfig config; - config.mSize = it.second["size"].as(config.mSize); - config.mFalloff = it.second["falloff"].as(config.mFalloff); - config.mFalloffDepth = it.second["falloffDepth"].as(config.mFalloffDepth); - - SceneUtil::setupSoftEffect(*node, config); - } - else if (key == "distortion") - { - SceneUtil::DistortionConfig config; - config.mStrength = it.second["strength"].as(config.mStrength); - - SceneUtil::setupDistortion(*node, config); - } - } - } } namespace NifOsg @@ -664,6 +637,64 @@ namespace NifOsg return node; } + void handleExtraData( + const Nif::NiAVObject* nifNode, const std::string& data, osg::Group* node, HandleNodeArgs& args) + { + YAML::Node root = YAML::Load(data); + + const bool isNiGeometry = isTypeNiGeometry(nifNode->recType); + const bool isBSGeometry = isTypeBSGeometry(nifNode->recType); + const bool isGeometry = isNiGeometry || isBSGeometry; + + for (const auto& it : root["shader"]) + { + std::string key = it.first.as(); + + if (key == "soft_effect" && NifOsg::Loader::getSoftEffectEnabled()) + { + SceneUtil::SoftEffectConfig config; + config.mSize = it.second["size"].as(config.mSize); + config.mFalloff = it.second["falloff"].as(config.mFalloff); + config.mFalloffDepth = it.second["falloffDepth"].as(config.mFalloffDepth); + + SceneUtil::setupSoftEffect(*node, config); + } + else if (key == "distortion") + { + SceneUtil::DistortionConfig config; + config.mStrength = it.second["strength"].as(config.mStrength); + + SceneUtil::setupDistortion(*node, config); + } + else if (key == "flowmap") + { + if (!isGeometry) + { + throw Nif::Exception( + "flowmap effect can only be used on geometry nodes such as NiTriShape", mFilename.string()); + } + + SceneUtil::FlowMapConfig config; + config.mStrength = it.second["strength"].as(config.mStrength); + config.mSpeed = it.second["speed"].as(config.mSpeed); + config.mOffset = it.second["offset"].as(config.mOffset); + config.mJump = it.second["jump"].as(config.mJump); + + std::string path = it.second["texture"]["path"].as(); + int uvSet = it.second["texture"]["uvSet"].as(0); + + config.mTexture = new osg::Texture2D(getTextureImage(path)); + config.mTexture->setWrap(osg::Texture2D::WRAP_S, osg::Texture2D::REPEAT); + config.mTexture->setWrap(osg::Texture2D::WRAP_T, osg::Texture2D::REPEAT); + config.mTextureUnit = args.mBoundTextures.size(); + + args.mBoundTextures.emplace_back(uvSet); + + SceneUtil::setupFlowMap(*node, config); + } + } + } + osg::ref_ptr handleNode( const Nif::NiAVObject* nifNode, const Nif::Parent* parent, osg::Group* parentNode, HandleNodeArgs args) { @@ -830,7 +861,7 @@ namespace NifOsg // Apply any extra effects after processing the nodes children and particle system handling if (!extraData.empty()) - handleExtraData(extraData, node); + handleExtraData(nifNode, extraData, node, args); if (composite->getNumControllers() > 0) { diff --git a/components/sceneutil/extradata.cpp b/components/sceneutil/extradata.cpp index 7455992dbb..4597c8d2db 100644 --- a/components/sceneutil/extradata.cpp +++ b/components/sceneutil/extradata.cpp @@ -1,9 +1,11 @@ #include "extradata.hpp" #include +#include #include #include +#include namespace SceneUtil { @@ -35,4 +37,18 @@ namespace SceneUtil stateset->setAttributeAndModes(depth, osg::StateAttribute::ON); } + + void setupFlowMap(osg::Node& node, const FlowMapConfig& config) + { + osg::StateSet* stateset = node.getOrCreateStateSet(); + + stateset->addUniform(new osg::Uniform("flowMapStrength", config.mStrength)); + stateset->addUniform(new osg::Uniform("flowMapSpeed", config.mSpeed)); + stateset->addUniform(new osg::Uniform("flowMapOffset", config.mOffset)); + stateset->addUniform(new osg::Uniform("flowMapJump", config.mJump)); + + stateset->setTextureAttribute(config.mTextureUnit, config.mTexture, osg::StateAttribute::ON); + stateset->setTextureAttribute( + config.mTextureUnit, new SceneUtil::TextureType("flowMap"), osg::StateAttribute::ON); + } } diff --git a/components/sceneutil/extradata.hpp b/components/sceneutil/extradata.hpp index b3c6388c78..01fccf381e 100644 --- a/components/sceneutil/extradata.hpp +++ b/components/sceneutil/extradata.hpp @@ -4,6 +4,7 @@ namespace osg { class Node; + class vec2f; } namespace SceneUtil @@ -20,8 +21,19 @@ namespace SceneUtil float mStrength = 0.1f; }; + struct FlowMapConfig + { + float mStrength = 1.f; + float mSpeed = 1.f; + float mOffset = 0.f; + osg::Vec2f mJump = { 0.f, 0.f }; + osg::ref_ptr mTexture = nullptr; + std::size_t mTextureUnit = 0; + }; + void setupSoftEffect(osg::Node& node, const SoftEffectConfig& config); void setupDistortion(osg::Node& node, const DistortionConfig& config); + void setupFlowMap(osg::Node& node, const FlowMapConfig& config); } #endif diff --git a/components/shader/shadervisitor.cpp b/components/shader/shadervisitor.cpp index 0e6b5e79c6..66841b2dbc 100644 --- a/components/shader/shadervisitor.cpp +++ b/components/shader/shadervisitor.cpp @@ -291,7 +291,7 @@ namespace Shader // shader defines. Normal maps and normal height maps both get sent to the shader as a normal map, so the latter // must be detected separately. const char* defaultTextures[] = { "diffuseMap", "normalMap", "emissiveMap", "darkMap", "detailMap", "envMap", - "specularMap", "decalMap", "bumpMap", "glossMap" }; + "specularMap", "decalMap", "bumpMap", "glossMap", "flowMap" }; bool isTextureNameRecognized(std::string_view name) { if (std::find(std::begin(defaultTextures), std::end(defaultTextures), name) != std::end(defaultTextures)) @@ -395,6 +395,10 @@ namespace Shader // As well as gloss maps writableStateSet->setTextureMode(unit, GL_TEXTURE_2D, osg::StateAttribute::ON); } + else if (texName == "flowMap") + { + mRequirements.back().mShaderRequired = true; + } } else Log(Debug::Error) << "ShaderVisitor encountered unknown texture " << texture; diff --git a/docs/source/reference/modding/custom-shader-effects.rst b/docs/source/reference/modding/custom-shader-effects.rst index 41dd4c7369..a41231a644 100644 --- a/docs/source/reference/modding/custom-shader-effects.rst +++ b/docs/source/reference/modding/custom-shader-effects.rst @@ -59,7 +59,7 @@ Distortion ---------- This effect is used to imitate effects such as refraction and heat distortion. A common use case is to assign a normal map to the -diffuse slot to a material and add uv scrolling. The red and green channels of the texture are used to offset the final scene texture. +diffuse slot to a material and add UV scrolling. The red and green channels of the texture are used to offset the final scene texture. Blue and alpha channels are ignored. To use this feature the :ref:`post processing ` setting must be enabled. @@ -90,3 +90,64 @@ Example usage. } } } + +Flow Map +-------- + ++-----------------+ +| Supported Nodes | ++-----------------+ +| NiTriShape | ++-----------------+ +| NiTriStrips | ++-----------------+ + +This effect allows textured geometry to be animated and distorted using flow map textures. This can be useful for simulating +flowing liquids or magical effects. UV maps are distorted according to the flow map texture. They are then blended with +another phase of the same UV at a different time offset. Flow maps can only be applied to geometry nodes, such as `NiTriShape`, +and affects the diffuse, normal, and specular maps. + +Variables. + ++---------+--------------------------------------------------------------------------------------------------------+---------+---------+ +| Name | Description | Type | Default | ++---------+--------------------------------------------------------------------------------------------------------+---------+---------+ +| strength| The strength of the flow map UV distortion. Scales linearly. | float | 1 | ++---------+--------------------------------------------------------------------------------------------------------+---------+---------+ +| speed | The speed of the flow map, affects both the UV distortion and the phase change. | float | 1 | ++---------+--------------------------------------------------------------------------------------------------------+---------+---------+ +| offset | The time offset of the flow map. Controls where the animation starts. | float | 0 | ++---------+--------------------------------------------------------------------------------------------------------+---------+---------+ +| jump | Controls the UV offset applied with each phase shift, along the x and y axis. | vec2f | [0, 0] | ++---------+--------------------------------------------------------------------------------------------------------+---------+---------+ + +The flow map requires a texture and UV set to use. The red and green channels of the flow map texture are the flow directions +in x and y respectively, the blue channel is used to offset the phase change, hiding repetition. The texture path and UV set +are defined in a JSON object with the name `texture`. + ++---------+--------------------------------------------------------------------------------------------------------+---------+---------+ +| Name | Description | Type | Default | ++---------+--------------------------------------------------------------------------------------------------------+---------+---------+ +| path | Path to the flow map texture in the VFS. This is required. | string | nullptr | ++---------+--------------------------------------------------------------------------------------------------------+---------+---------+ +| uvSet | ID of the UV set to use for the flow map texture. | int | 0 | ++---------+--------------------------------------------------------------------------------------------------------+---------+---------+ + +Example usage. + +:: + + omw:data { + "shader" : { + "flowmap" : { + "strength": 0.5, + "speed": 1.0, + "offset": 0.0, + "jump": [0.25, 0.24], + "texture": { + "path" : "flowmap.tga", + "uvSet": 1 + } + } + } + } diff --git a/files/shaders/CMakeLists.txt b/files/shaders/CMakeLists.txt index bcb1a22fb2..8f6e005875 100644 --- a/files/shaders/CMakeLists.txt +++ b/files/shaders/CMakeLists.txt @@ -17,6 +17,7 @@ set(SHADER_FILES lib/util/quickstep.glsl lib/util/coordinates.glsl lib/util/distortion.glsl + lib/util/flowmap.glsl lib/core/fragment.glsl lib/core/fragment.h.glsl lib/core/fragment_multiview.glsl diff --git a/files/shaders/compatibility/objects.frag b/files/shaders/compatibility/objects.frag index 2e3ea795fb..99fe7b1db2 100644 --- a/files/shaders/compatibility/objects.frag +++ b/files/shaders/compatibility/objects.frag @@ -62,6 +62,11 @@ uniform sampler2D glossMap; varying vec2 glossMapUV; #endif +#if @flowMap +uniform sampler2D flowMap; +varying vec2 flowMapUV; +#endif + uniform vec2 screenRes; uniform float near; uniform float far; @@ -94,6 +99,7 @@ varying vec4 passTangent; #include "lib/material/parallax.glsl" #include "lib/material/alpha.glsl" #include "lib/util/distortion.glsl" +#include "lib/util/flowmap.glsl" #include "fog.glsl" #include "vertexcolors.glsl" @@ -114,6 +120,8 @@ uniform sampler2D orthoDepthMap; varying vec3 orthoDepthMapCoord; #endif +uniform float osg_SimulationTime; + void main() { #if @particleOcclusion @@ -135,7 +143,12 @@ void main() vec2 screenCoords = gl_FragCoord.xy / screenRes; #if @diffuseMap + +#if @flowMap + gl_FragData[0] = applyFlowMap(diffuseMap, diffuseMapUV + offset, flowMap, flowMapUV, osg_SimulationTime); +#else gl_FragData[0] = texture2D(diffuseMap, diffuseMapUV + offset); +#endif #if defined(DISTORTION) && DISTORTION gl_FragData[0].a *= getDiffuseColor().a; @@ -163,7 +176,13 @@ vec2 screenCoords = gl_FragCoord.xy / screenRes; gl_FragData[0].a = alphaTest(gl_FragData[0].a, alphaRef); #if @normalMap + +#if @flowMap + vec4 normalTex = applyFlowMap(normalMap, normalMapUV + offset, flowMap, flowMapUV, osg_SimulationTime); +#else vec4 normalTex = texture2D(normalMap, normalMapUV + offset); +#endif + vec3 normal = normalTex.xyz * 2.0 - 1.0; #if @reconstructNormalZ normal.z = sqrt(1.0 - dot(normal.xy, normal.xy)); @@ -221,7 +240,13 @@ vec2 screenCoords = gl_FragCoord.xy / screenRes; specular = passSpecular + shadowSpecularLighting * shadowing; #else #if @specularMap + +#if @flowMap + vec4 specTex = applyFlowMap(specularMap, specularMapUV, flowMap, flowMapUV, osg_SimulationTime); +#else vec4 specTex = texture2D(specularMap, specularMapUV); +#endif + float shininess = specTex.a * 255.0; vec3 specularColor = specTex.xyz; #else diff --git a/files/shaders/compatibility/objects.vert b/files/shaders/compatibility/objects.vert index 081ff909cf..527a1c0a79 100644 --- a/files/shaders/compatibility/objects.vert +++ b/files/shaders/compatibility/objects.vert @@ -49,6 +49,10 @@ varying vec2 specularMapUV; varying vec2 glossMapUV; #endif +#if @flowMap +varying vec2 flowMapUV; +#endif + #define PER_PIXEL_LIGHTING (@normalMap || @specularMap || @forcePPL) #if !PER_PIXEL_LIGHTING @@ -147,6 +151,10 @@ void main(void) glossMapUV = (gl_TextureMatrix[@glossMapUV] * gl_MultiTexCoord@glossMapUV).xy; #endif +#if @flowMap + flowMapUV = (gl_TextureMatrix[@flowMapUV] * gl_MultiTexCoord@flowMapUV).xy; +#endif + #if !PER_PIXEL_LIGHTING vec3 diffuseLight, ambientLight, specularLight; doLighting(viewPos.xyz, viewNormal, gl_FrontMaterial.shininess, diffuseLight, ambientLight, specularLight, shadowDiffuseLighting, shadowSpecularLighting); diff --git a/files/shaders/lib/util/flowmap.glsl b/files/shaders/lib/util/flowmap.glsl new file mode 100644 index 0000000000..65a3cf81e5 --- /dev/null +++ b/files/shaders/lib/util/flowmap.glsl @@ -0,0 +1,41 @@ +#ifndef LIB_UTIL_FLOWMAP +#define LIB_UTIL_FLOWMAP + +uniform float flowMapStrength; +uniform float flowMapSpeed; +uniform float flowMapOffset; +uniform vec2 flowMapJump; + +vec3 flowUVW(vec2 uv, vec2 flowVector, vec2 jump, float flowOffset, float time, float phaseOffset) +{ + float alpha = fract(time + phaseOffset); + + vec3 uvw = vec3(0.0); + uvw.xy = uv - flowVector * (alpha + flowOffset); + uvw.xy += phaseOffset; + uvw.xy += (time - alpha) * jump; + uvw.z = 1.0 - abs(1.0 - 2.0 * alpha); + + return uvw; +} + +vec4 applyFlowMap(sampler2D tex, vec2 texUV, sampler2D flowMapTex, vec2 flowMapUV, float time) +{ + // Based on https://catlikecoding.com/unity/tutorials/flow/texture-distortion/ + // TODO: When using this on a normal map the blended normals are incorrect, does this need a unique function or is it 'good enough'? + vec2 flowVector = texture2D(flowMapTex, flowMapUV).xy * 2.0 - 1.0; + flowVector *= flowMapStrength; + + float noise = texture2D(flowMapTex, flowMapUV).z; + time = time * flowMapSpeed + noise; + + vec3 uvwA = flowUVW(texUV, flowVector, flowMapJump, flowMapOffset, time, 0.0); + vec3 uvwB = flowUVW(texUV, flowVector, flowMapJump, flowMapOffset, time, 0.5); + + vec4 texA = texture2D(tex, uvwA.xy) * uvwA.z; + vec4 texB = texture2D(tex, uvwB.xy) * uvwB.z; + + return texA + texB; +} + +#endif