I'm so happy I found your tutorials! Jumping into JavaScript / webGL / Three.js from Unity has been a rough splash. But your videos are very helpful! I love the lucid explanation and clarity!
These are the best tutorials on webgl2 on the internet. Thank you! I'm currently struggling with dealing with changing data and would love to see a video on it. For instance, if I changed a vertex position every frame, what do I have to do? Do I have to create a whole new buffer and bind to it to tell webgl something changed? Or can I use my original buffer? What if I actually change the size of the data. Like I replace my float32 array with a smaller one Then I definitely have to just create a new buffer ya. What if I only change my indices? And do VAOs help me with changing things at all? It kind of seems like they don't since they just store where data starts and stops, not what it is? I would love to see just simple examples of how data changes should be dealt with, with vertex or color data, plus indices, plus vaos. Thank you again! I've tried a bunch of other resources for learning and this is by far the best. Everything else I feel like I have to reverse engineer their examples or parse through bad code or both. This is straightforward and well made.
I should really do a video on this because I remember feeling the same confusion. I sort of go into some of this in my video on Instanced Drawing. Until I do a video on this, then that one is probably the closest I'll come to your question. That said, probably, you don't need to modify your vertex data at all (in fact, it's somewhat rare to change vertex *position* data ever). If you do need to do this, it's still best to create your array buffer *once* and reuse it every time (using `gl.subBufferData()` -- [EDIT: I said you can use `gl.bufferData()` but DON'T since that allocates space on the GPU, which you really want to do only once]). If your geometry grows (by adding vertices) then it's best to create a buffer big enough for the maximum expected size. You *can* resize your buffer, but this should be a last-resort because of how expensive the operations are. However, if the general shape of your object changes in sharp, uneven, irregular, non-uniform ways, then maybe you have no choice but to modify your vertex values. Examples of this would be data visualization (like a chart whose data changes over time) or a cloth simulation. But usually we treat our models as rigid bodies and use matrix transforms to move, rotate, scale and shear our models (using `mat4`). I'd recommend trying out a simple program that does matrix transformation. Try issuing a single draw call: your vertex buffer will contain all your position data (and anything else you need). And send in a `Float32Array(16)` containing the data of a mat4 using `gl.uniformMatrix4fv()`. You set your vertex shader output usually like this: `glPosition = myMatrix * aPosition;`. Then your render loop looks like: 1. update matrix uniform 2. call `gl.uniformMatrix4fv(matLoc, false, myMatrix)` 2. issue your draw call. Drawing two or more objects is more complicated. The simplest two options are: include TWO copies of your model data in a single buffer object and send in a your matrix transforms as an ARRAY of mat4s. Then in your vertex shader, figure out which mat4 to use (probably using `gl_VertexID` as a way to calculate the index). OR use *instanced drawing* so you can use a single copy of your model data and either use an array of mat4s in a single uniform (if you have one matrix for each instance, you can use gl_InstanceID). Or create a vertex buffer object just for the mat4s. You would modify this array buffer every draw call, but there's no duplication. Using arrays in your shader code isn't too hard. The syntax is a bit different from JS, but it's not completely alienating. Also, VAOs are really for when you do multiple draw calls (like for multiple objects) every animation frame. If you're doing a single draw call per animation frame, you almost certainly don't need to use VAOs as they just add bloat.
@@osakaandrew right on thanks. And ya I have a full scene graph implemented with matrix calculations at each level. I've abstracted my geometry into a class that has set attribute methods and that's where this question comes from. If I can set attributes freely from outside of the class, what is the best way to make that resulting object render properly. In theory those setters will all be called at init not each frame, but still I want that flexibility so the user of that class doesn't have to worry about it in detail
@@roblouie5604 I hope I understand your goal. It sounds like all you'll want to do is: retain your vertex buffer array AND the original `Float32Array` that you used to populate it. Every frame, make the modifications to your geometry the way you want -- since you can't modify the buffer data on the GPU directly, these changes will be in the Float32Array. When that's done, upload your Float32Array using `gl.bufferSubData()`. If your changes are isolated to a discrete slice of that array, you can simply upload that slice using the `dstByteOffset`, `srcOffset` and `length` arguments, but don't be afraid to just upload the whole thing. As an optimization step, consider splitting your data up so that the static parts of your vertex attribute data are in one buffer and the dynamic parts are in another. I said in an earlier comment you can use `bufferData()` to re-upload fresh data, but that was a dumb mistake. Use `bufferSubData()` instead, since `bufferData()` will always allocate space on the GPU, which is expensive and should only be done once. If you need to resize your array buffer, then you can use `bufferData()` again to reset things, but you should really avoid that at all costs. Remember that all draw functions (e.g. `drawArrays()`) allow you to specify how many vertices to use, so you can write all your visible geometry into the front of your Float32Array and upload that section, and then draw only those vertices.
@@osakaandrew Very helpful thank you! I'll start messing around with the subdata method. And regarding keeping the float32 array around, yes exactly. But ya use case is like. const geometry = new BufferGeometry(); geometry.setPositions(float32array); geometry.setNormals(float32array); Now, in almost all cases, this will only happen at geometry init, not every frame. But what I want to do is as simply and in as performant a way as possible, make sure the data set in those setters is then used for rendering when I do: gl.bindVertexArray(geometry.vao); gl.drawArray(geometry.positions); Or whatever, some would be draw elements with indices or whatever, kind of doesn't matter.
It seems like the way to do this is inside my setMethods, if I've never set that data before, or if the data size has changed, I use buffer data to effecitvely bind a larger buffer. I also then update my vao so it gets a snapshot of the updated size/offset whatever. However, if I'm only changing some existing data without changing the size, all I actually have to do is bufferSubData. My vao stays good. And then in theory I have a relatively simple wrapper class that I can reuse for setting attribues with buffers and rendering.
Amazing series and you have a soothing voice & manner of explanation. What a gem. On another note, I know opengl is old and maybe they had their reasons... but what a shit API.
This was helpful. I know the bare basics of WebGL and just do enough of the setup code so I can build 2D based renderers with shader code. Still trying to figure out what I did wrong. Infuriating because I am referencing 3 other code bases that I've written that work... And still haven't figured it out.
Thank you very much! The subject is very well explained 👍 There is one thing I don't understand. You were saying that in order to change the data, the rebinding of a VAO is not needed: th-cam.com/users/clipUgkx9ayxCr0xrYyeA7ESWrLVfdibsJ_h-7pq?si=wPYdxc_zaAJO64rB. And then a few seconds later you gave an example of the opposite: th-cam.com/users/clipUgkxJBFXHMxuf93hbu6Ul8FGCIYggsSfvT-N?si=F6Et9lMiF7gwofiE
Good question. When thinking about VAOs, we need to separate *buffer settings* from *buffer data*. "Settings" here refers to what type of data a buffer contains (e.g. float or unsigned int), how large each stride is, where each attribute will be found inside each stride (attribute 0 starts at byte 0, attribute 1 starts at byte 12, etc), whether it's enabled or disabled, and so on. "Data" is just the binary contents inside the buffer. So when calling `disableVertexAttribArray()` like I did in the video, I'm changing a "setting", thus I need to bind the VAO. But if I were changing some *buffer data* (using `bufferSubData()` for example), I wouldn't have to worry about the VAO: I would just bind the buffer, upload the changes and unbind -- without any calls to `bindVertexArray()`. It's also worth saying that while we commonly update *buffer data* over and over in our programs, I can't remember ever "updating" or changing `vertexAttribPointer()`, `enableVertexAttribArray()`, etc., once initialization is finished. So in reality there are only two situations where you'd call `bindVertexArray()`: during initialization when you declare your settings and then during your render loop, when all you do is bind and draw.
I'm so happy I found your tutorials! Jumping into JavaScript / webGL / Three.js from Unity has been a rough splash. But your videos are very helpful!
I love the lucid explanation and clarity!
PS. The URLS in the description of "10. Texture..." and "12. Vertex..." seem to be reversed.
Thank you for that. I'll try to fix that shortly.
These are the best tutorials on webgl2 on the internet. Thank you! I'm currently struggling with dealing with changing data and would love to see a video on it.
For instance, if I changed a vertex position every frame, what do I have to do? Do I have to create a whole new buffer and bind to it to tell webgl something changed? Or can I use my original buffer?
What if I actually change the size of the data. Like I replace my float32 array with a smaller one Then I definitely have to just create a new buffer ya.
What if I only change my indices?
And do VAOs help me with changing things at all? It kind of seems like they don't since they just store where data starts and stops, not what it is? I would love to see just simple examples of how data changes should be dealt with, with vertex or color data, plus indices, plus vaos. Thank you again! I've tried a bunch of other resources for learning and this is by far the best. Everything else I feel like I have to reverse engineer their examples or parse through bad code or both. This is straightforward and well made.
I should really do a video on this because I remember feeling the same confusion. I sort of go into some of this in my video on Instanced Drawing. Until I do a video on this, then that one is probably the closest I'll come to your question.
That said, probably, you don't need to modify your vertex data at all (in fact, it's somewhat rare to change vertex *position* data ever). If you do need to do this, it's still best to create your array buffer *once* and reuse it every time (using `gl.subBufferData()` -- [EDIT: I said you can use `gl.bufferData()` but DON'T since that allocates space on the GPU, which you really want to do only once]). If your geometry grows (by adding vertices) then it's best to create a buffer big enough for the maximum expected size. You *can* resize your buffer, but this should be a last-resort because of how expensive the operations are.
However, if the general shape of your object changes in sharp, uneven, irregular, non-uniform ways, then maybe you have no choice but to modify your vertex values. Examples of this would be data visualization (like a chart whose data changes over time) or a cloth simulation. But usually we treat our models as rigid bodies and use matrix transforms to move, rotate, scale and shear our models (using `mat4`).
I'd recommend trying out a simple program that does matrix transformation. Try issuing a single draw call: your vertex buffer will contain all your position data (and anything else you need). And send in a `Float32Array(16)` containing the data of a mat4 using `gl.uniformMatrix4fv()`. You set your vertex shader output usually like this: `glPosition = myMatrix * aPosition;`. Then your render loop looks like:
1. update matrix uniform
2. call `gl.uniformMatrix4fv(matLoc, false, myMatrix)`
2. issue your draw call.
Drawing two or more objects is more complicated. The simplest two options are: include TWO copies of your model data in a single buffer object and send in a your matrix transforms as an ARRAY of mat4s. Then in your vertex shader, figure out which mat4 to use (probably using `gl_VertexID` as a way to calculate the index).
OR use *instanced drawing* so you can use a single copy of your model data and either use an array of mat4s in a single uniform (if you have one matrix for each instance, you can use gl_InstanceID). Or create a vertex buffer object just for the mat4s. You would modify this array buffer every draw call, but there's no duplication.
Using arrays in your shader code isn't too hard. The syntax is a bit different from JS, but it's not completely alienating.
Also, VAOs are really for when you do multiple draw calls (like for multiple objects) every animation frame. If you're doing a single draw call per animation frame, you almost certainly don't need to use VAOs as they just add bloat.
@@osakaandrew right on thanks. And ya I have a full scene graph implemented with matrix calculations at each level. I've abstracted my geometry into a class that has set attribute methods and that's where this question comes from. If I can set attributes freely from outside of the class, what is the best way to make that resulting object render properly. In theory those setters will all be called at init not each frame, but still I want that flexibility so the user of that class doesn't have to worry about it in detail
@@roblouie5604 I hope I understand your goal. It sounds like all you'll want to do is: retain your vertex buffer array AND the original `Float32Array` that you used to populate it. Every frame, make the modifications to your geometry the way you want -- since you can't modify the buffer data on the GPU directly, these changes will be in the Float32Array. When that's done, upload your Float32Array using `gl.bufferSubData()`. If your changes are isolated to a discrete slice of that array, you can simply upload that slice using the `dstByteOffset`, `srcOffset` and `length` arguments, but don't be afraid to just upload the whole thing. As an optimization step, consider splitting your data up so that the static parts of your vertex attribute data are in one buffer and the dynamic parts are in another.
I said in an earlier comment you can use `bufferData()` to re-upload fresh data, but that was a dumb mistake. Use `bufferSubData()` instead, since `bufferData()` will always allocate space on the GPU, which is expensive and should only be done once. If you need to resize your array buffer, then you can use `bufferData()` again to reset things, but you should really avoid that at all costs. Remember that all draw functions (e.g. `drawArrays()`) allow you to specify how many vertices to use, so you can write all your visible geometry into the front of your Float32Array and upload that section, and then draw only those vertices.
@@osakaandrew Very helpful thank you! I'll start messing around with the subdata method. And regarding keeping the float32 array around, yes exactly. But ya use case is like.
const geometry = new BufferGeometry();
geometry.setPositions(float32array);
geometry.setNormals(float32array);
Now, in almost all cases, this will only happen at geometry init, not every frame. But what I want to do is as simply and in as performant a way as possible, make sure the data set in those setters is then used for rendering when I do:
gl.bindVertexArray(geometry.vao);
gl.drawArray(geometry.positions);
Or whatever, some would be draw elements with indices or whatever, kind of doesn't matter.
It seems like the way to do this is inside my setMethods, if I've never set that data before, or if the data size has changed, I use buffer data to effecitvely bind a larger buffer. I also then update my vao so it gets a snapshot of the updated size/offset whatever. However, if I'm only changing some existing data without changing the size, all I actually have to do is bufferSubData. My vao stays good.
And then in theory I have a relatively simple wrapper class that I can reuse for setting attribues with buffers and rendering.
Amazing series and you have a soothing voice & manner of explanation. What a gem.
On another note, I know opengl is old and maybe they had their reasons... but what a shit API.
Thank you! You explain everything very well.
This was helpful. I know the bare basics of WebGL and just do enough of the setup code so I can build 2D based renderers with shader code. Still trying to figure out what I did wrong. Infuriating because I am referencing 3 other code bases that I've written that work... And still haven't figured it out.
9:30 'long answer noooooooo' too good
You explained it very well.
KING!! 👑
Thank you very much! The subject is very well explained 👍
There is one thing I don't understand. You were saying that in order to change the data, the rebinding of a VAO is not needed: th-cam.com/users/clipUgkx9ayxCr0xrYyeA7ESWrLVfdibsJ_h-7pq?si=wPYdxc_zaAJO64rB. And then a few seconds later you gave an example of the opposite:
th-cam.com/users/clipUgkxJBFXHMxuf93hbu6Ul8FGCIYggsSfvT-N?si=F6Et9lMiF7gwofiE
Good question. When thinking about VAOs, we need to separate *buffer settings* from *buffer data*. "Settings" here refers to what type of data a buffer contains (e.g. float or unsigned int), how large each stride is, where each attribute will be found inside each stride (attribute 0 starts at byte 0, attribute 1 starts at byte 12, etc), whether it's enabled or disabled, and so on. "Data" is just the binary contents inside the buffer. So when calling `disableVertexAttribArray()` like I did in the video, I'm changing a "setting", thus I need to bind the VAO. But if I were changing some *buffer data* (using `bufferSubData()` for example), I wouldn't have to worry about the VAO: I would just bind the buffer, upload the changes and unbind -- without any calls to `bindVertexArray()`.
It's also worth saying that while we commonly update *buffer data* over and over in our programs, I can't remember ever "updating" or changing `vertexAttribPointer()`, `enableVertexAttribArray()`, etc., once initialization is finished. So in reality there are only two situations where you'd call `bindVertexArray()`: during initialization when you declare your settings and then during your render loop, when all you do is bind and draw.