[{"data":1,"prerenderedAt":2186},["ShallowReactive",2],{"\u002Fblog\u002Fanimating-grass-with-wind-in-tsl":3},{"id":4,"title":5,"body":6,"date":2165,"description":2166,"extension":2167,"meta":2168,"navigation":2174,"path":2175,"seo":2176,"sitemap":2177,"status":2178,"stem":2179,"tags":2180,"thumbnail":2184,"__hash__":2185},"blog\u002Fblog\u002Fanimating-grass-with-wind-in-tsl.md","Animating Grass with Wind in TSL 🌬️",{"type":7,"value":8,"toc":2154},"minimark",[9,17,27,57,73,78,89,96,99,106,496,499,505,519,522,556,561,568,585,612,616,630,718,732,736,743,846,864,875,881,896,900,911,918,998,1214,1550,1646,1760,1787,1792,1795,1799,1806,2092,2102,2106,2116,2120,2150],[10,11,13],"scene-wrapper",{"caption":12},"10,000 instanced blades bending in a noise-driven wind field",[14,15],"blog-grass-step-demo",{":step":16},"8",[18,19,20,21,26],"p",{},"A static grass field is a rug. The moment it moves, it becomes a place. This is the second post in the grass series, and it does one thing: take the instanced blade from ",[22,23,25],"a",{"href":24},"\u002Fblog\u002Fcreating-a-grass-field-with-tsl","part 1"," and put it in the wind.",[18,28,29,30,34,35,38,39,42,43,46,47,51,52,56],{},"Part 1 ended on a promise. We sculpted a ",[31,32,33],"code",{},"tipness"," attribute into the blade, ",[31,36,37],{},"0"," at the base, ",[31,40,41],{},"0.7"," at the mid vertices, ",[31,44,45],{},"1"," at the tip, and used it to fake ambient occlusion. I said it had a second job: if you displace each vertex horizontally ",[48,49,50],"em",{},"proportionally to it",", the base stays planted, the middle leans, and the tip travels furthest, so the blade ",[53,54,55],"strong",{},"bends"," instead of shearing. That's the whole plan. Let's cash it in.",[18,58,59,60,66,67,72],{},"Same two references as before, both GLSL: ",[22,61,65],{"href":62,"rel":63},"https:\u002F\u002Fgithub.com\u002Fbrunosimon\u002Ffolio-2025\u002Fblob\u002Fmain\u002Fsources\u002FGame\u002FWind.js",[64],"nofollow","Bruno Simon's folio-2025 wind"," and the ",[22,68,71],{"href":69,"rel":70},"https:\u002F\u002Fmedium.com\u002Fantaeus-ar\u002Fmaking-grass-with-triangles-in-glsl-using-three-js-e106771a71ff",[64],"Antaeus AR article",". We'll translate the ideas to TSL.",[74,75,77],"h2",{"id":76},"a-wind-field-not-per-blade-jitter","A Wind Field, Not Per-Blade Jitter",[18,79,80,81,84,85,88],{},"The naive instinct is to give every blade its own little oscillation: ",[31,82,83],{},"sin(time + random)",". Please don't 😅. Real wind is meant to be ",[53,86,87],{},"coherent",", a gust rolls across the field and neighboring blades lean together. That coherence is the whole reason wind reads as wind and not as static.",[18,90,91,92,95],{},"So wind isn't a per-blade property. It's a ",[53,93,94],{},"field",": a function of world position and time that every blade samples at its anchor. Blades close together sample nearly the same value and move together; blades a gust apart move differently. We get that for free from noise.",[97,98],"blog-wind-field-diagram",{},[18,100,101,102,105],{},"Here's the field, translated from Bruno's ",[31,103,104],{},"Wind.js",":",[107,108,113],"pre",{"className":109,"code":110,"language":111,"meta":112,"style":112},"language-ts shiki shiki-themes catppuccin-frappe","const WIND_ANGLE = Math.PI * 0.6\nconst windDirection = uniform(new Vector2(Math.sin(WIND_ANGLE), Math.cos(WIND_ANGLE)))\nconst windPositionFrequency = uniform(0.5)\nconst windStrength = uniform(0.45)\nconst windTimeFrequency = uniform(0.5)\nconst windLocalTime = uniform(0)\n\nconst windOffset = Fn(([worldXZ]) => {\n  const p = worldXZ.mul(windPositionFrequency)\n  const n1 = mx_noise_float(p.mul(0.2).add(windDirection.mul(windLocalTime)))\n  const n2 = mx_noise_float(p.mul(0.1).add(windDirection.mul(windLocalTime.mul(0.2)))).mul(0.5)\n  const intensity = n1.add(n2).add(0.4)\n  return windDirection.mul(intensity).mul(windStrength)\n})\n","ts","",[31,114,115,148,197,217,236,254,272,279,310,332,375,434,465,488],{"__ignoreMap":112},[116,117,120,124,128,132,135,138,142,145],"span",{"class":118,"line":119},"line",1,[116,121,123],{"class":122},"sHfct","const",[116,125,127],{"class":126},"sdZ07"," WIND_ANGLE ",[116,129,131],{"class":130},"sO16o","=",[116,133,134],{"class":126}," Math",[116,136,137],{"class":130},".",[116,139,141],{"class":140},"sQqXY","PI",[116,143,144],{"class":130}," *",[116,146,147],{"class":140}," 0.6\n",[116,149,151,153,156,158,162,165,169,172,175,177,180,183,187,189,191,194],{"class":118,"line":150},2,[116,152,123],{"class":122},[116,154,155],{"class":126}," windDirection ",[116,157,131],{"class":130},[116,159,161],{"class":160},"soURg"," uniform",[116,163,164],{"class":126},"(",[116,166,168],{"class":167},"sPs8m","new",[116,170,171],{"class":160}," Vector2",[116,173,174],{"class":126},"(Math",[116,176,137],{"class":130},[116,178,179],{"class":160},"sin",[116,181,182],{"class":126},"(WIND_ANGLE)",[116,184,186],{"class":185},"sABYQ",",",[116,188,134],{"class":126},[116,190,137],{"class":130},[116,192,193],{"class":160},"cos",[116,195,196],{"class":126},"(WIND_ANGLE)))\n",[116,198,200,202,205,207,209,211,214],{"class":118,"line":199},3,[116,201,123],{"class":122},[116,203,204],{"class":126}," windPositionFrequency ",[116,206,131],{"class":130},[116,208,161],{"class":160},[116,210,164],{"class":126},[116,212,213],{"class":140},"0.5",[116,215,216],{"class":126},")\n",[116,218,220,222,225,227,229,231,234],{"class":118,"line":219},4,[116,221,123],{"class":122},[116,223,224],{"class":126}," windStrength ",[116,226,131],{"class":130},[116,228,161],{"class":160},[116,230,164],{"class":126},[116,232,233],{"class":140},"0.45",[116,235,216],{"class":126},[116,237,239,241,244,246,248,250,252],{"class":118,"line":238},5,[116,240,123],{"class":122},[116,242,243],{"class":126}," windTimeFrequency ",[116,245,131],{"class":130},[116,247,161],{"class":160},[116,249,164],{"class":126},[116,251,213],{"class":140},[116,253,216],{"class":126},[116,255,257,259,262,264,266,268,270],{"class":118,"line":256},6,[116,258,123],{"class":122},[116,260,261],{"class":126}," windLocalTime ",[116,263,131],{"class":130},[116,265,161],{"class":160},[116,267,164],{"class":126},[116,269,37],{"class":140},[116,271,216],{"class":126},[116,273,275],{"class":118,"line":274},7,[116,276,278],{"emptyLinePlaceholder":277},true,"\n",[116,280,282,284,287,289,292,294,297,301,304,307],{"class":118,"line":281},8,[116,283,123],{"class":122},[116,285,286],{"class":126}," windOffset ",[116,288,131],{"class":130},[116,290,291],{"class":160}," Fn",[116,293,164],{"class":126},[116,295,296],{"class":185},"([",[116,298,300],{"class":299},"su21L","worldXZ",[116,302,303],{"class":185},"])",[116,305,306],{"class":130}," =>",[116,308,309],{"class":185}," {\n",[116,311,313,316,319,321,324,326,329],{"class":118,"line":312},9,[116,314,315],{"class":122},"  const",[116,317,318],{"class":126}," p ",[116,320,131],{"class":130},[116,322,323],{"class":126}," worldXZ",[116,325,137],{"class":130},[116,327,328],{"class":160},"mul",[116,330,331],{"class":126},"(windPositionFrequency)\n",[116,333,335,337,340,342,345,348,350,352,354,357,360,362,365,368,370,372],{"class":118,"line":334},10,[116,336,315],{"class":122},[116,338,339],{"class":126}," n1 ",[116,341,131],{"class":130},[116,343,344],{"class":160}," mx_noise_float",[116,346,347],{"class":126},"(p",[116,349,137],{"class":130},[116,351,328],{"class":160},[116,353,164],{"class":126},[116,355,356],{"class":140},"0.2",[116,358,359],{"class":126},")",[116,361,137],{"class":130},[116,363,364],{"class":160},"add",[116,366,367],{"class":126},"(windDirection",[116,369,137],{"class":130},[116,371,328],{"class":160},[116,373,374],{"class":126},"(windLocalTime)))\n",[116,376,378,380,383,385,387,389,391,393,395,398,400,402,404,406,408,410,413,415,417,419,421,424,426,428,430,432],{"class":118,"line":377},11,[116,379,315],{"class":122},[116,381,382],{"class":126}," n2 ",[116,384,131],{"class":130},[116,386,344],{"class":160},[116,388,347],{"class":126},[116,390,137],{"class":130},[116,392,328],{"class":160},[116,394,164],{"class":126},[116,396,397],{"class":140},"0.1",[116,399,359],{"class":126},[116,401,137],{"class":130},[116,403,364],{"class":160},[116,405,367],{"class":126},[116,407,137],{"class":130},[116,409,328],{"class":160},[116,411,412],{"class":126},"(windLocalTime",[116,414,137],{"class":130},[116,416,328],{"class":160},[116,418,164],{"class":126},[116,420,356],{"class":140},[116,422,423],{"class":126},"))))",[116,425,137],{"class":130},[116,427,328],{"class":160},[116,429,164],{"class":126},[116,431,213],{"class":140},[116,433,216],{"class":126},[116,435,437,439,442,444,447,449,451,454,456,458,460,463],{"class":118,"line":436},12,[116,438,315],{"class":122},[116,440,441],{"class":126}," intensity ",[116,443,131],{"class":130},[116,445,446],{"class":126}," n1",[116,448,137],{"class":130},[116,450,364],{"class":160},[116,452,453],{"class":126},"(n2)",[116,455,137],{"class":130},[116,457,364],{"class":160},[116,459,164],{"class":126},[116,461,462],{"class":140},"0.4",[116,464,216],{"class":126},[116,466,468,471,474,476,478,481,483,485],{"class":118,"line":467},13,[116,469,470],{"class":122},"  return",[116,472,473],{"class":126}," windDirection",[116,475,137],{"class":130},[116,477,328],{"class":160},[116,479,480],{"class":126},"(intensity)",[116,482,137],{"class":130},[116,484,328],{"class":160},[116,486,487],{"class":126},"(windStrength)\n",[116,489,491,494],{"class":118,"line":490},14,[116,492,493],{"class":185},"}",[116,495,216],{"class":126},[18,497,498],{},"Three ideas are doing the work here.",[18,500,501,504],{},[53,502,503],{},"A single direction."," Wind has a heading. We derive a 2D direction on the ground plane from one angle and reuse it everywhere: to scroll the noise, and as the axis every blade leans along. Change the angle uniform and the whole field re-orients.",[18,506,507,510,511,514,515,518],{},[53,508,509],{},"Scrolling the noise."," Adding ",[31,512,513],{},"direction × localTime"," to the sample position ",[48,516,517],{},"slides"," the noise field across the ground in the wind direction. That scroll is what turns a frozen noise texture into a gust traveling across the meadow.",[520,521],"blog-noise-scroll",{},[18,523,524,527,528,531,532,535,536,538,539,541,542,545,546,538,549,552,553,555],{},[53,525,526],{},"Two octaves, deliberately mismatched."," Notice ",[31,529,530],{},"n1"," and ",[31,533,534],{},"n2"," use different spatial frequencies (",[31,537,356],{}," vs ",[31,540,397],{},") ",[48,543,544],{},"and"," different time scales (",[31,547,548],{},"localTime",[31,550,551],{},"localTime × 0.2","). This matters. One octave alone reads as a single wave marching across the field in lockstep, obviously periodic. A slow, broad octave (the whole field surging) layered with a faster, finer one (smaller ripples riding on top) breaks the pattern and reads as natural turbulence. If you copy only one thing from this post, copy the \"different frequency ",[48,554,544],{}," time scale\" part.",[557,558,560],"h3",{"id":559},"the-downwind-lean","The downwind lean",[18,562,563,564,567],{},"That ",[31,565,566],{},".add(0.4)"," is small and easy to miss, and it's the difference between grass and seaweed.",[18,569,570,573,574,577,578,581,582,584],{},[31,571,572],{},"mx_noise_float"," is centered on zero, so without the constant, ",[31,575,576],{},"intensity"," swings symmetrically positive and negative: the field oscillates around its rest pose, blades rocking back and forth through vertical. Real wind doesn't do that. Wind ",[48,579,580],{},"bows"," vegetation persistently downwind and then modulates that bow. Adding a positive bias keeps ",[31,583,576],{}," mostly on one side of zero, so the field has a resting lean in the wind direction with gusts on top. Tiny constant, completely changes the character.",[586,587,588],"blockquote",{},[18,589,590,591,593,594,597,598,601,602,604,605,608,609,611],{},"A note on ",[31,592,572],{},": Bruno's original samples a ",[53,595,596],{},"perlin texture"," (",[31,599,600],{},"texture(perlin, uv)","). We already leaned on ",[31,603,572],{}," in part 1 for height patches, and it's the better fit here too, no texture to load or bind, it's centered on zero (a texture is in ",[31,606,607],{},"[0, 1]",", so his GLSL subtracts ",[31,610,213],{},"), and it's evaluated procedurally on the GPU. Same idea, one less asset.",[74,613,615],{"id":614},"advancing-time","Advancing Time",[18,617,618,621,622,626,627,105],{},[31,619,620],{},"windLocalTime"," is a uniform, so something has to move it. In a ",[623,624],"magic-link",{"label":625},"TresJS"," component that's ",[31,628,629],{},"useLoop",[107,631,633],{"className":109,"code":632,"language":111,"meta":112,"style":112},"const { onBeforeRender } = useLoop()\nonBeforeRender(({ delta }) => {\n  windLocalTime.value += delta * windTimeFrequency.value * windStrength.value\n})\n",[31,634,635,656,676,712],{"__ignoreMap":112},[116,636,637,639,642,645,647,650,653],{"class":118,"line":119},[116,638,123],{"class":122},[116,640,641],{"class":185}," {",[116,643,644],{"class":126}," onBeforeRender ",[116,646,493],{"class":185},[116,648,649],{"class":130}," =",[116,651,652],{"class":160}," useLoop",[116,654,655],{"class":126},"()\n",[116,657,658,661,663,666,669,672,674],{"class":118,"line":150},[116,659,660],{"class":160},"onBeforeRender",[116,662,164],{"class":126},[116,664,665],{"class":185},"({",[116,667,668],{"class":299}," delta",[116,670,671],{"class":185}," })",[116,673,306],{"class":130},[116,675,309],{"class":185},[116,677,678,681,683,686,689,692,695,698,700,702,704,707,709],{"class":118,"line":199},[116,679,680],{"class":126},"  windLocalTime",[116,682,137],{"class":130},[116,684,685],{"class":126},"value ",[116,687,688],{"class":130},"+=",[116,690,691],{"class":126}," delta ",[116,693,694],{"class":130},"*",[116,696,697],{"class":126}," windTimeFrequency",[116,699,137],{"class":130},[116,701,685],{"class":126},[116,703,694],{"class":130},[116,705,706],{"class":126}," windStrength",[116,708,137],{"class":130},[116,710,711],{"class":126},"value\n",[116,713,714,716],{"class":118,"line":219},[116,715,493],{"class":185},[116,717,216],{"class":126},[18,719,720,721,724,725,728,729,731],{},"The subtle bit is multiplying by ",[31,722,723],{},"windStrength",". Time advances ",[48,726,727],{},"faster when the wind is stronger",". So the strength uniform does double duty: it scales how far blades bend ",[53,730,544],{}," how fast the field scrolls. Turn strength up and the wind doesn't just push harder, it moves quicker, which is exactly how a picking-up wind feels. Decouple them and a \"strong\" wind that crawls looks wrong.",[74,733,735],{"id":734},"step-1-sway","Step 1: Sway",[18,737,738,739,742],{},"Now the payoff. In part 1's ",[31,740,741],{},"positionNode",", right after we place the blade at its anchor, we sample the field and push the vertex:",[107,744,746],{"className":109,"code":745,"language":111,"meta":112,"style":112},"\u002F\u002F taller blades sway more → × height. ×2 is just a visual gain\nconst windVec = windOffset(anchor).mul(height).mul(2).toVar()\npos.addAssign(vec3(windVec.x.mul(tipness), 0, windVec.y.mul(tipness)))\n",[31,747,748,754,794],{"__ignoreMap":112},[116,749,750],{"class":118,"line":119},[116,751,753],{"class":752},"sWSDe","\u002F\u002F taller blades sway more → × height. ×2 is just a visual gain\n",[116,755,756,758,761,763,766,769,771,773,776,778,780,782,785,787,789,792],{"class":118,"line":150},[116,757,123],{"class":122},[116,759,760],{"class":126}," windVec ",[116,762,131],{"class":130},[116,764,765],{"class":160}," windOffset",[116,767,768],{"class":126},"(anchor)",[116,770,137],{"class":130},[116,772,328],{"class":160},[116,774,775],{"class":126},"(height)",[116,777,137],{"class":130},[116,779,328],{"class":160},[116,781,164],{"class":126},[116,783,784],{"class":140},"2",[116,786,359],{"class":126},[116,788,137],{"class":130},[116,790,791],{"class":160},"toVar",[116,793,655],{"class":126},[116,795,796,799,801,804,806,809,812,814,817,819,821,824,826,829,831,834,836,839,841,843],{"class":118,"line":199},[116,797,798],{"class":126},"pos",[116,800,137],{"class":130},[116,802,803],{"class":160},"addAssign",[116,805,164],{"class":126},[116,807,808],{"class":160},"vec3",[116,810,811],{"class":126},"(windVec",[116,813,137],{"class":130},[116,815,816],{"class":126},"x",[116,818,137],{"class":130},[116,820,328],{"class":160},[116,822,823],{"class":126},"(tipness)",[116,825,186],{"class":185},[116,827,828],{"class":140}," 0",[116,830,186],{"class":185},[116,832,833],{"class":126}," windVec",[116,835,137],{"class":130},[116,837,838],{"class":126},"y",[116,840,137],{"class":130},[116,842,328],{"class":160},[116,844,845],{"class":126},"(tipness)))\n",[18,847,848,849,852,853,855,856,859,860,863],{},"Two things ride along. Multiplying by ",[31,850,851],{},"height"," means taller blades sway more, short stubble barely moves while the tall stuff whips, which is both physically right and reads great. And the whole offset is weighted by ",[31,854,33],{},": the base vertices (",[31,857,858],{},"tipness = 0",") don't move at all, the tip (",[31,861,862],{},"tipness = 1",") moves the full amount, the mids move partway. The blade curves along its length instead of sliding rigidly or hinging at the root.",[18,865,866,867,870,871,874],{},"Sampling at the ",[31,868,869],{},"anchor"," (constant per instance) rather than per-vertex means the ",[48,872,873],{},"whole blade"," reads the same gust, so it moves as one coherent object.",[10,876,878],{"caption":877},"Step 7: wind sway — coherent, but watch a strong gust stretch the blades",[14,879],{":step":880},"7",[18,882,883,884,887,888,891,892,895],{},"Let it run and it already looks alive. But watch a strong gust closely, especially the tall blades: they ",[53,885,886],{},"stretch",". The tip flies out sideways and the blade visibly gets longer, like a rubber band. That's because we only moved the vertex in ",[31,889,890],{},"xz",". A blade is (roughly) inextensible: if the tip swings out horizontally while the base stays planted, the blade would have to ",[48,893,894],{},"grow"," to reach the new spot. Ours happily does. It looks like taffy.",[74,897,899],{"id":898},"step-2-bend-not-stretch","Step 2: Bend, Not Stretch",[18,901,902,903,906,907,910],{},"A bent blade keeps its length. If the tip moves horizontally by some amount ",[31,904,905],{},"w"," while the base is fixed, the tip has to ",[53,908,909],{},"drop"," to compensate, tracing an arc rather than a straight slide.",[18,912,913],{},[914,915],"img",{"alt":916,"src":917},"Side view of one blade under a horizontal wind push w. Left: moving only in xz keeps the tip at the same height, so the blade grows to the hypotenuse √(h²+w²) and stretches. Right: the tip instead rides a circle of radius h, dropping by about w²\u002F2h so the blade keeps its length and bends, planted at the base with the tip travelling most.","\u002Fblog\u002Fanimating-grass-with-wind-in-tsl\u002Fbend-not-stretch.svg",[18,919,920,921,966,967,997],{},"The exact arc length is annoying to solve in a shader, but we don't need exact. For a small horizontal displacement ",[116,922,925,946],{"className":923},[924],"katex",[116,926,929],{"className":927},[928],"katex-mathml",[930,931,933],"math",{"xmlns":932},"http:\u002F\u002Fwww.w3.org\u002F1998\u002FMath\u002FMathML",[934,935,936,942],"semantics",{},[937,938,939],"mrow",{},[940,941,905],"mi",{},[943,944,905],"annotation",{"encoding":945},"application\u002Fx-tex",[116,947,951],{"className":948,"ariaHidden":950},[949],"katex-html","true",[116,952,955,960],{"className":953},[954],"base",[116,956],{"className":957,"style":959},[958],"strut","height:0.4306em;",[116,961,905],{"className":962,"style":965},[963,964],"mord","mathnormal","margin-right:0.02691em;"," on a blade of height ",[116,968,970,984],{"className":969},[924],[116,971,973],{"className":972},[928],[930,974,975],{"xmlns":932},[934,976,977,982],{},[937,978,979],{},[940,980,981],{},"h",[943,983,981],{"encoding":945},[116,985,987],{"className":986,"ariaHidden":950},[949],[116,988,990,994],{"className":989},[954],[116,991],{"className":992,"style":993},[958],"height:0.6944em;",[116,995,981],{"className":996},[963,964],", keeping the length constant means the tip drops by approximately:",[18,999,1000],{},[116,1001,1003,1047],{"className":1002},[924],[116,1004,1006],{"className":1005},[928],[930,1007,1008],{"xmlns":932},[934,1009,1010,1044],{},[937,1011,1012,1015,1019],{},[1013,1014,909],"mtext",{},[1016,1017,1018],"mo",{},"≈",[1020,1021,1022,1038],"mfrac",{},[937,1023,1024,1028,1030],{},[940,1025,1027],{"mathvariant":1026},"normal","∣",[940,1029,905],{},[1031,1032,1033,1035],"msup",{},[940,1034,1027],{"mathvariant":1026},[1036,1037,784],"mn",{},[937,1039,1040,1042],{},[1036,1041,784],{},[940,1043,981],{},[943,1045,1046],{"encoding":945},"\\text{drop} \\approx \\frac{|w|^2}{2h}",[116,1048,1050,1076],{"className":1049,"ariaHidden":950},[949],[116,1051,1053,1057,1064,1069,1073],{"className":1052},[954],[116,1054],{"className":1055,"style":1056},[958],"height:0.8889em;vertical-align:-0.1944em;",[116,1058,1061],{"className":1059},[963,1060],"text",[116,1062,909],{"className":1063},[963],[116,1065],{"className":1066,"style":1068},[1067],"mspace","margin-right:0.2778em;",[116,1070,1018],{"className":1071},[1072],"mrel",[116,1074],{"className":1075,"style":1068},[1067],[116,1077,1079,1083],{"className":1078},[954],[116,1080],{"className":1081,"style":1082},[958],"height:1.4539em;vertical-align:-0.345em;",[116,1084,1086,1091,1210],{"className":1085},[963],[116,1087],{"className":1088},[1089,1090],"mopen","nulldelimiter",[116,1092,1094],{"className":1093},[1020],[116,1095,1099,1201],{"className":1096},[1097,1098],"vlist-t","vlist-t2",[116,1100,1103,1196],{"className":1101},[1102],"vlist-r",[116,1104,1108,1132,1143],{"className":1105,"style":1107},[1106],"vlist","height:1.1089em;",[116,1109,1111,1116],{"style":1110},"top:-2.655em;",[116,1112],{"className":1113,"style":1115},[1114],"pstrut","height:3em;",[116,1117,1123],{"className":1118},[1119,1120,1121,1122],"sizing","reset-size6","size3","mtight",[116,1124,1126,1129],{"className":1125},[963,1122],[116,1127,784],{"className":1128},[963,1122],[116,1130,981],{"className":1131},[963,964,1122],[116,1133,1135,1138],{"style":1134},"top:-3.23em;",[116,1136],{"className":1137,"style":1115},[1114],[116,1139],{"className":1140,"style":1142},[1141],"frac-line","border-bottom-width:0.04em;",[116,1144,1146,1149],{"style":1145},"top:-3.485em;",[116,1147],{"className":1148,"style":1115},[1114],[116,1150,1152],{"className":1151},[1119,1120,1121,1122],[116,1153,1155,1158,1161],{"className":1154},[963,1122],[116,1156,1027],{"className":1157},[963,1122],[116,1159,905],{"className":1160,"style":965},[963,964,1122],[116,1162,1164,1167],{"className":1163},[963,1122],[116,1165,1027],{"className":1166},[963,1122],[116,1168,1171],{"className":1169},[1170],"msupsub",[116,1172,1174],{"className":1173},[1097],[116,1175,1177],{"className":1176},[1102],[116,1178,1181],{"className":1179,"style":1180},[1106],"height:0.8913em;",[116,1182,1184,1188],{"style":1183},"top:-2.931em;margin-right:0.0714em;",[116,1185],{"className":1186,"style":1187},[1114],"height:2.5em;",[116,1189,1193],{"className":1190},[1119,1191,1192,1122],"reset-size3","size1",[116,1194,784],{"className":1195},[963,1122],[116,1197,1200],{"className":1198},[1199],"vlist-s","​",[116,1202,1204],{"className":1203},[1102],[116,1205,1208],{"className":1206,"style":1207},[1106],"height:0.345em;",[116,1209],{},[116,1211],{"className":1212},[1213,1090],"mclose",[18,1215,1216,1217,1549],{},"That's the leading term of the arc-length correction (straight from Pythagoras: ",[116,1218,1220,1272],{"className":1219},[924],[116,1221,1223],{"className":1222},[928],[930,1224,1225],{"xmlns":932},[934,1226,1227,1269],{},[937,1228,1229,1249,1251,1253,1255],{},[1230,1231,1232],"msqrt",{},[937,1233,1234,1240,1243],{},[1031,1235,1236,1238],{},[940,1237,981],{},[1036,1239,784],{},[1016,1241,1242],{},"−",[1031,1244,1245,1247],{},[940,1246,905],{},[1036,1248,784],{},[1016,1250,1018],{},[940,1252,981],{},[1016,1254,1242],{},[1020,1256,1257,1263],{},[1031,1258,1259,1261],{},[940,1260,905],{},[1036,1262,784],{},[937,1264,1265,1267],{},[1036,1266,784],{},[940,1268,981],{},[943,1270,1271],{"encoding":945},"\\sqrt{h^2 - w^2} \\approx h - \\frac{w^2}{2h}",[116,1273,1275,1424,1443],{"className":1274,"ariaHidden":950},[949],[116,1276,1278,1282,1415,1418,1421],{"className":1277},[954],[116,1279],{"className":1280,"style":1281},[958],"height:1.04em;vertical-align:-0.1266em;",[116,1283,1286],{"className":1284},[963,1285],"sqrt",[116,1287,1289,1406],{"className":1288},[1097,1098],[116,1290,1292,1403],{"className":1291},[1102],[116,1293,1296,1380],{"className":1294,"style":1295},[1106],"height:0.9134em;",[116,1297,1301,1304],{"className":1298,"style":1300},[1299],"svg-align","top:-3em;",[116,1302],{"className":1303,"style":1115},[1114],[116,1305,1308,1340,1344,1348,1351],{"className":1306,"style":1307},[963],"padding-left:0.833em;",[116,1309,1311,1314],{"className":1310},[963],[116,1312,981],{"className":1313},[963,964],[116,1315,1317],{"className":1316},[1170],[116,1318,1320],{"className":1319},[1097],[116,1321,1323],{"className":1322},[1102],[116,1324,1327],{"className":1325,"style":1326},[1106],"height:0.7401em;",[116,1328,1330,1334],{"style":1329},"top:-2.989em;margin-right:0.05em;",[116,1331],{"className":1332,"style":1333},[1114],"height:2.7em;",[116,1335,1337],{"className":1336},[1119,1120,1121,1122],[116,1338,784],{"className":1339},[963,1122],[116,1341],{"className":1342,"style":1343},[1067],"margin-right:0.2222em;",[116,1345,1242],{"className":1346},[1347],"mbin",[116,1349],{"className":1350,"style":1343},[1067],[116,1352,1354,1357],{"className":1353},[963],[116,1355,905],{"className":1356,"style":965},[963,964],[116,1358,1360],{"className":1359},[1170],[116,1361,1363],{"className":1362},[1097],[116,1364,1366],{"className":1365},[1102],[116,1367,1369],{"className":1368,"style":1326},[1106],[116,1370,1371,1374],{"style":1329},[116,1372],{"className":1373,"style":1333},[1114],[116,1375,1377],{"className":1376},[1119,1120,1121,1122],[116,1378,784],{"className":1379},[963,1122],[116,1381,1383,1386],{"style":1382},"top:-2.8734em;",[116,1384],{"className":1385,"style":1115},[1114],[116,1387,1391],{"className":1388,"style":1390},[1389],"hide-tail","min-width:0.853em;height:1.08em;",[1392,1393,1399],"svg",{"xmlns":1394,"width":1395,"height":1396,"viewBox":1397,"preserveAspectRatio":1398},"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","400em","1.08em","0 0 400000 1080","xMinYMin slice",[1400,1401],"path",{"d":1402},"M95,702\nc-2.7,0,-7.17,-2.7,-13.5,-8c-5.8,-5.3,-9.5,-10,-9.5,-14\nc0,-2,0.3,-3.3,1,-4c1.3,-2.7,23.83,-20.7,67.5,-54\nc44.2,-33.3,65.8,-50.3,66.5,-51c1.3,-1.3,3,-2,5,-2c4.7,0,8.7,3.3,12,10\ns173,378,173,378c0.7,0,35.3,-71,104,-213c68.7,-142,137.5,-285,206.5,-429\nc69,-144,104.5,-217.7,106.5,-221\nl0 -0\nc5.3,-9.3,12,-14,20,-14\nH400000v40H845.2724\ns-225.272,467,-225.272,467s-235,486,-235,486c-2.7,4.7,-9,7,-19,7\nc-6,0,-10,-1,-12,-3s-194,-422,-194,-422s-65,47,-65,47z\nM834 80h400000v40h-400000z",[116,1404,1200],{"className":1405},[1199],[116,1407,1409],{"className":1408},[1102],[116,1410,1413],{"className":1411,"style":1412},[1106],"height:0.1266em;",[116,1414],{},[116,1416],{"className":1417,"style":1068},[1067],[116,1419,1018],{"className":1420},[1072],[116,1422],{"className":1423,"style":1068},[1067],[116,1425,1427,1431,1434,1437,1440],{"className":1426},[954],[116,1428],{"className":1429,"style":1430},[958],"height:0.7778em;vertical-align:-0.0833em;",[116,1432,981],{"className":1433},[963,964],[116,1435],{"className":1436,"style":1343},[1067],[116,1438,1242],{"className":1439},[1347],[116,1441],{"className":1442,"style":1343},[1067],[116,1444,1446,1450],{"className":1445},[954],[116,1447],{"className":1448,"style":1449},[958],"height:1.3629em;vertical-align:-0.345em;",[116,1451,1453,1456,1546],{"className":1452},[963],[116,1454],{"className":1455},[1089,1090],[116,1457,1459],{"className":1458},[1020],[116,1460,1462,1538],{"className":1461},[1097,1098],[116,1463,1465,1535],{"className":1464},[1102],[116,1466,1469,1486,1494],{"className":1467,"style":1468},[1106],"height:1.0179em;",[116,1470,1471,1474],{"style":1110},[116,1472],{"className":1473,"style":1115},[1114],[116,1475,1477],{"className":1476},[1119,1120,1121,1122],[116,1478,1480,1483],{"className":1479},[963,1122],[116,1481,784],{"className":1482},[963,1122],[116,1484,981],{"className":1485},[963,964,1122],[116,1487,1488,1491],{"style":1134},[116,1489],{"className":1490,"style":1115},[1114],[116,1492],{"className":1493,"style":1142},[1141],[116,1495,1497,1500],{"style":1496},"top:-3.394em;",[116,1498],{"className":1499,"style":1115},[1114],[116,1501,1503],{"className":1502},[1119,1120,1121,1122],[116,1504,1506],{"className":1505},[963,1122],[116,1507,1509,1512],{"className":1508},[963,1122],[116,1510,905],{"className":1511,"style":965},[963,964,1122],[116,1513,1515],{"className":1514},[1170],[116,1516,1518],{"className":1517},[1097],[116,1519,1521],{"className":1520},[1102],[116,1522,1524],{"className":1523,"style":1180},[1106],[116,1525,1526,1529],{"style":1183},[116,1527],{"className":1528,"style":1187},[1114],[116,1530,1532],{"className":1531},[1119,1191,1192,1122],[116,1533,784],{"className":1534},[963,1122],[116,1536,1200],{"className":1537},[1199],[116,1539,1541],{"className":1540},[1102],[116,1542,1544],{"className":1543,"style":1207},[1106],[116,1545],{},[116,1547],{"className":1548},[1213,1090],"). Cheap, and visually indistinguishable from the real thing at grass scale. In TSL:",[107,1551,1553],{"className":109,"code":1552,"language":111,"meta":112,"style":112},"\u002F\u002F arc-length approximation: as the tip leans out, it drops to keep the blade's length\nconst droop = windVec.dot(windVec).div(height.mul(2)).min(height.mul(0.35))\npos.y.subAssign(droop.mul(tipness).mul(tipness))\n",[31,1554,1555,1560,1617],{"__ignoreMap":112},[116,1556,1557],{"class":118,"line":119},[116,1558,1559],{"class":752},"\u002F\u002F arc-length approximation: as the tip leans out, it drops to keep the blade's length\n",[116,1561,1562,1564,1567,1569,1571,1573,1576,1579,1581,1584,1587,1589,1591,1593,1595,1598,1600,1603,1605,1607,1609,1611,1614],{"class":118,"line":150},[116,1563,123],{"class":122},[116,1565,1566],{"class":126}," droop ",[116,1568,131],{"class":130},[116,1570,833],{"class":126},[116,1572,137],{"class":130},[116,1574,1575],{"class":160},"dot",[116,1577,1578],{"class":126},"(windVec)",[116,1580,137],{"class":130},[116,1582,1583],{"class":160},"div",[116,1585,1586],{"class":126},"(height",[116,1588,137],{"class":130},[116,1590,328],{"class":160},[116,1592,164],{"class":126},[116,1594,784],{"class":140},[116,1596,1597],{"class":126},"))",[116,1599,137],{"class":130},[116,1601,1602],{"class":160},"min",[116,1604,1586],{"class":126},[116,1606,137],{"class":130},[116,1608,328],{"class":160},[116,1610,164],{"class":126},[116,1612,1613],{"class":140},"0.35",[116,1615,1616],{"class":126},"))\n",[116,1618,1619,1621,1623,1625,1627,1630,1633,1635,1637,1639,1641,1643],{"class":118,"line":199},[116,1620,798],{"class":126},[116,1622,137],{"class":130},[116,1624,838],{"class":126},[116,1626,137],{"class":130},[116,1628,1629],{"class":160},"subAssign",[116,1631,1632],{"class":126},"(droop",[116,1634,137],{"class":130},[116,1636,328],{"class":160},[116,1638,823],{"class":126},[116,1640,137],{"class":130},[116,1642,328],{"class":160},[116,1644,1645],{"class":126},"(tipness))\n",[18,1647,1648,1651,1652,1724,1725,1759],{},[31,1649,1650],{},"windVec.dot(windVec)"," is ",[116,1653,1655,1677],{"className":1654},[924],[116,1656,1658],{"className":1657},[928],[930,1659,1660],{"xmlns":932},[934,1661,1662,1674],{},[937,1663,1664,1666,1668],{},[940,1665,1027],{"mathvariant":1026},[940,1667,905],{},[1031,1669,1670,1672],{},[940,1671,1027],{"mathvariant":1026},[1036,1673,784],{},[943,1675,1676],{"encoding":945},"|w|^2",[116,1678,1680],{"className":1679,"ariaHidden":950},[949],[116,1681,1683,1687,1690,1693],{"className":1682},[954],[116,1684],{"className":1685,"style":1686},[958],"height:1.0641em;vertical-align:-0.25em;",[116,1688,1027],{"className":1689},[963],[116,1691,905],{"className":1692,"style":965},[963,964],[116,1694,1696,1699],{"className":1695},[963],[116,1697,1027],{"className":1698},[963],[116,1700,1702],{"className":1701},[1170],[116,1703,1705],{"className":1704},[1097],[116,1706,1708],{"className":1707},[1102],[116,1709,1712],{"className":1710,"style":1711},[1106],"height:0.8141em;",[116,1713,1715,1718],{"style":1714},"top:-3.063em;margin-right:0.05em;",[116,1716],{"className":1717,"style":1333},[1114],[116,1719,1721],{"className":1720},[1119,1120,1121,1122],[116,1722,784],{"className":1723},[963,1122]," (cheaper than a length, no square root). Dividing by ",[116,1726,1728,1744],{"className":1727},[924],[116,1729,1731],{"className":1730},[928],[930,1732,1733],{"xmlns":932},[934,1734,1735,1741],{},[937,1736,1737,1739],{},[1036,1738,784],{},[940,1740,981],{},[943,1742,1743],{"encoding":945},"2h",[116,1745,1747],{"className":1746,"ariaHidden":950},[949],[116,1748,1750,1753,1756],{"className":1749},[954],[116,1751],{"className":1752,"style":993},[958],[116,1754,784],{"className":1755},[963],[116,1757,981],{"className":1758},[963,964]," gives the drop. Two guards make it behave:",[1761,1762,1763,1772],"ul",{},[1764,1765,1766,1771],"li",{},[53,1767,1768],{},[31,1769,1770],{},".min(height.mul(0.35))"," clamps the drop so a violent gust bends the blade over but never folds it through the ground.",[1764,1773,1774,1779,1780,1782,1783,1786],{},[53,1775,1776],{},[31,1777,1778],{},"tipness²"," (not just ",[31,1781,33],{},") weights the drop. Squaring biases it hard toward the tip, so the base and mids barely dip while the tip does most of the falling. That's what gives the profile its ",[48,1784,1785],{},"curve",", a blade bent like a bow, rather than a straight blade tilted like a lever.",[10,1788,1790],{"caption":1789},"Step 8: arc-length droop — blades bend and hold their length through the gust",[14,1791],{":step":16},[18,1793,1794],{},"Same wind, same strength, but now the blades bend and hold their length. The taffy is gone. Orbit down to grazing angle and you can watch individual blades bow into a gust and spring back.",[74,1796,1798],{"id":1797},"the-wind-wired-in","The Wind, Wired In",[18,1800,1801,1802,1805],{},"Dropped into part 1's ",[31,1803,1804],{},"createGrass"," material, the whole addition to the vertex stage is small:",[107,1807,1809],{"className":109,"code":1808,"language":111,"meta":112,"style":112},"material.positionNode = Fn(() => {\n  \u002F\u002F …anchor, yaw, height and local blade placement from part 1…\n  const pos = vec3(local.x.add(anchor.x), local.y, local.z.add(anchor.y)).toVar()\n\n  \u002F\u002F wind: sample the shared field at the anchor, weight by tipness so the blade curves\n  const windVec = windOffset(anchor).mul(height).mul(2).toVar()\n  pos.addAssign(vec3(windVec.x.mul(tipness), 0, windVec.y.mul(tipness)))\n\n  \u002F\u002F bend, don't stretch: drop the tip to keep the blade's length\n  const droop = windVec.dot(windVec).div(height.mul(2)).min(height.mul(0.35))\n  pos.y.subAssign(droop.mul(tipness).mul(tipness))\n\n  return pos\n})()\n",[31,1810,1811,1834,1839,1905,1909,1914,1948,1991,1995,2000,2048,2074,2078,2085],{"__ignoreMap":112},[116,1812,1813,1816,1818,1821,1823,1825,1827,1830,1832],{"class":118,"line":119},[116,1814,1815],{"class":126},"material",[116,1817,137],{"class":130},[116,1819,1820],{"class":126},"positionNode ",[116,1822,131],{"class":130},[116,1824,291],{"class":160},[116,1826,164],{"class":126},[116,1828,1829],{"class":185},"()",[116,1831,306],{"class":130},[116,1833,309],{"class":185},[116,1835,1836],{"class":118,"line":150},[116,1837,1838],{"class":752},"  \u002F\u002F …anchor, yaw, height and local blade placement from part 1…\n",[116,1840,1841,1843,1846,1848,1851,1854,1856,1858,1860,1862,1865,1867,1870,1872,1875,1877,1879,1881,1883,1885,1888,1890,1892,1894,1896,1899,1901,1903],{"class":118,"line":199},[116,1842,315],{"class":122},[116,1844,1845],{"class":126}," pos ",[116,1847,131],{"class":130},[116,1849,1850],{"class":160}," vec3",[116,1852,1853],{"class":126},"(local",[116,1855,137],{"class":130},[116,1857,816],{"class":126},[116,1859,137],{"class":130},[116,1861,364],{"class":160},[116,1863,1864],{"class":126},"(anchor",[116,1866,137],{"class":130},[116,1868,1869],{"class":126},"x)",[116,1871,186],{"class":185},[116,1873,1874],{"class":126}," local",[116,1876,137],{"class":130},[116,1878,838],{"class":126},[116,1880,186],{"class":185},[116,1882,1874],{"class":126},[116,1884,137],{"class":130},[116,1886,1887],{"class":126},"z",[116,1889,137],{"class":130},[116,1891,364],{"class":160},[116,1893,1864],{"class":126},[116,1895,137],{"class":130},[116,1897,1898],{"class":126},"y))",[116,1900,137],{"class":130},[116,1902,791],{"class":160},[116,1904,655],{"class":126},[116,1906,1907],{"class":118,"line":219},[116,1908,278],{"emptyLinePlaceholder":277},[116,1910,1911],{"class":118,"line":238},[116,1912,1913],{"class":752},"  \u002F\u002F wind: sample the shared field at the anchor, weight by tipness so the blade curves\n",[116,1915,1916,1918,1920,1922,1924,1926,1928,1930,1932,1934,1936,1938,1940,1942,1944,1946],{"class":118,"line":256},[116,1917,315],{"class":122},[116,1919,760],{"class":126},[116,1921,131],{"class":130},[116,1923,765],{"class":160},[116,1925,768],{"class":126},[116,1927,137],{"class":130},[116,1929,328],{"class":160},[116,1931,775],{"class":126},[116,1933,137],{"class":130},[116,1935,328],{"class":160},[116,1937,164],{"class":126},[116,1939,784],{"class":140},[116,1941,359],{"class":126},[116,1943,137],{"class":130},[116,1945,791],{"class":160},[116,1947,655],{"class":126},[116,1949,1950,1953,1955,1957,1959,1961,1963,1965,1967,1969,1971,1973,1975,1977,1979,1981,1983,1985,1987,1989],{"class":118,"line":274},[116,1951,1952],{"class":126},"  pos",[116,1954,137],{"class":130},[116,1956,803],{"class":160},[116,1958,164],{"class":126},[116,1960,808],{"class":160},[116,1962,811],{"class":126},[116,1964,137],{"class":130},[116,1966,816],{"class":126},[116,1968,137],{"class":130},[116,1970,328],{"class":160},[116,1972,823],{"class":126},[116,1974,186],{"class":185},[116,1976,828],{"class":140},[116,1978,186],{"class":185},[116,1980,833],{"class":126},[116,1982,137],{"class":130},[116,1984,838],{"class":126},[116,1986,137],{"class":130},[116,1988,328],{"class":160},[116,1990,845],{"class":126},[116,1992,1993],{"class":118,"line":281},[116,1994,278],{"emptyLinePlaceholder":277},[116,1996,1997],{"class":118,"line":312},[116,1998,1999],{"class":752},"  \u002F\u002F bend, don't stretch: drop the tip to keep the blade's length\n",[116,2001,2002,2004,2006,2008,2010,2012,2014,2016,2018,2020,2022,2024,2026,2028,2030,2032,2034,2036,2038,2040,2042,2044,2046],{"class":118,"line":334},[116,2003,315],{"class":122},[116,2005,1566],{"class":126},[116,2007,131],{"class":130},[116,2009,833],{"class":126},[116,2011,137],{"class":130},[116,2013,1575],{"class":160},[116,2015,1578],{"class":126},[116,2017,137],{"class":130},[116,2019,1583],{"class":160},[116,2021,1586],{"class":126},[116,2023,137],{"class":130},[116,2025,328],{"class":160},[116,2027,164],{"class":126},[116,2029,784],{"class":140},[116,2031,1597],{"class":126},[116,2033,137],{"class":130},[116,2035,1602],{"class":160},[116,2037,1586],{"class":126},[116,2039,137],{"class":130},[116,2041,328],{"class":160},[116,2043,164],{"class":126},[116,2045,1613],{"class":140},[116,2047,1616],{"class":126},[116,2049,2050,2052,2054,2056,2058,2060,2062,2064,2066,2068,2070,2072],{"class":118,"line":377},[116,2051,1952],{"class":126},[116,2053,137],{"class":130},[116,2055,838],{"class":126},[116,2057,137],{"class":130},[116,2059,1629],{"class":160},[116,2061,1632],{"class":126},[116,2063,137],{"class":130},[116,2065,328],{"class":160},[116,2067,823],{"class":126},[116,2069,137],{"class":130},[116,2071,328],{"class":160},[116,2073,1645],{"class":126},[116,2075,2076],{"class":118,"line":436},[116,2077,278],{"emptyLinePlaceholder":277},[116,2079,2080,2082],{"class":118,"line":467},[116,2081,470],{"class":122},[116,2083,2084],{"class":126}," pos\n",[116,2086,2087,2089],{"class":118,"line":490},[116,2088,493],{"class":185},[116,2090,2091],{"class":126},")()\n",[18,2093,2094,2095,2097,2098,2101],{},"No new geometry, no new attributes. The ",[31,2096,33],{}," we already had carries the curve, and the wind field is a single shared ",[31,2099,2100],{},"Fn"," sampled once per blade. Everything else, instancing, the diffuse map, the AO gradient, is untouched from part 1.",[74,2103,2105],{"id":2104},"whats-next-trample","What's Next: Trample",[18,2107,2108,2109,2112,2113,2115],{},"The field breathes now, but it ignores you. Walk a character through it and the blades pass straight through their legs. The next post adds a ",[53,2110,2111],{},"trample system",": a character stamps into a small render target as it moves, the grass reads that map to flatten under foot and part radially around the interactor, and a recovery pass springs the blades back up once you've passed. It reuses the exact same ",[31,2114,33],{}," weighting, one attribute, three effects.",[74,2117,2119],{"id":2118},"resources","Resources",[1761,2121,2122,2128,2135,2143],{},[1764,2123,2124,2127],{},[22,2125,2126],{"href":24},"Creating a Grass Field with TSL",", part 1: instancing, the anchor trick, and the diffuse map",[1764,2129,2130,2134],{},[22,2131,2133],{"href":62,"rel":2132},[64],"Bruno Simon's folio-2025 Wind.js",", the GLSL wind field this translates",[1764,2136,2137,2142],{},[22,2138,2141],{"href":2139,"rel":2140},"https:\u002F\u002Fgithub.com\u002Fbrunosimon\u002Ffolio-2025\u002Fblob\u002Fmain\u002Fsources\u002FGame\u002FWorld\u002FGrass.js",[64],"Bruno Simon's folio-2025 Grass.js",", where the sway is applied",[1764,2144,2145],{},[22,2146,2149],{"href":2147,"rel":2148},"https:\u002F\u002Fgithub.com\u002Fmrdoob\u002Fthree.js\u002Fwiki\u002FThree.js-Shading-Language",[64],"Three.js TSL documentation",[2151,2152,2153],"style",{},"html pre.shiki code .sHfct, html code.shiki .sHfct{--shiki-default:#CA9EE6}html pre.shiki code .sdZ07, html code.shiki .sdZ07{--shiki-default:#C6D0F5}html pre.shiki code .sO16o, html code.shiki .sO16o{--shiki-default:#81C8BE}html pre.shiki code .sQqXY, html code.shiki .sQqXY{--shiki-default:#EF9F76}html pre.shiki code .soURg, html code.shiki .soURg{--shiki-default:#8CAAEE;--shiki-default-font-style:italic}html pre.shiki code .sPs8m, html code.shiki .sPs8m{--shiki-default:#CA9EE6;--shiki-default-font-weight:bold}html pre.shiki code .sABYQ, html code.shiki .sABYQ{--shiki-default:#949CBB}html pre.shiki code .su21L, html code.shiki .su21L{--shiki-default:#EA999C;--shiki-default-font-style:italic}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sWSDe, html code.shiki .sWSDe{--shiki-default:#949CBB;--shiki-default-font-style:italic}",{"title":112,"searchDepth":150,"depth":150,"links":2155},[2156,2159,2160,2161,2162,2163,2164],{"id":76,"depth":150,"text":77,"children":2157},[2158],{"id":559,"depth":199,"text":560},{"id":614,"depth":150,"text":615},{"id":734,"depth":150,"text":735},{"id":898,"depth":150,"text":899},{"id":1797,"depth":150,"text":1798},{"id":2104,"depth":150,"text":2105},{"id":2118,"depth":150,"text":2119},"2026-07-22","Part 2 of the TSL grass series. Adding a noise-driven wind field to an instanced grass field, with per-blade sway and the arc-length trick that makes blades bend instead of stretch.","md",{"readingTime":2169},{"text":2170,"minutes":2171,"time":2172,"words":2173},"7 min read",6.9,414000,1380,{"title":5,"description":2166},"\u002Fblog\u002Fanimating-grass-with-wind-in-tsl",{"title":5,"description":2166},{"loc":2175},"published","blog\u002Fanimating-grass-with-wind-in-tsl",[2181,2182,2183],"webgpu","tsl","shaders","\u002Fblog\u002Fanimating-grass-with-wind-in-tsl\u002Fanimating-grass-with-wind-in-tsl.jpg","kIRugUe-kAyt3sHxl20Oyr5aBCPxlzHqjvswQRmv-O0",1785748754870]