public:projects:nervland:notes:issue23_wind_sway_animation

Issue #23 - Implement support for tree animations (wind sway)

  • ✅ Generate list of RawSegments
  • ✅ Generate Segments from RawSegments + previous level data on CPU
  • Add support for time animation of the segments
  • Port RawSegments to Segment generation to GPU
  • Add support for shared stem data

Here is the initial version of the RawSegment struct:

struct StemRawSegment {
    Vec4f attitude;
    F32 length;
    F32 startRadius;
    F32 endRadius;
    F32 posOffset;
    I32 parentIndex;
    F32 sway_xoffset;
    F32 sway_yoffset;
    I32 segIndex;
};

And now I'm thinking that when I'm generating a tree, I might also need the segment list itself to generate the raw segs 🤔… Or maybe not ? Let's see.

For now stopping in method void TreeStem::generate(StemRawSegmentLists& rawSegs, LeafDataVector& leaves): currently replacing the “seg” variable to be a StemRawSegment

When starting a child stem, we need to keep track of the parent segment index in the full list, as well as the child stem offset from the starting point of that parent.

Note: the starting point itself, is not available when building the raw segments.

First I'm now trying to generate the raw segments along side the legacy tree segments.

Working on the generation of the raw segment data in TreeStem::generate():

        // For the "attitude" part we need the relative attitude change compared
        // to the parent segment, so that should be the "att" value we computed
        // just above.
        rawSeg.attitude.set(att.as_vec4());
        // Length is the length of each segment in this stem:
        rawSeg.length = _segLength;
        // Store the start/end radius for this segment:
        rawSeg.startRadius = startRadius;
        rawSeg.endRadius = endRadius;
        // The position offset is the translation that we should apply relative
        // to the **start** of the parent segment to get our starting location.
        // This should only apply to the first segment of a stem. And otherwise,
        // is should not be used for the following segments (?). Or it should be
        // set to _segLength maybe: to indicate that we continue from the end of
        // the parent segment.
        // TODO: Continue from here.
        rawSeg.posOffset = 0.0F;

Hmmm… I realize I'm now getting confused with my quaternion operations 🤔. Might be a good idea to add some unit tests on this.

And indeed, multiplying quaternions between them works in the reverse order compared to matrix multiplications! This is confirmed in the following unit test:

BOOST_AUTO_TEST_CASE(test_quat_mult) {

    // Test some random rotations
    for (int i = 0; i < 100; ++i) {

        // Angle in radians:
        double a1 = gen_double(-500.0, 500.0);
        auto axis1 = gen_vec3d();
        double a2 = gen_double(-500.0, 500.0);
        auto axis2 = gen_vec3d();

        auto q1 = Quatd(a1, axis1);
        auto q2 = Quatd(a2, axis2);

        // Compute the multiplications:
        auto q3 = q1 * q2;

        // Now compute with mats:
        auto r1 = Mat4d(q1);
        auto r2 = Mat4d(q2);

        // auto r3 = r1 * r2; // This fails!
        auto r3 = r2 * r1; // This works!

        // Now multiply in place:
        q1 *= q2;

        // we expect q1 == q3 now:
        BOOST_CHECK_EQUAL(q1, q3);

        // get the quat from the r3 mat:
        auto q3b = r3.get_rotate();
        BOOST_CHECK_EQUAL(q3, q3b);
    }
}

Just added another test on vector multiplications:

BOOST_AUTO_TEST_CASE(test_quat_mult_vec) {

    // Test some random rotations
    for (int i = 0; i < 100; ++i) {

        // Angle in radians:
        double a1 = gen_double(-500.0, 500.0);
        auto axis1 = gen_vec3d();
        double a2 = gen_double(-500.0, 500.0);
        auto axis2 = gen_vec3d();

        auto vec = gen_vec3d();

        auto q1 = Quatd(a1, axis1);
        auto q2 = Quatd(a2, axis2);

        // Compute the multiplications:
        auto q3 = q1 * q2;

        // Now transform the input vector:
#if 0
        // The following will FAIL!
        // auto vec2 = q2 * vec;
        // auto vec3 = q1 * vec2;
#else
        // The following WORKS!
        auto vec2 = q1 * vec;
        auto vec3 = q2 * vec2;
#endif

        auto vec3b = q3 * vec;

        auto diff = std::abs((vec3b - vec3).length());
        // BOOST_CHECK_EQUAL(vec3, vec3b);
        BOOST_CHECK_LT(diff, 1e-12);
    }
}

⇒ This confirms that the result of q1 * q2 is actually the quaternion that I would expect to compute as q2 * q1 😲! Which is just unbelievable…

Let's see where we use the quaternion multiplication in our code so far.

Now that we have our list of RawSegments and the actual list of Segments that we use for rendering, we can also generate another list of segments, this time using only the raw segments for construction.

Then we can compare our newly generated list with the reference list of segments (note: we do not apply any time animation yet)

OK, I now got the segments regenerated from raw segments almost correctly. There seem to be only one small issue remaining, as shown in the images below:

vs. with the reference segments:

⇒ And this is now fixed: we need to prepare the startUp vector differently for intermediate segments:

            // Apply the translation offset first:
            ltw.post_mult_translate({0.0F, raw.posOffset, 0.0F});

            // if we have a segment that is not at the start of the stem, we
            // should Get the startUp before applying the attitude offset:
            auto startUp = ltw.col(2).xyz();

            // Then apply the attitude offset:
            ltw.post_mult_rotate(Quatd(raw.attitude.x(), raw.attitude.y(),
                                       raw.attitude.z(), raw.attitude.w()));

            // If this is the first segment of the stem, then we retrieve the
            // startUp at this point:
            if (raw.segIndex == 0) {
                startUp = ltw.col(2).xyz();
            }

            // Scale by start radius:
            startUp = startUp.normalized() * raw.startRadius;

Idea: At some point I should introduce the concept of the StemData struct to avoid duplicating data in the raw segments. So could use for instance:

struct StemData {
    F32 totalLength;
    F32 startRadius;
    F32 endRadius;
    F32 posOffset;
    I32 parentIndex;
    I32 numSegs;
    I32 level;
};

struct StemRawSegment {
    Vec4f attitude;
    F32 swayXOffset;
    F32 swayYOffset;
    I32 stemIndex;
    I32 segIndex;
};

Just introduced a simple change in the tree/render_leaves.wgsl shader:

    var alpha = 1.0;

    if params.mode == 0 {
        albedo = textureSample(diffuseTex, linSampler, uv);
        alpha = textureSample(opacityTex, linSampler, uv).r;
    }

    if alpha < 1e-5 {
        // If the alpha value is 0, we discard this fragment:
        discard;
    }

And the fixed the leaf rendering order issue 😉!

Currently it seems that the leave normal is not computed correctly: even with the leaf oriented up we seem to get different normal directions. Investigating why.

  • public/projects/nervland/notes/issue23_wind_sway_animation.txt
  • Last modified: 2024/05/02 21:15
  • by 127.0.0.1