FDNavigate back to the homepage

Fractured Cube in Blender with Bloom Postprocessing in React Three Fiber

Rick
April 2nd, 2023 · 3 min read

Im going to run through a tutorial which combines using blender to create a fractured cube and then uses bloom in a dark environment to give the effect the fractured parts of the cube are glowing.

Fracturing the cube

This is really really simple. Open up blender and create a cube mesh. Once we have the cube mesh we will install a free addon in blender - Cell Fracture.

Once this is installed you should see a cell fracture tab on the tool section which is located on a side tab in the main window.

With the cube selected, go ahead and enter edit mode and right click and subdivide a few times (5-6 should be enough).

Then finally we can select the cube and choose the type of cell fracture we want. This creates a mesh which is broken up in to fractured parts - in the name.

With this now complete we can export the mesh as a GLTF format model, which we can then use in our @react-three/fiber project.

The coding part

The code sandbox below is an extract from a website I built not long ago. Its a pretty cool spinning cube on the actual site and this one is static and has OrbitControls from @react-three/drei.

The only purpose of this is something to do as the scene loads, if you have a lot of data or hefty sized meshes then this is perfect. Low size and low usage of valuable resources used in 3D scenes.

1import { Suspense } from "react";
2import FracturedCube from "./model/FracturedCube";
3import { OrbitControls } from "@react-three/drei";
4import styled from "styled-components";
5import { Canvas as Can } from "@react-three/fiber";
6import * as THREE from "three";
7
8export const Canvas = styled(Can)`
9 min-width: 100vw;
10 min-height: 100vh;
11`;
12export const Scene = () => {
13 return (
14 <>
15 <Canvas
16 onCreated={({ gl, scene }) => {
17 gl.outputEncoding = THREE.sRGBEncoding;
18 gl.setSize(window.innerWidth, window.innerHeight);
19 // black
20 scene.background = new THREE.Color("#151515");
21 }}
22 camera={{ position: [16, 10, -8] }}
23 >
24 <gridHelper args={[10, 10]} />
25 <Suspense fallback={null}>
26 <color attach="background" args={["#2c2c2c"]} />
27 <OrbitControls />
28 <FracturedCube />
29 </Suspense>
30 </Canvas>
31 </>
32 );
33};

We use styled components in this code sandbox or just one the Canvas component. This is a neat way to do it if you use styled components.

Few things to note which are worthy of comment. You can access the webgl context using the onCreated method on the Canvas component. This gives you access to gl, which in some other tutorials might be named renderer. We set the output encoding to be sRGB. Have a read up on color encoding, theres a few options depending on the scenario.

The renderer size is set to the width and height of the window, then we set the background color to be a light matt black color. The camera position is set using the camera property on the canvas, for ease as we don’t want to over complicate it.

1/*
2Auto-generated by: https://github.com/pmndrs/gltfjsx
3*/
4
5import React, { useRef, forwardRef, useState } from "react";
6import { useGLTF } from "@react-three/drei";
7import { useThree } from "@react-three/fiber";
8import FracturedGroup from "./FracturedGroup";
9import { a } from "@react-spring/three";
10import { EffectComposer, Bloom } from "@react-three/postprocessing";
11import { KernelSize } from "postprocessing";
12
13const FracturedCube = ({ loadingProgress, ...props }) => {
14 const mainGroup = useRef();
15 const [hovered, setHovered] = useState(false);
16 const { size } = useThree();
17 const { nodes } = useGLTF("/cell-fracture-cube-1.2.glb");
18
19
20
21 return (
22 <group {...props} dispose={null}>
23 <pointLight position={[0, 0, 0]} intensity={100} color={[0, 0, 0.1]} />
24 <EffectComposer>
25 <Bloom
26 intensity={3.9} // The bloom intensity.
27 width={size.width} // render width
28 height={size.height} // render height
29 kernelSize={KernelSize.VERY_LARGE} // blur kernel size
30 luminanceThreshold={0.005} // luminance threshold. Raise this value to mask out darker elements in the scene.
31 luminanceSmoothing={0.15} // smoothness of the luminance threshold. Range is [0, 1]
32 />
33 </EffectComposer>
34
35 <a.group
36 ref={mainGroup}
37 onPointerOver={() => setHovered(true)}
38 onPointerLeave={() => setHovered(false)}
39 >
40 {Object.keys(nodes).map((name, groupIndex) => {
41 const node = nodes[name];
42 const { x, y, z } = node?.position || {};
43
44 return (
45 node.isGroup && (
46 <FracturedGroup
47 key={`${x}-${y}-${z}`}
48 group={node}
49 loadingProgress={loadingProgress}
50 />
51 )
52 );
53 })}
54 </a.group>
55 </group>
56 );
57};
58
59export default FracturedCube;

In the above code, this was originally generated by gltftojsx npm package with the npx commands. It has been heavily modified though.

We grab the mesh nodes from the glb file when loaded in and use this to loop over every group node, with a simple check to see if its a group.

And finally we pass the group node down to the child component which is where the interesting animation occurs.

1import { useRef, useState, useEffect } from "react";
2import { useSpring } from "@react-spring/core";
3import { a } from "@react-spring/three";
4import FracturedMesh from "./FracturedItem";
5
6const FracturedGroup = ({ group, loadingProgress }) => {
7 const meshRef = useRef();
8 const [hovered, setHovered] = useState(false);
9 const { pos } = useSpring({
10 pos: hovered
11 ? [group.position.x * 1.1, group.position.y * 1.1, group.position.z * 1.1]
12 : [group.position.x, group.position.y, group.position.z],
13 config: { mass: 0.1, friction: 0, duration: 200, clamp: false }
14 });
15
16 // useEffect(() => {
17 // setInterval(() => {}, 3000);
18 // }, []);
19
20 // useEffect(() => {
21 // const interval = setInterval(() => {
22 // const random = Math.random();
23 // if (random > 0.82) {
24 // setHovered((old) => !old);
25 // }
26 // }, 3000);
27 // return () => clearInterval(interval);
28 // }, []);
29
30 return (
31 <a.group
32 ref={meshRef}
33 position={pos}
34 scale={group.scale}
35 onPointerMove={() => {
36 console.log(true);
37 setHovered(true);
38 }}
39 onPointerOut={() => {
40 setHovered(false);
41 }}
42 >
43 {group.children.map((mesh, meshId) => (
44 <FracturedMesh
45 key={meshId}
46 mesh={mesh}
47 material={mesh.material}
48 loadingProgress={loadingProgress}
49 />
50 ))}
51 </a.group>
52 );
53};
54
55export default FracturedGroup;

The crux of this effect is using @react-spring.

1const { pos } = useSpring({
2 pos: hovered
3 ? [group.position.x * 1.1, group.position.y * 1.1, group.position.z * 1.1]
4 : [group.position.x, group.position.y, group.position.z],
5 config: { mass: 0.1, friction: 0, duration: 200, clamp: false }
6});

A few things going on here, the trigger for the effect is on hover, and we set the hovered state which acts like a switch on/off. So when we hover we active this spring, we multiply the positional component by 1.1. So in effect scaling the positions outwards. And then when we don’t hover we go back to the original positions. With all the positional data coming from the group node we passed down.

The pos is defined inside the spring along with settings to enable a smooth transition, we can add some real world forces to the fracture groups. The pos is then destructured out of the spring and can be used on/in a position attribute defined on the group just below.

1import { useFrame } from "@react-three/fiber";
2import { useRef, useState } from "react";
3
4const FracturedMesh = ({ mesh, material = {}, loadingProgress }) => {
5 const meshRef = useRef();
6
7 return (
8 <mesh ref={meshRef} geometry={mesh.geometry}>
9 <meshPhysicalMaterial
10 attach="material"
11 roughness={0}
12 metalness={0}
13 clearcoat={1}
14 emissiveIntensity={0}
15 />
16 </mesh>
17 );
18};
19
20export default FracturedMesh;

The final and last part of the puzzle is the actual definition of the mesh and use of meshPhysicalMaterial. And thats a wrap for the rendering of the cube part.

Postprocessing and bloom

For postprocessing there are two packaged which I used, the postprocessing and the @react-three/postprocessing packages. Postprocessing was originally used with plain threejs and then someone ported them over to allow for use in @react-three/fiber and the @react-three/postprocessing package was born.

The setup for the postprocessing was pretty standard and you can have a pretty low threshold before the effect takes effect and then crank up the intensity abit. The kernelSize will influence how much bloom you get, not 100% sure on this setting but have a play around with it!

1<EffectComposer>
2 <Bloom
3 intensity={3.9} // The bloom intensity.
4 width={size.width} // render width
5 height={size.height} // render height
6 kernelSize={KernelSize.VERY_LARGE} // blur kernel size
7 luminanceThreshold={0.005} // luminance threshold. Raise this value to mask out darker elements in the scene.
8 luminanceSmoothing={0.15} // smoothness of the luminance threshold. Range is [0, 1]
9 />
10</EffectComposer>

The website where I used it

The website which I build was rick3d.com.

And if you go to rick3d.com/3d-world you will see the loader for the scene.

I originally wanted to use godrays from a mesh inside of the cube but I genuinely couldn’t get the @react-three/postprocessing and Godrays effect to work and gave up on that idea. Bloom was the final effect I chose and looks pretty good!

More articles from theFrontDev

How to build environments in unity

A complete guide to getting started with unity to create amazing environments. Along with how to get assets from the unity asset store. Covering post processing, terrain tools and using materials inside of unity.

April 1st, 2023 · 6 min read

How to animate grass and alpha clipped grass in react three fiber using GLSL

This is the perfect way to animate grass which is created using alpha clipped planes using alpha maps. This can easily be adapted to follow an origin or empty, which would signify the wind in a scene. Follow previous tutorials to instance grass onto a plane from blender. This would be the precursor to animating this grass!

March 31st, 2023 · 2 min read
© 2021–2024 theFrontDev
Link to $https://twitter.com/TheFrontDevLink to $https://github.com/Richard-ThompsonLink to $https://www.linkedin.com/in/richard-thompson-248ba3111/