To render live geometry, data sources will ingest your data and pack it into GPU buffers. You then pass them as props to one or more geometry layers. Each renders a particular kind of geometry in bulk: points, lines, labels, surfaces, etc.
By separating sources and layers, you can mix them à la carte. E.g. to draw multiple representations of the same data set.
The input can be either an array-of-structs or struct-of-arrays layout in JS, depending on which data component is used and how.
The output is a struct-of-arrays on the GPU, with a handle for each field. Except for StructData
which is array-of-structs.
You can also just supply a constant value as a prop. This is distinguished by the singular (e.g. size
, color
) instead of a plural (sizes
, colors
).
In the simplest case, data binding will produce an array or a struct-of-arrays.
You can also use composite data where each item in the data has several data points. These can be variable length, e.g. to render N paths with 1..M segments each.
There is also a raw loader to fill and upload a single TypedArray
to the GPU.
This is the easiest format to work with. The input data is an array of JS records, each a single data point. E.g.:
const data = [
{
position: [0, 1, 2],
color: [0.3, 0.5, 0.7],
size: 5,
},
{
position: [4, -1, 1],
color: [0.4, 0.1, 0.5],
size: 3,
},
];
We need a matching schema for one data point. This has a WGSL type T
for each field:
const schema = {
'position': 'vec3<f32>',
'color': 'vec3<f32>',
'size': 'f32',
};
We pass data
and schema
to <Data>
:
import { Data } from '@use-gpu/workbench';
return (
<Data
schema={schema}
data={data}
>{
({positions, colors, sizes}) => {
// ...
return <Component />;
}
}</Data>
);
The data is converted to a struct-of-arrays, provided as 3 separate LambdaSource
shader sources. They can be rendered with layers like <PointLayer>
:
import { PointLayer } from '@use-gpu/workbench';
return (
<PointLayer
positions={positions}
colors={colors}
sizes={sizes}
/>
);
<Data>
also allows you to use variable-length groups of data points. Here, each field of data
actually has N values.
Not all fields need to be composite. We can make e.g. only position
into an array positions
:
import { Data } from '@use-gpu/workbench';
const schema = [
positions: 'array<vec3<f32>>',
color: 'vec3<f32>',
size: 'f32',
};
const data = [
{
// 4 positions
positions: [
0, 0, 0,
0, 1, 0,
1, 1, 0,
1, 0, 0,
],
// 1 color and size
color: [0.3, 0.4, 0.5],
size: 5,
},
{
// 5 positions
positions: [
2, 2, 1,
2, 3, 1,
3, 3, 1,
2, 4, 2,
3, 1, 2,
],
// 1 color and size
color: [0.5, 0.3, 0.4],
size: 3,
},
]
You will end up with 9 data points total. The non-array props color
and size
will be repeated appropriately, i.e. 4 + 5 times.
return (
<Data
schema={schema}
data={data}
>{
({positions, colors, sizes}) => {
// ...
return <Component />;
}}
/>
);
As an alternative to a loose Struct-of-Arrays, you can use <GeometryData>
to load a single CPUGeometry
object and obtain a GPUGeometry
with LambdaSources
inside. This does the same thing as the previous example, but is less verbose. It's intended for passing a group of named attributes on, e.g. with prefab geometry:
import { makeBoxGeometry } from '@use-gpu/core';
const geometry = useOne(() => makeBoxGeometry());
return (
<GeometryData geometry={geometry}>{
(mesh: GPUGeometry) => {
// ...
return <Component />;
}
}</GeometryData>
);
If your data is interleaved as classic vertex attributes, you can unpack them with <InterleavedData>
. All elements must have the same numeric type (e.g. f32
).
const vertexArray = [
// vec4 position, vec4 normal, vec4 color, vec2 uv,
1, -1, 1, 1, 0, -1, 0, 0, 1, 0, 1, 1, 0, 1,
-1, -1, 1, 1, 0, -1, 0, 0, 0, 0, 1, 1, 1, 1,
//...
];
const MESH_SCHEMA = {
position: 'vec4<f32>',
normal: 'vec4<f32>',
color: 'vec4<f32>',
uv: 'vec2<f32>',
};
return (
<InterleavedData
schema={MESH_SCHEMA}
data={vertexArray}
>{
({positions, normals, colors, uvs}) => {
// ...
return <Component />;
}
}</InterleavedData>
);
To load a single flat array, use <RawData>
:
import { RawData } from '@use-gpu/workbench';
const positions = [
-5, -2.5, 0,
5, -2.5, 0,
0, -2.5, -5,
0, -2.5, 5,
];
return (
<RawData
format='vec3<f32>'
data={positions}
>{
(positions: StorageSource) => {
// ...
return <Component />;
}
}</RawData>
);
Struct types defined in WGSL shaders can be imported, and used as the binary format
for packed <StructData>
.
This is intended for specialized layers or custom draw calls, and is the only way to produce an actual array-of-structs in WGSL. It will have indeterminate array length, variable at run-time.
The binary layout will be derived automatically, following WebGPU alignment rules.
e.g.
import { Light } from '@use-gpu/wgsl/use/types.wgsl';
import { StructData } from '@use-gpu/workbench';
const data = [
{
position: [0, 1, 2, 0],
normal: [1, 0, 0, 0],
tangent: [0, 1, 0, 0],
size: 10,
color: [0.5, 0.3, 0.6, 1.0],
intensity: 1,
kind: 2,
},
// ...
]
return (
<StructData
format={Light}
data={data}
render={(source: StorageSource) => {
// ...
}}
/>
);
To generate data on-the-fly procedurally, use Sampler
from the plot package. Provide an expr
that calls an Emitter
. This is a function that emit
s one or more vectors per call, based on one or more indices (i, j, k, ...)
:
expr = (emit: Emit, i: number, j: number) => {
// Emit a 2D point
emit(i, j);
}
To emit multiple items per data point, set prop items
to > 1 and call emit
that many times. You will get an items x length
sized array.
To emit sparse data, set prop sparse
to true and choose whether to call emit
on the fly. You must either call it 0 or N = items
times. You will get an items x emitted
sized array.
Set prop time
to true to add the local TimeContext
as the last argument to expr
.
<RawData>
has a straight-forward 1D expr
prop:
import type { Emit } from '@use-gpu/core';
return (
<RawData
format='vec3<f32>'
length={100}
expr={(emit: Emit, i: number) => {
const s = ((i*i + i) % 13133.371) % 1000;
// Generate random 3D point
emit(
Math.cos(s * 1.31 + Math.sin(s * 0.31) + s) * 2,
Math.sin(s * 1.113 + Math.sin(s * 0.414) - s) * 2,
Math.cos(s * 0.981 + Math.cos((s + s*s) * 0.515) + s*s) * 2,
);
}}
render={(positions: StorageSource) => {
// ...
}}
/>
);
Use <Tensor>
to generate multi-dimensional arrays, where prop size
is a number[]
, i.e. [width, height?, depth?, layers?]
. This will live on the CPU and must be passed to <Data>
.
Some layers, e.g. SurfaceLayer
require multi-dimensional data.
E.g. a 2D array:
import { Tensor } from '@use-gpu/plot';
return (
<Tensor
format='vec3<f32>'
size={[40, 30]}
expr={(emit: Emit, i: number, j: number) => {
// emit(...)
}}
>{
(positions: TensorArray) => {
// ...
}
}</Tensor>
);
The general signature for an array emitter is:
(
emit: Emit,
i: number, j: number, ... // Index along dimensions
t: TimeContextProps, // Time (if prop time=true)
) => {
emit(...);
emit(...);
// ...
}
Use <Sampled>
to produce a multi-dimensional array sampled on a given range
[min, max][]
:
import { Sampled } from '@use-gpu/workbench';
return (
<Sampled
range={[[-1, 1], [-2, 2]]}
format='vec3<f32>'
size={[40, 30]}
expr={(emit: Emit, x: number, y: number, i: number, j: number) => {
// emit(...)
// emit(...)
}}
>{
(positions: StorageSource) => {
// ...
}
}</Sampled>
);
The general signature for an array emitter is:
(
emit: Emit,
x: number, y: number, ... // Sampled point
i: number, j: number, ... // Index along dimensions (if prop index=true)
t: TimeContextProps, // Time (if prop time=true)
) => {
emit(...);
emit(...);
// ...
}
Set centered
to true to sample half-a-sample away from the edges, i.e. to sample at pixel centers. Centered can also be an array boolean[]
to control centered sampling per axis.
With items
> 1, you can also emit interleaved data, producing an implicit array-of-structs layout.
When <RawData>
prop interleaved
is true, this data will be split into one source per field:
return (
<RawData
format='vec3<f32>'
length={100}
items={2}
interleaved={true}
expr={(emit: Emit, i: number) => {
emit(...); // Position
emit(...); // Color
}}
>{
(positions: ShaderSource, colors: ShaderSource) => {
// ...
}
}</RawData>
);
Both sources use the same de-interleaving shader to read from the same underlying GPU buffer, only with a different offset.
<StructData>
can be used with an emitter as well. Emit the fields in struct order:
expr = (emit: Emit, i: number) => {
emit(2+i, 2+i, 2+i, 1); // position
emit(0, 1, 0, 0); // normal
emit(1, 0, 0, 0); // tangent
emit(10); // size
// ...
};
<StructData>
expressions can be sparse
, but not items
> 1.
By default, a data component will only refresh if the data
prop changes by value, or if the schema
has changed. This means modifying data
in-place does not work by default.
To handle this case, you can set the live
prop to true. This will require a surrounding <Loop>
, to allow the component to re-render continuously, so it is far less efficient.
It is also important not to define the schema
directly during component render, but to define it ahead of time once, as a global constant. Otherwise, it is considered a new schema for every render, and all the buffers will be recreated constantly.
A LambdaSource
is a shader function with a known array size. Usually this function will read from a StorageSource
.
Each StorageSource
is a GPU array of a single data type, e.g. vec4<f32>
or a WGSL struct
. It consists of a GPU buffer handle, as well as additional metadata like array size, format, and so on.
As a handle, a StorageSource
's data can change on the fly, without creating a new StorageSource
. This means components that use a source will not be re-rendered when the data changes, even when it resizes.
This may be unexpected, however it is entirely in line with the concept of data-driven geometry: it is the job of individual low-level primitives and their shaders to adapt properly to input size, it should not be your concern.
Draw calls that use a source will evaluate its size just-in-time as needed. Derived sources will evaluate their own size lazily.
See Memoization for info on how changes are tracked for handles that do not change.
Data is joined into one big array, so you need additional segment information to describe the boundaries between the original segments. For line segments, this is a simple integer code 1-3-2 for start-middle-end:
o-o-o-o o-o-o o-o-o-o-o-o
1 3 3 2 1 3 2 1 3 3 3 3 2
While you can provide segment data yourself, it is easiest to auto-generate it as the data is being composed.
Here, we pass a getLineSegments
to <Data>
via segments
. This will cause an additional source to appear:
return (
<Data
schema={schema}
data={data}
segments={getLineSegments}
>{
({
positions,
colors,
sizes,
segments, // added by `getLineSegments`
}) => {
// ...
return <Component />;
}}
}</Data>
);
You can also draw closed loops of data, by passing a loop
prop to <Data>
. This is a function (item) => boolean
so you can mix looped and non-looped data freely.
When looped, data points will be padded with wrapping, with one item at the front, and two at the back:
[0 1 2 3] // data
[3 0 1 2 3 0 1] // padded and looped data
· o-o-o-o-· ·
[0 3 3 3 3 0 0] // line segments (no start or end)
The extra points are needed to draw a continuous loop. This connects each point with the next. We also need the vertex before and after each vertex used, for joins/tangents.
Some layers like <ArrowLayer>
support special end-point handling, e.g. to add an arrow head at start and/or end. For this there is a start
and end
prop that takes an (item) => boolean
just like loop
.
By passing getArrowSegments
to <Data>
's segments
prop, the necessary derived fields are auto-generated:
return (
<Data
schema={schema}
data={data}
segments={getArrowSegments}
end={true}
>{
({
positions,
colors,
sizes,
segments, // Added by getArrowSegments
anchors, //
trims, //
}) => {
// ...
return (
<ArrowLayer
positions={positions}
sizes={sizes}
colors={colors}
segments={segments}
anchors={anchors}
trims={trims}
/>
);
}
}</Data>
Here, anchors
is a data structure describing the end points with decorations, while trims
is used to trim lines so they don't stick through arrowheads.
To use sources from multiple data components together, you could nest one inside the other:
return (
<Data {...props}>{
({a, b}) =>
<Data {...props}>{
({c, d}) =>
<Component a={a} b={b} c={c} d={d} />
}
</Data>
}</Data>
);
But this is pretty unwieldy. It's cleaner to use a <Gather>
, and omit the render prop:
import React, { Gather } from '@use-gpu/live';
return (
<Gather
children={[
<Data {...props} />,
<Data {...props} />,
]}
// Note: this is an array!
then={([{a, b}, {c, d}]) => {
// Render here
}
/>
);
Each <Data>
instance will yeet
its sources, and <Gather>
will pass on a single array.