@Created: 29 Oct. 2018 (Unity version)
@Updated: 2023 (Threejs & @react-three/fiber)
@Author: Jason Holt Smith ([email protected])
A QuadSphere (a sphere made up of a mesh cube whose vertices are adjusted into a spherical shape) with QuadTree-based level of detail support.
npm install quadsphere
a @react-three/fiber
compatible THREE.Mesh
that can be used directly from the Canvas
to render a QuadSphere
all @react-three/fiber
MeshProps
are supported in addition to the following:
radius
one half the number of units width of the renderedQuadSphere
(without factoring in any displacement material offsets)segments
the number of divisions plus edges in each direction of eachQuad
of theQuadSphere
(i.e. a value of3
means one division and two edges when all sides activated). values must be odd numbers starting from 3 or greatermaxlevel
the maximum depth minus 1, starting from 0 meaning no subdivision allowed, that eachQuad
in theQuadSphere
can be. a value of 2 means that the top-levelQuad
can be subdivided as can it's child quads, but no further subdivision is allowedtextureMapping
a value of eitherunified
(default) orsplit
which indicates the type of UVs to generate for theQuadSphere
. ifunified
then an unwrapped cube texture should be used example, but ifsplit
then six separate materials should be supplied each mapping to a face of theQuadSphere
in the following order: [positive-x
,negative-x
,positive-y
,negative-y
,positive-z
,negative-z
]onCreateMesh
a function that will be called every time a newTHREE.Mesh
is created (each subdivision or unification results in the need to generate a new mesh otherwise the view will not update)onCreateSphere
a function that will be called every time a newQuadSphereGeometry
is created (changing any of the aboveprops
values results in a newQuadSphereGeometry
being created and the prior being disposed of)
import { Canvas, useLoader } from '@react-three/fiber';
import { QuadSphereMesh } from 'quadsphere';
function App() {
const texture = useLoader(THREE.TextureLoader, `./assets/texture.png`);
const bump = useLoader(THREE.TextureLoader, `./assets/bump.jpg`);
return (
<Canvas>
<QuadSphereMesh
position={[1.2, 0, 0]} {/* x, y, z position */}
radius={1} {/* radius of sphere */}
segments={5} {/* number of edges & divisions in each Quad */}
>
<meshStandardMaterial
map={texture}
displacementMap={bump}
displacementScale={0.2} />
</QuadSphereMesh>
</Canvas>
);
}
import { Canvas, useLoader, useFrame } from '@react-three/fiber';
import { useRef } from 'react';
import { QuadSphereMesh, V3, QuadSphereGeometry } from 'quadsphere';
import * as THREE from 'three';
function distanceCheck(sphereMeshRef: THREE.Mesh, worldLocation: V3) {
const distanceValues = new Array<number>(5, 4, 3, 2, 1, 0);
const geom = sphereMeshRef.geometry as QuadSphereGeometry;
// convert camera location to location relative to QuadSphere
const localLocation = new THREE.Vector3(worldLocation.x, worldLocation.y, worldLocation.z)
.sub(sphereMeshRef.position)
.applyQuaternion(sphereMeshRef.quaternion.invert());
let updated = false;
for (let i=0; i<distanceValues.length; i++) {
const dist = distanceValues[i];
const levelQuads = geom.sphere.registry.getQuadsAtLevel(i);
if (levelQuads.length > 0) {
const inRange = levelQuads.filter(q => geom.sphere.utils.isWithinDistance(q, dist, localLocation));
if (inRange.length > 0) {
const closest = geom.sphere.utils.getClosestQuad(localLocation, false, ...inRange);
closest.subdivide();
levelQuads.filter(q => q.id !== closest.id).forEach(q => q.unify());
} else {
levelQuads.forEach(q => q.unify());
}
updated = true;
}
}
if (updated) {
geom.updateAttributes();
}
}
function App() {
const texture = useLoader(THREE.TextureLoader, `./assets/texture.png`);
const bump = useLoader(THREE.TextureLoader, `./assets/bump.jpg`);
const ref = useRef<THREE.Mesh>(null);
useFrame((state: RootState, delta: number) => {
// check distance from camera to QuadSphere and subdivide when within range
distanceCheck(ref.current, state.camera.position);
});
return (
<Canvas>
<QuadSphereMesh
ref={ref} {/* provides access to QuadSphereGeometry and QuadSphere */}
position={[1.2, 0, 0]} {/* x, y, z position */}
radius={1} {/* radius of sphere */}
segments={5} {/* number of edges + divisions in each Quad */}
>
<meshStandardMaterial
map={texture}
displacementMap={bump}
displacementScale={0.2}
/>
</QuadSphereMesh>
</Canvas>
);
}
by default the QuadSphere
uses a unified
texture mapping that assumes all faces are contained in a single unwrapped cube texture that
like the following example, but you can also assign each face individually by specifying a textureMapping
of
split
import { Canvas, useLoader } from '@react-three/fiber';
import { QuadSphereMesh } from 'quadsphere';
function App() {
return (
<Canvas>
<QuadSphereMesh
position={[1.2, 0, 0]} {/* x, y, z position */}
radius={1} {/* radius of sphere */}
segments={5} {/* number of edges & divisions in each Quad */}
textureMapping="split" {/* indicates that texture groups should be used */}
>
<meshBasicMaterial attach="material-0" color="red" /* px */ />
<meshBasicMaterial attach="material-1" color="blue" /* nx */ />
<meshBasicMaterial attach="material-2" color="green" /* py */ />
<meshBasicMaterial attach="material-3" color="purple" /* ny */ />
<meshBasicMaterial attach="material-4" color="white" /* pz */ />
<meshBasicMaterial attach="material-5" color="black" /* nz */ />
</QuadSphereMesh>
</Canvas>
);
}
a @react-three/fiber
compatible THREE.Mesh
that can be used directly from the Canvas
to render one face of the QuadSphere
all QuadSphereMesh
properties are supported in addition to the following:
applyCurve
iftrue
then the renderedQuad
will be curved based on the distance of thecentre
property from thecurveOrigin
.centre
aV3
made up of anx
,y
, andz
number as an offset from thecurveOrigin
. Note that theTHREE.Mesh
position
property is used for position and thecentre
is only used if you need to useapplyCurve
so you can offset from the origin to ensure the curve is applied to round the mesh instead of creating a flattened circlecurveOrigin
the location to move out from byradius
amount when applying the curveuvStart
aV2
containing au
andv
number representing the starting offset for uvs in thisQuadMesh
(defaults to{u: 0, v: 0}
)uvEnd
aV2
containing au
andv
number representing the ending offset for uvs in thisQuadMesh
(defaults to{u: 1, v: 1}
)
a THREE.js
compatible BufferGeometry
that can be passed to a THREE.Mesh
. all properties available to a QuadSphereMesh
and that aren't part of the @react-three/fiber
MeshProps
is available as a contructor argument
subdivide
takes in aV3
specifying a local-space translatedV3
and will find the closestQuad
or childQuad
and call thesubdivide
function on itunify
takes in aV3
specifying a local-space translatedV3
and will find the closestQuad
or childQuad
and call theunify
function on it's parentupdateAttributes
automatically called after callingsubdivide
orunify
, this enables you to force an update of the vertices, normals, uvs and indices of theBufferGeometry
a THREE.js
compatible BufferGeometry
that can be passed to a THREE.Mesh
. all properties available to a QuadMesh
and that aren't part of the @react-three/fiber
MeshProps
is available as a contructor argument
same as QuadSphereGeometry
an implementation of a CubeSphere that uses Quad-Tree subdivision algorithm to increase the mesh complexity near a specific area with minimal increase to the overall mesh complexity as you move away from the specified location. Having this object separated from the specific rendering technology, namely THREE.js
allows it to be used with other renderers if so desired
a single face used in the QuadSphere
. this can be used to further reduce the complexity of a scene by removing other, unseen faces of the sphere when up close or can be used in a flattened mode as a Terrain implementation that supports the same level of detail adjustments as the QuadSphere