@@ -82,6 +82,9 @@ public Tree3D(Plane pln, double globalScale, double trunkScale, int seed = 0,
8282
8383 mMaxSideBranchLen = 0.5 * mScaledLen ;
8484 mMinSideBranchLen = 0.25 * mScaledLen * mTScale / mStage1 ;
85+
86+ // The ratio between top and bottom max branch lengths (top is this fraction of bottom)
87+ mBranchLenTaperRatio = 0.6 ;
8588
8689 mId = id ;
8790 }
@@ -102,6 +105,7 @@ public Tree3D Copy() {
102105 copy . mAngleTop = this . mAngleTop ;
103106 copy . mMaxSideBranchLen = this . mMaxSideBranchLen ;
104107 copy . mMinSideBranchLen = this . mMinSideBranchLen ;
108+ copy . mBranchLenTaperRatio = this . mBranchLenTaperRatio ;
105109 copy . mNearestTreeDist = this . mNearestTreeDist ;
106110 copy . mNearestTree = new Point3d ( this . mNearestTree ) ;
107111 copy . mSoloRadius = this . mSoloRadius ;
@@ -276,12 +280,22 @@ public void GrowStage1() {
276280 // Initial rotation for the first (bottom) layer
277281 curDir . Rotate ( baseAngle , mPln . XAxis ) ;
278282
279- // Calculate branch length
280- double branchLenIncrement = ( mMaxSideBranchLen - mMinSideBranchLen ) / mStage1 ;
281- var bottomBranchLen = mMinSideBranchLen + ( auxPhaseS1 * branchLenIncrement ) ;
283+ // Calculate branch length for Stage 1
284+ // Stage 1 should reach ~60% of the final max length
285+ // This leaves room for gradual growth in phases 5-10
286+ double stage1MaxRatio = 0.6 ; // Stage 1 reaches 60% of final max
287+ double stage1MaxLen = mMaxSideBranchLen * stage1MaxRatio ;
288+ double stage1MinLen = mMinSideBranchLen ;
289+
290+ double branchLenIncrement = ( stage1MaxLen - stage1MinLen ) / mStage1 ;
291+ var bottomBranchLen = stage1MinLen + ( auxPhaseS1 * branchLenIncrement ) ;
282292
283293 // Track current layer for gradual angle calculation
284294 int layerCount = 0 ;
295+
296+ // Stage 1 taper ratio - more aggressive than final taper to create pyramid shape
297+ // Top layer branches are much shorter than bottom to leave room for trunk tip branching
298+ double stage1TaperRatio = 0.3 ; // Top branches are 30% of bottom (more aggressive than final 60%)
285299
286300 // Calculate branch position on the trunk
287301 for ( int segIdx = 0 ; segIdx < auxPhaseS1 ; segIdx ++ ) {
@@ -295,11 +309,13 @@ public void GrowStage1() {
295309 for ( int brNum = 0 ; brNum < mNumBranchPerLayer ; brNum ++ ) {
296310 var node = new BranchNode3D ( mAllNode . Count , segIdx + 1 , pt ) ;
297311
298- // Calculate branch length based on position and growth
299- double branchLen =
300- ( totalBranchLayer == 1 ? mMinSideBranchLen
301- : Utils . remap ( curBranchLayer , 1 , totalBranchLayer ,
302- bottomBranchLen , mMinSideBranchLen ) ) ;
312+ // Apply aggressive taper for Stage 1: create pyramid/triangle shape
313+ // Bottom branches are full length, top branches are much shorter
314+ double layerRatio = ( totalBranchLayer == 1 ) ? 0.0
315+ : ( double ) curBranchLayer / ( totalBranchLayer - 1 ) ;
316+ double layerTaperFactor = 1.0 - ( 1.0 - stage1TaperRatio ) * layerRatio ;
317+
318+ double branchLen = bottomBranchLen * layerTaperFactor ;
303319
304320 // Rotation in XY-plane
305321 double horRotRadian = Math . PI * 2 / mNumBranchPerLayer ;
@@ -348,31 +364,115 @@ public void GrowStage1() {
348364 AddNodeToTree ( mBaseNode , topNode ) ;
349365 }
350366
367+ /// <summary>
368+ /// Calculate the target branch length for a given node based on its height and current phase.
369+ /// This provides a unified length calculation across all stages for gradual growth.
370+ /// </summary>
371+ /// <param name="node">The branch node</param>
372+ /// <param name="currentPhase">The current growth phase</param>
373+ /// <returns>Target branch length for this node at this phase</returns>
374+ private double GetTargetBranchLen ( BranchNode3D node , int currentPhase ) {
375+ // Get the height ratio of this node (0 = bottom, 1 = top)
376+ double nodeHeight = 0 ;
377+ double trunkHeight = mScaledLen * mTScale ;
378+
379+ if ( node . mBranch . Count > 0 ) {
380+ mPln . RemapToPlaneSpace ( node . mBranch [ 0 ] . PointAtStart , out Point3d localPt ) ;
381+ nodeHeight = localPt . Z ;
382+ }
383+ double heightRatio = Math . Clamp ( nodeHeight / trunkHeight , 0.0 , 1.0 ) ;
384+
385+ // Taper transitions from aggressive (Stage 1) to final (Stage 3)
386+ // Stage 1 taper: 0.3 (pyramid shape)
387+ // Final taper: 0.6 (more uniform mature tree)
388+ double stage1TaperRatio = 0.3 ;
389+ double finalTaperRatio = mBranchLenTaperRatio ; // 0.6
390+
391+ // Calculate taper factor based on phase
392+ double taperFactor ;
393+ if ( currentPhase <= mStage1 ) {
394+ // Phase 1-4: use aggressive Stage 1 taper
395+ taperFactor = 1.0 - ( 1.0 - stage1TaperRatio ) * heightRatio ;
396+ } else if ( currentPhase <= mStage3 ) {
397+ // Phase 5-10: transition from Stage 1 taper to final taper
398+ double phasesInTransition = mStage3 - mStage1 ;
399+ double transitionProgress = ( double ) ( currentPhase - mStage1 ) / phasesInTransition ;
400+ // Interpolate taper ratio from stage1 to final
401+ double currentTaperRatio = stage1TaperRatio + ( finalTaperRatio - stage1TaperRatio ) * transitionProgress ;
402+ taperFactor = 1.0 - ( 1.0 - currentTaperRatio ) * heightRatio ;
403+ } else {
404+ // Phase 11-12: use final taper
405+ taperFactor = 1.0 - ( 1.0 - finalTaperRatio ) * heightRatio ;
406+ }
407+
408+ // Calculate the final max length for this node (at phase 10)
409+ double finalMaxLen = mMaxSideBranchLen * ( 1.0 - ( 1.0 - finalTaperRatio ) * heightRatio ) ;
410+
411+ // Calculate growth progress based on phase
412+ // Phase 1-4: grow from minLen to 60% of finalMaxLen
413+ // Phase 5-10: grow from 60% to 100% of finalMaxLen (with front-loaded curve)
414+ // Phase 11-12: no growth (dying phase)
415+
416+ double stage1MaxRatio = 0.6 ; // Stage 1 reaches 60% of final max
417+ double minLen = mMinSideBranchLen * taperFactor ;
418+ double stage1MaxLen = finalMaxLen * stage1MaxRatio ;
419+
420+ double targetLen ;
421+
422+ if ( currentPhase <= mStage1 ) {
423+ // Phase 1-4: grow from minLen toward stage1MaxLen with Stage 1 taper
424+ double progress = ( double ) currentPhase / mStage1 ;
425+ // Apply Stage 1 taper to the target length
426+ double stage1FinalMaxLen = mMaxSideBranchLen * ( 1.0 - ( 1.0 - stage1TaperRatio ) * heightRatio ) ;
427+ double stage1Target = stage1FinalMaxLen * stage1MaxRatio ;
428+ targetLen = minLen + ( stage1Target - minLen ) * progress ;
429+ } else if ( currentPhase <= mStage3 ) {
430+ // Phase 5-10: grow from stage1MaxLen toward finalMaxLen
431+ // Use front-loaded growth curve (square root) so branches grow faster early
432+ double phasesInMatureGrowth = mStage3 - mStage1 ; // 6 phases (5,6,7,8,9,10)
433+ double linearProgress = ( double ) ( currentPhase - mStage1 ) / phasesInMatureGrowth ;
434+ // Square root curve: faster growth in early phases, slower as it approaches max
435+ double progress = Math . Sqrt ( linearProgress ) ;
436+
437+ // Calculate what the length was at end of Stage 1
438+ double stage1FinalMaxLen = mMaxSideBranchLen * ( 1.0 - ( 1.0 - stage1TaperRatio ) * heightRatio ) ;
439+ double stage1EndLen = stage1FinalMaxLen * stage1MaxRatio ;
440+
441+ targetLen = stage1EndLen + ( finalMaxLen - stage1EndLen ) * progress ;
442+ } else {
443+ // Phase 11-12: no growth, stay at phase 10 level
444+ targetLen = finalMaxLen ;
445+ }
446+
447+ return targetLen ;
448+ }
449+
351450 public void GrowStage2 ( int dupNum = 0 ) {
352451 // auxiliary phase variable
353452 var auxPhaseS2 = Math . Min ( mPhase , mStage2 ) ;
354453
355- // ! phase 5-10 : branching phase
454+ // ! phase 5-8 : branching phase
356455 var splitInitLen = mScaledLen * 0.2 ;
357456
358457 // Select nodes to branch: top nodes from previous phase and selected side branches
359458 for ( int curPhase = mStage1 + 1 ; curPhase <= auxPhaseS2 ; curPhase ++ ) {
360- // Continue to grow branch emerged in Stage 1
361- var addedPhase = curPhase - mStage1 ;
362- var lenIncrementPerPhase = ( mMaxSideBranchLen - mMinSideBranchLen ) / mStage1 ;
459+ // Continue to grow branches emerged in Stage 1
363460 foreach ( var node in mTrunkBranchNode ) {
364- if ( ! mBranchRelation . ContainsKey ( node . mID ) ) {
365- var tmpLst = new List < Curve > ( ) ;
366- foreach ( var br in node . mBranch ) {
367- var dir = br . PointAtEnd - br . PointAtStart ;
368- dir . Unitize ( ) ;
369- var increLen = addedPhase * lenIncrementPerPhase ;
370- var len = Math . Min ( mMaxSideBranchLen , br . GetLength ( ) + increLen ) ;
371-
372- tmpLst . Add ( new Line ( br . PointAtStart , dir * len ) . ToNurbsCurve ( ) ) ;
373- } ;
374- node . mBranch = tmpLst ;
375- }
461+ var tmpLst = new List < Curve > ( ) ;
462+ // Get the target length for this node at this phase
463+ double targetLen = GetTargetBranchLen ( node , curPhase ) ;
464+
465+ foreach ( var br in node . mBranch ) {
466+ var currentLen = br . GetLength ( ) ;
467+ var dir = br . PointAtEnd - br . PointAtStart ;
468+ dir . Unitize ( ) ;
469+
470+ // Grow toward target, but never shrink
471+ var newLen = Math . Max ( currentLen , targetLen ) ;
472+
473+ tmpLst . Add ( new Line ( br . PointAtStart , dir * newLen ) . ToNurbsCurve ( ) ) ;
474+ } ;
475+ node . mBranch = tmpLst ;
376476 }
377477
378478 // for each end node, branch out several new branches
@@ -435,35 +535,36 @@ public void GrowStage3() {
435535 // auxiliary phase variable
436536 var auxPhaseS3 = Math . Min ( mPhase , mStage3 ) ;
437537
438- // ! phase 5 -10: branching phase
538+ // ! phase 9 -10: mature phase - side branches continue to grow
439539 var splitInitLen = mScaledLen * 0.2 ;
440540
441541 // Select nodes to branch: top nodes from previous phase and selected side branches
442542 for ( int curPhase = mStage2 + 1 ; curPhase <= auxPhaseS3 ; curPhase ++ ) {
443- // Continue to grow branch emerged in Stage 1
444- var addedPhase = curPhase - mStage1 ;
445- var lenIncrementPerPhase = ( mMaxSideBranchLen - mMinSideBranchLen ) / mStage1 ;
543+ // Continue to grow ALL trunk branches during mature phases
446544 foreach ( var node in mTrunkBranchNode ) {
447- if ( ! mBranchRelation . ContainsKey ( node . mID ) ) {
448- var tmpLst = new List < Curve > ( ) ;
449- foreach ( var br in node . mBranch ) {
450- var dir = br . PointAtEnd - br . PointAtStart ;
451- dir . Unitize ( ) ;
452- var increLen = addedPhase * lenIncrementPerPhase ;
453- var len = Math . Min ( mMaxSideBranchLen , br . GetLength ( ) + increLen ) ;
454-
455- tmpLst . Add ( new Line ( br . PointAtStart , dir * len ) . ToNurbsCurve ( ) ) ;
456- } ;
457- node . mBranch = tmpLst ;
458- }
545+ var tmpLst = new List < Curve > ( ) ;
546+ // Get the target length for this node at this phase
547+ double targetLen = GetTargetBranchLen ( node , curPhase ) ;
548+
549+ foreach ( var br in node . mBranch ) {
550+ var currentLen = br . GetLength ( ) ;
551+ var dir = br . PointAtEnd - br . PointAtStart ;
552+ dir . Unitize ( ) ;
553+
554+ // Grow toward target, but never shrink
555+ var newLen = Math . Max ( currentLen , targetLen ) ;
556+
557+ tmpLst . Add ( new Line ( br . PointAtStart , dir * newLen ) . ToNurbsCurve ( ) ) ;
558+ } ;
559+ node . mBranch = tmpLst ;
459560 }
460561
461562 // for each end node, branch out several new branches
462563 var startNodeId = mAllNode . Count ;
463564 var nodesToSplit = mAllNode . Where ( node => node . flagSplittable == true ) . ToList ( ) ;
464565
465- // Select additional side branches to grow (only the 1st phase of stage 2 )
466- if ( curPhase == mStage1 + 1 ) {
566+ // Select additional side branches to grow (only the 1st phase of stage 3 )
567+ if ( curPhase == mStage2 + 1 ) {
467568 var sideNodeToBranch = SelectTopUnbranchedNodes ( dupNum ) ;
468569 sideNodeToBranch . ForEach ( x => x . ToggleSplitable ( ) ) ;
469570 nodesToSplit . AddRange ( sideNodeToBranch ) ;
@@ -499,11 +600,6 @@ public void GrowStage3() {
499600 newNode . flagBranchSplit . Add ( true ) ; // make sure new node labeled split
500601 newNode . ToggleSplitable ( ) ;
501602 AddNodeToTree ( node , newNode ) ;
502-
503- // store the root splitted branches
504- if ( curPhase == mStage1 + 1 ) {
505- mBaseSplittedNode . Add ( newNode ) ;
506- }
507603 }
508604
509605 // after the split, toggle it so that the next iteration will not split it again
@@ -916,6 +1012,7 @@ public void GetTrunckVolume(in int curPhase, out Mesh trunkMesh) {
9161012 public double mAngleTop ;
9171013 public double mMaxSideBranchLen ;
9181014 public double mMinSideBranchLen ;
1015+ public double mBranchLenTaperRatio ; // Ratio of top branch max length to bottom branch max length
9191016 public double mHeight ;
9201017 public string mId ;
9211018
0 commit comments