Merge branch 'freakier-flowmaps' into 'master'

Flowmap extradata effect

See merge request OpenMW/openmw!4917
This commit is contained in:
Epoch 2025-09-10 02:27:20 +00:00
commit 4f7a9c1a5f
9 changed files with 229 additions and 30 deletions

View File

@ -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<std::string>();
if (key == "soft_effect" && NifOsg::Loader::getSoftEffectEnabled())
{
SceneUtil::SoftEffectConfig config;
config.mSize = it.second["size"].as<float>(config.mSize);
config.mFalloff = it.second["falloff"].as<bool>(config.mFalloff);
config.mFalloffDepth = it.second["falloffDepth"].as<float>(config.mFalloffDepth);
SceneUtil::setupSoftEffect(*node, config);
}
else if (key == "distortion")
{
SceneUtil::DistortionConfig config;
config.mStrength = it.second["strength"].as<float>(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<std::string>();
if (key == "soft_effect" && NifOsg::Loader::getSoftEffectEnabled())
{
SceneUtil::SoftEffectConfig config;
config.mSize = it.second["size"].as<float>(config.mSize);
config.mFalloff = it.second["falloff"].as<bool>(config.mFalloff);
config.mFalloffDepth = it.second["falloffDepth"].as<float>(config.mFalloffDepth);
SceneUtil::setupSoftEffect(*node, config);
}
else if (key == "distortion")
{
SceneUtil::DistortionConfig config;
config.mStrength = it.second["strength"].as<float>(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<float>(config.mStrength);
config.mSpeed = it.second["speed"].as<float>(config.mSpeed);
config.mOffset = it.second["offset"].as<float>(config.mOffset);
config.mJump = it.second["jump"].as<osg::Vec2f>(config.mJump);
std::string path = it.second["texture"]["path"].as<std::string>();
int uvSet = it.second["texture"]["uvSet"].as<int>(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<osg::Node> 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)
{

View File

@ -1,9 +1,11 @@
#include "extradata.hpp"
#include <osg/Node>
#include <osg/Vec2f>
#include <components/misc/osguservalues.hpp>
#include <components/sceneutil/depth.hpp>
#include <components/sceneutil/texturetype.hpp>
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);
}
}

View File

@ -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<osg::Texture2D> 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

View File

@ -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;

View File

@ -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 <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
}
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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);

View File

@ -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