Sun damage tweaks

Feedback on past, current, and future development.
MaxCross
Posts: 9
Joined: 18 Jun 2020, 08:56
Gitlab profile: https://gitlab.com/MaxCross

Sun damage tweaks

Post by MaxCross »

Well, I guess i'm more or less done!
MaxCross wrote: 23 Jun 2020, 19:36 I forked openmw and created an merge request. Since, as I said, I don't think that this very speciffic feature will ever be merged into master branch of the official repository, target branch is in my fork also.
Just, if someone wants to have realistic sun damage in their version of openmw, maybe with this merge request it will be easier for them...
This is a temporary feature anyway - until the scripting system is added. And I really hope that when it is added, functions such as rendering raycast or getting the coordinates of the actor’s body parts will be available in it, so I don’t have to modify the engine source code only for realistic sun damage.
---------------------------------------------------------------------------------

(note: i hope it isn't very noticeable that i'm not a native english speaker. If it is - i'm sorry)
Since shadows were added, I really wanted to get the opportunity to hide in them from the sun when I play as a vampire.
I understand that this is a very specific feature and it's unlikely to be added to engine officially, so I decided to try adding it myself.
Doesn’t sound very difficult - just raycast from the player’s position in the direction of the sun.
As far as I understand, currently there is no way to raycast scripted-way (at least I did not find one), so I have to modify the source code of the engine for this.
The problem is that I am not at all familiar with this source code at all.
However, after ~15 hours of setting up the build environment and digging in the source code, I got something similar to what I want.
Currently, code looks like this (there are several other changes here, but they are not relevant to this topic):

Code: Select all

//efffectTick() in mwmechanics/tickableeffect.cpp:145
        case ESM::MagicEffect::SunDamage:
        {
            // isInCell shouldn't be needed, but updateActor called during game start
            if (!actor.isInCell() || !actor.getCell()->isExterior())
                break;
            float time = MWBase::Environment::get().getWorld()->getTimeStamp().getHour();

            float adjTime = time;
            float dayStart = 6.f;
            float dayEnd = 20.f;
            float dayDuration = dayEnd - dayStart;
            float nightDuration = 24.f - dayDuration;

            if(adjTime < dayStart) adjTime += 24.f;

            double theta;
            if(adjTime < dayEnd) theta = osg::PI * (adjTime - dayStart) / dayDuration;
            else theta = osg::PI - osg::PI * (adjTime - dayEnd) / nightDuration;
            
            auto actorPos = actor.getRefData().getPosition().asVec3() + osg::Vec3f(0,0,125);
            osg::Vec3f sunDirection(
                cos(theta),
                -0.268f,//~=tan(-15)
                sin(theta)
            );
            sunDirection.normalize();
            auto sunPos = actorPos + sunDirection * 500000;

            int mask = MWPhysics::CollisionType_World | MWPhysics::CollisionType_Door | MWPhysics::CollisionType_Actor | MWPhysics::CollisionType_HeightMap;
            if(MWBase::Environment::get().getWorld()->castRay(actorPos, sunPos, mask, actor)) break;

            float sunriseDuration = 1.0f;
            float sunriseMult = std::min(std::max((adjTime - dayStart) / sunriseDuration, 0.f), 1.f);

            float sunsetDuration = 1.0f;
            float sunsetMult = std::min(std::max((dayEnd - adjTime) / sunsetDuration, 0.f), 1.f);

            float sunMagnitude = std::min(sunriseMult, sunsetMult);

            // When cloudy, the sun damage effect is halved
            static float fMagicSunBlockedMult = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find(
                        "fMagicSunBlockedMult")->mValue.getFloat();
            int weather = MWBase::Environment::get().getWorld()->getCurrentWeather();
            if (weather > 1)
                sunMagnitude *= fMagicSunBlockedMult;

            if (magnitude * sunMagnitude > 0.f) {
                adjustDynamicStat(creatureStats, 0, -magnitude * sunMagnitude);
                receivedMagicDamage = true;
            }

            break;
        }
And I have a few questions about this implementation:
  • Is there any better way to get the direction of the sun than to calculate it manually? This method I actually copied from mwworld/weather.cpp:753.
  • Is there any way to get the coordinates of a certain body part of the actor (MWWorld::Ptr&)? As a minimum - I would like to raycast from the actor’s head, not from a point which is 125 units higher than the actor’s position. As a maximum, I would like to raycast from each part of the actor’s body, so that the damage depends on how many body parts are actualy under the sun.
  • Is there any analogue of a raycast that can hit meshes, not just collisions? The current version will not allow me, for example, to hide under the leaves of trees.
  • Do I understand correctly that the coordinates are in centimeters? I mean, I had to raise the actor’s coordinates by 125 units to cast from the head...
  • Any other suggestions on how this can be improved?
PS Current version is actualy working: https://youtu.be/GWAn2IFSdWE
Last edited by MaxCross on 23 Jun 2020, 19:48, edited 1 time in total.
User avatar
psi29a
Posts: 5361
Joined: 29 Sep 2011, 10:13
Location: Belgium
Gitlab profile: https://gitlab.com/psi29a/
Contact:

Re: Sun damage tweaks

Post by psi29a »

This is pretty cool, thanks for the video, it helps demonstrate what you're trying to achieve. :)
User avatar
AnyOldName3
Posts: 2677
Joined: 26 Nov 2015, 03:25

Re: Sun damage tweaks

Post by AnyOldName3 »

Coordinates aren't centimetres, they're a stupid unit that makes not very much sense: https://wiki.openmw.org/index.php?title ... ment_Units

As for hiding behind things without collision, I think it's possible (look at how things get selected when you click on them with the console open), but you're probably going to find surprising side effects, as there are meshes that clearly let light through (or even *are* light, like sunbeams that come in through windows) that you won't be able to exclude.

One thing that may come to mind is doing it based on actual shadows, but it would be a nightmare to get that data off the GPU. The simplest way would probably be to do occlusion queries against the shadow maps, but there's not any easy place to slip that in, and you'll find you tank the framerate if you try and get the results without waiting a frame or two after submitting the query. If you're not already a graphics programmer, don't even think about attempting it.
MaxCross
Posts: 9
Joined: 18 Jun 2020, 08:56
Gitlab profile: https://gitlab.com/MaxCross

Re: Sun damage tweaks

Post by MaxCross »

AnyOldName3, thanks for the reply!

It looks like object selection via console ends up in RenderingManager::castCameraToViewportRay(), wich, as far as i understand, rely on some rendering-speciffic things, so it cant be used for raycasting from any point to any direction..

But, it seems like the version of castRay() in the same file (in the RenderingManager class) does exactly what i need. It calls RenderingManager::getIntersectionVisitor() and there is some layer mask that makes me thing this is actualy rendering rayCast... I could even, maybe, create an overload of castRay that create different intersectionVisitor with different layer mask (without, for example, Mask_Debug and Mask_Effect).
Spoiler: Show
But, i didn't really understand how different components of the engine interact with each other. In this case, i need to get a reference to RenderingManager to call this version of castRay, and i dont know if that's even possible with just MWBase::Enviroment...
MaxCross
Posts: 9
Joined: 18 Jun 2020, 08:56
Gitlab profile: https://gitlab.com/MaxCross

Re: Sun damage tweaks

Post by MaxCross »

Okay, I tried - it doesn't work... + I had to edit several components of the engine which i probably shouldn't touch to just change one effect:
1) I made my own versions of castRay and getIntersectionVisitor in renderingmanager.cpp (beacuse i probably will want to change mask in the future):

Code: Select all

    osg::ref_ptr<osgUtil::IntersectionVisitor> RenderingManager::getIntersectionVisitor(osgUtil::Intersector *intersector, int mask)
    {
        if (!mIntersectionVisitor)
            mIntersectionVisitor = new osgUtil::IntersectionVisitor;

        mIntersectionVisitor->setTraversalNumber(mViewer->getFrameStamp()->getFrameNumber());
        mIntersectionVisitor->setFrameStamp(mViewer->getFrameStamp());
        mIntersectionVisitor->setIntersector(intersector);

        mIntersectionVisitor->setTraversalMask(mask);
        return mIntersectionVisitor;
    }

    RenderingManager::RayResult RenderingManager::castRay(const osg::Vec3f& origin, const osg::Vec3f& dest, int mask)
    {
        osg::ref_ptr<osgUtil::LineSegmentIntersector> intersector (new osgUtil::LineSegmentIntersector(osgUtil::LineSegmentIntersector::MODEL,
            origin, dest));
        intersector->setIntersectionLimit(osgUtil::LineSegmentIntersector::LIMIT_NEAREST);

        mRootNode->accept(*getIntersectionVisitor(intersector, mask));

        return getIntersectionResult(intersector);
    }
(and i also had to declare them in renderingmanager.hpp)
2) in worldimp.cpp i create function castRenderingRay():

Code: Select all

    MWRender::RenderingManager::RayResult World::castRenderingRay(const osg::Vec3f from, const osg::Vec3f to, int mask)
    {
        return mRendering->castRay(from, to, mask);
    }
(and i also had declare them in worldimp.hpp, again...)
3) I create virtual function castRenderingRay in world.hpp:

Code: Select all

virtual MWRender::RenderingManager::RayResult castRenderingRay(const osg::Vec3f from, const osg::Vec3f to, int mask) = 0;
4) call castRenderingRay() instead of normal one in tickable effect.hpp

Code: Select all

            // int mask = MWPhysics::CollisionType_World | MWPhysics::CollisionType_Door | MWPhysics::CollisionType_Actor | MWPhysics::CollisionType_HeightMap;
            // if(MWBase::Environment::get().getWorld()->castRay(actorPos, sunPos, mask, actor)) break;
            int mask = MWRender::VisMask::Mask_RenderToTexture
                            |MWRender::VisMask::Mask_Sky
                            |MWRender::VisMask::Mask_Debug
                            |MWRender::VisMask::Mask_Effect
                            |MWRender::VisMask::Mask_Water
                            |MWRender::VisMask::Mask_SimpleWater
                            |MWRender::VisMask::Mask_Player
                            |MWRender::VisMask::Mask_Actor;
            auto rr = MWBase::Environment::get().getWorld()->castRenderingRay(actorPos, sunPos, mask);
            if(rr.mHit) break;
And, as i said - it doesn't work anyway - I always get damage from the sun => rr.mHit is always false => castRenderingRay() doesn't work.

+, as a bonus - if I even touch renderingmanager, or worldimpl, or world, it cause ~ 15 minutes of recompilation of the half of the engine... Yeah, "c++ is very sad" (C) Johnatan Blow...

Well, it's too much changes in different parts of the engine to just get realistic sun damage anyway.
It seems that I will have to come to terms with the fact that I can hide only behind things with a collision.

I'm still want to now how to get coordinats of actor's body parts...
User avatar
AnyOldName3
Posts: 2677
Joined: 26 Nov 2015, 03:25

Re: Sun damage tweaks

Post by AnyOldName3 »

It should only cause a big recompilation if you modify the headers, and fifteen minutes is a lot even if you do. My CPU isn't that fast for compilation by modern standards, but it can do the whole engine, CS and launcher etc. in not hugely much longer than that. Are you on a particularly weak machine or are you forgetting something like the -j parameter for make?
MaxCross
Posts: 9
Joined: 18 Jun 2020, 08:56
Gitlab profile: https://gitlab.com/MaxCross

Re: Sun damage tweaks

Post by MaxCross »

AnyOldName3, okay, i will know that...
I just folowing guide from the wiki ("Development Environment Setup - NMake 2013-2019 scripted way"), so i compile in git bash with

Code: Select all

$ cd MSVC2019_64_NMake_Release/
$ source activate_msvc.sh
$ nmake openmw
I didn’t even know about -j parameter, because in the guide, in the "NMake 2013-2019 scripted way" section, there was nothing about it...
User avatar
AnyOldName3
Posts: 2677
Joined: 26 Nov 2015, 03:25

Re: Sun damage tweaks

Post by AnyOldName3 »

NMake is literally the dumbest way of building OpenMW. No one should actually use it. It's the only way you can build OpenMW where it's guaranteed only to use a single core as NMake is the only CMake generator we support that doesn't have a -j or equivalent.

If you want something command-line on Windows, then run the script in Ninja mode. It should be:

Code: Select all

CI/before_script.msvc.sh -k -v whateveryourvsversionis -p 64 -N -c configurationname
cd MSVCwhateveryourvsversionis_64_Ninja_configurationnamebutonlyifyourcmakeversionislessthan3.17
. activate_msvc.sh
cmake --build . --config configurationname
If you're on CMake 3.16.x or older, then the last step can just be ninja as they only added multi-config Ninja to 3.17, and you don't need to specify which configuration if you've only got one.
MaxCross
Posts: 9
Joined: 18 Jun 2020, 08:56
Gitlab profile: https://gitlab.com/MaxCross

Re: Sun damage tweaks

Post by MaxCross »

Well, I guess I found a way how to get actor's body parts positions, but... Now I really feel like i'm doing something unsafe/performance unfriendly... I found some head tracking code in mwmechanics/character.cpp:2846, that somehow gets position of character's head. I have no idea what Matrix magic happening there, but I, mostly, copied it:

Code: Select all

            float rayLength = 500000;
            auto anim = MWBase::Environment::get().getWorld()->getAnimation(actor);

            struct {char* boneName; float boneDamageScale;} nodeMap[] = {
                {"Head", 0.3f},
                {"Bip01 L UpperArm", 0.075f},
                {"Bip01 R UpperArm", 0.075f},
                {"Bip01 L Forearm", 0.075f},
                {"Bip01 R Forearm", 0.075f},
                {"Bip01 L Hand", 0.05f},
                {"Bip01 R Hand", 0.05f},
                {"Bip01 Spine1", 0.2f},
                {"Bip01 L Foot", 0.1f},
                {"Bip01 R Foot", 0.1f},
            };

            int mask = MWPhysics::CollisionType_World| MWPhysics::CollisionType_Door
                        | MWPhysics::CollisionType_Actor | MWPhysics::CollisionType_HeightMap;
            float sunVisibilityScale = 0;
            for(int i = 0; i < sizeof(nodeMap)/sizeof(nodeMap[1]); i++) {
                auto node = anim->getNode(nodeMap[i].boneName);
                auto nodePaths = node->getParentalNodePaths();
                auto matrix = osg::computeLocalToWorld(nodePaths[0]);
                auto pos = matrix.getTrans();
                if(!MWBase::Environment::get().getWorld()->castRay(pos, pos + sunDirection * rayLength, mask, actor))
                    sunVisibilityScale += nodeMap[i].boneDamageScale;
            }
and... It works! Now I have sun damage that depends on how many body parts are actualy under the sun: https://youtu.be/3-mKJOj1naU

But... Is that really only way to get actor's body parts/bones positions without rewriting half of the engine?
MaxCross
Posts: 9
Joined: 18 Jun 2020, 08:56
Gitlab profile: https://gitlab.com/MaxCross

Re: Sun damage tweaks

Post by MaxCross »

That rendering raycast is actualy works! I just misunderstood how that mask in getIntersectionVisitor works - I had to negate it...
So, now I can hide behind things that don't have collisions. And yes, it's indeed doesn't take into a count the fact that, for example, some pixels on the texture could be transparrent (like trees leaves)... But it's still much better than just check for collisions.

I also added "idlestorm" animation when actor's head is under the sun and he looks directly at it, but for some reason it keeps reseting, so it look's like he stop covering his eyes for a moment every 3 seconds: https://youtu.be/rudZ9aq53FM
Is there any way to fix it without modifying any files except tickableeffect.cpp?

Code: Select all

       case ESM::MagicEffect::SunDamage:
        {
            // isInCell shouldn't be needed, but updateActor called during game start
            if (!actor.isInCell() || !actor.getCell()->isExterior())
                break;
            float time = MWBase::Environment::get().getWorld()->getTimeStamp().getHour();

            float adjTime = time;
            float dayStart = 6.f;
            float dayEnd = 20.f;
            float dayDuration = dayEnd - dayStart;
            float nightDuration = 24.f - dayDuration;

            if(adjTime < dayStart) adjTime += 24.f;

            double theta;
            if(adjTime < dayEnd) theta = osg::PI * (adjTime - dayStart) / dayDuration;
            else theta = osg::PI - osg::PI * (adjTime - dayEnd) / nightDuration;
            
            auto actorPos = actor.getRefData().getPosition().asVec3() + osg::Vec3f(0,0,125);
            osg::Vec3f sunDirection(
                cos(theta),
                -0.268f,//~=tan(-15)
                sin(theta)
            );
            sunDirection.normalize();

            struct {char* boneName; float boneDamageScale;} nodeMap[] = {
                {"Head", 0.3f},
                {"Bip01 L UpperArm", 0.075f},
                {"Bip01 R UpperArm", 0.075f},
                {"Bip01 L Forearm", 0.075f},
                {"Bip01 R Forearm", 0.075f},
                {"Bip01 L Hand", 0.05f},
                {"Bip01 R Hand", 0.05f},
                {"Bip01 Spine1", 0.2f},
                {"Bip01 L Foot", 0.1f},
                {"Bip01 R Foot", 0.1f},
            };

            float sunVisibilityMult = 0;
            float rayLength = 500000;
            auto anim = MWBase::Environment::get().getWorld()->getAnimation(actor);
            
            int mask = ~(
                MWRender::Mask_RenderToTexture
                |MWRender::Mask_Sky
                |MWRender::Mask_Debug
                |MWRender::Mask_Effect
                |MWRender::Mask_Water
                |MWRender::Mask_SimpleWater
                |MWRender::Mask_Actor
                |MWRender::Mask_Player
            );
            for(int i = 0; i < sizeof(nodeMap)/sizeof(nodeMap[1]); i++) {
                auto node = anim->getNode(nodeMap[i].boneName);
                auto nodePaths = node->getParentalNodePaths();
                auto matrix = osg::computeLocalToWorld(nodePaths[0]);
                auto pos = matrix.getTrans();
                if(!MWBase::Environment::get().getWorld()->castRenderingRay(pos, pos + sunDirection * rayLength, mask)) {
                    sunVisibilityMult += nodeMap[i].boneDamageScale;
                    if(i == 0) {
                        auto characterDirection = actor.getRefData().getBaseNode()->getAttitude() * osg::Vec3f(0,1,0);
                        if(characterDirection * sunDirection <= 0) continue;
                        if(!anim->isPlaying("idlestorm")) {
                            int animMask = MWRender::Animation::BlendMask_Torso | MWRender::Animation::BlendMask_RightArm;
                            anim->play("idlestorm", Priority_Storm, animMask, true, 1.0f, "start", "stop", 0.0f, ~0ul);
                        }
                        else anim->setLoopingEnabled("idlestorm", true);
                    }
                }
            }

            float sunriseDuration = 1.0f;
            float sunriseMult = std::min(std::max((adjTime - dayStart) / sunriseDuration, 0.f), 1.f);

            float sunsetDuration = 1.0f;
            float sunsetMult = std::min(std::max((dayEnd - adjTime) / sunsetDuration, 0.f), 1.f);

            float sunMagnitude = std::min(sunriseMult, sunsetMult);

            // When cloudy, the sun damage effect is halved
            static float fMagicSunBlockedMult = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find(
                        "fMagicSunBlockedMult")->mValue.getFloat();
            int weather = MWBase::Environment::get().getWorld()->getCurrentWeather();
            if (weather > 1)
                sunMagnitude *= fMagicSunBlockedMult;

            if (magnitude * sunMagnitude * sunVisibilityMult > 0.f) {
                adjustDynamicStat(creatureStats, 0, -magnitude * sunMagnitude * sunVisibilityMult);
                receivedMagicDamage = true;
            }

            break;
        }
Post Reply