Skip to content

Instantly share code, notes, and snippets.

@jairusjoer
Last active February 19, 2023 22:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jairusjoer/8a9c137ac10dc4b930783b9e8fc46a4b to your computer and use it in GitHub Desktop.
Save jairusjoer/8a9c137ac10dc4b930783b9e8fc46a4b to your computer and use it in GitHub Desktop.
A simple wrapper for three.js to integrate 3D objects into a website
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import {
AnimationMixer,
Clock,
Color,
DirectionalLight,
HemisphereLight,
PerspectiveCamera,
Scene,
WebGLRenderer,
} from "three";
// MODELS ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― //
const Model = ({ file, animate = true }) => {
const mesh = file.scene.children[0];
const mixer = new AnimationMixer(mesh);
if (file.animations[0] && animate) {
const clip = file.animations[0];
const action = mixer.clipAction(clip);
action.play();
}
mesh.tick = (delta) => mixer.update(delta);
return mesh;
};
async function Models(models) {
const loader = new GLTFLoader();
return Promise.all(
models.map(async (model) => {
model.file = await loader.loadAsync(model.file);
const mesh = Model(model);
model.position && mesh.position.set(...model.position);
return mesh;
})
);
}
// RESIZE ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― //
class Resize {
constructor(container, camera, renderer) {
// set initial size
this.set(container, camera, renderer);
window.addEventListener("resize", () => {
// set the size again if a resize occurs
this.set(container, camera, renderer);
// perform any custom actions
this.onResize();
});
}
set(container, camera, renderer) {
camera.aspect = container.clientWidth / container.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(container.clientWidth, container.clientHeight);
renderer.setPixelRatio(window.devicePixelRatio);
}
onResize() {}
}
// LIGHTS ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― //
const Lights = ({ ambient = ["white", "#606060", 5], sources = [] }) => {
ambient = new HemisphereLight(...ambient);
sources = sources.map(({ options, position }) => {
const light = new DirectionalLight(...options);
light.position.set(...position);
return light;
});
return { ambient, sources };
};
// CONTROLS ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― //
const Controls = (
camera,
canvas,
{
damping = true,
zoom = false,
autoRotate = false,
rotate = {
x: true,
y: true,
},
}
) => {
const controls = new OrbitControls(camera, canvas);
controls.enableDamping = damping;
controls.enableZoom = zoom;
controls.autoRotate = autoRotate;
if (rotate.y == false) {
controls.minPolarAngle = Math.PI / 2;
controls.maxPolarAngle = Math.PI / 2;
}
if (rotate.x == false) {
controls.minAzimuthAngle = Math.PI * 2;
controls.maxAzimuthAngle = Math.PI * 2;
}
controls.tick = () => controls.update();
return controls;
};
// LOOP ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― //
const clock = new Clock();
class Loop {
constructor(camera, scene, renderer) {
this.camera = camera;
this.scene = scene;
this.renderer = renderer;
this.updatables = [];
}
start() {
this.renderer.setAnimationLoop(() => {
this.tick();
this.renderer.render(this.scene, this.camera);
});
}
stop() {
this.renderer.setAnimationLoop(null);
}
tick() {
const delta = clock.getDelta();
// console.log(
// `The last frame rendered in ${delta * 1000} milliseconds`,
// );
for (const object of this.updatables) {
object.tick(delta);
}
}
}
// CAMERA ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― //
const Camera = ({ options = [35, 1, 0.1, 100], position = [0, 0, 6.5] }) => {
const camera = new PerspectiveCamera(...options);
camera.position.set(...position);
return camera;
};
// RENDERER ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― //
const Renderer = (canvas) => {
const renderer = new WebGLRenderer({
alpha: true,
antialias: true,
canvas,
});
renderer.physicallyCorrectLights = true;
return renderer;
};
// SCENE ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― //
const Scenery = ({ background = false }) => {
const scene = new Scene();
background && (scene.background = new Color(background));
return scene;
};
// WORLD ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― //
let camera;
let controls;
let renderer;
let scene;
let loop;
class World {
constructor({ canvas, ...options }) {
camera = Camera(options?.camera ?? {});
renderer = Renderer(canvas);
scene = Scenery(options?.scene ?? {});
loop = new Loop(camera, scene, renderer);
controls = Controls(camera, renderer.domElement, options?.controls ?? {});
const { ambient, sources } = Lights(options?.lights ?? {});
scene.add(ambient, ...sources);
loop.updatables.push(controls);
const resize = new Resize(canvas, camera, renderer);
}
async init(models) {
const meshes = await Models(models);
Object.values(meshes).forEach((mesh) => {
loop.updatables.push(mesh);
scene.add(mesh);
});
}
render() {
renderer.render(scene, camera);
}
start() {
loop.start();
}
stop() {
loop.stop();
}
}
// WRAPPER ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― //
const Three = {
async init({ models, animate = true, ...options }) {
const world = new World(options);
await world.init(models);
!animate ? world.render() : world.start();
},
};
export { Three };
import { Three } from "./three";
// INTERFACE ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― //
Three.init({
canvas: document.querySelector("canvas"),
models: [
{
file: "/assets/models/Cube.glb",
},
],
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment