GraphicsComplex: a direct pipeline to your GPU. Part 1
Ever wondered what happens when you push thousands of vertices into a graphics engine? GraphicsComplex can write directly into GPU buffers with no overhead. Let's see how far we can take it.
Most high-level plotting functions hide the machinery behind a convenient interface. You call ListPlot3D, you get a surface — magic. But what if you want to control every vertex, every triangle, every color? What if you need to push 10³ animated points at least at 30 fps?
This is where GraphicsComplex comes in...
Please take everything with a grain of salt, since this is only applicable to WLJS Notebook, but not to Wolfram Research products. Wolfram Mathematica might still heavily rely on a CPU pipeline.
GraphicsComplex mimics the interface of a low-level graphics API like OpenGL, WebGL, Vulkan, and others, since the GPU loves to eat vertices and polygon faces as separate dishes. Under the hood, it writes your vertex data directly into WebGL buffer attributes — later automatically synchronized with GPU memory buffers. The list of coordinates becomes a typed array Float32Array.
No intermediate conversions (at least for 3D), no SVG paths, no DOM nodes per point. It's as close to the metal as you can get from a digital notebook nowadays!
Packed and Numeric Arrays
Numeric lists or tensors in Wolfram Language generated by Table, Map can become so-called packed arrays. For example, a manually constructed list is stored in memory as an array of pointers to some objects:
%7B1%2C2%2C3%7D%20%2F%2F%20Developer%60PackedArrayQ False However, if you generate it by a function with consistent types (only reals or only integers):
Table%5Bi%2C%20%7Bi%2C10%7D%5D%20%2F%2F%20Developer%60PackedArrayQ False Oops! Let's try bigger:
Table%5Bi%2C%20%7Bi%2C1000%7D%5D%20%2F%2F%20Developer%60PackedArrayQ True Here we go. In this case, this list is stored as a whole thing linearly in memory, which makes it especially efficient for storing data and processing.
In order to deoptimize it, we just need to break its homogeneity:
Table%5BIf%5Bi%3D%3D1000%2C%20N%5Bi%5D%2C%20i%5D%2C%20%7Bi%2C1000%7D%5D%20%2F%2F%20Developer%60PackedArrayQ False Now we can compare it in size and time required for processing the data:
packed = Table[i, {i,1000}];
unpacked = Table[If[i==1000, N[i], i], {i,1000}]; Quantity[ByteCount[packed], "Bytes"]//UnitSimplify
Quantity[ByteCount[unpacked], "Bytes"]//UnitSimplify (*VB[*)(Quantity[1037/128, "Kibibytes"])(*,*)(*"1:eJxTTMoPSmNkYGAoZgESHvk5KWnMIB4vkAjLTC13SU3OL0osyS8K5gCKBJYm5pVkllSmMYHUgESCEksy8/MSczJ5gfozG4AiwZxAwjszKTOpsiS1GAApIxcN"*)(*]VB*) (*VB[*)(Quantity[189, "Kibibits"])(*,*)(*"1:eJxTTMoPSmNkYGAoZgESHvk5KWnMIB4vkAjLTC13SU3OL0osyS8K5gCKBJYm5pVkllRm7gVywCLemUlAWFIMAFkpEj0="*)(*]VB*) Now the processing time with a simple function:
Quantity[Map[(# #)&, packed]; // AbsoluteTiming // First, "Seconds"]//UnitSimplify
Quantity[Map[(# #)&, unpacked]; // AbsoluteTiming // First, "Seconds"]//UnitSimplify (*VB[*)(Quantity[3.279, "Milliseconds"])(*,*)(*"1:eJxTTMoPSmNkYGAoZgESHvk5KWnMIB4vkAjLTC13SU3OL0osyS8K5gCKBJYm5pVkllQWcayRiUqx5nII5gGK+mbm5GQWAxXmpRQDAP14FVU="*)(*]VB*) (*VB[*)(Quantity[0.375, "Milliseconds"])(*,*)(*"1:eJxTTMoPSmNkYGAoZgESHvk5KWnMIB4vkAjLTC13SU3OL0osyS8K5gCKBJYm5pVkllQWMYDBDftgHiDlm5mTk1kMVJiXUgwA5LgUWQ=="*)(*]VB*) A cool thing, you can force Wolfram Kernel to keep your array as packed one with a given type and never unpack it using NumericArray wrapper:
packed // ByteCount
NumericArray[unpacked, "SignedInteger64"] // ByteCount 8296 8296 NumericArray gives you granular control over how your data is stored! This is amazing. If you know the boundaries of your data, it can save you a lot of resources:
NumericArray[unpacked, "SignedInteger16"] // ByteCount 2152 In WLJS, if you pass such an array explicitly or implicitly as a packed array to GraphicsComplex, it will be copied akin to memcpy directly to the corresponding typed array of JavaScript on our frontend. For example, "SignedInteger16" becomes an Int16Array view to ArrayBuffer. This is a key ingredient to performance and pays off extremely for Image and Raster primitives as well.
GraphicsComplex
The structure is simple:
GraphicsComplex[{pt1, pt2, ...}, primitives, attributes] Coordinates given as integers i in primitives are replaced by the actual vertex pti. This separation of geometry data from topology is exactly how every modern graphics API works.
Starting simple: 2D
Let's start with a single triangle. Three vertices, one polygon:
Sequence[GraphicsComplex[{{-1,-1}, {1,-1}, {1,1}}, Polygon[{1,2,3}], VertexColors->{{1,1,0}, {0,1,1}, {0,1,1}}], "Controls"->False, ImageSize->250] // Graphics (*VB[*)(Graphics[GraphicsComplex[{{-1, -1}, {1, -1}, {1, 1}}, Polygon[{1, 2, 3}], VertexColors -> {{1, 1, 0}, {0, 1, 1}, {0, 1, 1}}], "Controls" -> False, ImageSize -> 250])(*,*)(*"1:eJyFUMsKwkAM3PpABcFv8FsKPsCDtOC9aloX0qZsKlQ/3YsmXdC2KO5hyMxskiHLI0VpYIzhkcCG8JwOlU0F1i4pL/bEXlm0lJDyEqH2hjbuLFfpoM3sU14DPV2X/dQVfJyJwJ7wllHRXeM/aq9V/TMluiLwXIoDuArqkJAc9yJ+mdSA0Uld07zN4K/ZyRDr7UIqKkfIPBaySpChF3QmxTZPMojtHexD2AufbFR9"*)(*]VB*) That gradient isn't computed on the CPU — the vertex colors are interpolated by the GPU's rasterizer, just like in any shader pipeline. You can do the same with points:
Sequence[GraphicsComplex[{{-1,-1}, {1,-1}, {1,1}}, Point[{1,2,3}], VertexColors->{{1,0,0}, {0,1,0}, {0,0,1}}], "Controls"->False, ImageSize->250] // Graphics (*VB[*)(Graphics[GraphicsComplex[{{-1, -1}, {1, -1}, {1, 1}}, Point[{1, 2, 3}], VertexColors -> {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}], "Controls" -> False, ImageSize -> 250])(*,*)(*"1:eJx1UMsKwjAQjC9QEPwGv6XgAzxIC96jprqQdks2QvHTvehuQ7UNNodhZ/Y1m/UZ03yklKIpww7tNZ8ImzNsna7ucKGgrDpKgkVlTR0S0ngA8vm4y+DNr4FIl2WDukCwM2M4IpS+vySUSSeI/puRPqyhJQcn47ypE7ToKDL4Z5JqIUqq1s1g8lvR95DJzyVYeoeWmis22pKJjC442Bf6ZjJ4Gngx+wB+nFOa"*)(*]VB*) Line and Arrow are also supported, though they don't use hardware acceleration. Polygon and Point are where the GPU really shines.
Automatic triangulation
As you know, for GPU only triangles exist. Therefore if you provide something to Polygon with a face containing more than 3 indices it might use some power of CPU to triangulate the mesh:
Graphics[{
GraphicsComplex[CirclePoints[0.7, 50]//N, {
LightBlue, Polygon[Range[1,50]], Red, Point[Range[1,50]]
}]
}, PlotRange->{{-1,1}, {-1,1}}, "Controls"->False, ImageSize->250] (*VB[*)(FrontEndRef["1f10b584-38e6-4de4-8d9f-401b5216aabd"])(*,*)(*"1:eJxTTMoPSmNkYGAoZgESHvk5KRCeEJBwK8rPK3HNS3GtSE0uLUlMykkNVgEKG6YZGiSZWpjoGlukmumapKSa6FqkWKbpmhgYJpkaGZolJialAAB9hRWd"*)(*]VB*) In this example, we have a polygon of 50 vertices. This might not be optimal for the case if indices change over time (animated), since then the data will be unpacked and reprocessed by JavaScript on the frontend. But for static graphics, this is the way to go.
Vertex Colors
For best performance, provide VertexColors as a plain {{r,g,b}, ...} list rather than symbolic colors like Red or Hue[...]. This avoids the overhead of converting color expressions on every frame:
v = {{1, -1}, {1, 1}, {-1, -1}};
Graphics[{GraphicsComplex[v, Polygon[{1,2,3}], VertexColors -> {Red, Blue, Green}]}] VertexColors -> {{r1,g1,b1}, {r2,g2,b2}, ...} img = ImageResize[ExampleData[{"TestImage", "Lena"}], Scaled[1/4]];
Graphics[{
Texture[img],
GraphicsComplex[
{{-1, -1}, {1, -1}, 0.2 {1, 1}, {-1, 1}},
Polygon[{1, 2, 3, 4}],
VertexTextureCoordinates -> {{0, 1}, {0, 0}, {1, 0}, {1, 1}}
]
}, "Controls"->False, ImageSize->250] (*VB[*)(FrontEndRef["f1cceb97-e8a8-44da-bf4a-49e88a528836"])(*,*)(*"1:eJxTTMoPSmNkYGAoZgESHvk5KRCeEJBwK8rPK3HNS3GtSE0uLUlMykkNVgEKpxkmJ6cmWZrrplokWuiamKQk6ialmSTqmlimWlgkmhpZWBibAQCYohYa"*)(*]VB*) Or let the engine figure out the mapping automatically:
Graphics[{
Texture[Array[BitXor, {128,128}]//Rescale//Image],
GraphicsComplex[RandomReal[{-1,1}, {3,2}], Polygon[{1,2,3}]]
}, Axes->True, "Controls"->False, ImageSize->250] How to update things
The magic happens, when you mutate the underlying data. You see, originally GraphicsComplex uses WebGL to draw things on a canvas, and then bakes it into a static PNG image freeing the resources used. This allows most static plots to be compact in memory and not hold GPU resources. However, when the data changes, it effectively deoptimizes this backed image back to canvas and reallocates the required resources preparing for the next updates. It allows us in WLJS to have the advantages of both worlds: just an image if graph is static and WebGL canvas element if the data changes. Let's explore it!
Fixed indices
Since the vertex buffer is just data, we can mutate it on every frame. Use Offload wrapper to prevent Wolfram Kernel from evaluating the data in place.
v1 = {{1, -1}, {1, 1}, {-1, -1}} // N;
Graphics[{GraphicsComplex[Offload[v1], Polygon[{1,2,3}]]}, "Controls"->False, ImageSize->250]
EventHandler[InputRange[0,2.0Pi,0.1,0], (v1 =(*GB[*){{Cos[#1]+Sin[#1](*|*),(*|*)-Cos[#1]+Sin[#1]}(*||*),(*||*){Cos[#1]-Sin[#1](*|*),(*|*)Cos[#1]+Sin[#1]}(*||*),(*||*){-Cos[#1]+Sin[#1](*|*),(*|*)-Cos[#1]-Sin[#1]}}(*||*)(*1:eJxTTMoPSmNkYGAo5gUSYZmp5S6pyflFiSX5RcFsQBHfxJKizAoAs04KOA==*)(*]GB*)//N)&] (*VB[*)(Graphics[{GraphicsComplex[Offload[v1], Polygon[{1, 2, 3}]]}, "Controls" -> False, ImageSize -> 250])(*,*)(*"1:eJxTTMoPSmNkYGAoZgESHvk5KWnMIB4HkHAvSizIyEwuRsj7ZBaXpDGBePxI8s75uQU5qRUQZexAwj8tLSc/MaUYpLLMECEekJ9TmZ6fB7EBZl4mSDoTpDQTJA4xHiQZVJqTGgxyh3N+XklRfk5xMSuQ45aYU5yKqqiYE8jwzE1MTw3OrErN/AXkAQDdUC3H"*)(*]VB*) (*VB[*)(EventObject[<|"Id" -> "84009046-f464-40b2-9c86-818ab0dbe8cf", "Initial" -> 0, "View" -> "966fb748-aa0f-4a8a-9141-80f9fc89502f"|>])(*,*)(*"1:eJxTTMoPSmNkYGAoZgESHvk5KRCeEJBwK8rPK3HNS3GtSE0uLUlMykkNVgEKW5qZpSWZm1joJiYapOmaJFok6loamhjqWhikWaYlW1iaGhilAQCD0hWG"*)(*]VB*) However, this is quite boring. Let's add some colors:
Graphics[{GraphicsComplex[Offload[v1], Polygon[{1,2,3}], VertexColors -> {{1,0,0}, {0,1,0}, {0,0,1}}]}, "Controls"->False, ImageSize->250] (*VB[*)(Graphics[{GraphicsComplex[Offload[v1], Polygon[{1, 2, 3}], VertexColors -> {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}]}, "Controls" -> False, ImageSize -> 250])(*,*)(*"1:eJx9kF0KwjAQhNOqqCB4Bo/gGQL+gKC04Hu0SQ1suyVbpXp0n9xYam0RX4ad+TbDksUJIxMIIWjIskFIzMC7CcvaqeJiz9TynaWy5vMvLjErQFf12phlbwygSijk+bZs8wPCPcW8bmj6rMfWr1qfm7CB0RU0zXg4alfqSiKgo+7bX02ikR5858E/+Nno3hD7v5CYlw6BaMRmpYB079ApD9tMpTq2D22f7F5S1kNS"*)(*]VB*) Here is a catch:
VertexColors must be provided as triples of RGB colors or RGBA, otherwise it may throw an error upon update.
If you used Hue or something else, it is easy to convert them with
List@@RGBColor@Hue[0.1] %7B1.%60%2C0.6000000000000001%60%2C0.%60%7D Let's update colors as well:
v11 = {{1, -1}, {1, 1}, {-1, -1}} // N;
cols1 = {{1,0,0}, {0,1,0}, {0,0,1}};
Graphics[{GraphicsComplex[Offload[v11], Polygon[{1,2,3}], VertexColors->Offload[cols1]]}, "Controls"->False, ImageSize->250]
EventHandler[InputRange[0,2.0Pi,0.1,0], (
v11 =(*GB[*){{Cos[#1]+Sin[#1](*|*),(*|*)-Cos[#1]+Sin[#1]}(*||*),(*||*){Cos[#1]-Sin[#1](*|*),(*|*)Cos[#1]+Sin[#1]}(*||*),(*||*){-Cos[#1]+Sin[#1](*|*),(*|*)-Cos[#1]-Sin[#1]}}(*||*)(*1:eJxTTMoPSmNkYGAo5gUSYZmp5S6pyflFiSX5RcFsQBHfxJKizAoAs04KOA==*)(*]GB*) // N;
cols1 = Table[List@@Blend[{Red, Green, Blue, Red}, Mod[#1/(2Pi) + i/3.0, 1.0]], {i, 0, 2}] // N;
)&] (*VB[*)(Graphics[{GraphicsComplex[Offload[v11], Polygon[{1, 2, 3}], VertexColors -> Offload[cols1]]}, "Controls" -> False, ImageSize -> 250])(*,*)(*"1:eJxlj90KgkAQhc2KCoKeoUfYZxD6gaBQ6H7TWV0YHdmxsB69q2YTsZ+bw5n9zpxl1heKzSgIAp6I7AgzM/bTXGTrdF3YlAd+sNx0fPXBIyprhLaLzUSOxiDpjH3yptQAToT3nKquoi+0HtvQi383YQ/jKwIvxZzBNdBGhOT4/5ep+JSQ1fdm4k+IqGqcsHdoo5Hhp34hZl/qHBL7APuU6QUF5TvW"*)(*]VB*) (*VB[*)(EventObject[<|"Id" -> "40171bb3-8b3b-44d6-a46e-8aebb08bf700", "Initial" -> 0, "View" -> "854d1fb2-58ef-4a77-ad4f-1a1189061c10"|>])(*,*)(*"1:eJxTTMoPSmNkYGAoZgESHvk5KRCeEJBwK8rPK3HNS3GtSE0uLUlMykkNVgEKW5iapBimJRnpmlqkpumaJJqb6yammKTpGiYaGlpYGpgZJhsaAACGIBVt"*)(*]VB*) It might be a better strategy to pack VertexColors together with v1 symbol. Otherwise an update of v1 triggers redraw, while cols1 update will only be taken into account on the next redraw. And it will still be a packed tensor on a larger scale.
Going bigger
Let's try more vertices:
vert21 = CirclePoints[0.7, 50]//N;
f = (*VB[*)(Uncompress["1:eJxTTMoPSmNkYGAo5gASbqV5ySWZ+XlpTCARFiARnlmSgeD5ZBaXQHjMQCI4taQYpLUAYgBIQXBOfkkmiIepqiSNASTEAyQck4rzc0pLUkMyc1MRhgfklBZDzUOxMJMBbh4rkABpKi76s/LjJd+kAntU7RCHgG3NzEOTQjUgkwvFkS75JVitBolB7GdG1f4fCDJByqB+Q3OeMRg8tifBQXxEOQjkFkgAY3MQG9xBAJbbXNw="])(*,*)(*"1:eJxTTMoPSmNmYGAo5gUSYZmp5S6pyflFiSX5RcEcQBHP5Py8zKrUlEwBRgaGNCaQQhYgEVSakxrMCmT4JCal5gSzAVlJqSVATQAC1BMO"*)(*]VB*);
Graphics[{
GraphicsComplex[vert21//Offload, {
LightBlue, Polygon[Range[1,50]], Red, Point[Range[1,50]]
}],
EventHandler[AnimationFrameListener[vert21//Offload],
Function[Null, vert21 = f /@ vert21]
]
}, PlotRange->{{-1,1}, {-1,1}}] (*VB[*)(FrontEndRef["85c4db76-17a6-497f-a355-4c11ef94d3ec"])(*,*)(*"1:eJxTTMoPSmNkYGAoZgESHvk5KRCeEJBwK8rPK3HNS3GtSE0uLUlMykkNVgEKW5gmm6QkmZvpGponmumaWJqn6SYam5rqmiQbGqamWZqkGKcmAwCAeBWx"*)(*]VB*) Here we animate things using AnimatingFrameListener as a trigger. This symbol simply subscribes to the repaint cycle of a window (OS or browser) and requires vert2 to be updated before reloading. This has 2 advantages:
- animation goes as fast as possible;
- the next cycle starts only after the previous one is fully finished;
- update is in sync with GPU / window / OS refresh.
Now what if we push it further and animate 2500 dancing points?
This demo is based on the original work of Simon Woods "Dancing with friends and enemies: boids' swarm intelligence".
ClearAll[handler, n, r, f, s, x, p , q]
n = 10000/4;
r := RandomInteger[{1, n}];
f := (#/(.01 + Sqrt[#.#])) & /@ (x[[#]] - x) &;
s := With[{r1 = r}, p[[r1]] = r; q[[r1]] = r];
x = RandomReal[{-1, 1}, {n, 2}];
ox = x;
{p, q} = RandomInteger[{1, n}, {2, n}];
x = 0.995 x + 0.02 f[p] - 0.01 f[q];
colors = MapThread[(List@@(RGBColor[Hue[Norm[#1-#2] 20.0]]))&, {x,ox}];
ox = x;
data = {x,colors}; Graphics[{
PointSize[0.007], GraphicsComplex[
data[[1]]//Offload, Point[Range[n]], VertexColors->Offload[data[[2]]]
],
EventHandler[AnimationFrameListener[data // Offload], With[{},
x = 0.995 x + 0.02 f[p] - 0.01 f[q];
colors = MapThread[(List@@(RGBColor[Hue[Norm[#1-#2] 20.0]]))&, {x,ox}];
ox = x;
If[r < 100, s];
data = {x,colors};
]&]
},
PlotRange -> {{-2,2}, {-2,2}}, TransitionType->None,
Background->Black, "Controls"->False, ImageSize->300
] We did exactly what we discussed regarding packing colors and vertices to the same symbol. This also requires less data transfer calls. You can find, that the symbol itself is not packed array, but its parts are.
data%5B%5B1%5D%5D%20%2F%2F%20Developer%60PackedArrayQ True Each frame, the kernel computes new positions and colors, then Offload ships the raw arrays straight into the GPU buffers. The browser never touches individual point objects — it's all bulk buffer updates.
Dynamic topology: updating everything
We start from the indices and try to change it in real-time:
ind1 = Range[1,50];
Graphics[{
GraphicsComplex[CirclePoints[0.7, 50]//N, {
LightBlue, Polygon[ind1//Offload], Red, Point[ind1//Offload]
}]
}, PlotRange->{{-1,1}, {-1,1}}]
EventHandler[InputRange[3,50,1,50], Function[p, ind1 = Range[1,p]]] (*VB[*)(Graphics[{GraphicsComplex[{{0.043953363670519455, -0.69861870989979}, {0.1311669202100073, -0.6876010755100821}, {0.21631189606246323, -0.6657395614066075}, {0.298045504095551, -0.6333789367262135}, {0.3750787564852977, -0.5910295478514105}, {0.4461967928240828, -0.5393592699430524}, {0.5102780391949883, -0.479182974150082}, {0.5663118960624633, -0.4114496766047309}, {0.6134146760307044, -0.3372275718712005}, {0.650843540121776, -0.25768718687927444}, {0.6780082127900419, -0.17408292101539824}, {0.6944802909201343, -0.08773326349501281}, {0.6999999999999998, 1.554312234475219*^-16}, {0.6944802909201344, 0.08773326349501313}, {0.6780082127900418, 0.17408292101539852}, {0.6508435401217758, 0.2576871868792747}, {0.6134146760307043, 0.3372275718712008}, {0.5663118960624631, 0.41144967660473125}, {0.5102780391949879, 0.47918297415008215}, {0.44619679282408253, 0.5393592699430527}, {0.3750787564852974, 0.5910295478514107}, {0.2980455040955507, 0.6333789367262136}, {0.21631189606246293, 0.6657395614066076}, {0.131166920210007, 0.6876010755100821}, {0.043953363670519156, 0.6986187098997901}, {-0.04395336367051953, 0.69861870989979}, {-0.1311669202100074, 0.6876010755100821}, {-0.21631189606246348, 0.6657395614066074}, {-0.2980455040955509, 0.6333789367262136}, {-0.3750787564852977, 0.5910295478514105}, {-0.4461967928240829, 0.5393592699430523}, {-0.5102780391949882, 0.479182974150082}, {-0.5663118960624633, 0.4114496766047309}, {-0.6134146760307047, 0.33722757187120034}, {-0.650843540121776, 0.2576871868792744}, {-0.678008212790042, 0.17408292101539807}, {-0.6944802909201345, 0.08773326349501255}, {-0.6999999999999998, -2.2513717095472913*^-16}, {-0.6944802909201343, -0.08773326349501333}, {-0.6780082127900418, -0.17408292101539852}, {-0.6508435401217758, -0.25768718687927483}, {-0.6134146760307042, -0.337227571871201}, {-0.5663118960624631, -0.41144967660473125}, {-0.5102780391949879, -0.4791829741500823}, {-0.44619679282408264, -0.5393592699430526}, {-0.3750787564852973, -0.5910295478514107}, {-0.2980455040955505, -0.6333789367262137}, {-0.21631189606246273, -0.6657395614066076}, {-0.13116692021000664, -0.6876010755100821}, {-0.04395336367051923, -0.6986187098997901}}, {RGBColor[0.87, 0.94, 1], Polygon[Offload[ind1]], RGBColor[1, 0, 0], Point[Offload[ind1]]}]}, PlotRange -> {{-1, 1}, {-1, 1}}])(*,*)(*"1:eJx9lFFIU2EUx+daLsPU1KiIMFdNiIq0HnRoBzTcCNdkLkh6kNl27cJtd2wTsgz0OVhE2pMQthhpUJZllMYJoh5sD8tVC4vY1jYxpSAMepptO2csibpwv8t3z/3+55z/7/tudbdsFgoUCoVblR4MsmQTlJnZhvSgd1mdZ8Uz7ny8TXR7KF76R7xFPueU7OftmUBd+s48vx5R15QM+iE1vHxjy+kk7mk0RSpmn0OgY+BXSJHEcN/Y5I6J15Ac7d/0zJjAmfnD7gMVIYh8FBZ8x+II76stxws+wL6nXYGpxRiqRw6l+r2fYLdd2+VojuLmh60jdZYIGK/dL/s+/hnHr+7cpauKQSLeVFh7ch5vFXqDW0e/gOlF1Ypm+B3qOlfePgrF4cpYqfDjUhDfGC7rjz5IwLa51u5Owys0prTTBxuSMDRwcfFe7xTahcyVhOxD8Dee4PgdikOQ12toPdSz/hDpg4/zWyg/3Ob6lqg+KOH626l+UHJ/WuoPdNz/fuofHrM/UfIHAuzfAvkH29f6C8j+r5L/8JPmyDyghr7H3PcR0sME601TPszlYx7IPKCI6kUN11tG/SDzAOaBzAP85Ae2sR/MA73s1xz5ieXsp4n8xkH2m3lgqnxZV9wwMcO88AnzYh7IPJB54HXmfZPzn+L9wDyQeSDzwA7eTyruby/vt3run3ngJPsT4/36kv1jHli81l+cZf+ZBwqqNWdqXe7MmfXNLbIku1xhn7NpY3gJXJV39dELld9AzJxCOorq9NAuS309siP/wiQIkmy1ZTVFh632b82sgqjIDbR0fVZLdHj+o6TM1WrulezuoswKSfaYrY4eez6W/zfkZuJq+qKy//H+NzXK7mw="*)(*]VB*) (*VB[*)(EventObject[<|"Id" -> "13f5c9aa-3cb6-4352-928f-fad3ee955e56", "Initial" -> 50, "View" -> "cfa0920e-43e6-47d5-9bdb-61cd85e0bfd5"|>])(*,*)(*"1:eJxTTMoPSmNkYGAoZgESHvk5KRCeEJBwK8rPK3HNS3GtSE0uLUlMykkNVgEKJ6clGlgaGaTqmhinmumamKeY6lompSTpmhkmp1iYphokpaWYAgCLihY1"*)(*]VB*) When you need to change vertices and triangles, things get trickier. The index buffer and vertex buffer update independently, so you can get visual glitches if they go out of sync. That's what "VertexFence" is for — it creates an update barrier ensuring primitives only read vertex data after it has been fully written.
ind2 = Range[1,50];
vert2 = CirclePoints[0.7, 50]//N;
Graphics[{
GraphicsComplex[vert2//Offload, {
LightBlue, Polygon[ind2//Offload], Red, Point[ind2//Offload]
}, "VertexFence"->True]
}, PlotRange->{{-1,1}, {-1,1}}]
EventHandler[InputRange[3,50,1,50], Function[p,
ind2 = Range[1,p];
vert2 = CirclePoints[0.7, p]//N;
]] (*VB[*)(Graphics[{GraphicsComplex[Offload[vert2], {RGBColor[0.87, 0.94, 1], Polygon[Offload[ind2]], RGBColor[1, 0, 0], Point[Offload[ind2]]}, "VertexFence" -> True]}, PlotRange -> {{-1, 1}, {-1, 1}}])(*,*)(*"1:eJxTTMoPSmNkYGAoZgESHvk5KWlMIB4HkHAvSizIyEwuRsj7ZBaXpDGDePxI8s75uQU5qRUQZexAwj8tLSc/MaWYFcguSy0qMUpjwTQBZEOQu5Nzfk5+UdH1xQW2XNdf2xeJrHN/WCXyzj4TZBjCxID8nMr0/DxMK0BmZualGGGaCTYhkwFGQLSygs3KzCvBYxITzK1BpTmpwdxARhjQD6kVbql5yalgiZCi0lRUZcWcIINz8kuCEvPSkeTA3kXhZf4HAojvcIgDAOuKXhg="*)(*]VB*) (*VB[*)(EventObject[<|"Id" -> "ccdbe531-3107-4672-aaeb-6a6002e23fbd", "Initial" -> 50, "View" -> "769c75b0-63b0-4856-a595-6e03121beed5"|>])(*,*)(*"1:eJxTTMoPSmNkYGAoZgESHvk5KRCeEJBwK8rPK3HNS3GtSE0uLUlMykkNVgEKm5tZJpubJhnomhkDCRMLUzPdRFNLU12zVANjQyPDpNTUFFMAdLwVFw=="*)(*]VB*) You don't need to worry about GPU buffer management. If the data does not fit, WLJS reallocates a bigger buffer (2x the requested size) and lets you use it automatically.
Going Bigger
Here's a 2D mesh with orbiting holes that dynamically removes and adds triangles, mutating indices, colors, and vertices as is usually done in games or 3D software — a full update cycle:
(* Grid dimensions *)
nx = 50; ny = 50;
xs = Subdivide[-1., 1., nx - 1];
ys = Subdivide[-1., 1., ny - 1];
allIndices = Flatten[Table[
With[{v = (i - 1) ny + j},
{{v, v + 1, v + ny + 1}, {v, v + ny + 1, v + ny}}
],
{i, nx - 1}, {j, ny - 1}
], 2];
restXY = N @ Flatten[Table[{x, y}, {x, xs}, {y, ys}], 1];
nVerts = Length[restXY];
restX = restXY[[All, 1]];
restY = restXY[[All, 2]];
(* Pre-extract per-triangle vertex columns *)
tri1 = allIndices[[All, 1]];
tri2 = allIndices[[All, 2]];
tri3 = allIndices[[All, 3]];
generateFrame2D[t_] := Module[
{vx, vy, holes, holeR = 0.22, insideAny,
dx, dy, dist, inH, safe, triKeep, verts},
(* Ripple distortion *)
vx = restX + 0.04 Sin[6 restY - 3 t];
vy = restY + 0.04 Sin[6 restX + 2 t];
holes = {{0.55 Cos[t], 0.55 Sin[t]},
{0.55 Cos[t + Pi], 0.55 Sin[t + Pi]}};
insideAny = ConstantArray[0, nVerts];
Do[
dx = vx - h[[1]];
dy = vy - h[[2]];
dist = Sqrt[dx^2 + dy^2];
inH = UnitStep[holeR - dist];
insideAny = Clip[insideAny + inH, {0, 1}];
(* Project inside vertices onto the hole boundary circle *)
safe = dist + 0.001; (* avoid division by zero *)
vx = vx (1 - inH) + inH (h[[1]] + holeR dx / safe);
vy = vy (1 - inH) + inH (h[[2]] + holeR dy / safe);
, {h, holes}];
(* Only remove triangles where ALL 3 vertices are inside *)
triKeep = 1 - insideAny[[tri1]] insideAny[[tri2]] insideAny[[tri3]];
verts = Transpose[{vx, vy}];
{NumericArray[Pick[allIndices, triKeep, 1], "UnsignedInteger16"], {NumericArray[verts, "Real32"],
NumericArray[Map[With[{s = Rescale[Sqrt[#[[1]]^2 + #[[2]]^2], {0, 1.4}]},
List @@ RGBColor[Hue[Mod[s + 0.2 t, 1.], 0.8, 0.95]]] &, verts], "Real32"]}}
];
{indices, complex2D} = generateFrame2D[0.];
time = AbsoluteTime[];
delta;
Graphics[{
EdgeForm[],
GraphicsComplex[
Offload[complex2D[[1]]],
{Polygon[Offload[indices]]},
VertexColors -> Offload[complex2D[[2]]],
"VertexFence"->True
],
EventHandler[AnimationFrameListener[complex2D // Offload],
Function[Null, {indices, complex2D} = generateFrame2D[AbsoluteTime[]]; delta = AbsoluteTime[]-time; time = AbsoluteTime[]]
]
}, PlotRange -> {{-1.1, 1.1}, {-1.1, 1.1}}] Of course, we can't just remove triangles from the given positions. In this particular example, it is done by subdividing the edges into much smaller triangles, avoiding sharp edges.
In addition, we forced NumericArray on the generated data, which gives us a 20-30% boost.
We hope you can find many great applications for this primitive we implemented. See you in the next part, where we will explore the 3D version of it.