====== Issue #23 - Implement support for tree animations (wind sway) ====== ===== Tasks ===== * ✅ 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 ===== Generating list of RawSegments ===== 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** ===== Reconsidering the generation of the RawSegments ===== 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. ===== Re-generating Segments from RawSegments ===== 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: {{ projects:nervland:03_segment_from_raw_segs_error.png }} vs. with the reference segments: {{ projects:nervland:04_ref_segments.png }} => 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; }; ===== Improvement on leaves rendering with fragment discard ===== 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 😉! {{ public:projects:nervland:0022_leaf_alpha_masking.png }} ===== Improving on leaves lighting ===== 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.