I wish I knew this before using React Three Fiber

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
How does React Three Fiber actually work under the hood? Maybe you have a great Three.js foundation but bringing this to React has been challenging. Or maybe you're like me who came from the React background, but finds the Three.js world a bit much to understand. This video was created for both of you. What I'm going to do today is walk you through a side-by-side, one-to-one build of a Three.js project and a React Three Fiber project at the same time. Sometimes the best way to understand a library like React Three Fiber is to compare it to the OG, tried and true Three.js library so you can see exactly what's happening on each side. We're going to start by creating a scene on both the Three and the React Three Fiber side. Specifically we'll talk about how cameras work, meshes and their geometries and materials, animation and lighting, and how all these are implemented within JSX and how they actually work within React. We'll dive into the little tricks that React Three Fiber does to make your renders look nice, including anti-aliasing, tone mapping, and color output. And then we'll toss in a couple extras just for good measure. All right so we got our two environments side by side here. On the left we have Three.js, and on the right we have our React Three Fiber. We're going to start on the Three.js side first just because there's a lot more involved in the setup, whereas React Three Fiber wraps a lot of this for us. First we need to create a scene by creating a new Scene instance. For those not familiar with Three.js, the entire library consists of classes that you can create instances of. Next we have our camera. In this case we're going to use a PerspectiveCamera with these defaults. These arguments refer to the field of view, the aspect ratio, the near frustum, and the far frustum (which is talking about at what point does the scene get clipped). These are the default values that React Three Fiber uses, so I'm just going to put them in there so that we can have identical scenes. Next when you're building a Three.js project, you're going to need to create a renderer. Three.js supports multiple renders, the most common being the WebGL render. So we're going to use that. And by default it does not have a transparent background, whereas React Three Fiber does. So we're going to turn alpha to true to enable that, otherwise it would be a black canvas. We're going to set the renderer to the window inner width and height, and then we're going to append it as a child on the web page. This is a Next.js project, so that's why I'm using this element as my container. And then finally in Three we need to have our render loop which is accomplished through this pattern where you have a function (typically called animate), you call requestAnimationFrame() from the browser, and then every frame it will render the scene. And with the request animation frame - that will call animate() every frame available the browser provides us. And then of course we need to call animate() at the very beginning, otherwise nothing kicks it off. So this is the Three.js side. On the React Three Fiber side, as you can see much simpler to start. We just have our Canvas. Note that this isn't an HTML5 canvas - this is a special React Three Fiber Canvas that we're importing from their library. At this point in time we have no children, so let's go ahead and get started by adding a mesh. So let's come back on the Three.js side and we're going to add a mesh to the scene. In this case let's add a cube. In Three.js your mesh needs to have a geometry and a material in order for it to appear on the screen, so we're going to add a new geometry. We're going to use a BoxGeometry so that it shows up as a cube. The parameters expect a width, height, depth, and additional parameters. By default the width, height, and depth will be 1 which works for us. And then we're also going to add a material. And for now we're going to use a MeshStandardMaterial which has a default color of black. To add that to the cube, we're just going to say cube.geometry equals geometry and we'll do the same with our material. And then finally if we want to see the cube on the screen, we need to do a scene.add() our cube. And when we save that, we don't see anything. Why don't we see anything? Well by default our camera is at the 0, 0, 0 position. 0 x, 0 y, 0 z. And so is our cube, so our cube and our camera are on top of each other. So what we're going to do is we're going to move our camera back by setting its camera.position.z to 5, which is the default that React Three Fiber sets it as. Now we can see our mesh! On the React Three Fiber side, we're going to go ahead and do the same thing. So to add a mesh, it's a simple JSX element called mesh. And then to add our geometry and material to it, they're simply going to be children of the mesh. So in this case we're going to have a BoxGeometry and we're going to have a MeshStandardMaterial. So you might be thinking, "Hold on. You just planned this perfect example, but this doesn't really explain how any of this actually works. Like where is my scene on the React Three Fiber side? How does the camera work I don't see that in here? We did the rendering - where is the rendering?" And my response to that is, "You need to calm down!" Do you know what channel you're on? Of course we're gonna go over that stuff. So let's start with the scene. In Three.js, we have to create a new scene. And then at the bottom we are rendering that scene within our animation loop. In React Three Fiber, this is all handled for us within the canvas. Since React Three Fiber is trying to make Three.js accessible within the React ecosystem, this is something that generally most people don't want to manage themselves and don't need to manage themselves, so they abstract this for us. And React Three Fiber does a pretty good job of choosing things that you normally don't really want to have to worry about, but still giving you the option to tweak those things if you really wanted to, which is best case scenario. So pretty sane defaults. So canvas is creating a scene for us - what about the render loop? They're also doing that. So this animate() & request animation frame - that's all being handled within this Canvas component. We don't need to worry about that. Okay what about cameras? So cameras - again, we're explicitly creating that on the Three.js side. On React Three Fiber our camera again, is being created for us within the Canvas. And why is that? Why don't they require us to create the camera here ourselves? It feels like maybe that could be a little bit too much magic happening. But I think it's a good amount of magic, because if you come down here again to the animation loop, this render() function explicitly depends on the scene and the camera. So if this camera was something that we added to the scene ourselves, then that makes it complicated for React Three Fiber to also manage this animation loop for us. So after understanding that, I think it's fair that the Canvas actually manages our camera for us. And if you're wondering, "Well what if I want to manage the camera myself?" No problem, there's actually a couple options. Number one, you can manage it through a prop on the canvas. You can go "camera". And then this is props passed to that camera. So for example, you could set the position. And by default we have 0, 0, and 5 which is matching what we have here. Which is camera position "z" 5. If I save that nothing will change. If I were to make that 10 and refresh this page, then you can see we move back further in the z direction. And that behaved as we expected. So let's go ahead and actually just remove this. You'll notice that the hot reloading doesn't apply for the camera. That just has something to do with the way the props work. If you want to manage the camera as a JSX element, you can. And you can do that by importing a special component called (you guess it) PerspectiveCamera, but this one comes from the @react-three/drei package. React Three Drei is a separate package by React Three Fiber that holds a bunch of goodies for React Three Fiber. So React Three Fiber itself is just the minimal core to get Three.js working within React, and then Drei provides a lot of extras to assist with it. So we just insert the perspective camera here. We're going to add a field of view to make it match what we have on the Three.js side. So that's 75. And then we need to give it a position as well to match the Three.js side as well. The perspective camera component from Drei will default to just 0 for the position, so let's change that to 5. We also can't forget to say makeDefault true, which will override the original default camera and make this camera now the active camera. So we save that and then we can now change this to anything else and we can see it update in real time. This prospective camera does work with hot reloading within React. So we're going to get rid of this and go back to our original setup. So next let's talk about how React Three Fiber treats Three.js elements within JSX. So starting with the Canvas, you can basically treat the Canvas as the Scene in this context. Anything that's added to the root level within the Canvas will be added to the Scene. So in this case we have our mesh and just as we do in the Three.js side, we're adding our cube to the scene. That's the exact same thing as what's happening here. This mesh is getting added to the scene. And then from there any nested children will get added in the same way. For example, if I were to add another mesh inside of this mesh - which is to be honest not that common - but if you're to do that, then that would be equivalent to saying mesh.add() another mesh. So in this case, say I had a cube and then I had a another mesh equals new Mesh(). Then I could say cube.add() another mesh, and that would actually add another mesh as a child to cube. And you can see that's equivalent of what's happening on the React Three Fiber side. Now in general you're not going to be nesting mesh elements. You certainly can if you want to - more commonly you'll probably see what's called a "group". And then you can have multiple meshes inside of a group and then groups have their own position & rotation properties that can allow you to move all its children as a whole. From the Three.js side this is not a new concept. You would just create a group and then you'd say group.add() cube for example. So then how does BoxGeometry and MeshStandardMaterial work? Since these are children of mesh, are these actually getting added in the same way you say cube.add() geometry? And that's not true, right? You don't add geometry as a child of cube - you set its geometry property and its material property. So how come these two JSX elements get special treatment? How do they actually work? And to answer that, you need to understand a pretty key concept in React Three Fiber and it's called "attaching". So essentially React Three Fiber behaves like this: any child of an element will be added with the .add() function, unless they have a special property called "attach". If they have the property called "attach", then instead of being added as a child of its parent, it will actually get added as that property of the parent. So in this case, if I say attach geometry, that's saying, "Add this BoxGeometry to the geometry property of the mesh." So in this case, you can see over here cube.geometry equals geometry - that will actually attach it. Same with the material - I could say attach equals material, and it will attach this mesh standard material to the material property in the same way we are doing on the left hand side here. Now how come I didn't need to do that before? Well for convenience, React Three Fiber automatically attaches all geometries that end with the word "Geometry" to the geometry property, and all elements end with "Material" to the material property. And that's just to make your life easy, because these are things that you'll be adding all the time. The last thing to understand is the naming behind all the Three.js elements within React Three Fiber. So you may have noticed that we followed this camel case (that has a lowercase first character) within React Three Fiber, and then within Three.js we of course follow the standard class-based pascal case (with the capital at the beginning). And so you may be wondering, "Can I literally just one-to-one, for every Three.js class that exists, do the equivalent within JSX, just with the difference of changing the first letter to lower case?" And the answer is: Yes! That's basically correct. An important note when it comes to React Three Fiber is that React Three Fiber actually knows very little about Three.js. And that might sound kind of crazy, but the way React Three Fiber works is it actually just simply takes this object and finds a Three.js equivalent, and then inserts it, right? It by default tries to add children using the add() function, unless it sees the "attach". If you go and actually look at the React Three Fiber renderer which is their actual reconciler code (the way they actually convert JSX within React to something React understands), you can see that it's actually very, very simple. And React Three Fiber at its core is just a very thin wrapper around Three.js. And the reason why this is so important is because, especially for those who are coming from the React side (people who understand React relatively well but they're trying to dive into the Three.js world), you really need to understand that React Three Fiber isn't doing anything special. It's literally just putting normal regular vanilla Three.js components within the React ecosystem. But they're not creating anything special, unless of course you're pulling additional components from the React Three Drei package - those are more or less new things. But React Three Fiber itself is actually not doing anything special. And this is really important when you're first getting started because if you're anything like me who is somebody who started with React and really didn't know anything about Three.js, and then tried to dive straight into Three.js within the React Three Fiber library, I almost kind of expected React Three Fiber to give me all the documentation (give me all the instruction) necessary to build a 3D world within React. And that's really the wrong approach. You want to first and foremost understand how Three.js works, and then from there it's much easier to build it within React, because all React Three Fiber doing again, is taking Three.js concepts and making them work within a React ecosystem. But to actually build within the Three.js world, you need to know Three.js first and foremost. And why did React Three Fiber decide that these should be camel case instead of pascal case? AKA, how come the first character is lowercase? Well that's convention for React. If you're familiar with React for the DOM, all your div's, h1's, a-tag's - everything is lowercase by default. And that's the convention for for any element you consider primitive, right? So React Three Fiber follows that exact same convention - all the primitive components (in this case primitive with respect to Three.js), are going to follow that first letter lowercase convention. So in summary React Three Fiber JSX elements map one-to-one to their respective Three.js component, children within JSX elements are added to its parent via the .add() function, unless they have a special "attach" method which in that case gets added to the respective property that you chose, with geometry and material nodes getting special treatment because you use them all the time. Because of this, technically nothing stops you from using things like Vector3 in here if you wanted to. Now I've never seen anyone actually do that. This doesn't make sense - you can't add a plain old Vector3 to a mesh. But if you're familiar with Three.js, you can create vectors and you'd say new Vector3() for example, which would consist of an x, y, and z. And I could easily add that in here. And nothing stops me from adding that in here if I wanted to, of course I need to have a reason for it. In this case this would not work because cube can't add a Vector3. But hopefully this helps you understand a bit more about what's actually happening under the hood when it comes to React Three Fiber. The last thing we need to talk about real quick is arguments. So in Three.js I haven't been using arguments - I've just been using the defaults. But if you can imagine we have our BoxGeometry, and the arguments that this constructor expects are width, height, depth, and a bunch of other things. So let's say I want to set my geometry to be 2 x 2 x 2. Let's save that. So our box is twice as big. How would I do the equivalent on the React Three Fiber side? Well React Three Fiber has a special prop called "args", and this will exist on nearly every single element. And this expects an array. And this will literally get passed in as arguments to the constructor of the component that you're trying to build. So in this case, I would say an array of 2, 2, 2 and under the hood literally this is an array that's getting spread into the arguments (if you can imagine like this) of the relative component. So if I save this, we can see it went ahead and changed the BoxGeometry to be 2 width, 2 height, and 2 deep. It's not going to be long until you're going to want to start componentizing your objects within the 3D scene. So on the Three.js side - since we're using more or less vanilla JavaScript here, you can basically componentize your objects however you like. Though one common pattern would be to use classes to do this. So for example, we create a class called our Cube, and this would extend a mesh. And then within the constructor we might create our geometries. Within React Three Fiber, this would of course just be another React component. So let's talk about animation. In Three.js, you might, let's say, rotate this cube in a circle by modifying its rotation within the animation loop. Since we've abstracted this cube into a component, we'd want to move this logic into that component. Typically you do this with a function called update(). Then we just need to make sure we call update() on the cube within the animation loop. Now we have an animating cube! Now how do we do the same thing in React? So React Three Fiber gives us this nice hook called useFrame(). And you can think of this useFrame() hook as behaving almost exactly like our update() function on the cube. Every single frame this function is going to get called, and you can do whatever you like with it. So in React we need to somehow get access to this mesh in order to modify its rotation x and y like we've done in Three.js. To do that, we can use refs. Perfect, now we have a rotating cube in React as well. So there's two things we need to talk about here: So first off, what really is a ref with context to Three.js? Well in React Three Fiber, the ref on each JSX element is literally going to point to the exact instance created within Three.js. So attaching a ref on this mesh is literally going to point to the exact Three.js mesh instance. The same thing would apply if I were to add a ref to the geometry or the material - that would point to an instance of that geometry within Three.js. If you've used refs within regular old React on the DOM, it behaves the exact same way. If you add a ref to a div, you literally have a reference to the instance of that div within the DOM. Okay second thing to point out is if you're OCD like me, the left-hand side Three.js is probably driving you crazy right now. Why is that looking so pixelated whereas the React Three Fiber is looking really smooth? Don't worry, we're going to talk about this in just a little bit. By the way, there are other ways to animate within React Three Fiber, specifically there's a library called React Spring which integrates seamlessly with React Three Fiber. React Spring essentially allows you to create animations in a much more declarative way and is actually cross platform - works in the DOM, works on React Native, and works in Three of course. But we'll save that for another video. Before we add lighting to the scene, I want to bring up one last point and that is around disposing. If you've used Three.js before, you might be familiar with disposing where Three.js actually requires you to manually dispose your geometry and material for example. And the reason for that is because usually you would cache these and Three.js doesn't know when you are done with them. So one thing you might do is add a dispose() function and then you would dispose, for example, your geometry here and anything else that you need to dispose. And then at the time that you want to get rid of your cube, you call that cube.dispose(). Within React we actually don't need to worry about this at all, and that's because React actually already understands the exact life cycle of all the different components within React. So React Three Fiber takes advantage of this and actually will automatically call dispose for you when elements are removed from React. Pretty handy. All right so let's add some light. An ambient light in Three.js is a light that is spread evenly across all objects. Let's do the same in React. Let's also add a point light so that we can add a little bit more dimension to the scene. Point lights exist within a very specific position on the scene. Now that didn't seem to do anything - we'll come back to that in a second. Let's add on the React side. And now we can see (at least on the React side) we're actually getting a bit of dimension here. Okay so why is the React version looking so much better than Three.js version? So now is a great time to talk about the rendering appearance. And there's a number of different ways we can configure how Three.js will appear when rendered. So first of all let's address this pixelated look. You can fix that by adding another property to the WebGL renderer called anti-aliasing. This actually isn't on by default within Three.js, however it is within React Three Fiber. So if I save that we're already looking a little bit better. Still not perfect though - it looks a little bit fuzzy on the edges here, where it's quite crisp here. And this has to do with the pixel ratio. So by default, Three.js will render at a pixel ratio of 1, whereas React Three Fiber will try to match what your browser supports. So we'll have to manually do that within Three.js. So that looks a lot crisper now. We still have a flat white looking box compared to the 3D one on the React side, and this has to do with tone mapping. And by default React Three Fiber chooses to use a ACES Filmic tone mapping which we'll use here. Okay so finally we have a 3D looking object on the Three.js side, but it still looks a little darker than the React Three Fiber side. And it has to do with the color space that Three.js is outputting. We can fix that by setting the output encoding to the sRGB encoding. And now our cubes are looking identical. So why does React Three Fiber have different defaults than Three.js? Well React Three Fiber is making the decision here that they want to provide defaults that they think the majority people will want. And setting this tone mapping and output encoding to these values achieves that. Of course nothing stops you from changing these if you want to. Within React Three Fiber, you can get access to the GL renderer through the "gl" prop on the canvas and you can set your configuration there. Of course our cube would not be complete without adding some color, so let's make our cube blue. In Three.js we can do this by setting the color on our material. So we could go ahead and set the color within the constructor, or alternatively we can set the color on the material itself. In React Three Fiber it's done similarly, where color would be a prop on the MeshStandardMaterial element. Or if you wanted to be absolutely crazy and do something absurd, you could actually add a color as an element within MeshStandardMaterial, and use our "attach" prop to attach this color to the MeshStandardMaterial color property. Please don't do this - I see almost no value of making this an actual child of MeshStandardMaterial, but I'm just trying to really demonstrate what kind of options are available within React Three Fiber once you understand a little bit more how the JSX elements work. Okay last thing we never addressed here is how this array of three values (also known as a triplet) actually works. From the Three.js side of the world, you don't really see this convention that much. So how come we're using that all the time within React Three Fiber? And to answer that you need to understand how something like position actually works within Three.js. So when I create a new point light for example, it's going to have a position. This would also be true for most any other thing within the scene (like your mesh would have a position, groups do, etc). So looking back at our point light, if I hover over position you can see that this is a Vector3. Now I never created this position - as soon as I create a new point light, that position is automatically added to the point light for me. And under the hood what's actually happening there is - this position - we're creating a new instance of a Vector3 (which we talked about before consists of an "x", "y", and "z" value). Another thing worth noting here is I can't just assign a new Vector3 to my position. As you can see here, position is a read-only value and cannot be reassigned. So the way that you actually would change your position is instead of overriding the Vector3, you would just call set() on that Vector3. And there's good reason why this is the case - Vector3's aren't necessarily cheap to create. You can imagine Vector3's are used all the time within Three.js. All the positions need Vector3's, and many other things. So if every time we want to move an object around we had to create a brand new Vector3, that would be extremely expensive. So instead we're just mutating that Vector3 by calling set(). So how does this translate to the React Three Fiber world? Well if we look at the type of position here we can see that it's a Vector3, which is interesting. If we actually drill into that we can see that this is actually a special Vector3 wrapped by React Three Fiber that allows you to input a number, an actual Vector3, or this triplet notation. So technically I actually could put a new Vector3 in here with my values and this would work. Now this is an anti-pattern within React Three Fiber for the exact same reason we just talked about. Within React, you should know that the code within this functional component will be called every render and if we are creating a new Vector3 every single render, this is going to be extremely expensive and not performant at all to run. So instead, best practice would be to do what we did originally, which is this triplet notation. And an array of three values is relatively cheap to recreate every render. But this still feels a little bit like magic - how is this triplet of three values getting passed into my point light's position? Well almost like the args, what's happening here is this array of three values is actually getting spread into the set() function of position. So what React Three Fiber has done here is they do allow you use Vector3's (and if you cache them properly that's reasonable), but in order to take advantage of the declarative nature of React and allowing you to be changing the values within JSX, pretty much every property that's available on these objects allow you to use these different notations. And specifically this triplet notation you can use, which will always be spread into the set() function of that respective prop. So there you have it, Three.js and React Three Fiber side by side. I hope this video has helped you really understand what's happening in React Three Fiber. I really enjoy making these videos, but they do take a lot of time and effort. So if you found this video useful and would like to see more, liking the video and subscribing would no doubt help with that. Have a great day and I'll catch you down the next rabbit hole!
Info
Channel: Rabbit Hole Syndrome
Views: 81,178
Rating: undefined out of 5
Keywords:
Id: DPl34H2ISsk
Channel Id: undefined
Length: 28min 22sec (1702 seconds)
Published: Thu Jun 09 2022
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.