WebGL is destination based. That means it does 1 operation for each result it wants to write to the destination. The only kinds of destinations you can set are points (squares of pixels), lines, and triangles in a 2D plane. That means writing to a 3D texture will require handling each plane separately. At best you might be able to do N planes separately on where N is 4 to 8 by setting up multiple attachments to a framebuffer up to the maximum allowed attachments
So I'm assuming you understand how to render to 100 layers 1 at a time. At init time either make 100 framebuffers and attach different layer to each one. OR, at render time update a single framebuffer with a different attachment. Knowing how much validation happens I'd choose making 100 framebuffers
So
const framebuffers = [];
for (let layer = 0; layer < numLayers; ++layer) {
const fb = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
gl.framebufferTextureLayer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, texture,
0, layer);
framebuffers.push(fb);
}
now at render time render to each layer
framebuffers.forEach((fb, layer) => {
gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
// pass in the layer number to the shader it can use for calculations
gl.uniform1f(layerLocation, layer);
....
gl.drawXXX(...);
});
WebGL1 does not support 3D textures so we know you're using WebGL2 since you mentioned using sampler3D
.
In WebGL2 you generally use #version 300 es
at the top of your shaders to signify you want to use the more modern GLSL ES 3.00.
Drawing to multiple layers requires first figuring out how many layers you want to render to. WebGL2 supports a minimum of 4 at once so we could just assume 4 layers. To do that you'd attach 4 layers to each framebuffer
const layersPerFramebuffer = 4;
const framebuffers = [];
for (let baseLayer = 0; baseLayer < numLayers; baseLayer += layersPerFramebuffer) {
const fb = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
for (let layer = 0; layer < layersPerFramebuffer; ++layer) {
gl.framebufferTextureLayer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + layer, texture, 0, baseLayer + layer);
}
framebuffers.push(fb);
}
GLSL ES 3.0 shaders do not use gl_FragCoord
they use a user defined output so we'd declare an array output
out vec4 ourOutput[4];
and then use that just like you were previously using gl_FragColor
except add an index. Below we're processing 4 layers. We're only passing in a vec2 for v_texCoord
and computing the 3rd coordinate based on baseLayerTexCoord
, something we pass in each draw call.
varying vec2 v_texCoord;
uniform float baseLayerTexCoord;
vec4 results[4];
vec3 onePixel = vec3(1.0, 1.0, 1.0)/u_textureSize;
const int numLayers = 4;
for (int layer = 0; layer < numLayers; ++layer) {
vec3 baseTexCoord = vec3(v_texCoord, baseLayerTexCoord + onePixel * float(layer));
vec4 currentState = texture(u_image, baseTexCoord);
float fTotal = 0.0;
for (int i=-1; i<=1; i++){
for (int j=-1; j<=1; j++){
for (int k=-1; k<=1; k++){
if (i == 0 && j == 0 && k == 0) continue;
vec3 neighborCoord = baseTexCoord + vec3(onePixel.x*float(i), onePixel.y*float(j), onePixel.z*float(k));
vec4 neighborState;
if (neighborCoord.x < 0.0 || neighborCoord.y < 0.0 || neighborCoord.z < 0.0 || neighborCoord.x >= 1.0 || neighborCoord.y >= 1.0 || neighborCoord.z >= 1.0){
neighborState = vec4(0.0,0.0,0.0,1.0);
} else {
neighborState = texture(u_image, neighborCoord);
}
float deltaP = neighborState.r - currentState.r; //Distance from neighbor
float springDeltaLength = (deltaP - u_springOrigLength[counter]);
//Add the force on our point of interest from the current neighbor point. We'll be adding up to 26 of these together.
fTotal += u_kSpring[counter]*springDeltaLength;
}
}
}
float acceleration = fTotal/u_mass;
float velocity = acceleration*u_dt + currentState.g;
float position = velocity*u_dt + currentState.r;
results[layer] = vec4(position,velocity,acceleration,1);
}
ourOutput[0] = results[0];
ourOutput[1] = results[1];
ourOutput[2] = results[2];
ourOutput[3] = results[3];
The last thing to do is we need to call gl.drawBuffers
to tell WebGL2 where to store the outputs. Since we're doing 4 layers at a time we'd use
gl.drawBuffers([
gl.COLOR_ATTACHMENT0,
gl.COLOR_ATTACHMENT1,
gl.COLOR_ATTACHMENT2,
gl.COLOR_ATTACHMENT3,
]);
framebuffers.forEach((fb, ndx) => {
gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
gl.uniform1f(baseLayerTexCoordLocation, (ndx * layersPerFramebuffer + 0.5) / numLayers);
....
gl.drawXXX(...);
});
Example:
function main() {
const gl = document.querySelector('canvas').getContext('webgl2');
if (!gl) {
return alert('need webgl2');
}
const ext = gl.getExtension('EXT_color_buffer_float');
if (!ext) {
return alert('need EXT_color_buffer_float');
}
const vs = `#version 300 es
in vec4 position;
out vec2 v_texCoord;
void main() {
gl_Position = position;
// position will be a quad -1 to +1 so we
// can use that for our texcoords
v_texCoord = position.xy * 0.5 + 0.5;
}
`;
const fs = `#version 300 es
precision highp float;
in vec2 v_texCoord;
uniform float baseLayerTexCoord;
uniform highp sampler3D u_image;
uniform mat3 u_kernel[3];
out vec4 ourOutput[4];
void main() {
vec3 textureSize = vec3(textureSize(u_image, 0));
vec3 onePixel = vec3(1.0, 1.0, 1.0)/textureSize;
const int numLayers = 4;
vec4 results[4];
for (int layer = 0; layer < numLayers; ++layer) {
vec3 baseTexCoord = vec3(v_texCoord, baseLayerTexCoord + onePixel * float(layer));
float fTotal = 0.0;
vec4 color;
for (int i=-1; i<=1; i++){
for (int j=-1; j<=1; j++){
for (int k=-1; k<=1; k++){
vec3 neighborCoord = baseTexCoord + vec3(onePixel.x*float(i), onePixel.y*float(j), onePixel.z*float(k));
color += u_kernel[k + 1][j + 1][i + 1] * texture(u_image, neighborCoord);
}
}
}
results[layer] = color;
}
ourOutput[0] = results[0];
ourOutput[1] = results[1];
ourOutput[2] = results[2];
ourOutput[3] = results[3];
}
`;
const vs2 = `#version 300 es
uniform vec4 position;
uniform float size;
void main() {
gl_Position = position;
gl_PointSize = size;
}
`;
const fs2 = `#version 300 es
precision highp float;
uniform highp sampler3D u_image;
uniform float slice;
out vec4 outColor;
void main() {
outColor = texture(u_image, vec3(gl_PointCoord.xy, slice));
}
`;
const computeProgramInfo = twgl.createProgramInfo(gl, [vs, fs]);
const drawProgramInfo = twgl.createProgramInfo(gl, [vs2, fs2]);
const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
position: {
numComponents: 2,
data: [
-1, -1,
1, -1,
-1, 1,
-1, 1,
1, -1,
1, 1,
],
},
});
function create3DTexture(gl, size) {
const tex = gl.createTexture();
const data = new Float32Array(size * size * size * 4);
for (let i = 0; i < data.length; i += 4) {
data[i + 0] = i % 100 / 100;
data[i + 1] = i % 10000 / 10000;
data[i + 2] = i % 100000 / 100000;
data[i + 3] = 1;
}
gl.bindTexture(gl.TEXTURE_3D, tex);
gl.texImage3D(gl.TEXTURE_3D, 0, gl.RGBA32F, size, size, size, 0, gl.RGBA, gl.FLOAT, data);
gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
return tex;
}
const size = 100;
let inTex = create3DTexture(gl, size);
let outTex = create3DTexture(gl, size);
const numLayers = size;
const layersPerFramebuffer = 4;
function makeFramebufferSet(gl, tex) {
const framebuffers = [];
for (let baseLayer = 0; baseLayer < numLayers; baseLayer += layersPerFramebuffer) {
const fb = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
for (let layer = 0; layer < layersPerFramebuffer; ++layer) {
gl.framebufferTextureLayer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + layer, tex, 0, baseLayer + layer);
}
framebuffers.push(fb);
}
return framebuffers;
};
let inFramebuffers = makeFramebufferSet(gl, inTex);
let outFramebuffers = makeFramebufferSet(gl, outTex);
function render() {
gl.viewport(0, 0, size, size);
gl.useProgram(computeProgramInfo.program);
twgl.setBuffersAndAttributes(gl, computeProgramInfo, bufferInfo);
outFramebuffers.forEach((fb, ndx) => {
gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
gl.drawBuffers([
gl.COLOR_ATTACHMENT0,
gl.COLOR_ATTACHMENT1,
gl.COLOR_ATTACHMENT2,
gl.COLOR_ATTACHMENT3,
]);
const baseLayerTexCoord = (ndx * layersPerFramebuffer + 0.5) / numLayers;
twgl.setUniforms(computeProgramInfo, {
baseLayerTexCoord,
u_kernel: [
0, 0, 0,
0, 0, 0,
0, 0, 0,
0, 0, 1,
0, 0, 0,
0, 0, 0,
0, 0, 0,
0, 0, 0,
0, 0, 0,
],
u_image: inTex,
});
gl.drawArrays(gl.TRIANGLES, 0, 6);
});
{
const t = inFramebuffers;
inFramebuffers = outFramebuffers;
outFramebuffers = t;
}
{
const t = inTex;
inTex = outTex;
outTex = t;
}
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.drawBuffers([gl.BACK]);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.useProgram(drawProgramInfo.program);
const slices = 10.0;
const sliceSize = 25.0
for (let slice = 0; slice < slices; ++slice) {
const sliceZTexCoord = (slice / slices * size + 0.5) / size;
twgl.setUniforms(drawProgramInfo, {
position: [
((slice * (sliceSize + 1) + sliceSize * .5) / gl.canvas.width * 2) - 1,
0,
0,
1,
],
slice: sliceZTexCoord,
size: sliceSize,
});
gl.drawArrays(gl.POINTS, 0, 1);
}
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
main();
function glEnumToString(gl, v) {
const hits = [];
for (const key in gl) {
if (gl[key] === v) {
hits.push(key);
}
}
return hits.length ? hits.join(' | ') : `0x${v.toString(16)}`;
}
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<canvas></canvas>
Some other things to note: In GLSL ES 3.00 you don't need to pass in a texture size as you can query the texture size with the function textureSize
. It returns an ivec2
or ivec3
depending on the type of texture.
You can also use texelFetch
instead of texture
. texelFetch
takes an integer texel coordinate and a mip level so for example vec4 color = texelFetch(some3DTexture, ivec3(12, 23, 45), 0);
gets the texel at x = 12, y = 23, z = 45 from mip level 0. That means you don't need to do the math about 'onePixel` you have in your code if you find it easier to work with pixels instead of normalized texture coordinates.