[{"data":1,"prerenderedAt":4659},["ShallowReactive",2],{"\u002Fblog\u002Fcreating-a-grass-field-with-tsl":3},{"id":4,"title":5,"body":6,"date":4638,"description":4639,"extension":4640,"meta":4641,"navigation":4647,"path":4648,"seo":4649,"sitemap":4650,"status":4651,"stem":4652,"tags":4653,"thumbnail":4657,"__hash__":4658},"blog\u002Fblog\u002Fcreating-a-grass-field-with-tsl.md","Creating a Grass Field with TSL 🌱",{"type":7,"value":8,"toc":4621},"minimark",[9,17,21,28,36,39,56,61,66,72,89,788,791,817,823,833,1207,1210,1218,1223,1227,1238,1384,1409,1415,1419,1426,1737,1758,1761,1766,1770,1773,1932,1945,1951,1955,1958,1992,1999,2003,2007,2018,2024,2230,2239,2250,3095,3102,3106,3124,3446,3449,3455,3459,3466,3477,3659,3669,3672,3678,3683,3687,3690,4548,4559,4563,4583,4587,4617],[10,11,13],"scene-wrapper",{"caption":12},"10,000 instanced grass blades picking their color from a ground texture",[14,15],"blog-grass-step-demo",{":step":16},"6",[18,19,20],"p",{},"Stylized grass is one of those effects that instantly sells a game world. Think of the rolling meadows in Zelda: Breath of the Wild or any Studio Ghibli frame: thousands of simple blades, swaying and catching light, with barely any geometric detail per blade.",[18,22,23],{},[24,25],"img",{"alt":26,"src":27},"Zelda: Breath of the Wild beautiful grass shading","\u002Fblog\u002Fcreating-a-grass-field-with-tsl\u002Fzelda_botw.webp",[18,29,30,31,35],{},"In this post we'll build a field like that with ",[32,33],"magic-link",{"label":34},"ThreeJS"," TSL, and we'll do it twice: first the naive way, one triangle per blade, then the way I actually ship it in my RPG project, with instancing and a diffuse map.",[18,37,38],{},"The naive version is worth building first. It introduces every trick the final version relies on, and hitting its limits is the best way to understand why the instanced version is shaped the way it is.",[18,40,41,42,49,50,55],{},"This technique stands on two great references: ",[43,44,48],"a",{"href":45,"rel":46},"https:\u002F\u002Fmedium.com\u002Fantaeus-ar\u002Fmaking-grass-with-triangles-in-glsl-using-three-js-e106771a71ff",[47],"nofollow","Making grass with triangles in GLSL"," by Antaeus AR and ",[43,51,54],{"href":52,"rel":53},"https:\u002F\u002Fgithub.com\u002Fbrunosimon\u002Ffolio-2025\u002Fblob\u002Fmain\u002Fsources\u002FGame\u002FWorld\u002FGrass.js",[47],"Bruno Simon's folio-2025 grass",". Both are GLSL; here we'll translate the ideas to TSL and push them further.",[57,58,60],"h2",{"id":59},"part-1-a-triangle-per-blade","Part 1: A Triangle Per Blade",[62,63,65],"h3",{"id":64},"the-anchor-trick","The anchor trick",[18,67,68],{},[24,69],{"alt":70,"src":71},"Diagram of a single blade: a triangle whose three vertices all start from the same orange anchor point, the tip offset up by template.y times bladeHeight and the base corners offset sideways by template.x times bladeWidth","\u002Fblog\u002Fcreating-a-grass-field-with-tsl\u002Fblade-template.svg",[18,73,74,75,79,80,84,85,88],{},"A grass field is thousands of blades, but we want ",[76,77,78],"strong",{},"one draw call",". The classic trick: create a single ",[81,82,83],"code",{},"BufferGeometry"," with 3 vertices per blade, but don't store actual vertex positions in it. Instead, all 3 vertices of a blade store the same value: the point on the ground where the blade is planted. I'll call it the ",[76,86,87],{},"anchor",".",[90,91,96],"pre",{"className":92,"code":93,"language":94,"meta":95,"style":95},"language-ts shiki shiki-themes catppuccin-frappe","function createGrassGeometry(subdivisions: number, size: number) {\n  const count = subdivisions * subdivisions\n  const fragmentSize = size \u002F subdivisions\n  const anchors = new Float32Array(count * 3 * 2) \u002F\u002F 3 verts × (x, z)\n  const heightRandomness = new Float32Array(count * 3)\n\n  for (let iX = 0; iX \u003C subdivisions; iX++) {\n    for (let iZ = 0; iZ \u003C subdivisions; iZ++) {\n      const i = iX * subdivisions + iZ\n\n      \u002F\u002F cell center + jitter — all 3 verts share the SAME anchor\n      const x = (iX + 0.5) \u002F subdivisions * size - size \u002F 2 + (Math.random() - 0.5) * fragmentSize\n      const z = (iZ + 0.5) \u002F subdivisions * size - size \u002F 2 + (Math.random() - 0.5) * fragmentSize\n\n      for (let v = 0; v \u003C 3; v++) {\n        anchors[i * 6 + v * 2] = x\n        anchors[i * 6 + v * 2 + 1] = z\n        heightRandomness[i * 3 + v] = Math.random()\n      }\n    }\n  }\n\n  const geometry = new BufferGeometry()\n  geometry.setAttribute('position', new BufferAttribute(anchors, 2)) \u002F\u002F itemSize 2!\n  geometry.setAttribute('heightRandomness', new BufferAttribute(heightRandomness, 1))\n  \u002F\u002F position doesn't hold real positions anymore, so bounds must be set by hand\n  geometry.boundingSphere = new Sphere(new Vector3(), (size \u002F 2) * Math.SQRT2 + 2)\n  return geometry\n}\n","ts","",[81,97,98,142,164,182,221,244,251,295,331,354,359,365,427,482,487,523,550,579,606,612,618,624,629,646,683,713,719,773,782],{"__ignoreMap":95},[99,100,103,107,111,115,119,123,126,129,132,134,136,139],"span",{"class":101,"line":102},"line",1,[99,104,106],{"class":105},"sHfct","function",[99,108,110],{"class":109},"soURg"," createGrassGeometry",[99,112,114],{"class":113},"sABYQ","(",[99,116,118],{"class":117},"su21L","subdivisions",[99,120,122],{"class":121},"sO16o",":",[99,124,125],{"class":105}," number",[99,127,128],{"class":113},",",[99,130,131],{"class":117}," size",[99,133,122],{"class":121},[99,135,125],{"class":105},[99,137,138],{"class":113},")",[99,140,141],{"class":113}," {\n",[99,143,145,148,152,155,158,161],{"class":101,"line":144},2,[99,146,147],{"class":105},"  const",[99,149,151],{"class":150},"sdZ07"," count ",[99,153,154],{"class":121},"=",[99,156,157],{"class":150}," subdivisions ",[99,159,160],{"class":121},"*",[99,162,163],{"class":150}," subdivisions\n",[99,165,167,169,172,174,177,180],{"class":101,"line":166},3,[99,168,147],{"class":105},[99,170,171],{"class":150}," fragmentSize ",[99,173,154],{"class":121},[99,175,176],{"class":150}," size ",[99,178,179],{"class":121},"\u002F",[99,181,163],{"class":150},[99,183,185,187,190,192,196,199,202,204,208,211,214,217],{"class":101,"line":184},4,[99,186,147],{"class":105},[99,188,189],{"class":150}," anchors ",[99,191,154],{"class":121},[99,193,195],{"class":194},"sPs8m"," new",[99,197,198],{"class":109}," Float32Array",[99,200,201],{"class":150},"(count ",[99,203,160],{"class":121},[99,205,207],{"class":206},"sQqXY"," 3",[99,209,210],{"class":121}," *",[99,212,213],{"class":206}," 2",[99,215,216],{"class":150},") ",[99,218,220],{"class":219},"sWSDe","\u002F\u002F 3 verts × (x, z)\n",[99,222,224,226,229,231,233,235,237,239,241],{"class":101,"line":223},5,[99,225,147],{"class":105},[99,227,228],{"class":150}," heightRandomness ",[99,230,154],{"class":121},[99,232,195],{"class":194},[99,234,198],{"class":109},[99,236,201],{"class":150},[99,238,160],{"class":121},[99,240,207],{"class":206},[99,242,243],{"class":150},")\n",[99,245,247],{"class":101,"line":246},6,[99,248,250],{"emptyLinePlaceholder":249},true,"\n",[99,252,254,257,260,263,266,268,271,274,276,279,282,284,287,290,292],{"class":101,"line":253},7,[99,255,256],{"class":105},"  for",[99,258,259],{"class":150}," (",[99,261,262],{"class":105},"let",[99,264,265],{"class":150}," iX ",[99,267,154],{"class":121},[99,269,270],{"class":206}," 0",[99,272,273],{"class":113},";",[99,275,265],{"class":150},[99,277,278],{"class":121},"\u003C",[99,280,281],{"class":150}," subdivisions",[99,283,273],{"class":113},[99,285,286],{"class":150}," iX",[99,288,289],{"class":121},"++",[99,291,216],{"class":150},[99,293,294],{"class":113},"{\n",[99,296,298,301,303,305,308,310,312,314,316,318,320,322,325,327,329],{"class":101,"line":297},8,[99,299,300],{"class":105},"    for",[99,302,259],{"class":150},[99,304,262],{"class":105},[99,306,307],{"class":150}," iZ ",[99,309,154],{"class":121},[99,311,270],{"class":206},[99,313,273],{"class":113},[99,315,307],{"class":150},[99,317,278],{"class":121},[99,319,281],{"class":150},[99,321,273],{"class":113},[99,323,324],{"class":150}," iZ",[99,326,289],{"class":121},[99,328,216],{"class":150},[99,330,294],{"class":113},[99,332,334,337,340,342,344,346,348,351],{"class":101,"line":333},9,[99,335,336],{"class":105},"      const",[99,338,339],{"class":150}," i ",[99,341,154],{"class":121},[99,343,265],{"class":150},[99,345,160],{"class":121},[99,347,157],{"class":150},[99,349,350],{"class":121},"+",[99,352,353],{"class":150}," iZ\n",[99,355,357],{"class":101,"line":356},10,[99,358,250],{"emptyLinePlaceholder":249},[99,360,362],{"class":101,"line":361},11,[99,363,364],{"class":219},"      \u002F\u002F cell center + jitter — all 3 verts share the SAME anchor\n",[99,366,368,370,373,375,378,380,383,385,387,389,391,393,396,398,400,402,405,408,410,413,416,418,420,422,424],{"class":101,"line":367},12,[99,369,336],{"class":105},[99,371,372],{"class":150}," x ",[99,374,154],{"class":121},[99,376,377],{"class":150}," (iX ",[99,379,350],{"class":121},[99,381,382],{"class":206}," 0.5",[99,384,216],{"class":150},[99,386,179],{"class":121},[99,388,157],{"class":150},[99,390,160],{"class":121},[99,392,176],{"class":150},[99,394,395],{"class":121},"-",[99,397,176],{"class":150},[99,399,179],{"class":121},[99,401,213],{"class":206},[99,403,404],{"class":121}," +",[99,406,407],{"class":150}," (Math",[99,409,88],{"class":121},[99,411,412],{"class":109},"random",[99,414,415],{"class":150},"() ",[99,417,395],{"class":121},[99,419,382],{"class":206},[99,421,216],{"class":150},[99,423,160],{"class":121},[99,425,426],{"class":150}," fragmentSize\n",[99,428,430,432,435,437,440,442,444,446,448,450,452,454,456,458,460,462,464,466,468,470,472,474,476,478,480],{"class":101,"line":429},13,[99,431,336],{"class":105},[99,433,434],{"class":150}," z ",[99,436,154],{"class":121},[99,438,439],{"class":150}," (iZ ",[99,441,350],{"class":121},[99,443,382],{"class":206},[99,445,216],{"class":150},[99,447,179],{"class":121},[99,449,157],{"class":150},[99,451,160],{"class":121},[99,453,176],{"class":150},[99,455,395],{"class":121},[99,457,176],{"class":150},[99,459,179],{"class":121},[99,461,213],{"class":206},[99,463,404],{"class":121},[99,465,407],{"class":150},[99,467,88],{"class":121},[99,469,412],{"class":109},[99,471,415],{"class":150},[99,473,395],{"class":121},[99,475,382],{"class":206},[99,477,216],{"class":150},[99,479,160],{"class":121},[99,481,426],{"class":150},[99,483,485],{"class":101,"line":484},14,[99,486,250],{"emptyLinePlaceholder":249},[99,488,490,493,495,497,500,502,504,506,508,510,512,514,517,519,521],{"class":101,"line":489},15,[99,491,492],{"class":105},"      for",[99,494,259],{"class":150},[99,496,262],{"class":105},[99,498,499],{"class":150}," v ",[99,501,154],{"class":121},[99,503,270],{"class":206},[99,505,273],{"class":113},[99,507,499],{"class":150},[99,509,278],{"class":121},[99,511,207],{"class":206},[99,513,273],{"class":113},[99,515,516],{"class":150}," v",[99,518,289],{"class":121},[99,520,216],{"class":150},[99,522,294],{"class":113},[99,524,526,529,531,534,536,538,540,542,545,547],{"class":101,"line":525},16,[99,527,528],{"class":150},"        anchors[i ",[99,530,160],{"class":121},[99,532,533],{"class":206}," 6",[99,535,404],{"class":121},[99,537,499],{"class":150},[99,539,160],{"class":121},[99,541,213],{"class":206},[99,543,544],{"class":150},"] ",[99,546,154],{"class":121},[99,548,549],{"class":150}," x\n",[99,551,553,555,557,559,561,563,565,567,569,572,574,576],{"class":101,"line":552},17,[99,554,528],{"class":150},[99,556,160],{"class":121},[99,558,533],{"class":206},[99,560,404],{"class":121},[99,562,499],{"class":150},[99,564,160],{"class":121},[99,566,213],{"class":206},[99,568,404],{"class":121},[99,570,571],{"class":206}," 1",[99,573,544],{"class":150},[99,575,154],{"class":121},[99,577,578],{"class":150}," z\n",[99,580,582,585,587,589,591,594,596,599,601,603],{"class":101,"line":581},18,[99,583,584],{"class":150},"        heightRandomness[i ",[99,586,160],{"class":121},[99,588,207],{"class":206},[99,590,404],{"class":121},[99,592,593],{"class":150}," v] ",[99,595,154],{"class":121},[99,597,598],{"class":150}," Math",[99,600,88],{"class":121},[99,602,412],{"class":109},[99,604,605],{"class":150},"()\n",[99,607,609],{"class":101,"line":608},19,[99,610,611],{"class":113},"      }\n",[99,613,615],{"class":101,"line":614},20,[99,616,617],{"class":113},"    }\n",[99,619,621],{"class":101,"line":620},21,[99,622,623],{"class":113},"  }\n",[99,625,627],{"class":101,"line":626},22,[99,628,250],{"emptyLinePlaceholder":249},[99,630,632,634,637,639,641,644],{"class":101,"line":631},23,[99,633,147],{"class":105},[99,635,636],{"class":150}," geometry ",[99,638,154],{"class":121},[99,640,195],{"class":194},[99,642,643],{"class":109}," BufferGeometry",[99,645,605],{"class":150},[99,647,649,652,654,657,659,663,665,667,670,673,675,677,680],{"class":101,"line":648},24,[99,650,651],{"class":150},"  geometry",[99,653,88],{"class":121},[99,655,656],{"class":109},"setAttribute",[99,658,114],{"class":150},[99,660,662],{"class":661},"sNt6F","'position'",[99,664,128],{"class":113},[99,666,195],{"class":194},[99,668,669],{"class":109}," BufferAttribute",[99,671,672],{"class":150},"(anchors",[99,674,128],{"class":113},[99,676,213],{"class":206},[99,678,679],{"class":150},")) ",[99,681,682],{"class":219},"\u002F\u002F itemSize 2!\n",[99,684,686,688,690,692,694,697,699,701,703,706,708,710],{"class":101,"line":685},25,[99,687,651],{"class":150},[99,689,88],{"class":121},[99,691,656],{"class":109},[99,693,114],{"class":150},[99,695,696],{"class":661},"'heightRandomness'",[99,698,128],{"class":113},[99,700,195],{"class":194},[99,702,669],{"class":109},[99,704,705],{"class":150},"(heightRandomness",[99,707,128],{"class":113},[99,709,571],{"class":206},[99,711,712],{"class":150},"))\n",[99,714,716],{"class":101,"line":715},26,[99,717,718],{"class":219},"  \u002F\u002F position doesn't hold real positions anymore, so bounds must be set by hand\n",[99,720,722,724,726,729,731,733,736,738,741,744,747,749,752,754,756,758,760,762,764,767,769,771],{"class":101,"line":721},27,[99,723,651],{"class":150},[99,725,88],{"class":121},[99,727,728],{"class":150},"boundingSphere ",[99,730,154],{"class":121},[99,732,195],{"class":194},[99,734,735],{"class":109}," Sphere",[99,737,114],{"class":150},[99,739,740],{"class":194},"new",[99,742,743],{"class":109}," Vector3",[99,745,746],{"class":150},"()",[99,748,128],{"class":113},[99,750,751],{"class":150}," (size ",[99,753,179],{"class":121},[99,755,213],{"class":206},[99,757,216],{"class":150},[99,759,160],{"class":121},[99,761,598],{"class":150},[99,763,88],{"class":121},[99,765,766],{"class":206},"SQRT2",[99,768,404],{"class":121},[99,770,213],{"class":206},[99,772,243],{"class":150},[99,774,776,779],{"class":101,"line":775},28,[99,777,778],{"class":105},"  return",[99,780,781],{"class":150}," geometry\n",[99,783,785],{"class":101,"line":784},29,[99,786,787],{"class":113},"}\n",[18,789,790],{},"Two things to notice:",[792,793,794,802],"ul",{},[795,796,797,798,801],"li",{},"Blades are distributed on a grid (",[81,799,800],{},"subdivisions × subdivisions","), one per cell, jittered inside the cell. This guarantees even coverage with no clumps or bald patches, and it's cheap: no need to check distances between blades or re-roll positions.",[795,803,804,805,808,809,812,813,816],{},"The ",[81,806,807],{},"position"," attribute has ",[76,810,811],{},"itemSize 2",". It only stores ",[81,814,815],{},"(x, z)",". Three.js doesn't mind, as long as we compute the real position ourselves in the vertex stage, and set a bounding sphere manually since it can't be derived from fake positions.",[18,818,819],{},[24,820],{"alt":821,"src":822},"Top-down diagram of the grass field: a subdivisions × subdivisions grid on the xz plane, with one anchor per cell placed at the cell center plus a random jitter","\u002Fblog\u002Fcreating-a-grass-field-with-tsl\u002Fgrid-distribution.svg",[18,824,825,826,829,830,122],{},"The actual triangle is expanded in the vertex stage. A tiny ",[81,827,828],{},"uniformArray"," holds the blade's silhouette as 2D offsets, and each vertex figures out which corner it is with ",[81,831,832],{},"vertexIndex.mod(3)",[90,834,836],{"className":92,"code":835,"language":94,"meta":95,"style":95},"const bladeWidth = uniform(0.1)\nconst bladeHeight = uniform(0.6)\nconst bladeShape = uniformArray([\n  new Vector2(0, 1), \u002F\u002F tip\n  new Vector2(1, 0), \u002F\u002F bottom-right\n  new Vector2(-1, 0), \u002F\u002F bottom-left\n], 'vec2')\n\nconst material = new MeshBasicNodeMaterial()\nmaterial.side = DoubleSide\n\nmaterial.positionNode = Fn(() => {\n  const anchor = attribute('position', 'vec2')\n  const vertexLoopIndex = vertexIndex.toFloat().mod(3) \u002F\u002F 0, 1, 2, 0, 1, 2…\n  const template = bladeShape.element(vertexLoopIndex.toInt())\n\n  return vec3(\n    anchor.x.add(template.x.mul(bladeWidth)),\n    template.y.mul(bladeHeight),\n    anchor.y,\n  )\n})()\n",[81,837,838,858,876,891,915,937,960,972,976,992,1007,1011,1034,1056,1090,1118,1122,1132,1165,1184,1194,1199],{"__ignoreMap":95},[99,839,840,843,846,848,851,853,856],{"class":101,"line":102},[99,841,842],{"class":105},"const",[99,844,845],{"class":150}," bladeWidth ",[99,847,154],{"class":121},[99,849,850],{"class":109}," uniform",[99,852,114],{"class":150},[99,854,855],{"class":206},"0.1",[99,857,243],{"class":150},[99,859,860,862,865,867,869,871,874],{"class":101,"line":144},[99,861,842],{"class":105},[99,863,864],{"class":150}," bladeHeight ",[99,866,154],{"class":121},[99,868,850],{"class":109},[99,870,114],{"class":150},[99,872,873],{"class":206},"0.6",[99,875,243],{"class":150},[99,877,878,880,883,885,888],{"class":101,"line":166},[99,879,842],{"class":105},[99,881,882],{"class":150}," bladeShape ",[99,884,154],{"class":121},[99,886,887],{"class":109}," uniformArray",[99,889,890],{"class":150},"([\n",[99,892,893,896,899,901,904,906,908,910,912],{"class":101,"line":184},[99,894,895],{"class":194},"  new",[99,897,898],{"class":109}," Vector2",[99,900,114],{"class":150},[99,902,903],{"class":206},"0",[99,905,128],{"class":113},[99,907,571],{"class":206},[99,909,138],{"class":150},[99,911,128],{"class":113},[99,913,914],{"class":219}," \u002F\u002F tip\n",[99,916,917,919,921,923,926,928,930,932,934],{"class":101,"line":223},[99,918,895],{"class":194},[99,920,898],{"class":109},[99,922,114],{"class":150},[99,924,925],{"class":206},"1",[99,927,128],{"class":113},[99,929,270],{"class":206},[99,931,138],{"class":150},[99,933,128],{"class":113},[99,935,936],{"class":219}," \u002F\u002F bottom-right\n",[99,938,939,941,943,945,947,949,951,953,955,957],{"class":101,"line":246},[99,940,895],{"class":194},[99,942,898],{"class":109},[99,944,114],{"class":150},[99,946,395],{"class":121},[99,948,925],{"class":206},[99,950,128],{"class":113},[99,952,270],{"class":206},[99,954,138],{"class":150},[99,956,128],{"class":113},[99,958,959],{"class":219}," \u002F\u002F bottom-left\n",[99,961,962,965,967,970],{"class":101,"line":253},[99,963,964],{"class":150},"]",[99,966,128],{"class":113},[99,968,969],{"class":661}," 'vec2'",[99,971,243],{"class":150},[99,973,974],{"class":101,"line":297},[99,975,250],{"emptyLinePlaceholder":249},[99,977,978,980,983,985,987,990],{"class":101,"line":333},[99,979,842],{"class":105},[99,981,982],{"class":150}," material ",[99,984,154],{"class":121},[99,986,195],{"class":194},[99,988,989],{"class":109}," MeshBasicNodeMaterial",[99,991,605],{"class":150},[99,993,994,997,999,1002,1004],{"class":101,"line":356},[99,995,996],{"class":150},"material",[99,998,88],{"class":121},[99,1000,1001],{"class":150},"side ",[99,1003,154],{"class":121},[99,1005,1006],{"class":150}," DoubleSide\n",[99,1008,1009],{"class":101,"line":361},[99,1010,250],{"emptyLinePlaceholder":249},[99,1012,1013,1015,1017,1020,1022,1025,1027,1029,1032],{"class":101,"line":367},[99,1014,996],{"class":150},[99,1016,88],{"class":121},[99,1018,1019],{"class":150},"positionNode ",[99,1021,154],{"class":121},[99,1023,1024],{"class":109}," Fn",[99,1026,114],{"class":150},[99,1028,746],{"class":113},[99,1030,1031],{"class":121}," =>",[99,1033,141],{"class":113},[99,1035,1036,1038,1041,1043,1046,1048,1050,1052,1054],{"class":101,"line":429},[99,1037,147],{"class":105},[99,1039,1040],{"class":150}," anchor ",[99,1042,154],{"class":121},[99,1044,1045],{"class":109}," attribute",[99,1047,114],{"class":150},[99,1049,662],{"class":661},[99,1051,128],{"class":113},[99,1053,969],{"class":661},[99,1055,243],{"class":150},[99,1057,1058,1060,1063,1065,1068,1070,1073,1075,1077,1080,1082,1085,1087],{"class":101,"line":484},[99,1059,147],{"class":105},[99,1061,1062],{"class":150}," vertexLoopIndex ",[99,1064,154],{"class":121},[99,1066,1067],{"class":150}," vertexIndex",[99,1069,88],{"class":121},[99,1071,1072],{"class":109},"toFloat",[99,1074,746],{"class":150},[99,1076,88],{"class":121},[99,1078,1079],{"class":109},"mod",[99,1081,114],{"class":150},[99,1083,1084],{"class":206},"3",[99,1086,216],{"class":150},[99,1088,1089],{"class":219},"\u002F\u002F 0, 1, 2, 0, 1, 2…\n",[99,1091,1092,1094,1097,1099,1102,1104,1107,1110,1112,1115],{"class":101,"line":489},[99,1093,147],{"class":105},[99,1095,1096],{"class":150}," template ",[99,1098,154],{"class":121},[99,1100,1101],{"class":150}," bladeShape",[99,1103,88],{"class":121},[99,1105,1106],{"class":109},"element",[99,1108,1109],{"class":150},"(vertexLoopIndex",[99,1111,88],{"class":121},[99,1113,1114],{"class":109},"toInt",[99,1116,1117],{"class":150},"())\n",[99,1119,1120],{"class":101,"line":525},[99,1121,250],{"emptyLinePlaceholder":249},[99,1123,1124,1126,1129],{"class":101,"line":552},[99,1125,778],{"class":105},[99,1127,1128],{"class":109}," vec3",[99,1130,1131],{"class":150},"(\n",[99,1133,1134,1137,1139,1142,1144,1147,1150,1152,1154,1156,1159,1162],{"class":101,"line":581},[99,1135,1136],{"class":150},"    anchor",[99,1138,88],{"class":121},[99,1140,1141],{"class":150},"x",[99,1143,88],{"class":121},[99,1145,1146],{"class":109},"add",[99,1148,1149],{"class":150},"(template",[99,1151,88],{"class":121},[99,1153,1141],{"class":150},[99,1155,88],{"class":121},[99,1157,1158],{"class":109},"mul",[99,1160,1161],{"class":150},"(bladeWidth))",[99,1163,1164],{"class":113},",\n",[99,1166,1167,1170,1172,1175,1177,1179,1182],{"class":101,"line":608},[99,1168,1169],{"class":150},"    template",[99,1171,88],{"class":121},[99,1173,1174],{"class":150},"y",[99,1176,88],{"class":121},[99,1178,1158],{"class":109},[99,1180,1181],{"class":150},"(bladeHeight)",[99,1183,1164],{"class":113},[99,1185,1186,1188,1190,1192],{"class":101,"line":614},[99,1187,1136],{"class":150},[99,1189,88],{"class":121},[99,1191,1174],{"class":150},[99,1193,1164],{"class":113},[99,1195,1196],{"class":101,"line":620},[99,1197,1198],{"class":150},"  )\n",[99,1200,1201,1204],{"class":101,"line":626},[99,1202,1203],{"class":113},"}",[99,1205,1206],{"class":150},")()\n",[18,1208,1209],{},"Every vertex starts at its anchor and offsets itself sideways and up according to the shape template. One draw call, and the blade shape is still editable at runtime because it lives in a uniform.",[18,1211,1212,1213,1215,1216,88],{},"The demo below is deliberately sparse so you can read the structure. Toggle the wireframe: every blade is a single triangle standing on its anchor. From here on we crank ",[81,1214,118],{}," up to 100, which is 10,000 triangles from this same single ",[81,1217,83],{},[10,1219,1221],{"caption":1220},"Step 1: one triangle per blade — toggle the wireframe to see the expanded geometry",[14,1222],{":step":925},[62,1224,1226],{"id":1225},"height-variation","Height variation",[18,1228,1229,1230,1233,1234,1237],{},"Uniform height reads as plastic turf. Two frequencies of variation fix that: a ",[76,1231,1232],{},"per-blade random"," (every blade slightly different from its neighbor) and a ",[76,1235,1236],{},"low-frequency noise"," (patches of taller and shorter grass across the field):",[90,1239,1241],{"className":92,"code":1240,"language":94,"meta":95,"style":95},"const bladeHeightRandomness = uniform(0.6)\n\nconst heightRand = attribute('heightRandomness', 'float')\nconst heightVariation = mx_noise_float(anchor.mul(0.35)).mul(0.5).add(1) \u002F\u002F [0.5, 1.5]\nconst height = bladeHeight\n  .mul(mix(1, heightRand, bladeHeightRandomness))\n  .mul(heightVariation)\n",[81,1242,1243,1260,1264,1286,1337,1349,1375],{"__ignoreMap":95},[99,1244,1245,1247,1250,1252,1254,1256,1258],{"class":101,"line":102},[99,1246,842],{"class":105},[99,1248,1249],{"class":150}," bladeHeightRandomness ",[99,1251,154],{"class":121},[99,1253,850],{"class":109},[99,1255,114],{"class":150},[99,1257,873],{"class":206},[99,1259,243],{"class":150},[99,1261,1262],{"class":101,"line":144},[99,1263,250],{"emptyLinePlaceholder":249},[99,1265,1266,1268,1271,1273,1275,1277,1279,1281,1284],{"class":101,"line":166},[99,1267,842],{"class":105},[99,1269,1270],{"class":150}," heightRand ",[99,1272,154],{"class":121},[99,1274,1045],{"class":109},[99,1276,114],{"class":150},[99,1278,696],{"class":661},[99,1280,128],{"class":113},[99,1282,1283],{"class":661}," 'float'",[99,1285,243],{"class":150},[99,1287,1288,1290,1293,1295,1298,1301,1303,1305,1307,1310,1313,1315,1317,1319,1322,1324,1326,1328,1330,1332,1334],{"class":101,"line":184},[99,1289,842],{"class":105},[99,1291,1292],{"class":150}," heightVariation ",[99,1294,154],{"class":121},[99,1296,1297],{"class":109}," mx_noise_float",[99,1299,1300],{"class":150},"(anchor",[99,1302,88],{"class":121},[99,1304,1158],{"class":109},[99,1306,114],{"class":150},[99,1308,1309],{"class":206},"0.35",[99,1311,1312],{"class":150},"))",[99,1314,88],{"class":121},[99,1316,1158],{"class":109},[99,1318,114],{"class":150},[99,1320,1321],{"class":206},"0.5",[99,1323,138],{"class":150},[99,1325,88],{"class":121},[99,1327,1146],{"class":109},[99,1329,114],{"class":150},[99,1331,925],{"class":206},[99,1333,216],{"class":150},[99,1335,1336],{"class":219},"\u002F\u002F [0.5, 1.5]\n",[99,1338,1339,1341,1344,1346],{"class":101,"line":223},[99,1340,842],{"class":105},[99,1342,1343],{"class":150}," height ",[99,1345,154],{"class":121},[99,1347,1348],{"class":150}," bladeHeight\n",[99,1350,1351,1354,1356,1358,1361,1363,1365,1367,1370,1372],{"class":101,"line":246},[99,1352,1353],{"class":121},"  .",[99,1355,1158],{"class":109},[99,1357,114],{"class":150},[99,1359,1360],{"class":109},"mix",[99,1362,114],{"class":150},[99,1364,925],{"class":206},[99,1366,128],{"class":113},[99,1368,1369],{"class":150}," heightRand",[99,1371,128],{"class":113},[99,1373,1374],{"class":150}," bladeHeightRandomness))\n",[99,1376,1377,1379,1381],{"class":101,"line":253},[99,1378,1353],{"class":121},[99,1380,1158],{"class":109},[99,1382,1383],{"class":150},"(heightVariation)\n",[18,1385,1386,1389,1390,1393,1394,1397,1398,1401,1402,1404,1405,1408],{},[81,1387,1388],{},"mx_noise_float"," returns roughly ",[81,1391,1392],{},"[-1, 1]",", so ",[81,1395,1396],{},".mul(0.5).add(1)"," remaps it to ",[81,1399,1400],{},"[0.5, 1.5]",": patches half as tall up to 50% taller. The ",[81,1403,1309],{}," scale controls the patch size (roughly ",[81,1406,1407],{},"1 \u002F scale"," world units), tune it to your field size — too low and the whole field samples a flat slice of the noise, and the patches disappear.",[10,1410,1412],{"caption":1411},"Step 2: per-blade randomness + noise patches",[14,1413],{":step":1414},"2",[62,1416,1418],{"id":1417},"color-and-fake-ambient-occlusion","Color and fake ambient occlusion",[18,1420,1421,1422,1425],{},"For color, the blade doesn't have its own identity, it ",[76,1423,1424],{},"inherits the ground color"," under it. We define the ground color procedurally, as a noise-driven mix between two greens, and evaluate it at the anchor:",[90,1427,1429],{"className":92,"code":1428,"language":94,"meta":95,"style":95},"const shadowIntensity = uniform(0.5)\n\nconst groundColor = Fn(([worldXZ]) => {\n  const variation = mx_noise_float(worldXZ.mul(0.25)).mul(0.5).add(0.5)\n  return mix(colorA, colorB, variation)\n})\n\nmaterial.colorNode = Fn(() => {\n  const vertexLoopIndex = varying(vertexIndex.toFloat().mod(3))\n  const tipness = varying(step(vertexLoopIndex, 0.5)) \u002F\u002F 1 on the tip vertex\n  const worldXZ = varying(attribute('position', 'vec2'))\n\n  const base = groundColor(worldXZ)\n  const ao = tipness.oneMinus().mul(shadowIntensity)\n  return mix(base, base.mul(0.35), ao)\n})()\n",[81,1430,1431,1448,1452,1478,1523,1543,1549,1553,1574,1604,1631,1657,1661,1676,1702,1731],{"__ignoreMap":95},[99,1432,1433,1435,1438,1440,1442,1444,1446],{"class":101,"line":102},[99,1434,842],{"class":105},[99,1436,1437],{"class":150}," shadowIntensity ",[99,1439,154],{"class":121},[99,1441,850],{"class":109},[99,1443,114],{"class":150},[99,1445,1321],{"class":206},[99,1447,243],{"class":150},[99,1449,1450],{"class":101,"line":144},[99,1451,250],{"emptyLinePlaceholder":249},[99,1453,1454,1456,1459,1461,1463,1465,1468,1471,1474,1476],{"class":101,"line":166},[99,1455,842],{"class":105},[99,1457,1458],{"class":150}," groundColor ",[99,1460,154],{"class":121},[99,1462,1024],{"class":109},[99,1464,114],{"class":150},[99,1466,1467],{"class":113},"([",[99,1469,1470],{"class":117},"worldXZ",[99,1472,1473],{"class":113},"])",[99,1475,1031],{"class":121},[99,1477,141],{"class":113},[99,1479,1480,1482,1485,1487,1489,1492,1494,1496,1498,1501,1503,1505,1507,1509,1511,1513,1515,1517,1519,1521],{"class":101,"line":184},[99,1481,147],{"class":105},[99,1483,1484],{"class":150}," variation ",[99,1486,154],{"class":121},[99,1488,1297],{"class":109},[99,1490,1491],{"class":150},"(worldXZ",[99,1493,88],{"class":121},[99,1495,1158],{"class":109},[99,1497,114],{"class":150},[99,1499,1500],{"class":206},"0.25",[99,1502,1312],{"class":150},[99,1504,88],{"class":121},[99,1506,1158],{"class":109},[99,1508,114],{"class":150},[99,1510,1321],{"class":206},[99,1512,138],{"class":150},[99,1514,88],{"class":121},[99,1516,1146],{"class":109},[99,1518,114],{"class":150},[99,1520,1321],{"class":206},[99,1522,243],{"class":150},[99,1524,1525,1527,1530,1533,1535,1538,1540],{"class":101,"line":223},[99,1526,778],{"class":105},[99,1528,1529],{"class":109}," mix",[99,1531,1532],{"class":150},"(colorA",[99,1534,128],{"class":113},[99,1536,1537],{"class":150}," colorB",[99,1539,128],{"class":113},[99,1541,1542],{"class":150}," variation)\n",[99,1544,1545,1547],{"class":101,"line":246},[99,1546,1203],{"class":113},[99,1548,243],{"class":150},[99,1550,1551],{"class":101,"line":253},[99,1552,250],{"emptyLinePlaceholder":249},[99,1554,1555,1557,1559,1562,1564,1566,1568,1570,1572],{"class":101,"line":297},[99,1556,996],{"class":150},[99,1558,88],{"class":121},[99,1560,1561],{"class":150},"colorNode ",[99,1563,154],{"class":121},[99,1565,1024],{"class":109},[99,1567,114],{"class":150},[99,1569,746],{"class":113},[99,1571,1031],{"class":121},[99,1573,141],{"class":113},[99,1575,1576,1578,1580,1582,1585,1588,1590,1592,1594,1596,1598,1600,1602],{"class":101,"line":333},[99,1577,147],{"class":105},[99,1579,1062],{"class":150},[99,1581,154],{"class":121},[99,1583,1584],{"class":109}," varying",[99,1586,1587],{"class":150},"(vertexIndex",[99,1589,88],{"class":121},[99,1591,1072],{"class":109},[99,1593,746],{"class":150},[99,1595,88],{"class":121},[99,1597,1079],{"class":109},[99,1599,114],{"class":150},[99,1601,1084],{"class":206},[99,1603,712],{"class":150},[99,1605,1606,1608,1611,1613,1615,1617,1620,1622,1624,1626,1628],{"class":101,"line":356},[99,1607,147],{"class":105},[99,1609,1610],{"class":150}," tipness ",[99,1612,154],{"class":121},[99,1614,1584],{"class":109},[99,1616,114],{"class":150},[99,1618,1619],{"class":109},"step",[99,1621,1109],{"class":150},[99,1623,128],{"class":113},[99,1625,382],{"class":206},[99,1627,679],{"class":150},[99,1629,1630],{"class":219},"\u002F\u002F 1 on the tip vertex\n",[99,1632,1633,1635,1638,1640,1642,1644,1647,1649,1651,1653,1655],{"class":101,"line":361},[99,1634,147],{"class":105},[99,1636,1637],{"class":150}," worldXZ ",[99,1639,154],{"class":121},[99,1641,1584],{"class":109},[99,1643,114],{"class":150},[99,1645,1646],{"class":109},"attribute",[99,1648,114],{"class":150},[99,1650,662],{"class":661},[99,1652,128],{"class":113},[99,1654,969],{"class":661},[99,1656,712],{"class":150},[99,1658,1659],{"class":101,"line":367},[99,1660,250],{"emptyLinePlaceholder":249},[99,1662,1663,1665,1668,1670,1673],{"class":101,"line":429},[99,1664,147],{"class":105},[99,1666,1667],{"class":150}," base ",[99,1669,154],{"class":121},[99,1671,1672],{"class":109}," groundColor",[99,1674,1675],{"class":150},"(worldXZ)\n",[99,1677,1678,1680,1683,1685,1688,1690,1693,1695,1697,1699],{"class":101,"line":484},[99,1679,147],{"class":105},[99,1681,1682],{"class":150}," ao ",[99,1684,154],{"class":121},[99,1686,1687],{"class":150}," tipness",[99,1689,88],{"class":121},[99,1691,1692],{"class":109},"oneMinus",[99,1694,746],{"class":150},[99,1696,88],{"class":121},[99,1698,1158],{"class":109},[99,1700,1701],{"class":150},"(shadowIntensity)\n",[99,1703,1704,1706,1708,1711,1713,1716,1718,1720,1722,1724,1726,1728],{"class":101,"line":489},[99,1705,778],{"class":105},[99,1707,1529],{"class":109},[99,1709,1710],{"class":150},"(base",[99,1712,128],{"class":113},[99,1714,1715],{"class":150}," base",[99,1717,88],{"class":121},[99,1719,1158],{"class":109},[99,1721,114],{"class":150},[99,1723,1309],{"class":206},[99,1725,138],{"class":150},[99,1727,128],{"class":113},[99,1729,1730],{"class":150}," ao)\n",[99,1732,1733,1735],{"class":101,"line":525},[99,1734,1203],{"class":113},[99,1736,1206],{"class":150},[18,1738,1739,1740,1743,1744,1746,1747,1749,1750,1753,1754,1757],{},"The fun part is ",[81,1741,1742],{},"tipness",". It's ",[81,1745,925],{}," on the tip vertex and ",[81,1748,903],{}," on the two base vertices, a binary value. But because it's passed through a ",[81,1751,1752],{},"varying",", the rasterizer ",[76,1755,1756],{},"interpolates it across the triangle",", and we get a smooth base-to-tip gradient for free. Darkening the base fakes ambient occlusion: dense grass is darker near the soil where light can't reach. It's the single highest-impact line in the whole shader.",[18,1759,1760],{},"If you render the same ground color on a plane underneath, the blades melt into it and the field reads as one continuous surface.",[10,1762,1764],{"caption":1763},"Step 3: ground color inheritance + tipness AO",[14,1765],{":step":1084},[62,1767,1769],{"id":1768},"billboarding","Billboarding",[18,1771,1772],{},"Flat triangles all facing the same way disappear when viewed edge-on. The classic fix is billboarding: rotate every blade around its own base so it always faces the camera.",[90,1774,1776],{"className":92,"code":1775,"language":94,"meta":95,"style":95},"const pos = vec3(\u002F* …shape from before… *\u002F).toVar()\n\n\u002F\u002F billboard: rotate around the blade base (anchor), not the origin\nconst angle = atan(\n  anchor.y.sub(cameraPosition.z),\n  anchor.x.sub(cameraPosition.x),\n).sub(PI.div(2))\npos.xz.assign(rotateUV(pos.xz, angle, anchor))\n",[81,1777,1778,1803,1807,1812,1826,1850,1871,1895],{"__ignoreMap":95},[99,1779,1780,1782,1785,1787,1789,1791,1794,1796,1798,1801],{"class":101,"line":102},[99,1781,842],{"class":105},[99,1783,1784],{"class":150}," pos ",[99,1786,154],{"class":121},[99,1788,1128],{"class":109},[99,1790,114],{"class":150},[99,1792,1793],{"class":219},"\u002F* …shape from before… *\u002F",[99,1795,138],{"class":150},[99,1797,88],{"class":121},[99,1799,1800],{"class":109},"toVar",[99,1802,605],{"class":150},[99,1804,1805],{"class":101,"line":144},[99,1806,250],{"emptyLinePlaceholder":249},[99,1808,1809],{"class":101,"line":166},[99,1810,1811],{"class":219},"\u002F\u002F billboard: rotate around the blade base (anchor), not the origin\n",[99,1813,1814,1816,1819,1821,1824],{"class":101,"line":184},[99,1815,842],{"class":105},[99,1817,1818],{"class":150}," angle ",[99,1820,154],{"class":121},[99,1822,1823],{"class":109}," atan",[99,1825,1131],{"class":150},[99,1827,1828,1831,1833,1835,1837,1840,1843,1845,1848],{"class":101,"line":223},[99,1829,1830],{"class":150},"  anchor",[99,1832,88],{"class":121},[99,1834,1174],{"class":150},[99,1836,88],{"class":121},[99,1838,1839],{"class":109},"sub",[99,1841,1842],{"class":150},"(cameraPosition",[99,1844,88],{"class":121},[99,1846,1847],{"class":150},"z)",[99,1849,1164],{"class":113},[99,1851,1852,1854,1856,1858,1860,1862,1864,1866,1869],{"class":101,"line":246},[99,1853,1830],{"class":150},[99,1855,88],{"class":121},[99,1857,1141],{"class":150},[99,1859,88],{"class":121},[99,1861,1839],{"class":109},[99,1863,1842],{"class":150},[99,1865,88],{"class":121},[99,1867,1868],{"class":150},"x)",[99,1870,1164],{"class":113},[99,1872,1873,1875,1877,1879,1881,1884,1886,1889,1891,1893],{"class":101,"line":253},[99,1874,138],{"class":150},[99,1876,88],{"class":121},[99,1878,1839],{"class":109},[99,1880,114],{"class":150},[99,1882,1883],{"class":206},"PI",[99,1885,88],{"class":121},[99,1887,1888],{"class":109},"div",[99,1890,114],{"class":150},[99,1892,1414],{"class":206},[99,1894,712],{"class":150},[99,1896,1897,1900,1902,1905,1907,1910,1912,1915,1918,1920,1922,1924,1927,1929],{"class":101,"line":297},[99,1898,1899],{"class":150},"pos",[99,1901,88],{"class":121},[99,1903,1904],{"class":150},"xz",[99,1906,88],{"class":121},[99,1908,1909],{"class":109},"assign",[99,1911,114],{"class":150},[99,1913,1914],{"class":109},"rotateUV",[99,1916,1917],{"class":150},"(pos",[99,1919,88],{"class":121},[99,1921,1904],{"class":150},[99,1923,128],{"class":113},[99,1925,1926],{"class":150}," angle",[99,1928,128],{"class":113},[99,1930,1931],{"class":150}," anchor))\n",[18,1933,1934,1937,1938,1940,1941,1944],{},[81,1935,1936],{},"atan"," gives the angle from the blade to the camera on the ground plane, and ",[81,1939,1914],{}," rotates the vertex around the anchor. ",[81,1942,1943],{},"cameraPosition"," is a built-in TSL node, so this updates every frame without any uniform juggling.",[10,1946,1948],{"caption":1947},"Step 4: camera-facing blades — orbit around and watch them follow",[14,1949],{":step":1950},"4",[57,1952,1954],{"id":1953},"hitting-the-wall","Hitting the Wall",[18,1956,1957],{},"This version looks decent and runs fast. It's also a dead end, and you can see why by orbiting the demo above:",[792,1959,1960,1966,1977,1986],{},[795,1961,1962,1965],{},[76,1963,1964],{},"The field swims."," Every blade rotates to face you, so when the camera moves, the whole field visibly reorganizes itself. From above, blades collapse into lines.",[795,1967,1968,1971,1972,1976],{},[76,1969,1970],{},"One triangle can't bend."," A blade that sways in the wind needs to ",[1973,1974,1975],"em",{},"curve",", and a single triangle can only shear. There's no vertex in the middle to displace.",[795,1978,1979,1982,1983,1985],{},[76,1980,1981],{},"The GPU repeats work every frame."," That ",[81,1984,1388],{}," for height patches? It runs per vertex, per frame, for a value that never changes.",[795,1987,1988,1991],{},[76,1989,1990],{},"Every per-blade feature costs 3×."," Want a per-blade yaw? A baked color? You have to duplicate it into all 3 vertices of the blade.",[18,1993,1994,1995,1998],{},"All four problems have the same root: the geometry has no concept of \"a blade\". It's just a soup of vertices. What we want is to define ",[76,1996,1997],{},"one blade"," and stamp it thousands of times, which is exactly what instancing is.",[57,2000,2002],{"id":2001},"part-2-the-instanced-blade","Part 2: The Instanced Blade",[62,2004,2006],{"id":2005},"one-blade-many-instances","One blade, many instances",[18,2008,2009,2010,2013,2014,2017],{},"With ",[81,2011,2012],{},"InstancedBufferGeometry",", the vertex buffer describes a single blade, and a second set of attributes, ",[76,2015,2016],{},"instanced attributes",", holds the per-blade data. The GPU re-runs the blade geometry once per instance, advancing the instanced attributes each time.",[18,2019,2020,2021,2023],{},"Since geometry is now shared, upgrading the blade is nearly free. Let's give it 5 vertices and 3 indexed triangles: a quad with a tip. The mid vertices are what will let blades ",[1973,2022,1975],{}," later:",[90,2025,2027],{"className":92,"code":2026,"language":94,"meta":95,"style":95},"\u002F\u002F unit blade in the XY plane: 2 bottom verts, 2 mid at 70% height, 1 tip\nconst BLADE_POSITIONS = new Float32Array([\n  -1, 0, 0, \u002F\u002F bottom-left\n  1, 0, 0, \u002F\u002F bottom-right\n  -0.5, 0.7, 0, \u002F\u002F mid-left\n  0.5, 0.7, 0, \u002F\u002F mid-right\n  0, 1, 0, \u002F\u002F tip\n])\nconst BLADE_TIPNESS = new Float32Array([0, 0, 0.7, 0.7, 1])\nconst BLADE_INDICES = [0, 1, 2, 1, 3, 2, 2, 3, 4]\n",[81,2028,2029,2034,2049,2068,2085,2105,2123,2140,2145,2180],{"__ignoreMap":95},[99,2030,2031],{"class":101,"line":102},[99,2032,2033],{"class":219},"\u002F\u002F unit blade in the XY plane: 2 bottom verts, 2 mid at 70% height, 1 tip\n",[99,2035,2036,2038,2041,2043,2045,2047],{"class":101,"line":144},[99,2037,842],{"class":105},[99,2039,2040],{"class":150}," BLADE_POSITIONS ",[99,2042,154],{"class":121},[99,2044,195],{"class":194},[99,2046,198],{"class":109},[99,2048,890],{"class":150},[99,2050,2051,2054,2056,2058,2060,2062,2064,2066],{"class":101,"line":166},[99,2052,2053],{"class":121},"  -",[99,2055,925],{"class":206},[99,2057,128],{"class":113},[99,2059,270],{"class":206},[99,2061,128],{"class":113},[99,2063,270],{"class":206},[99,2065,128],{"class":113},[99,2067,959],{"class":219},[99,2069,2070,2073,2075,2077,2079,2081,2083],{"class":101,"line":184},[99,2071,2072],{"class":206},"  1",[99,2074,128],{"class":113},[99,2076,270],{"class":206},[99,2078,128],{"class":113},[99,2080,270],{"class":206},[99,2082,128],{"class":113},[99,2084,936],{"class":219},[99,2086,2087,2089,2091,2093,2096,2098,2100,2102],{"class":101,"line":223},[99,2088,2053],{"class":121},[99,2090,1321],{"class":206},[99,2092,128],{"class":113},[99,2094,2095],{"class":206}," 0.7",[99,2097,128],{"class":113},[99,2099,270],{"class":206},[99,2101,128],{"class":113},[99,2103,2104],{"class":219}," \u002F\u002F mid-left\n",[99,2106,2107,2110,2112,2114,2116,2118,2120],{"class":101,"line":246},[99,2108,2109],{"class":206},"  0.5",[99,2111,128],{"class":113},[99,2113,2095],{"class":206},[99,2115,128],{"class":113},[99,2117,270],{"class":206},[99,2119,128],{"class":113},[99,2121,2122],{"class":219}," \u002F\u002F mid-right\n",[99,2124,2125,2128,2130,2132,2134,2136,2138],{"class":101,"line":253},[99,2126,2127],{"class":206},"  0",[99,2129,128],{"class":113},[99,2131,571],{"class":206},[99,2133,128],{"class":113},[99,2135,270],{"class":206},[99,2137,128],{"class":113},[99,2139,914],{"class":219},[99,2141,2142],{"class":101,"line":297},[99,2143,2144],{"class":150},"])\n",[99,2146,2147,2149,2152,2154,2156,2158,2160,2162,2164,2166,2168,2170,2172,2174,2176,2178],{"class":101,"line":333},[99,2148,842],{"class":105},[99,2150,2151],{"class":150}," BLADE_TIPNESS ",[99,2153,154],{"class":121},[99,2155,195],{"class":194},[99,2157,198],{"class":109},[99,2159,1467],{"class":150},[99,2161,903],{"class":206},[99,2163,128],{"class":113},[99,2165,270],{"class":206},[99,2167,128],{"class":113},[99,2169,2095],{"class":206},[99,2171,128],{"class":113},[99,2173,2095],{"class":206},[99,2175,128],{"class":113},[99,2177,571],{"class":206},[99,2179,2144],{"class":150},[99,2181,2182,2184,2187,2189,2192,2194,2196,2198,2200,2202,2204,2206,2208,2210,2212,2214,2216,2218,2220,2222,2224,2227],{"class":101,"line":356},[99,2183,842],{"class":105},[99,2185,2186],{"class":150}," BLADE_INDICES ",[99,2188,154],{"class":121},[99,2190,2191],{"class":150}," [",[99,2193,903],{"class":206},[99,2195,128],{"class":113},[99,2197,571],{"class":206},[99,2199,128],{"class":113},[99,2201,213],{"class":206},[99,2203,128],{"class":113},[99,2205,571],{"class":206},[99,2207,128],{"class":113},[99,2209,207],{"class":206},[99,2211,128],{"class":113},[99,2213,213],{"class":206},[99,2215,128],{"class":113},[99,2217,213],{"class":206},[99,2219,128],{"class":113},[99,2221,207],{"class":206},[99,2223,128],{"class":113},[99,2225,2226],{"class":206}," 4",[99,2228,2229],{"class":150},"]\n",[18,2231,2232,2234,2235,2238],{},[81,2233,1742],{}," is now a real vertex attribute instead of a ",[81,2236,2237],{},"vertexIndex"," trick, with intermediate values on the mid vertices. Same AO gradient as before, but sculpted by hand.",[18,2240,2241,2242,2245,2246,2249],{},"The geometry setup moves all per-blade data into ",[81,2243,2244],{},"InstancedBufferAttribute","s, and, since it runs once at build time on the CPU, we can ",[76,2247,2248],{},"bake the noise"," there too instead of re-evaluating it in the shader:",[90,2251,2253],{"className":92,"code":2252,"language":94,"meta":95,"style":95},"function createGrassGeometry(subdivisions: number, size: number) {\n  const count = subdivisions * subdivisions\n  const fragmentSize = size \u002F subdivisions\n  const anchors = new Float32Array(count * 2)\n  const randoms = new Float32Array(count)\n  const yaws = new Float32Array(count)\n  \u002F\u002F anchor-only noises baked at build time instead of per-vertex in the shader\n  const heightNoises = new Float32Array(count)\n  const colorNoises = new Float32Array(count)\n  const noise = new ImprovedNoise()\n\n  for (let iX = 0; iX \u003C subdivisions; iX++) {\n    for (let iZ = 0; iZ \u003C subdivisions; iZ++) {\n      const i = iX * subdivisions + iZ\n\n      const x = (iX + 0.5) \u002F subdivisions * size - size \u002F 2 + (Math.random() - 0.5) * fragmentSize\n      const z = (iZ + 0.5) \u002F subdivisions * size - size \u002F 2 + (Math.random() - 0.5) * fragmentSize\n\n      anchors[i * 2] = x\n      anchors[i * 2 + 1] = z\n      randoms[i] = Math.random()\n      yaws[i] = Math.random() * Math.PI * 2\n      heightNoises[i] = noise.noise(x * 0.35, z * 0.35, 0) * 0.5 + 1\n      colorNoises[i] = noise.noise(x * 0.25, z * 0.25, 0) * 0.5 + 0.5\n    }\n  }\n\n  const geometry = new InstancedBufferGeometry()\n  geometry.instanceCount = count\n  geometry.setIndex(BLADE_INDICES)\n  geometry.setAttribute('position', new BufferAttribute(BLADE_POSITIONS, 3))\n  geometry.setAttribute('tipness', new BufferAttribute(BLADE_TIPNESS, 1))\n  geometry.setAttribute('anchor', new InstancedBufferAttribute(anchors, 2))\n  geometry.setAttribute('random', new InstancedBufferAttribute(randoms, 1))\n  geometry.setAttribute('yaw', new InstancedBufferAttribute(yaws, 1))\n  geometry.setAttribute('heightNoise', new InstancedBufferAttribute(heightNoises, 1))\n  geometry.setAttribute('colorNoise', new InstancedBufferAttribute(colorNoises, 1))\n  \u002F\u002F real bounds (field half-diagonal + height margin) so frustum culling can skip the draw\n  geometry.boundingSphere = new Sphere(new Vector3(), (size \u002F 2) * Math.SQRT2 + 2)\n  return geometry\n}\n",[81,2254,2255,2281,2295,2309,2329,2345,2360,2365,2380,2395,2411,2415,2447,2479,2497,2501,2553,2605,2609,2624,2642,2657,2685,2731,2774,2778,2782,2786,2801,2815,2828,2856,2885,2914,2943,2972,3001,3030,3036,3083,3090],{"__ignoreMap":95},[99,2256,2257,2259,2261,2263,2265,2267,2269,2271,2273,2275,2277,2279],{"class":101,"line":102},[99,2258,106],{"class":105},[99,2260,110],{"class":109},[99,2262,114],{"class":113},[99,2264,118],{"class":117},[99,2266,122],{"class":121},[99,2268,125],{"class":105},[99,2270,128],{"class":113},[99,2272,131],{"class":117},[99,2274,122],{"class":121},[99,2276,125],{"class":105},[99,2278,138],{"class":113},[99,2280,141],{"class":113},[99,2282,2283,2285,2287,2289,2291,2293],{"class":101,"line":144},[99,2284,147],{"class":105},[99,2286,151],{"class":150},[99,2288,154],{"class":121},[99,2290,157],{"class":150},[99,2292,160],{"class":121},[99,2294,163],{"class":150},[99,2296,2297,2299,2301,2303,2305,2307],{"class":101,"line":166},[99,2298,147],{"class":105},[99,2300,171],{"class":150},[99,2302,154],{"class":121},[99,2304,176],{"class":150},[99,2306,179],{"class":121},[99,2308,163],{"class":150},[99,2310,2311,2313,2315,2317,2319,2321,2323,2325,2327],{"class":101,"line":184},[99,2312,147],{"class":105},[99,2314,189],{"class":150},[99,2316,154],{"class":121},[99,2318,195],{"class":194},[99,2320,198],{"class":109},[99,2322,201],{"class":150},[99,2324,160],{"class":121},[99,2326,213],{"class":206},[99,2328,243],{"class":150},[99,2330,2331,2333,2336,2338,2340,2342],{"class":101,"line":223},[99,2332,147],{"class":105},[99,2334,2335],{"class":150}," randoms ",[99,2337,154],{"class":121},[99,2339,195],{"class":194},[99,2341,198],{"class":109},[99,2343,2344],{"class":150},"(count)\n",[99,2346,2347,2349,2352,2354,2356,2358],{"class":101,"line":246},[99,2348,147],{"class":105},[99,2350,2351],{"class":150}," yaws ",[99,2353,154],{"class":121},[99,2355,195],{"class":194},[99,2357,198],{"class":109},[99,2359,2344],{"class":150},[99,2361,2362],{"class":101,"line":253},[99,2363,2364],{"class":219},"  \u002F\u002F anchor-only noises baked at build time instead of per-vertex in the shader\n",[99,2366,2367,2369,2372,2374,2376,2378],{"class":101,"line":297},[99,2368,147],{"class":105},[99,2370,2371],{"class":150}," heightNoises ",[99,2373,154],{"class":121},[99,2375,195],{"class":194},[99,2377,198],{"class":109},[99,2379,2344],{"class":150},[99,2381,2382,2384,2387,2389,2391,2393],{"class":101,"line":333},[99,2383,147],{"class":105},[99,2385,2386],{"class":150}," colorNoises ",[99,2388,154],{"class":121},[99,2390,195],{"class":194},[99,2392,198],{"class":109},[99,2394,2344],{"class":150},[99,2396,2397,2399,2402,2404,2406,2409],{"class":101,"line":356},[99,2398,147],{"class":105},[99,2400,2401],{"class":150}," noise ",[99,2403,154],{"class":121},[99,2405,195],{"class":194},[99,2407,2408],{"class":109}," ImprovedNoise",[99,2410,605],{"class":150},[99,2412,2413],{"class":101,"line":361},[99,2414,250],{"emptyLinePlaceholder":249},[99,2416,2417,2419,2421,2423,2425,2427,2429,2431,2433,2435,2437,2439,2441,2443,2445],{"class":101,"line":367},[99,2418,256],{"class":105},[99,2420,259],{"class":150},[99,2422,262],{"class":105},[99,2424,265],{"class":150},[99,2426,154],{"class":121},[99,2428,270],{"class":206},[99,2430,273],{"class":113},[99,2432,265],{"class":150},[99,2434,278],{"class":121},[99,2436,281],{"class":150},[99,2438,273],{"class":113},[99,2440,286],{"class":150},[99,2442,289],{"class":121},[99,2444,216],{"class":150},[99,2446,294],{"class":113},[99,2448,2449,2451,2453,2455,2457,2459,2461,2463,2465,2467,2469,2471,2473,2475,2477],{"class":101,"line":429},[99,2450,300],{"class":105},[99,2452,259],{"class":150},[99,2454,262],{"class":105},[99,2456,307],{"class":150},[99,2458,154],{"class":121},[99,2460,270],{"class":206},[99,2462,273],{"class":113},[99,2464,307],{"class":150},[99,2466,278],{"class":121},[99,2468,281],{"class":150},[99,2470,273],{"class":113},[99,2472,324],{"class":150},[99,2474,289],{"class":121},[99,2476,216],{"class":150},[99,2478,294],{"class":113},[99,2480,2481,2483,2485,2487,2489,2491,2493,2495],{"class":101,"line":484},[99,2482,336],{"class":105},[99,2484,339],{"class":150},[99,2486,154],{"class":121},[99,2488,265],{"class":150},[99,2490,160],{"class":121},[99,2492,157],{"class":150},[99,2494,350],{"class":121},[99,2496,353],{"class":150},[99,2498,2499],{"class":101,"line":489},[99,2500,250],{"emptyLinePlaceholder":249},[99,2502,2503,2505,2507,2509,2511,2513,2515,2517,2519,2521,2523,2525,2527,2529,2531,2533,2535,2537,2539,2541,2543,2545,2547,2549,2551],{"class":101,"line":525},[99,2504,336],{"class":105},[99,2506,372],{"class":150},[99,2508,154],{"class":121},[99,2510,377],{"class":150},[99,2512,350],{"class":121},[99,2514,382],{"class":206},[99,2516,216],{"class":150},[99,2518,179],{"class":121},[99,2520,157],{"class":150},[99,2522,160],{"class":121},[99,2524,176],{"class":150},[99,2526,395],{"class":121},[99,2528,176],{"class":150},[99,2530,179],{"class":121},[99,2532,213],{"class":206},[99,2534,404],{"class":121},[99,2536,407],{"class":150},[99,2538,88],{"class":121},[99,2540,412],{"class":109},[99,2542,415],{"class":150},[99,2544,395],{"class":121},[99,2546,382],{"class":206},[99,2548,216],{"class":150},[99,2550,160],{"class":121},[99,2552,426],{"class":150},[99,2554,2555,2557,2559,2561,2563,2565,2567,2569,2571,2573,2575,2577,2579,2581,2583,2585,2587,2589,2591,2593,2595,2597,2599,2601,2603],{"class":101,"line":552},[99,2556,336],{"class":105},[99,2558,434],{"class":150},[99,2560,154],{"class":121},[99,2562,439],{"class":150},[99,2564,350],{"class":121},[99,2566,382],{"class":206},[99,2568,216],{"class":150},[99,2570,179],{"class":121},[99,2572,157],{"class":150},[99,2574,160],{"class":121},[99,2576,176],{"class":150},[99,2578,395],{"class":121},[99,2580,176],{"class":150},[99,2582,179],{"class":121},[99,2584,213],{"class":206},[99,2586,404],{"class":121},[99,2588,407],{"class":150},[99,2590,88],{"class":121},[99,2592,412],{"class":109},[99,2594,415],{"class":150},[99,2596,395],{"class":121},[99,2598,382],{"class":206},[99,2600,216],{"class":150},[99,2602,160],{"class":121},[99,2604,426],{"class":150},[99,2606,2607],{"class":101,"line":581},[99,2608,250],{"emptyLinePlaceholder":249},[99,2610,2611,2614,2616,2618,2620,2622],{"class":101,"line":608},[99,2612,2613],{"class":150},"      anchors[i ",[99,2615,160],{"class":121},[99,2617,213],{"class":206},[99,2619,544],{"class":150},[99,2621,154],{"class":121},[99,2623,549],{"class":150},[99,2625,2626,2628,2630,2632,2634,2636,2638,2640],{"class":101,"line":614},[99,2627,2613],{"class":150},[99,2629,160],{"class":121},[99,2631,213],{"class":206},[99,2633,404],{"class":121},[99,2635,571],{"class":206},[99,2637,544],{"class":150},[99,2639,154],{"class":121},[99,2641,578],{"class":150},[99,2643,2644,2647,2649,2651,2653,2655],{"class":101,"line":620},[99,2645,2646],{"class":150},"      randoms[i] ",[99,2648,154],{"class":121},[99,2650,598],{"class":150},[99,2652,88],{"class":121},[99,2654,412],{"class":109},[99,2656,605],{"class":150},[99,2658,2659,2662,2664,2666,2668,2670,2672,2674,2676,2678,2680,2682],{"class":101,"line":626},[99,2660,2661],{"class":150},"      yaws[i] ",[99,2663,154],{"class":121},[99,2665,598],{"class":150},[99,2667,88],{"class":121},[99,2669,412],{"class":109},[99,2671,415],{"class":150},[99,2673,160],{"class":121},[99,2675,598],{"class":150},[99,2677,88],{"class":121},[99,2679,1883],{"class":206},[99,2681,210],{"class":121},[99,2683,2684],{"class":206}," 2\n",[99,2686,2687,2690,2692,2695,2697,2700,2703,2705,2708,2710,2712,2714,2716,2718,2720,2722,2724,2726,2728],{"class":101,"line":631},[99,2688,2689],{"class":150},"      heightNoises[i] ",[99,2691,154],{"class":121},[99,2693,2694],{"class":150}," noise",[99,2696,88],{"class":121},[99,2698,2699],{"class":109},"noise",[99,2701,2702],{"class":150},"(x ",[99,2704,160],{"class":121},[99,2706,2707],{"class":206}," 0.35",[99,2709,128],{"class":113},[99,2711,434],{"class":150},[99,2713,160],{"class":121},[99,2715,2707],{"class":206},[99,2717,128],{"class":113},[99,2719,270],{"class":206},[99,2721,216],{"class":150},[99,2723,160],{"class":121},[99,2725,382],{"class":206},[99,2727,404],{"class":121},[99,2729,2730],{"class":206}," 1\n",[99,2732,2733,2736,2738,2740,2742,2744,2746,2748,2751,2753,2755,2757,2759,2761,2763,2765,2767,2769,2771],{"class":101,"line":648},[99,2734,2735],{"class":150},"      colorNoises[i] ",[99,2737,154],{"class":121},[99,2739,2694],{"class":150},[99,2741,88],{"class":121},[99,2743,2699],{"class":109},[99,2745,2702],{"class":150},[99,2747,160],{"class":121},[99,2749,2750],{"class":206}," 0.25",[99,2752,128],{"class":113},[99,2754,434],{"class":150},[99,2756,160],{"class":121},[99,2758,2750],{"class":206},[99,2760,128],{"class":113},[99,2762,270],{"class":206},[99,2764,216],{"class":150},[99,2766,160],{"class":121},[99,2768,382],{"class":206},[99,2770,404],{"class":121},[99,2772,2773],{"class":206}," 0.5\n",[99,2775,2776],{"class":101,"line":685},[99,2777,617],{"class":113},[99,2779,2780],{"class":101,"line":715},[99,2781,623],{"class":113},[99,2783,2784],{"class":101,"line":721},[99,2785,250],{"emptyLinePlaceholder":249},[99,2787,2788,2790,2792,2794,2796,2799],{"class":101,"line":775},[99,2789,147],{"class":105},[99,2791,636],{"class":150},[99,2793,154],{"class":121},[99,2795,195],{"class":194},[99,2797,2798],{"class":109}," InstancedBufferGeometry",[99,2800,605],{"class":150},[99,2802,2803,2805,2807,2810,2812],{"class":101,"line":784},[99,2804,651],{"class":150},[99,2806,88],{"class":121},[99,2808,2809],{"class":150},"instanceCount ",[99,2811,154],{"class":121},[99,2813,2814],{"class":150}," count\n",[99,2816,2818,2820,2822,2825],{"class":101,"line":2817},30,[99,2819,651],{"class":150},[99,2821,88],{"class":121},[99,2823,2824],{"class":109},"setIndex",[99,2826,2827],{"class":150},"(BLADE_INDICES)\n",[99,2829,2831,2833,2835,2837,2839,2841,2843,2845,2847,2850,2852,2854],{"class":101,"line":2830},31,[99,2832,651],{"class":150},[99,2834,88],{"class":121},[99,2836,656],{"class":109},[99,2838,114],{"class":150},[99,2840,662],{"class":661},[99,2842,128],{"class":113},[99,2844,195],{"class":194},[99,2846,669],{"class":109},[99,2848,2849],{"class":150},"(BLADE_POSITIONS",[99,2851,128],{"class":113},[99,2853,207],{"class":206},[99,2855,712],{"class":150},[99,2857,2859,2861,2863,2865,2867,2870,2872,2874,2876,2879,2881,2883],{"class":101,"line":2858},32,[99,2860,651],{"class":150},[99,2862,88],{"class":121},[99,2864,656],{"class":109},[99,2866,114],{"class":150},[99,2868,2869],{"class":661},"'tipness'",[99,2871,128],{"class":113},[99,2873,195],{"class":194},[99,2875,669],{"class":109},[99,2877,2878],{"class":150},"(BLADE_TIPNESS",[99,2880,128],{"class":113},[99,2882,571],{"class":206},[99,2884,712],{"class":150},[99,2886,2888,2890,2892,2894,2896,2899,2901,2903,2906,2908,2910,2912],{"class":101,"line":2887},33,[99,2889,651],{"class":150},[99,2891,88],{"class":121},[99,2893,656],{"class":109},[99,2895,114],{"class":150},[99,2897,2898],{"class":661},"'anchor'",[99,2900,128],{"class":113},[99,2902,195],{"class":194},[99,2904,2905],{"class":109}," InstancedBufferAttribute",[99,2907,672],{"class":150},[99,2909,128],{"class":113},[99,2911,213],{"class":206},[99,2913,712],{"class":150},[99,2915,2917,2919,2921,2923,2925,2928,2930,2932,2934,2937,2939,2941],{"class":101,"line":2916},34,[99,2918,651],{"class":150},[99,2920,88],{"class":121},[99,2922,656],{"class":109},[99,2924,114],{"class":150},[99,2926,2927],{"class":661},"'random'",[99,2929,128],{"class":113},[99,2931,195],{"class":194},[99,2933,2905],{"class":109},[99,2935,2936],{"class":150},"(randoms",[99,2938,128],{"class":113},[99,2940,571],{"class":206},[99,2942,712],{"class":150},[99,2944,2946,2948,2950,2952,2954,2957,2959,2961,2963,2966,2968,2970],{"class":101,"line":2945},35,[99,2947,651],{"class":150},[99,2949,88],{"class":121},[99,2951,656],{"class":109},[99,2953,114],{"class":150},[99,2955,2956],{"class":661},"'yaw'",[99,2958,128],{"class":113},[99,2960,195],{"class":194},[99,2962,2905],{"class":109},[99,2964,2965],{"class":150},"(yaws",[99,2967,128],{"class":113},[99,2969,571],{"class":206},[99,2971,712],{"class":150},[99,2973,2975,2977,2979,2981,2983,2986,2988,2990,2992,2995,2997,2999],{"class":101,"line":2974},36,[99,2976,651],{"class":150},[99,2978,88],{"class":121},[99,2980,656],{"class":109},[99,2982,114],{"class":150},[99,2984,2985],{"class":661},"'heightNoise'",[99,2987,128],{"class":113},[99,2989,195],{"class":194},[99,2991,2905],{"class":109},[99,2993,2994],{"class":150},"(heightNoises",[99,2996,128],{"class":113},[99,2998,571],{"class":206},[99,3000,712],{"class":150},[99,3002,3004,3006,3008,3010,3012,3015,3017,3019,3021,3024,3026,3028],{"class":101,"line":3003},37,[99,3005,651],{"class":150},[99,3007,88],{"class":121},[99,3009,656],{"class":109},[99,3011,114],{"class":150},[99,3013,3014],{"class":661},"'colorNoise'",[99,3016,128],{"class":113},[99,3018,195],{"class":194},[99,3020,2905],{"class":109},[99,3022,3023],{"class":150},"(colorNoises",[99,3025,128],{"class":113},[99,3027,571],{"class":206},[99,3029,712],{"class":150},[99,3031,3033],{"class":101,"line":3032},38,[99,3034,3035],{"class":219},"  \u002F\u002F real bounds (field half-diagonal + height margin) so frustum culling can skip the draw\n",[99,3037,3039,3041,3043,3045,3047,3049,3051,3053,3055,3057,3059,3061,3063,3065,3067,3069,3071,3073,3075,3077,3079,3081],{"class":101,"line":3038},39,[99,3040,651],{"class":150},[99,3042,88],{"class":121},[99,3044,728],{"class":150},[99,3046,154],{"class":121},[99,3048,195],{"class":194},[99,3050,735],{"class":109},[99,3052,114],{"class":150},[99,3054,740],{"class":194},[99,3056,743],{"class":109},[99,3058,746],{"class":150},[99,3060,128],{"class":113},[99,3062,751],{"class":150},[99,3064,179],{"class":121},[99,3066,213],{"class":206},[99,3068,216],{"class":150},[99,3070,160],{"class":121},[99,3072,598],{"class":150},[99,3074,88],{"class":121},[99,3076,766],{"class":206},[99,3078,404],{"class":121},[99,3080,213],{"class":206},[99,3082,243],{"class":150},[99,3084,3086,3088],{"class":101,"line":3085},40,[99,3087,778],{"class":105},[99,3089,781],{"class":150},[99,3091,3093],{"class":101,"line":3092},41,[99,3094,787],{"class":113},[18,3096,3097,3098,3101],{},"Do the maths on memory: v1 stored 3 floats per blade × 3 vertices. Any new per-blade feature multiplies by 3 again. v2 stores the 5-vertex blade ",[76,3099,3100],{},"once",", plus 6 floats per blade, and adding a new per-blade attribute costs exactly one value. On top of that, the bounding sphere is now real, so when the field leaves the camera frustum, Three.js skips the draw entirely.",[62,3103,3105],{"id":3104},"the-vertex-stage-revisited","The vertex stage, revisited",[18,3107,3108,3109,3112,3113,3115,3116,3119,3120,3123],{},"In the shader, ",[81,3110,3111],{},"positionGeometry"," now gives us the unit blade's local vertex, and per-blade attributes arrive automatically per instance. No more ",[81,3114,2237],{}," arithmetic. And instead of billboarding, each blade gets a ",[76,3117,3118],{},"random fixed yaw",", with ",[81,3121,3122],{},"DoubleSide"," making sure it's visible from both sides:",[90,3125,3127],{"className":92,"code":3126,"language":94,"meta":95,"style":95},"material.positionNode = Fn(() => {\n  const anchor = attribute('anchor', 'vec2')\n  const random = attribute('random', 'float')\n  const yaw = attribute('yaw', 'float')\n\n  const height = bladeHeight\n    .mul(mix(1, random, bladeHeightRandomness))\n    .mul(attribute('heightNoise', 'float'))\n\n  \u002F\u002F unit blade → world scale, spun around its own base by the per-blade yaw\n  const local = vec3(\n    positionGeometry.x.mul(bladeWidth),\n    positionGeometry.y.mul(height),\n    0,\n  ).toVar()\n  local.xz.assign(rotateUV(local.xz, yaw, vec2(0)))\n\n  return vec3(local.x.add(anchor.x), local.y, local.z.add(anchor.y))\n})()\n",[81,3128,3129,3149,3169,3190,3211,3215,3225,3249,3269,3273,3278,3291,3309,3326,3333,3344,3385,3389,3440],{"__ignoreMap":95},[99,3130,3131,3133,3135,3137,3139,3141,3143,3145,3147],{"class":101,"line":102},[99,3132,996],{"class":150},[99,3134,88],{"class":121},[99,3136,1019],{"class":150},[99,3138,154],{"class":121},[99,3140,1024],{"class":109},[99,3142,114],{"class":150},[99,3144,746],{"class":113},[99,3146,1031],{"class":121},[99,3148,141],{"class":113},[99,3150,3151,3153,3155,3157,3159,3161,3163,3165,3167],{"class":101,"line":144},[99,3152,147],{"class":105},[99,3154,1040],{"class":150},[99,3156,154],{"class":121},[99,3158,1045],{"class":109},[99,3160,114],{"class":150},[99,3162,2898],{"class":661},[99,3164,128],{"class":113},[99,3166,969],{"class":661},[99,3168,243],{"class":150},[99,3170,3171,3173,3176,3178,3180,3182,3184,3186,3188],{"class":101,"line":166},[99,3172,147],{"class":105},[99,3174,3175],{"class":150}," random ",[99,3177,154],{"class":121},[99,3179,1045],{"class":109},[99,3181,114],{"class":150},[99,3183,2927],{"class":661},[99,3185,128],{"class":113},[99,3187,1283],{"class":661},[99,3189,243],{"class":150},[99,3191,3192,3194,3197,3199,3201,3203,3205,3207,3209],{"class":101,"line":184},[99,3193,147],{"class":105},[99,3195,3196],{"class":150}," yaw ",[99,3198,154],{"class":121},[99,3200,1045],{"class":109},[99,3202,114],{"class":150},[99,3204,2956],{"class":661},[99,3206,128],{"class":113},[99,3208,1283],{"class":661},[99,3210,243],{"class":150},[99,3212,3213],{"class":101,"line":223},[99,3214,250],{"emptyLinePlaceholder":249},[99,3216,3217,3219,3221,3223],{"class":101,"line":246},[99,3218,147],{"class":105},[99,3220,1343],{"class":150},[99,3222,154],{"class":121},[99,3224,1348],{"class":150},[99,3226,3227,3230,3232,3234,3236,3238,3240,3242,3245,3247],{"class":101,"line":253},[99,3228,3229],{"class":121},"    .",[99,3231,1158],{"class":109},[99,3233,114],{"class":150},[99,3235,1360],{"class":109},[99,3237,114],{"class":150},[99,3239,925],{"class":206},[99,3241,128],{"class":113},[99,3243,3244],{"class":150}," random",[99,3246,128],{"class":113},[99,3248,1374],{"class":150},[99,3250,3251,3253,3255,3257,3259,3261,3263,3265,3267],{"class":101,"line":297},[99,3252,3229],{"class":121},[99,3254,1158],{"class":109},[99,3256,114],{"class":150},[99,3258,1646],{"class":109},[99,3260,114],{"class":150},[99,3262,2985],{"class":661},[99,3264,128],{"class":113},[99,3266,1283],{"class":661},[99,3268,712],{"class":150},[99,3270,3271],{"class":101,"line":333},[99,3272,250],{"emptyLinePlaceholder":249},[99,3274,3275],{"class":101,"line":356},[99,3276,3277],{"class":219},"  \u002F\u002F unit blade → world scale, spun around its own base by the per-blade yaw\n",[99,3279,3280,3282,3285,3287,3289],{"class":101,"line":361},[99,3281,147],{"class":105},[99,3283,3284],{"class":150}," local ",[99,3286,154],{"class":121},[99,3288,1128],{"class":109},[99,3290,1131],{"class":150},[99,3292,3293,3296,3298,3300,3302,3304,3307],{"class":101,"line":367},[99,3294,3295],{"class":150},"    positionGeometry",[99,3297,88],{"class":121},[99,3299,1141],{"class":150},[99,3301,88],{"class":121},[99,3303,1158],{"class":109},[99,3305,3306],{"class":150},"(bladeWidth)",[99,3308,1164],{"class":113},[99,3310,3311,3313,3315,3317,3319,3321,3324],{"class":101,"line":429},[99,3312,3295],{"class":150},[99,3314,88],{"class":121},[99,3316,1174],{"class":150},[99,3318,88],{"class":121},[99,3320,1158],{"class":109},[99,3322,3323],{"class":150},"(height)",[99,3325,1164],{"class":113},[99,3327,3328,3331],{"class":101,"line":484},[99,3329,3330],{"class":206},"    0",[99,3332,1164],{"class":113},[99,3334,3335,3338,3340,3342],{"class":101,"line":489},[99,3336,3337],{"class":150},"  )",[99,3339,88],{"class":121},[99,3341,1800],{"class":109},[99,3343,605],{"class":150},[99,3345,3346,3349,3351,3353,3355,3357,3359,3361,3364,3366,3368,3370,3373,3375,3378,3380,3382],{"class":101,"line":525},[99,3347,3348],{"class":150},"  local",[99,3350,88],{"class":121},[99,3352,1904],{"class":150},[99,3354,88],{"class":121},[99,3356,1909],{"class":109},[99,3358,114],{"class":150},[99,3360,1914],{"class":109},[99,3362,3363],{"class":150},"(local",[99,3365,88],{"class":121},[99,3367,1904],{"class":150},[99,3369,128],{"class":113},[99,3371,3372],{"class":150}," yaw",[99,3374,128],{"class":113},[99,3376,3377],{"class":109}," vec2",[99,3379,114],{"class":150},[99,3381,903],{"class":206},[99,3383,3384],{"class":150},")))\n",[99,3386,3387],{"class":101,"line":552},[99,3388,250],{"emptyLinePlaceholder":249},[99,3390,3391,3393,3395,3397,3399,3401,3403,3405,3407,3409,3411,3413,3416,3418,3420,3422,3424,3426,3429,3431,3433,3435,3437],{"class":101,"line":581},[99,3392,778],{"class":105},[99,3394,1128],{"class":109},[99,3396,3363],{"class":150},[99,3398,88],{"class":121},[99,3400,1141],{"class":150},[99,3402,88],{"class":121},[99,3404,1146],{"class":109},[99,3406,1300],{"class":150},[99,3408,88],{"class":121},[99,3410,1868],{"class":150},[99,3412,128],{"class":113},[99,3414,3415],{"class":150}," local",[99,3417,88],{"class":121},[99,3419,1174],{"class":150},[99,3421,128],{"class":113},[99,3423,3415],{"class":150},[99,3425,88],{"class":121},[99,3427,3428],{"class":150},"z",[99,3430,88],{"class":121},[99,3432,1146],{"class":109},[99,3434,1300],{"class":150},[99,3436,88],{"class":121},[99,3438,3439],{"class":150},"y))\n",[99,3441,3442,3444],{"class":101,"line":608},[99,3443,1203],{"class":113},[99,3445,1206],{"class":150},[18,3447,3448],{},"The field stops swimming: blades keep their orientation while the camera orbits, and the random yaw distribution means you always see a believable mix of face-on and edge-on blades.",[10,3450,3452],{"caption":3451},"Step 5: instanced 5-vertex blades with fixed random yaws",[14,3453],{":step":3454},"5",[62,3456,3458],{"id":3457},"a-diffuse-map-for-the-ground","A diffuse map for the ground",[18,3460,3461,3462,3465],{},"Procedural two-color noise is fine, but the real unlock is sampling an actual ",[76,3463,3464],{},"texture",": dirt paths, scorched patches, painted splat maps from your level editor, anything. The blade keeps inheriting the ground color, we just change where that color comes from.",[18,3467,3468,3469,3472,3473,3476],{},"The anchor lives in world units ",[81,3470,3471],{},"[-size\u002F2, size\u002F2]",", so remapping it to ",[81,3474,3475],{},"[0, 1]"," gives us a UV into a field-sized texture:",[90,3478,3480],{"className":92,"code":3479,"language":94,"meta":95,"style":95},"\u002F\u002F anchor ∈ [-size\u002F2, size\u002F2] → normalized field UV [0, 1]\nconst diffuseNode = texture(diffuseMap, anchor.div(size).add(0.5))\n\nmaterial.colorNode = Fn(() => {\n  \u002F\u002F sampled at the anchor only → constant across the blade (flat color per blade)\n  const base = varying(diffuseNode ?? mix(colorA, colorB, attribute('colorNoise', 'float')))\n  const ao = varying(attribute('tipness', 'float')).oneMinus().mul(shadowIntensity)\n  return mix(base, base.mul(0.35), ao)\n})()\n",[81,3481,3482,3487,3524,3528,3548,3553,3591,3627,3653],{"__ignoreMap":95},[99,3483,3484],{"class":101,"line":102},[99,3485,3486],{"class":219},"\u002F\u002F anchor ∈ [-size\u002F2, size\u002F2] → normalized field UV [0, 1]\n",[99,3488,3489,3491,3494,3496,3499,3502,3504,3507,3509,3511,3514,3516,3518,3520,3522],{"class":101,"line":144},[99,3490,842],{"class":105},[99,3492,3493],{"class":150}," diffuseNode ",[99,3495,154],{"class":121},[99,3497,3498],{"class":109}," texture",[99,3500,3501],{"class":150},"(diffuseMap",[99,3503,128],{"class":113},[99,3505,3506],{"class":150}," anchor",[99,3508,88],{"class":121},[99,3510,1888],{"class":109},[99,3512,3513],{"class":150},"(size)",[99,3515,88],{"class":121},[99,3517,1146],{"class":109},[99,3519,114],{"class":150},[99,3521,1321],{"class":206},[99,3523,712],{"class":150},[99,3525,3526],{"class":101,"line":166},[99,3527,250],{"emptyLinePlaceholder":249},[99,3529,3530,3532,3534,3536,3538,3540,3542,3544,3546],{"class":101,"line":184},[99,3531,996],{"class":150},[99,3533,88],{"class":121},[99,3535,1561],{"class":150},[99,3537,154],{"class":121},[99,3539,1024],{"class":109},[99,3541,114],{"class":150},[99,3543,746],{"class":113},[99,3545,1031],{"class":121},[99,3547,141],{"class":113},[99,3549,3550],{"class":101,"line":223},[99,3551,3552],{"class":219},"  \u002F\u002F sampled at the anchor only → constant across the blade (flat color per blade)\n",[99,3554,3555,3557,3559,3561,3563,3566,3569,3571,3573,3575,3577,3579,3581,3583,3585,3587,3589],{"class":101,"line":246},[99,3556,147],{"class":105},[99,3558,1667],{"class":150},[99,3560,154],{"class":121},[99,3562,1584],{"class":109},[99,3564,3565],{"class":150},"(diffuseNode ",[99,3567,3568],{"class":121},"??",[99,3570,1529],{"class":109},[99,3572,1532],{"class":150},[99,3574,128],{"class":113},[99,3576,1537],{"class":150},[99,3578,128],{"class":113},[99,3580,1045],{"class":109},[99,3582,114],{"class":150},[99,3584,3014],{"class":661},[99,3586,128],{"class":113},[99,3588,1283],{"class":661},[99,3590,3384],{"class":150},[99,3592,3593,3595,3597,3599,3601,3603,3605,3607,3609,3611,3613,3615,3617,3619,3621,3623,3625],{"class":101,"line":253},[99,3594,147],{"class":105},[99,3596,1682],{"class":150},[99,3598,154],{"class":121},[99,3600,1584],{"class":109},[99,3602,114],{"class":150},[99,3604,1646],{"class":109},[99,3606,114],{"class":150},[99,3608,2869],{"class":661},[99,3610,128],{"class":113},[99,3612,1283],{"class":661},[99,3614,1312],{"class":150},[99,3616,88],{"class":121},[99,3618,1692],{"class":109},[99,3620,746],{"class":150},[99,3622,88],{"class":121},[99,3624,1158],{"class":109},[99,3626,1701],{"class":150},[99,3628,3629,3631,3633,3635,3637,3639,3641,3643,3645,3647,3649,3651],{"class":101,"line":297},[99,3630,778],{"class":105},[99,3632,1529],{"class":109},[99,3634,1710],{"class":150},[99,3636,128],{"class":113},[99,3638,1715],{"class":150},[99,3640,88],{"class":121},[99,3642,1158],{"class":109},[99,3644,114],{"class":150},[99,3646,1309],{"class":206},[99,3648,138],{"class":150},[99,3650,128],{"class":113},[99,3652,1730],{"class":150},[99,3654,3655,3657],{"class":101,"line":333},[99,3656,1203],{"class":113},[99,3658,1206],{"class":150},[18,3660,3661,3662,3664,3665,3668],{},"Wrapping the sample in ",[81,3663,1752],{}," forces it to be evaluated in the ",[76,3666,3667],{},"vertex stage",", once per vertex instead of once per fragment. And since the UV comes from the anchor, which is constant per instance, every fragment of a blade gets the same flat color. That flatness is a feature: it's what gives the field its stylized, painterly look. Render the same texture on the ground plane and the blades become an extension of the terrain.",[18,3670,3671],{},"This is the map the demo below samples, a soft hand-painted mix of greens; every blade picks the single color under its anchor:",[18,3673,3674],{},[24,3675],{"alt":3676,"src":3677},"The diffuse map: a blurry, painterly mottle of greens, teals and yellow patches covering the whole field","\u002Fblog\u002Fcreating-a-grass-field-with-tsl\u002Fsplat.webp",[10,3679,3681],{"caption":3680},"Step 6: blades sampling their color from the ground texture",[14,3682],{":step":16},[57,3684,3686],{"id":3685},"the-complete-material","The Complete Material",[18,3688,3689],{},"Everything together, as a reusable function:",[90,3691,3693],{"className":92,"code":3692,"language":94,"meta":95,"style":95},"import { BufferAttribute, Color, DoubleSide, InstancedBufferAttribute, InstancedBufferGeometry, Sphere, Vector3 } from 'three'\nimport { attribute, Fn, mix, positionGeometry, rotateUV, texture, uniform, varying, vec2, vec3 } from 'three\u002Ftsl'\nimport { MeshBasicNodeMaterial } from 'three\u002Fwebgpu'\nimport { ImprovedNoise } from 'three\u002Fexamples\u002Fjsm\u002Fmath\u002FImprovedNoise.js'\n\nconst bladeWidth = uniform(0.1)\nconst bladeHeight = uniform(0.6)\nconst bladeHeightRandomness = uniform(0.6)\nconst shadowIntensity = uniform(0.5)\n\nexport function createGrass({ subdivisions, size, colorA, colorB, diffuseMap }) {\n  const geometry = createGrassGeometry(subdivisions, size) \u002F\u002F from above\n\n  const colorAUniform = uniform(new Color(colorA))\n  const colorBUniform = uniform(new Color(colorB))\n  const anchor = attribute('anchor', 'vec2')\n  const diffuseNode = diffuseMap\n    ? texture(diffuseMap, anchor.div(size).add(0.5))\n    : null\n\n  const material = new MeshBasicNodeMaterial()\n  material.side = DoubleSide\n\n  material.positionNode = Fn(() => {\n    const random = attribute('random', 'float')\n    const yaw = attribute('yaw', 'float')\n\n    const height = bladeHeight\n      .mul(mix(1, random, bladeHeightRandomness))\n      .mul(attribute('heightNoise', 'float'))\n\n    const local = vec3(\n      positionGeometry.x.mul(bladeWidth),\n      positionGeometry.y.mul(height),\n      0,\n    ).toVar()\n    local.xz.assign(rotateUV(local.xz, yaw, vec2(0)))\n\n    return vec3(local.x.add(anchor.x), local.y, local.z.add(anchor.y))\n  })()\n\n  material.colorNode = Fn(() => {\n    const base = varying(diffuseNode ?? mix(colorAUniform, colorBUniform, attribute('colorNoise', 'float')))\n    const ao = varying(attribute('tipness', 'float')).oneMinus().mul(shadowIntensity)\n    return mix(base, base.mul(0.35), ao)\n  })()\n\n  return { geometry, material }\n}\n",[81,3694,3695,3740,3794,3810,3826,3830,3846,3862,3878,3894,3898,3937,3958,3962,3982,4002,4022,4033,4062,4070,4074,4088,4101,4105,4125,4146,4166,4170,4180,4203,4223,4227,4239,4256,4272,4279,4290,4327,4331,4380,4387,4391,4412,4451,4488,4515,4522,4527,4543],{"__ignoreMap":95},[99,3696,3697,3700,3703,3705,3707,3710,3712,3715,3717,3719,3721,3723,3725,3727,3729,3732,3734,3737],{"class":101,"line":102},[99,3698,3699],{"class":105},"import",[99,3701,3702],{"class":113}," {",[99,3704,669],{"class":150},[99,3706,128],{"class":113},[99,3708,3709],{"class":150}," Color",[99,3711,128],{"class":113},[99,3713,3714],{"class":150}," DoubleSide",[99,3716,128],{"class":113},[99,3718,2905],{"class":150},[99,3720,128],{"class":113},[99,3722,2798],{"class":150},[99,3724,128],{"class":113},[99,3726,735],{"class":150},[99,3728,128],{"class":113},[99,3730,3731],{"class":150}," Vector3 ",[99,3733,1203],{"class":113},[99,3735,3736],{"class":105}," from",[99,3738,3739],{"class":661}," 'three'\n",[99,3741,3742,3744,3746,3748,3750,3752,3754,3756,3758,3761,3763,3766,3768,3770,3772,3774,3776,3778,3780,3782,3784,3787,3789,3791],{"class":101,"line":144},[99,3743,3699],{"class":105},[99,3745,3702],{"class":113},[99,3747,1045],{"class":150},[99,3749,128],{"class":113},[99,3751,1024],{"class":150},[99,3753,128],{"class":113},[99,3755,1529],{"class":150},[99,3757,128],{"class":113},[99,3759,3760],{"class":150}," positionGeometry",[99,3762,128],{"class":113},[99,3764,3765],{"class":150}," rotateUV",[99,3767,128],{"class":113},[99,3769,3498],{"class":150},[99,3771,128],{"class":113},[99,3773,850],{"class":150},[99,3775,128],{"class":113},[99,3777,1584],{"class":150},[99,3779,128],{"class":113},[99,3781,3377],{"class":150},[99,3783,128],{"class":113},[99,3785,3786],{"class":150}," vec3 ",[99,3788,1203],{"class":113},[99,3790,3736],{"class":105},[99,3792,3793],{"class":661}," 'three\u002Ftsl'\n",[99,3795,3796,3798,3800,3803,3805,3807],{"class":101,"line":166},[99,3797,3699],{"class":105},[99,3799,3702],{"class":113},[99,3801,3802],{"class":150}," MeshBasicNodeMaterial ",[99,3804,1203],{"class":113},[99,3806,3736],{"class":105},[99,3808,3809],{"class":661}," 'three\u002Fwebgpu'\n",[99,3811,3812,3814,3816,3819,3821,3823],{"class":101,"line":184},[99,3813,3699],{"class":105},[99,3815,3702],{"class":113},[99,3817,3818],{"class":150}," ImprovedNoise ",[99,3820,1203],{"class":113},[99,3822,3736],{"class":105},[99,3824,3825],{"class":661}," 'three\u002Fexamples\u002Fjsm\u002Fmath\u002FImprovedNoise.js'\n",[99,3827,3828],{"class":101,"line":223},[99,3829,250],{"emptyLinePlaceholder":249},[99,3831,3832,3834,3836,3838,3840,3842,3844],{"class":101,"line":246},[99,3833,842],{"class":105},[99,3835,845],{"class":150},[99,3837,154],{"class":121},[99,3839,850],{"class":109},[99,3841,114],{"class":150},[99,3843,855],{"class":206},[99,3845,243],{"class":150},[99,3847,3848,3850,3852,3854,3856,3858,3860],{"class":101,"line":253},[99,3849,842],{"class":105},[99,3851,864],{"class":150},[99,3853,154],{"class":121},[99,3855,850],{"class":109},[99,3857,114],{"class":150},[99,3859,873],{"class":206},[99,3861,243],{"class":150},[99,3863,3864,3866,3868,3870,3872,3874,3876],{"class":101,"line":297},[99,3865,842],{"class":105},[99,3867,1249],{"class":150},[99,3869,154],{"class":121},[99,3871,850],{"class":109},[99,3873,114],{"class":150},[99,3875,873],{"class":206},[99,3877,243],{"class":150},[99,3879,3880,3882,3884,3886,3888,3890,3892],{"class":101,"line":333},[99,3881,842],{"class":105},[99,3883,1437],{"class":150},[99,3885,154],{"class":121},[99,3887,850],{"class":109},[99,3889,114],{"class":150},[99,3891,1321],{"class":206},[99,3893,243],{"class":150},[99,3895,3896],{"class":101,"line":356},[99,3897,250],{"emptyLinePlaceholder":249},[99,3899,3900,3903,3906,3909,3912,3914,3916,3918,3920,3923,3925,3927,3929,3932,3935],{"class":101,"line":361},[99,3901,3902],{"class":105},"export",[99,3904,3905],{"class":105}," function",[99,3907,3908],{"class":109}," createGrass",[99,3910,3911],{"class":113},"({",[99,3913,281],{"class":117},[99,3915,128],{"class":113},[99,3917,131],{"class":117},[99,3919,128],{"class":113},[99,3921,3922],{"class":117}," colorA",[99,3924,128],{"class":113},[99,3926,1537],{"class":117},[99,3928,128],{"class":113},[99,3930,3931],{"class":117}," diffuseMap",[99,3933,3934],{"class":113}," })",[99,3936,141],{"class":113},[99,3938,3939,3941,3943,3945,3947,3950,3952,3955],{"class":101,"line":367},[99,3940,147],{"class":105},[99,3942,636],{"class":150},[99,3944,154],{"class":121},[99,3946,110],{"class":109},[99,3948,3949],{"class":150},"(subdivisions",[99,3951,128],{"class":113},[99,3953,3954],{"class":150}," size) ",[99,3956,3957],{"class":219},"\u002F\u002F from above\n",[99,3959,3960],{"class":101,"line":429},[99,3961,250],{"emptyLinePlaceholder":249},[99,3963,3964,3966,3969,3971,3973,3975,3977,3979],{"class":101,"line":484},[99,3965,147],{"class":105},[99,3967,3968],{"class":150}," colorAUniform ",[99,3970,154],{"class":121},[99,3972,850],{"class":109},[99,3974,114],{"class":150},[99,3976,740],{"class":194},[99,3978,3709],{"class":109},[99,3980,3981],{"class":150},"(colorA))\n",[99,3983,3984,3986,3989,3991,3993,3995,3997,3999],{"class":101,"line":489},[99,3985,147],{"class":105},[99,3987,3988],{"class":150}," colorBUniform ",[99,3990,154],{"class":121},[99,3992,850],{"class":109},[99,3994,114],{"class":150},[99,3996,740],{"class":194},[99,3998,3709],{"class":109},[99,4000,4001],{"class":150},"(colorB))\n",[99,4003,4004,4006,4008,4010,4012,4014,4016,4018,4020],{"class":101,"line":525},[99,4005,147],{"class":105},[99,4007,1040],{"class":150},[99,4009,154],{"class":121},[99,4011,1045],{"class":109},[99,4013,114],{"class":150},[99,4015,2898],{"class":661},[99,4017,128],{"class":113},[99,4019,969],{"class":661},[99,4021,243],{"class":150},[99,4023,4024,4026,4028,4030],{"class":101,"line":552},[99,4025,147],{"class":105},[99,4027,3493],{"class":150},[99,4029,154],{"class":121},[99,4031,4032],{"class":150}," diffuseMap\n",[99,4034,4035,4038,4040,4042,4044,4046,4048,4050,4052,4054,4056,4058,4060],{"class":101,"line":581},[99,4036,4037],{"class":121},"    ?",[99,4039,3498],{"class":109},[99,4041,3501],{"class":150},[99,4043,128],{"class":113},[99,4045,3506],{"class":150},[99,4047,88],{"class":121},[99,4049,1888],{"class":109},[99,4051,3513],{"class":150},[99,4053,88],{"class":121},[99,4055,1146],{"class":109},[99,4057,114],{"class":150},[99,4059,1321],{"class":206},[99,4061,712],{"class":150},[99,4063,4064,4067],{"class":101,"line":608},[99,4065,4066],{"class":121},"    :",[99,4068,4069],{"class":105}," null\n",[99,4071,4072],{"class":101,"line":614},[99,4073,250],{"emptyLinePlaceholder":249},[99,4075,4076,4078,4080,4082,4084,4086],{"class":101,"line":620},[99,4077,147],{"class":105},[99,4079,982],{"class":150},[99,4081,154],{"class":121},[99,4083,195],{"class":194},[99,4085,989],{"class":109},[99,4087,605],{"class":150},[99,4089,4090,4093,4095,4097,4099],{"class":101,"line":626},[99,4091,4092],{"class":150},"  material",[99,4094,88],{"class":121},[99,4096,1001],{"class":150},[99,4098,154],{"class":121},[99,4100,1006],{"class":150},[99,4102,4103],{"class":101,"line":631},[99,4104,250],{"emptyLinePlaceholder":249},[99,4106,4107,4109,4111,4113,4115,4117,4119,4121,4123],{"class":101,"line":648},[99,4108,4092],{"class":150},[99,4110,88],{"class":121},[99,4112,1019],{"class":150},[99,4114,154],{"class":121},[99,4116,1024],{"class":109},[99,4118,114],{"class":150},[99,4120,746],{"class":113},[99,4122,1031],{"class":121},[99,4124,141],{"class":113},[99,4126,4127,4130,4132,4134,4136,4138,4140,4142,4144],{"class":101,"line":685},[99,4128,4129],{"class":105},"    const",[99,4131,3175],{"class":150},[99,4133,154],{"class":121},[99,4135,1045],{"class":109},[99,4137,114],{"class":150},[99,4139,2927],{"class":661},[99,4141,128],{"class":113},[99,4143,1283],{"class":661},[99,4145,243],{"class":150},[99,4147,4148,4150,4152,4154,4156,4158,4160,4162,4164],{"class":101,"line":715},[99,4149,4129],{"class":105},[99,4151,3196],{"class":150},[99,4153,154],{"class":121},[99,4155,1045],{"class":109},[99,4157,114],{"class":150},[99,4159,2956],{"class":661},[99,4161,128],{"class":113},[99,4163,1283],{"class":661},[99,4165,243],{"class":150},[99,4167,4168],{"class":101,"line":721},[99,4169,250],{"emptyLinePlaceholder":249},[99,4171,4172,4174,4176,4178],{"class":101,"line":775},[99,4173,4129],{"class":105},[99,4175,1343],{"class":150},[99,4177,154],{"class":121},[99,4179,1348],{"class":150},[99,4181,4182,4185,4187,4189,4191,4193,4195,4197,4199,4201],{"class":101,"line":784},[99,4183,4184],{"class":121},"      .",[99,4186,1158],{"class":109},[99,4188,114],{"class":150},[99,4190,1360],{"class":109},[99,4192,114],{"class":150},[99,4194,925],{"class":206},[99,4196,128],{"class":113},[99,4198,3244],{"class":150},[99,4200,128],{"class":113},[99,4202,1374],{"class":150},[99,4204,4205,4207,4209,4211,4213,4215,4217,4219,4221],{"class":101,"line":2817},[99,4206,4184],{"class":121},[99,4208,1158],{"class":109},[99,4210,114],{"class":150},[99,4212,1646],{"class":109},[99,4214,114],{"class":150},[99,4216,2985],{"class":661},[99,4218,128],{"class":113},[99,4220,1283],{"class":661},[99,4222,712],{"class":150},[99,4224,4225],{"class":101,"line":2830},[99,4226,250],{"emptyLinePlaceholder":249},[99,4228,4229,4231,4233,4235,4237],{"class":101,"line":2858},[99,4230,4129],{"class":105},[99,4232,3284],{"class":150},[99,4234,154],{"class":121},[99,4236,1128],{"class":109},[99,4238,1131],{"class":150},[99,4240,4241,4244,4246,4248,4250,4252,4254],{"class":101,"line":2887},[99,4242,4243],{"class":150},"      positionGeometry",[99,4245,88],{"class":121},[99,4247,1141],{"class":150},[99,4249,88],{"class":121},[99,4251,1158],{"class":109},[99,4253,3306],{"class":150},[99,4255,1164],{"class":113},[99,4257,4258,4260,4262,4264,4266,4268,4270],{"class":101,"line":2916},[99,4259,4243],{"class":150},[99,4261,88],{"class":121},[99,4263,1174],{"class":150},[99,4265,88],{"class":121},[99,4267,1158],{"class":109},[99,4269,3323],{"class":150},[99,4271,1164],{"class":113},[99,4273,4274,4277],{"class":101,"line":2945},[99,4275,4276],{"class":206},"      0",[99,4278,1164],{"class":113},[99,4280,4281,4284,4286,4288],{"class":101,"line":2974},[99,4282,4283],{"class":150},"    )",[99,4285,88],{"class":121},[99,4287,1800],{"class":109},[99,4289,605],{"class":150},[99,4291,4292,4295,4297,4299,4301,4303,4305,4307,4309,4311,4313,4315,4317,4319,4321,4323,4325],{"class":101,"line":3003},[99,4293,4294],{"class":150},"    local",[99,4296,88],{"class":121},[99,4298,1904],{"class":150},[99,4300,88],{"class":121},[99,4302,1909],{"class":109},[99,4304,114],{"class":150},[99,4306,1914],{"class":109},[99,4308,3363],{"class":150},[99,4310,88],{"class":121},[99,4312,1904],{"class":150},[99,4314,128],{"class":113},[99,4316,3372],{"class":150},[99,4318,128],{"class":113},[99,4320,3377],{"class":109},[99,4322,114],{"class":150},[99,4324,903],{"class":206},[99,4326,3384],{"class":150},[99,4328,4329],{"class":101,"line":3032},[99,4330,250],{"emptyLinePlaceholder":249},[99,4332,4333,4336,4338,4340,4342,4344,4346,4348,4350,4352,4354,4356,4358,4360,4362,4364,4366,4368,4370,4372,4374,4376,4378],{"class":101,"line":3038},[99,4334,4335],{"class":105},"    return",[99,4337,1128],{"class":109},[99,4339,3363],{"class":150},[99,4341,88],{"class":121},[99,4343,1141],{"class":150},[99,4345,88],{"class":121},[99,4347,1146],{"class":109},[99,4349,1300],{"class":150},[99,4351,88],{"class":121},[99,4353,1868],{"class":150},[99,4355,128],{"class":113},[99,4357,3415],{"class":150},[99,4359,88],{"class":121},[99,4361,1174],{"class":150},[99,4363,128],{"class":113},[99,4365,3415],{"class":150},[99,4367,88],{"class":121},[99,4369,3428],{"class":150},[99,4371,88],{"class":121},[99,4373,1146],{"class":109},[99,4375,1300],{"class":150},[99,4377,88],{"class":121},[99,4379,3439],{"class":150},[99,4381,4382,4385],{"class":101,"line":3085},[99,4383,4384],{"class":113},"  }",[99,4386,1206],{"class":150},[99,4388,4389],{"class":101,"line":3092},[99,4390,250],{"emptyLinePlaceholder":249},[99,4392,4394,4396,4398,4400,4402,4404,4406,4408,4410],{"class":101,"line":4393},42,[99,4395,4092],{"class":150},[99,4397,88],{"class":121},[99,4399,1561],{"class":150},[99,4401,154],{"class":121},[99,4403,1024],{"class":109},[99,4405,114],{"class":150},[99,4407,746],{"class":113},[99,4409,1031],{"class":121},[99,4411,141],{"class":113},[99,4413,4415,4417,4419,4421,4423,4425,4427,4429,4432,4434,4437,4439,4441,4443,4445,4447,4449],{"class":101,"line":4414},43,[99,4416,4129],{"class":105},[99,4418,1667],{"class":150},[99,4420,154],{"class":121},[99,4422,1584],{"class":109},[99,4424,3565],{"class":150},[99,4426,3568],{"class":121},[99,4428,1529],{"class":109},[99,4430,4431],{"class":150},"(colorAUniform",[99,4433,128],{"class":113},[99,4435,4436],{"class":150}," colorBUniform",[99,4438,128],{"class":113},[99,4440,1045],{"class":109},[99,4442,114],{"class":150},[99,4444,3014],{"class":661},[99,4446,128],{"class":113},[99,4448,1283],{"class":661},[99,4450,3384],{"class":150},[99,4452,4454,4456,4458,4460,4462,4464,4466,4468,4470,4472,4474,4476,4478,4480,4482,4484,4486],{"class":101,"line":4453},44,[99,4455,4129],{"class":105},[99,4457,1682],{"class":150},[99,4459,154],{"class":121},[99,4461,1584],{"class":109},[99,4463,114],{"class":150},[99,4465,1646],{"class":109},[99,4467,114],{"class":150},[99,4469,2869],{"class":661},[99,4471,128],{"class":113},[99,4473,1283],{"class":661},[99,4475,1312],{"class":150},[99,4477,88],{"class":121},[99,4479,1692],{"class":109},[99,4481,746],{"class":150},[99,4483,88],{"class":121},[99,4485,1158],{"class":109},[99,4487,1701],{"class":150},[99,4489,4491,4493,4495,4497,4499,4501,4503,4505,4507,4509,4511,4513],{"class":101,"line":4490},45,[99,4492,4335],{"class":105},[99,4494,1529],{"class":109},[99,4496,1710],{"class":150},[99,4498,128],{"class":113},[99,4500,1715],{"class":150},[99,4502,88],{"class":121},[99,4504,1158],{"class":109},[99,4506,114],{"class":150},[99,4508,1309],{"class":206},[99,4510,138],{"class":150},[99,4512,128],{"class":113},[99,4514,1730],{"class":150},[99,4516,4518,4520],{"class":101,"line":4517},46,[99,4519,4384],{"class":113},[99,4521,1206],{"class":150},[99,4523,4525],{"class":101,"line":4524},47,[99,4526,250],{"emptyLinePlaceholder":249},[99,4528,4530,4532,4534,4537,4539,4541],{"class":101,"line":4529},48,[99,4531,778],{"class":105},[99,4533,3702],{"class":113},[99,4535,4536],{"class":150}," geometry",[99,4538,128],{"class":113},[99,4540,982],{"class":150},[99,4542,787],{"class":113},[99,4544,4546],{"class":101,"line":4545},49,[99,4547,787],{"class":113},[18,4549,4550,4551,4554,4555,4558],{},"In my game engine this lives inside a ",[32,4552],{"label":4553},"TresJS"," component, so a whole field is just ",[81,4556,4557],{},"\u003CGrass :subdivisions=\"200\" :size=\"30\" :diffuse-map=\"splatMap\" \u002F>",", but the geometry and material code is exactly what you see here.",[57,4560,4562],{"id":4561},"whats-next-wind","What's Next: Wind",[18,4564,4565,4566,4568,4569,4571,4572,4575,4576,4578,4579,4582],{},"Notice that ",[81,4567,1742],{}," attribute we sculpted into the blade, ",[81,4570,903],{}," at the base, ",[81,4573,4574],{},"0.7"," at the mid vertices, ",[81,4577,925],{}," at the tip? It has a second job. If you displace each vertex horizontally proportionally to it, the base stays planted, the middle leans, and the tip travels furthest: the blade ",[76,4580,4581],{},"bends"," instead of shearing. That's the entire foundation of grass wind, and together with a scrolling noise field and a trample system (characters flattening the grass they walk through), it's what the next post is about.",[57,4584,4586],{"id":4585},"resources","Resources",[792,4588,4589,4596,4603,4610],{},[795,4590,4591,4595],{},[43,4592,4594],{"href":45,"rel":4593},[47],"Making grass with triangles in GLSL using Three.js",", the article that introduced me to the anchor trick",[795,4597,4598,4602],{},[43,4599,4601],{"href":52,"rel":4600},[47],"Bruno Simon's folio-2025 Grass.js",", a beautiful production GLSL implementation",[795,4604,4605],{},[43,4606,4609],{"href":4607,"rel":4608},"https:\u002F\u002Fgithub.com\u002Fmrdoob\u002Fthree.js\u002Fwiki\u002FThree.js-Shading-Language",[47],"Three.js TSL documentation",[795,4611,4612,4616],{},[43,4613,4615],{"href":4614},"\u002Fblog\u002Fintro-to-tsl","Intro to TSL",", if node-based shaders are new to you",[4618,4619,4620],"style",{},"html pre.shiki code .sHfct, html code.shiki .sHfct{--shiki-default:#CA9EE6}html pre.shiki code .soURg, html code.shiki .soURg{--shiki-default:#8CAAEE;--shiki-default-font-style:italic}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 pre.shiki code .sO16o, html code.shiki .sO16o{--shiki-default:#81C8BE}html pre.shiki code .sdZ07, html code.shiki .sdZ07{--shiki-default:#C6D0F5}html pre.shiki code .sPs8m, html code.shiki .sPs8m{--shiki-default:#CA9EE6;--shiki-default-font-weight:bold}html pre.shiki code .sQqXY, html code.shiki .sQqXY{--shiki-default:#EF9F76}html pre.shiki code .sWSDe, html code.shiki .sWSDe{--shiki-default:#949CBB;--shiki-default-font-style:italic}html pre.shiki code .sNt6F, html code.shiki .sNt6F{--shiki-default:#A6D189}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);}",{"title":95,"searchDepth":144,"depth":144,"links":4622},[4623,4629,4630,4635,4636,4637],{"id":59,"depth":144,"text":60,"children":4624},[4625,4626,4627,4628],{"id":64,"depth":166,"text":65},{"id":1225,"depth":166,"text":1226},{"id":1417,"depth":166,"text":1418},{"id":1768,"depth":166,"text":1769},{"id":1953,"depth":144,"text":1954},{"id":2001,"depth":144,"text":2002,"children":4631},[4632,4633,4634],{"id":2005,"depth":166,"text":2006},{"id":3104,"depth":166,"text":3105},{"id":3457,"depth":166,"text":3458},{"id":3685,"depth":144,"text":3686},{"id":4561,"depth":144,"text":4562},{"id":4585,"depth":144,"text":4586},"2026-07-14","Rendering thousands of stylized grass blades with Three.js TSL, starting from a naive triangle-per-blade approach and rebuilding it with instancing and a diffuse map.","md",{"readingTime":4642},{"text":4643,"minutes":4644,"time":4645,"words":4646},"12 min read",11.97,718200,2394,{"title":5,"description":4639},"\u002Fblog\u002Fcreating-a-grass-field-with-tsl",{"title":5,"description":4639},{"loc":4648},"published","blog\u002Fcreating-a-grass-field-with-tsl",[4654,4655,4656],"webgpu","tsl","shaders","\u002Fblog\u002Fcreating-a-grass-field-with-tsl\u002Fcreating-a-grass-field-with-tsl.jpg","L2LrfdbIkp4f_RKCw3BOceo0Zsge5wty0lsekxw896E",1784190707320]