1
0

TreeGenerator.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. import {
  2. BufferAttribute,
  3. BufferGeometry,
  4. Mesh,
  5. Vector3
  6. } from 'three';
  7. import { MeshStandardNodeMaterial } from 'three/webgpu';
  8. import { color, float, mx_fractal_noise_float, positionLocal, vec3 } from 'three/tsl';
  9. // the golden angle ( 137.5° ): rolling each sibling branch by this much around the
  10. // parent axis spreads them like a real stem, so they never line up
  11. const GOLDEN_ANGLE = Math.PI * ( 3 - Math.sqrt( 5 ) );
  12. const DEG2RAD = Math.PI / 180;
  13. const TAU = Math.PI * 2;
  14. const UP = /*@__PURE__*/ new Vector3( 0, 1, 0 );
  15. const _axis = /*@__PURE__*/ new Vector3();
  16. // reusable scratch for one tube's ring vertices ( grows to the largest tube seen )
  17. let _ring = new Float32Array( 0 );
  18. /**
  19. * Grows a procedural tree skeleton — trunk, branches and twigs, each swept as a tapered
  20. * tube — and bakes it into one non-indexed {@link BufferGeometry} (position and normal
  21. * only), ready to instance into a forest. It produces *branches only*; add foliage as a
  22. * separate layer.
  23. *
  24. * The branching is deterministic for a given `seed`: a recursive sweep lays down gently
  25. * curved tubes with a parallel-transport frame (so they never twist), forking by the
  26. * pipe model (each child much thinner than its parent), spreading children along the
  27. * upper part of each branch with a golden-angle roll, and pulling them back up toward
  28. * the light. A flared root, non-linear taper and gravity droop fill in the character.
  29. *
  30. * Parameters are set with a fluent builder: a `set<Param>()` exists for every default
  31. * ( `setSeed`, `setLevels`, `setChildren`, … ), each returning `this` for chaining.
  32. *
  33. * Each `build()` returns a fresh, independent mesh that the caller owns, so one
  34. * generator can be re-parametrized and built repeatedly to grow a varied stand:
  35. *
  36. * ```js
  37. * const generator = new TreeGenerator( material );
  38. * const oak = generator.setSeed( 1 ).setLevels( 4 ).build();
  39. * const pine = generator.setSeed( 2 ).setLevels( 5 ).build();
  40. * ```
  41. */
  42. class TreeGenerator {
  43. constructor( material = null ) {
  44. this.material = material;
  45. this.parameters = {}; // overrides; defaults fill the rest at build time
  46. }
  47. build() {
  48. const p = Object.assign( {}, TreeGenerator.defaults, this.parameters );
  49. const random = createRandom( p.seed );
  50. // grow the skeleton into a flat list of tubes, then size and fill the geometry in
  51. // one pass — no per-vertex objects, no array growth
  52. const tubes = [];
  53. growBranch( tubes, new Vector3(), UP, p.trunkLength, p.trunkRadius, 0, p, random );
  54. let vertexCount = 0;
  55. for ( const tube of tubes ) vertexCount += ( tube.rings.length - 1 ) * tube.radial * 6;
  56. const positions = new Float32Array( vertexCount * 3 );
  57. const normals = new Float32Array( vertexCount * 3 );
  58. let offset = 0;
  59. for ( const tube of tubes ) offset = emitTube( positions, normals, offset, tube.rings, tube.radial );
  60. const geometry = new BufferGeometry();
  61. geometry.setAttribute( 'position', new BufferAttribute( positions, 3 ) );
  62. geometry.setAttribute( 'normal', new BufferAttribute( normals, 3 ) );
  63. geometry.computeBoundingSphere();
  64. const mesh = new Mesh( geometry, this.material || createTreeMaterial() );
  65. mesh.name = 'Tree';
  66. return mesh;
  67. }
  68. }
  69. TreeGenerator.defaults = {
  70. seed: 1,
  71. levels: 4, // recursion depth: trunk, branch, twig, sub-twig
  72. children: [ 3, 12, 8 ], // sub-branches per level; density comes from many children spread along each parent, not depth
  73. branchAngle: [ 38, 50, 58 ], // degrees a child tilts off its parent axis, per level
  74. angleVariance: 14, // degrees of random jitter on the branch angle, breaks fractal regularity
  75. lengthRatio: 0.62, // child length / parent length
  76. trunkLength: 9, // trunk length in world units; sets the tree's height
  77. trunkRadius: 0.42, // base radius of the trunk
  78. taper: 0.55, // a branch thins to ( 1 - taper ) of its base radius along its own length
  79. taperCurve: 0.7, // < 1 keeps the bole full then tapers ( real trees ), 1 = straight cone
  80. rootFlare: 0.6, // how much the trunk swells at the very base
  81. flareFrac: 0.18, // fraction of the trunk over which the flare acts
  82. radiusExponent: 2.3, // pipe model ( da Vinci ): childBase = parentBase × ( 1 / children )^( 1 / radiusExponent )
  83. minRadius: 0.05, // hair-thin floor so twigs don't taper to a sliver
  84. minLength: 0.6, // branches shorter than this stop recursing
  85. droop: 0.05, // gravity sag per branch ( ≈ droop × length ); the trunk stays upright
  86. upPull: 0.3, // phototropism: 0 = a bare spread cone ( many branches aim down ), 1 = straight up
  87. gnarl: [ 0.05, 0.16, 0.26, 0.32 ], // per-level random wobble on each tube segment
  88. radialSegments: 6, // ring vertices around a tube ( drops by one per level for thin twigs )
  89. sectionLength: 1.3, // world units per tube segment, so a tall trunk stays smooth
  90. childStart: 0.12, // fraction up a sub-branch before children appear
  91. trunkClear: 0.25 // fraction of the trunk kept bare before the crown ( raise for a tall clean bole )
  92. };
  93. // a fluent setter for every default — setSeed(), setLevels(), setChildren(), … — each
  94. // storing its value and returning `this`, so the API stays in sync with the parameters
  95. for ( const key of Object.keys( TreeGenerator.defaults ) ) {
  96. TreeGenerator.prototype[ 'set' + key[ 0 ].toUpperCase() + key.slice( 1 ) ] = function ( value ) {
  97. this.parameters[ key ] = value;
  98. return this;
  99. };
  100. }
  101. // --- skeleton ------------------------------------------------------------
  102. // Grows one branch as a gently curved, tapered tube and recurses, collecting the tube
  103. // into `tubes`. The tube is swept with a parallel-transport frame ( rotated by the same
  104. // rotation that bends the tangent each step ) so it never twists, unlike a naive Frenet
  105. // frame. Children fork off the upper part of the branch by the pipe model.
  106. function growBranch( tubes, base, dir, length, baseRadius, level, p, random ) {
  107. const sections = Math.max( 3, Math.min( 24, Math.round( length / p.sectionLength ) ) ); // ring count tracks length
  108. const radial = Math.max( 3, p.radialSegments - level );
  109. const step = length / sections;
  110. const gnarl = p.gnarl[ Math.min( level, p.gnarl.length - 1 ) ];
  111. const start = level === 0 ? p.trunkClear : p.childStart; // the trunk carries a clean bole below its crown
  112. let tangent = dir.clone().normalize();
  113. const normal = perpendicular( tangent );
  114. const rings = [];
  115. const pos = base.clone();
  116. for ( let s = 0; s <= sections; s ++ ) {
  117. const t = s / sections;
  118. // non-linear taper down to ( 1 - taper ) of the base, with a flared root on the trunk
  119. let radius = baseRadius * ( ( 1 - p.taper ) + p.taper * Math.pow( 1 - t, p.taperCurve ) );
  120. if ( level === 0 && p.rootFlare > 0 ) {
  121. const flare = Math.max( 0, ( p.flareFrac - t ) / p.flareFrac );
  122. radius *= 1 + p.rootFlare * flare * flare * flare; // sharp knee, confined to the base
  123. }
  124. rings.push( {
  125. pos: pos.clone(),
  126. tangent: tangent.clone(),
  127. normal: normal.clone(),
  128. binormal: new Vector3().crossVectors( tangent, normal ),
  129. radius
  130. } );
  131. if ( s < sections ) {
  132. const next = tangent.clone();
  133. next.x += ( random() * 2 - 1 ) * gnarl;
  134. next.y += ( random() * 2 - 1 ) * gnarl;
  135. next.z += ( random() * 2 - 1 ) * gnarl;
  136. if ( level > 0 ) next.y -= p.droop * step; // branches sag; the trunk stays vertical
  137. next.normalize();
  138. transport( tangent, next, normal ); // keep the frame torsion-free
  139. pos.addScaledVector( next, step );
  140. tangent = next;
  141. }
  142. }
  143. tubes.push( { rings, radial } );
  144. if ( level >= p.levels - 1 || length < p.minLength ) return;
  145. // fork: children spread along the upper branch, each tilted off the local tangent,
  146. // rolled by the golden angle, and much thinner than the parent ( pipe model )
  147. const n = p.children[ Math.min( level, p.children.length - 1 ) ];
  148. const angle = p.branchAngle[ Math.min( level, p.branchAngle.length - 1 ) ];
  149. const pipeDrop = Math.pow( 1 / n, 1 / p.radiusExponent );
  150. for ( let i = 0; i < n; i ++ ) {
  151. const t = start + ( i + 0.5 + ( random() - 0.5 ) * 0.6 ) / n * ( 1 - start );
  152. const ring = ringAt( rings, t );
  153. const tilt = ( angle + ( random() * 2 - 1 ) * p.angleVariance ) * DEG2RAD;
  154. const roll = i * GOLDEN_ANGLE + ( random() * 2 - 1 ) * 0.4;
  155. // tilt off a perpendicular axis FIRST, then roll about the parent axis, then pull
  156. // back toward the light ( else the roll sends half the children downward )
  157. const childDir = ring.tangent.clone()
  158. .applyAxisAngle( ring.normal, tilt )
  159. .applyAxisAngle( ring.tangent, roll );
  160. if ( p.upPull > 0 ) childDir.lerp( UP, p.upPull ).normalize();
  161. // the pipe-model drop, but never fatter than the wood it leaves nor below the floor
  162. const childBase = Math.max( p.minRadius, Math.min( baseRadius * pipeDrop, ring.radius ) );
  163. growBranch( tubes, ring.pos, childDir, length * p.lengthRatio, childBase, level + 1, p, random );
  164. }
  165. }
  166. // a unit vector perpendicular to v ( cross with the least-aligned axis )
  167. function perpendicular( v ) {
  168. const a = Math.abs( v.x ) < 0.9 ? _axis.set( 1, 0, 0 ) : _axis.set( 0, 1, 0 );
  169. return new Vector3().crossVectors( v, a ).normalize();
  170. }
  171. // rotate frame vector n by the rotation that maps tangent t0 onto t1
  172. function transport( t0, t1, n ) {
  173. _axis.crossVectors( t0, t1 );
  174. const sin = _axis.length();
  175. if ( sin < 1e-6 ) return; // already parallel
  176. _axis.divideScalar( sin );
  177. n.applyAxisAngle( _axis, Math.atan2( sin, t0.dot( t1 ) ) );
  178. }
  179. // sample the branch frame at fraction t ( 0..1 ) for spawning a child
  180. function ringAt( rings, t ) {
  181. const f = Math.max( 0, Math.min( 0.999, t ) ) * ( rings.length - 1 );
  182. const i = Math.floor( f );
  183. const frac = f - i;
  184. const a = rings[ i ];
  185. const b = rings[ Math.min( i + 1, rings.length - 1 ) ];
  186. return {
  187. pos: a.pos.clone().lerp( b.pos, frac ),
  188. tangent: a.tangent.clone().lerp( b.tangent, frac ).normalize(),
  189. normal: a.normal.clone().lerp( b.normal, frac ).normalize(),
  190. radius: a.radius + ( b.radius - a.radius ) * frac
  191. };
  192. }
  193. // --- geometry ------------------------------------------------------------
  194. // Sweeps a tube through the rings: each ring is a loop of `radial` vertices in its
  195. // ( normal, binormal ) plane, the outward radial direction being the vertex normal.
  196. // Ring vertices are computed once into a reused scratch, then stitched straight into the
  197. // preallocated geometry arrays — no per-vertex objects.
  198. function emitTube( positions, normals, offset, rings, radial ) {
  199. const stride = ( radial + 1 ) * 6; // one ring loop: ( position, normal ) per vertex
  200. const needed = rings.length * stride;
  201. if ( _ring.length < needed ) _ring = new Float32Array( needed );
  202. const ring = _ring;
  203. for ( let r = 0; r < rings.length; r ++ ) {
  204. const { pos, normal, binormal, radius } = rings[ r ];
  205. let o = r * stride;
  206. for ( let j = 0; j <= radial; j ++ ) {
  207. const a = j / radial * TAU;
  208. const c = Math.cos( a );
  209. const s = Math.sin( a );
  210. const nx = c * normal.x + s * binormal.x;
  211. const ny = c * normal.y + s * binormal.y;
  212. const nz = c * normal.z + s * binormal.z;
  213. ring[ o ++ ] = pos.x + nx * radius;
  214. ring[ o ++ ] = pos.y + ny * radius;
  215. ring[ o ++ ] = pos.z + nz * radius;
  216. ring[ o ++ ] = nx;
  217. ring[ o ++ ] = ny;
  218. ring[ o ++ ] = nz;
  219. }
  220. }
  221. // stitch consecutive rings into quads ( two triangles ), wound so normals face out
  222. for ( let r = 0; r < rings.length - 1; r ++ ) {
  223. const a = r * stride;
  224. const b = ( r + 1 ) * stride;
  225. for ( let j = 0; j < radial; j ++ ) {
  226. const aL = a + j * 6, aR = a + ( j + 1 ) * 6;
  227. const bL = b + j * 6, bR = b + ( j + 1 ) * 6;
  228. offset = copyVertex( positions, normals, offset, ring, aL );
  229. offset = copyVertex( positions, normals, offset, ring, bR );
  230. offset = copyVertex( positions, normals, offset, ring, bL );
  231. offset = copyVertex( positions, normals, offset, ring, aL );
  232. offset = copyVertex( positions, normals, offset, ring, aR );
  233. offset = copyVertex( positions, normals, offset, ring, bR );
  234. }
  235. }
  236. return offset;
  237. }
  238. // copies one ( position, normal ) vertex from the ring scratch into the geometry arrays
  239. function copyVertex( positions, normals, offset, ring, i ) {
  240. const o = offset * 3;
  241. positions[ o ] = ring[ i ]; positions[ o + 1 ] = ring[ i + 1 ]; positions[ o + 2 ] = ring[ i + 2 ];
  242. normals[ o ] = ring[ i + 3 ]; normals[ o + 1 ] = ring[ i + 4 ]; normals[ o + 2 ] = ring[ i + 5 ];
  243. return offset + 1;
  244. }
  245. // --- deterministic PRNG ( mulberry32 ) -----------------------------------
  246. function createRandom( seed ) {
  247. let s = ( seed >>> 0 ) || 1;
  248. return function () {
  249. s = ( s + 0x6D2B79F5 ) | 0;
  250. let t = Math.imul( s ^ ( s >>> 15 ), 1 | s );
  251. t = ( t + Math.imul( t ^ ( t >>> 7 ), 61 | t ) ) ^ t;
  252. return ( ( t ^ ( t >>> 14 ) ) >>> 0 ) / 4294967296;
  253. };
  254. }
  255. // --- material ------------------------------------------------------------
  256. /**
  257. * A simple bark material for a {@link TreeGenerator} mesh: a low-saturation brown with a
  258. * faint, vertically-stretched grain, so trunks read near-black against bright fog.
  259. *
  260. * @param {Object} [parameters] - `barkColor` ( a hex, THREE.Color or TSL node ).
  261. * @return {MeshStandardNodeMaterial}
  262. */
  263. function createTreeMaterial( parameters = {} ) {
  264. const c = parameters.barkColor;
  265. const barkColor = c === undefined ? color( 0x4b3a2b ) : ( c.isColor || typeof c === 'number' ? color( c ) : c );
  266. const material = new MeshStandardNodeMaterial();
  267. const grain = mx_fractal_noise_float( positionLocal.mul( vec3( 2.5, 0.4, 2.5 ) ), 3 ).mul( 0.18 );
  268. material.colorNode = barkColor.mul( grain.add( 0.9 ) );
  269. material.roughnessNode = float( 0.95 );
  270. material.metalnessNode = float( 0 );
  271. return material;
  272. }
  273. export { TreeGenerator, createTreeMaterial };
粤ICP备19079148号