Files

99 lines
3.7 KiB
JavaScript

function init(context, state) {
const asset = Array.isArray(context.assets) ? context.assets[0] : null;
const THREE = globalThis.THREE;
if (!asset?.url || !THREE) {
state.error = "Three.js dependencies are unavailable or no model attachment was found.";
return;
}
const glCanvas = new OffscreenCanvas(context.width || 1, context.height || 1);
const renderer = new THREE.WebGLRenderer({ canvas: glCanvas, alpha: true, antialias: true });
renderer.setPixelRatio(1);
renderer.setClearColor(0x000000, 0);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(35, 1, 0.1, 100);
const ambient = new THREE.AmbientLight(0xffffff, 0.85);
const directional = new THREE.DirectionalLight(0xffffff, 0.65);
directional.position.set(1, 1, 1);
scene.add(ambient);
scene.add(directional);
const lowerUrl = asset.url.toLowerCase();
const isObj = asset.mediaType === "model/obj" || lowerUrl.endsWith(".obj");
const loader = isObj && typeof THREE.OBJLoader === "function" ? new THREE.OBJLoader() : new THREE.GLTFLoader();
fetch(asset.url)
.then((response) => {
if (!response.ok) {
throw new Error("Unable to fetch model");
}
return isObj ? response.text() : response.arrayBuffer();
})
.then((data) => {
if (isObj) {
return loader.parse(data);
}
return new Promise((resolve, reject) => {
loader.parse(data, "", resolve, reject);
});
})
.then((result) => {
const model = isObj ? result : result.scene || result.scenes?.[0];
if (!model) {
state.error = "Failed to read model data.";
return;
}
const box = new THREE.Box3().setFromObject(model);
const size = box.getSize(new THREE.Vector3());
const center = box.getCenter(new THREE.Vector3());
model.position.sub(center);
const maxDim = Math.max(size.x, size.y, size.z) || 1;
const scale = 1.5 / maxDim;
model.scale.setScalar(scale);
scene.add(model);
const distance = maxDim * 2.2;
camera.position.set(0, 0, distance);
camera.near = Math.max(0.01, distance / 100);
camera.far = distance * 100;
camera.updateProjectionMatrix();
state.model = model;
})
.catch((error) => {
state.error = error?.message || "Failed to load model.";
});
state.glCanvas = glCanvas;
state.renderer = renderer;
state.scene = scene;
state.camera = camera;
state.rotation = 0;
}
function tick(context, state) {
const { ctx, width, height, deltaMs } = context;
if (!ctx || !state.renderer || !state.scene || !state.camera || !state.glCanvas) {
return;
}
const nextWidth = Math.max(1, Math.round(width || 1));
const nextHeight = Math.max(1, Math.round(height || 1));
if (state.glCanvas.width !== nextWidth || state.glCanvas.height !== nextHeight) {
state.glCanvas.width = nextWidth;
state.glCanvas.height = nextHeight;
state.renderer.setSize(nextWidth, nextHeight, false);
state.camera.aspect = nextWidth / nextHeight;
state.camera.updateProjectionMatrix();
}
ctx.clearRect(0, 0, nextWidth, nextHeight);
if (state.model) {
state.rotation = (state.rotation + (deltaMs || 0) * 0.0008) % (Math.PI * 2);
state.model.rotation.y = state.rotation;
}
state.renderer.render(state.scene, state.camera);
ctx.drawImage(state.glCanvas, 0, 0, nextWidth, nextHeight);
}