
Shaders 102 - sending data
Planted:
Status: seed
Hits: 1192
Intended Audience: Creative coders, Front-end developers
There are three different ways to send data to shaders; attributes, uniforms and varyings.
Attributes
Purpose: to send unique data to each vertex.
Convention: prefix variable name with 'a' (example: aMyAttribute
).
How: store the data in a buffer using BufferAttribute(<array>, <itemSize>)
and send using the geometry's setAttribute
method.
/index.js
geometry.setAttribute("aMyAttribute", new THREE.BufferAttribute(values, 1))
<array>
size should be <itemSize>
multiplied by number of vertices.
<itemSize>
is the number of items you want to send per vertex.
Access the value in the shader using keyword in
.
vertexShader.glsl
in float aMyAttribute;...
Example: send a random value to each vertex to change z-position. Creating a terrain-like surface.
Uniforms
Purpose: to send the same data to all vertices and fragments (all data is equal, aka uniform).
Convention: prefix variable name with 'u' (example: uMyUniform
).
How: use the material's uniforms property.
/index.js
// set initial valueconst material = new THREE.ShaderMaterial({uniforms: {uMyUniform: {value: ...}},...// change valuematerial.uniforms.uMyUniform.value = ...;
Example: set the same color for all fragments.
Varyings
Purpose: to send data from the vertex shader to the fragment shader.
Useful because if a primitive (like a triangle) has two vertices with different data, the fragment shader will smoothly interpolate the data across the surface.
For example, if a triangle has one red and two blue vertices, the surface will be a red to blue gradient.
Convention: prefix variable name with 'v' (example: vMyVarying
).
How: declare a varying variable in the vertex shader (keyword: out
) and fragment shader (keyword: in
).
Set its value in the vertex shader and read it in the fragment shader.
/vertexShader.glsl
out float vMyVarying;void main() {...vMyVarying = ...}
/fragmentShader.glsl
in float vMyVarying;void main() {gl_FragColor = vec4(vec3(vMyVarying), 1.0);}
Example: set each vertex color depending on its z-position. The lower, the more black, the higher the more white.
Debugging
Shaders don't provide many debugging tools.
We don't have console.log
to confirm data in a shader is correct.
Instead, we can use grayscale color.
Normalize the data, send it to the fragment shader and use it as the color to confirm data is correct.
This is used in the Pointer Position example below.
Pointer Position
To send the pointer (mouse or touch) position to a shader, we can use a Raycaster
.
This will signal if the pointer is intersecting with a mesh.
If intersecting, convert the coordinates and send them as a uniform.
/index.js
const raycaster = new THREE.Raycaster()const pointer = new THREE.Vector2()function onPointerMove(event) {// Convert screen coords to clip-space coords (go from -1 to +1, left to right, top to bottom)pointer.x = (event.clientX / sizes.width) * 2 - 1pointer.y = (-event.clientY / sizes.height) * 2 + 1// Update the ray with a new origin and direction.raycaster.setFromCamera(pointer, camera)const intersections = raycaster.intersectObjects(scene.children)if (intersections.length) {const intersection = intersections[0]material.uniforms.uUvPointer.value = intersection.uv}}
To confirm the data is correct in the shader, I'm using the distance(...)
function.
It calculates the distance between two vec2
positions.
The first is the position of the current vertex the shader is processing (uv
).
The second is the mouse position (uUvPointer
).
Using 1.0 - distance(uv, uUvPointer)
returns a higher value the smaller the distance.
Using this value for color render a gradient radiating from the pointer.
Feedback
Have any feedback about this note or just want to comment on the state of the economy?