Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- "use client";
- import { useEffect, useRef } from "react";
- import {
- Engine,
- Scene,
- ArcRotateCamera,
- Vector3,
- HemisphericLight,
- MeshBuilder,
- StandardMaterial,
- Texture,
- Vector4,
- Mesh,
- } from "@babylonjs/core";
- // Enhanced Molang implementation with proper math functions
- class Molang {
- private context: any;
- private options: any;
- constructor(context: any = {}, options: any = {}) {
- this.context = context;
- this.options = options;
- }
- execute(expression: any, time: number = 0): number {
- if (typeof expression === "number") return expression;
- if (typeof expression !== "string") return 0;
- // Replace Molang variables and functions with JavaScript equivalents
- let jsExpression = expression
- .replace(/q\.anim_time/g, time.toString())
- .replace(/math\.sin/g, "Math.sin")
- .replace(/math\.cos/g, "Math.cos")
- .replace(/math\.clamp/g, "Math.min(Math.max")
- .replace(/math\.min/g, "Math.min")
- .replace(/math\.max/g, "Math.max")
- .replace(/math\.abs/g, "Math.abs")
- .replace(/math\.floor/g, "Math.floor")
- .replace(/math\.ceil/g, "Math.ceil");
- // Handle math.clamp function properly - convert clamp(value, min, max) to Math.min(Math.max(value, min), max)
- jsExpression = jsExpression.replace(
- /Math\.min\(Math\.max\(([^,]+),([^,]+),([^)]+)\)/g,
- "Math.min(Math.max($1, $2), $3)"
- );
- try {
- // Safely evaluate the expression
- return Function(`"use strict"; return (${jsExpression})`)();
- } catch (error) {
- console.warn("Failed to evaluate Molang expression:", expression, error);
- return 0;
- }
- }
- }
- // Extend Mesh type to include userData
- interface ExtendedMesh extends Mesh {
- userData?: any;
- }
- // Define props interface for the component
- interface BlockbenchRendererProps {
- pokemonName: string;
- geoData: any;
- animData: any;
- size?: { width: number; height: number };
- }
- export default function BlockbenchRenderer({
- pokemonName,
- geoData,
- animData,
- size = { width: 150, height: 150 },
- }: BlockbenchRendererProps) {
- const canvasRef = useRef<HTMLCanvasElement>(null);
- const pokemon = pokemonName;
- const doPosAnim = true;
- const doRotAnim = true;
- useEffect(() => {
- if (!canvasRef.current || !geoData || !animData) return;
- const engine = new Engine(canvasRef.current, true, {
- preserveDrawingBuffer: true,
- stencil: true,
- });
- const scene = new Scene(engine);
- // Helper functions
- const toRadians = (degrees: number) => degrees * (Math.PI / 180);
- const buildFaceUv = (
- x1: number,
- y1: number,
- x2: number,
- y2: number,
- tw: number,
- th: number
- ) => {
- return new Vector4(x1 / tw, (th - y2) / th, x2 / tw, (th - y1) / th);
- };
- const buildTexture = ({ url }: { url: string }) => {
- const mat = new StandardMaterial("mat", scene);
- const texture = new Texture(url, scene, {
- noMipmap: true,
- samplingMode: Texture.NEAREST_SAMPLINGMODE,
- });
- mat.diffuseTexture = texture;
- texture.hasAlpha = true;
- return mat;
- };
- const buildCube = ({
- size = [0.1, 0.1, 0.1],
- position = [0, 0, 0],
- rotation = [0, 0, 0],
- uv,
- textureSize,
- texture,
- inflate = 0,
- name,
- }: {
- size?: number[];
- position?: number[];
- rotation?: number[];
- uv?: number[];
- textureSize?: number[];
- texture?: StandardMaterial;
- inflate?: number;
- name?: string;
- } = {}) => {
- const [sx, sy, sz] = size;
- const rsx = sx || 0.01;
- const rsy = sy || 0.01;
- const rsz = sz || 0.01;
- let faceUv;
- if (uv && textureSize) {
- const [ox, oy] = uv;
- const [tw, th] = textureSize;
- faceUv = [];
- faceUv[0] = buildFaceUv(
- ox + sx + 2 * sz,
- oy + sz,
- ox + 2 * sx + 2 * sz,
- oy + sz + sy,
- tw,
- th
- );
- faceUv[1] = buildFaceUv(
- ox + sz,
- oy + sz,
- ox + sz + sx,
- oy + sz + sy,
- tw,
- th
- );
- faceUv[2] = buildFaceUv(
- ox + sz + sx,
- oy + sz,
- ox + 2 * sz + sx,
- oy + sz + sy,
- tw,
- th
- );
- faceUv[3] = buildFaceUv(ox, oy + sz, ox + sz, oy + sz + sy, tw, th);
- faceUv[4] = buildFaceUv(ox + sz, oy, ox + sz + sx, oy + sz, tw, th);
- faceUv[5] = buildFaceUv(
- ox + sz + sx,
- oy + sz,
- ox + sz + 2 * sx,
- oy,
- tw,
- th
- );
- }
- const actualInflate = inflate + 1;
- const box = MeshBuilder.CreateBox(
- name || "box",
- {
- faceUV: faceUv,
- wrap: true,
- width: rsx,
- height: rsy,
- depth: rsz,
- },
- scene
- ) as ExtendedMesh;
- if (texture) {
- box.material = texture;
- }
- box.position.x = position[0] + rsx / 2;
- box.position.y = position[1] + rsy / 2;
- box.position.z = position[2] + rsz / 2;
- box.rotation.x = toRadians(rotation[0]);
- box.rotation.y = toRadians(rotation[1]);
- box.rotation.z = toRadians(rotation[2]);
- box.scaling.setAll(actualInflate);
- return box;
- };
- const maybeMolang = (v: any, molang: Molang, time: number) => {
- return typeof v === "number" ? v : molang.execute(v, time);
- };
- const evalAnimPart = (
- animPart: any,
- molang: Molang,
- time: number
- ): number[] => {
- if (Array.isArray(animPart)) {
- const [rx, ry, rz] = animPart;
- const result = [
- maybeMolang(rx, molang, time),
- maybeMolang(ry, molang, time),
- maybeMolang(rz, molang, time),
- ];
- return result;
- } else if (typeof animPart === "object" && animPart !== null) {
- const keys = Object.keys(animPart)
- .map(Number)
- .sort((a, b) => a - b);
- if (keys.length === 0) return [0, 0, 0];
- const maxTime = keys[keys.length - 1];
- const currentTime = time % maxTime;
- let prevKeyIndex = 0;
- for (let i = 0; i < keys.length; i++) {
- if (keys[i] <= currentTime) {
- prevKeyIndex = i;
- } else {
- break;
- }
- }
- const prevKey = keys[prevKeyIndex];
- const nextKey = keys[(prevKeyIndex + 1) % keys.length] || prevKey;
- const prevValue = animPart[prevKey];
- const nextValue = animPart[nextKey];
- if (!Array.isArray(prevValue) || !Array.isArray(nextValue)) {
- if (Array.isArray(prevValue)) return prevValue;
- if (Array.isArray(nextValue)) return nextValue;
- return [0, 0, 0];
- }
- if (prevKey === nextKey || currentTime === prevKey) {
- return [
- maybeMolang(prevValue[0], molang, time),
- maybeMolang(prevValue[1], molang, time),
- maybeMolang(prevValue[2], molang, time),
- ];
- }
- let alpha = (currentTime - prevKey) / (nextKey - prevKey);
- if (nextKey < prevKey) {
- alpha = (currentTime - prevKey) / (maxTime - prevKey);
- }
- alpha = Math.max(0, Math.min(1, alpha));
- return [
- maybeMolang(prevValue[0], molang, time) +
- (maybeMolang(nextValue[0], molang, time) -
- maybeMolang(prevValue[0], molang, time)) *
- alpha,
- maybeMolang(prevValue[1], molang, time) +
- (maybeMolang(nextValue[1], molang, time) -
- maybeMolang(prevValue[1], molang, time)) *
- alpha,
- maybeMolang(prevValue[2], molang, time) +
- (maybeMolang(nextValue[2], molang, time) -
- maybeMolang(prevValue[2], molang, time)) *
- alpha,
- ];
- }
- return [0, 0, 0];
- };
- function applyAnimation(
- model: ExtendedMesh,
- animName: string,
- molang: Molang,
- boneCubes: Record<string, ExtendedMesh>,
- time: number
- ) {
- if (!model?.userData?.anims) {
- return;
- }
- const anims = model.userData.anims;
- const animEntry = Object.entries(anims.animations).find(
- ([k]) => k.split(".").pop() === animName
- ) as [string, any] | undefined;
- if (!animEntry?.[1]?.bones) {
- return;
- }
- const anim = animEntry[1];
- Object.entries(anim.bones).forEach(
- ([boneName, boneAnim]: [string, any]) => {
- const targetBone = boneCubes[boneName];
- if (!targetBone) {
- return;
- }
- if (boneAnim.rotation && doRotAnim) {
- const [frx, fry, frz] = evalAnimPart(
- boneAnim.rotation,
- molang,
- time
- );
- const [brx, bry, brz] = targetBone.userData?.baseRot || [0, 0, 0];
- // Debug logging for problematic bones
- if (boneName === "head" || boneName === "torso") {
- console.log(`${boneName} rotation:`, {
- base: [brx, bry, brz],
- anim: [frx, fry, frz],
- final: [brx + frx, bry + fry, brz + frz],
- });
- }
- targetBone.rotation.x = toRadians(brx + frx);
- targetBone.rotation.y = toRadians(bry + fry);
- targetBone.rotation.z = toRadians(brz + frz);
- }
- if (boneAnim.position && doPosAnim) {
- const [frx, fry, frz] = evalAnimPart(
- boneAnim.position,
- molang,
- time
- );
- const [brx, bry, brz] = targetBone.userData?.basePos || [0, 0, 0];
- // Debug logging for problematic bones
- if (boneName === "head" || boneName === "torso") {
- console.log(`${boneName} position:`, {
- base: [brx, bry, brz],
- anim: [frx, fry, frz],
- final: [brx + frx, bry + fry, brz + frz],
- });
- }
- targetBone.position.x = brx + frx;
- targetBone.position.y = bry + fry;
- targetBone.position.z = brz + frz;
- }
- }
- );
- }
- const light = new HemisphericLight("light", new Vector3(1, 3, -1), scene);
- // Get the geometry identifier to determine texture name
- const geometryIdentifier =
- geoData["minecraft:geometry"][0]?.description?.identifier || "";
- const geometryName =
- geometryIdentifier.split(".").pop() || pokemon.split("_")[1];
- const texture = buildTexture({
- url: `../../3dmons/textures/${pokemon}/${geometryName}.png`.toLowerCase(),
- });
- const boneCubes: Record<string, ExtendedMesh> = {};
- const buildModel = (geo: any, anims: any, texture: StandardMaterial) => {
- const relevantGeoData = geo["minecraft:geometry"][0];
- const bones = relevantGeoData.bones;
- const textureSize = [
- relevantGeoData.description.texture_width,
- relevantGeoData.description.texture_height,
- ];
- console.log("Model info:", {
- totalBones: bones.length,
- textureSize,
- boneNames: bones.map((b: any) => b.name),
- });
- bones.forEach(
- ({ name, pivot, parent, rotation = [0, 0, 0], cubes = [] }: any) => {
- const [pX, pY, pZ] = pivot;
- const boneCube = buildCube({
- position: [pX, pY, pZ],
- rotation,
- name,
- });
- // Initialize userData
- boneCube.userData = boneCube.userData || {};
- boneCube.userData.state = { pivot, rotation };
- const parentBoneCube = boneCubes[parent];
- boneCubes[name] = boneCube;
- // Debug bone hierarchy
- console.log(`Bone "${name}":`, {
- parent: parent || "ROOT",
- pivot: [pX, pY, pZ],
- rotation,
- cubesCount: cubes.length,
- });
- if (!parentBoneCube) {
- return;
- }
- cubes.forEach(
- (
- { origin, size, uv, rotation, inflate = 0 }: any,
- index: number
- ) => {
- const cube = buildCube({
- size: size.map((e: number) => (e ? e : 0.01)),
- position: origin,
- rotation,
- texture,
- textureSize,
- uv,
- inflate,
- name: `${name}-${index}`,
- });
- boneCube.addChild(cube);
- }
- );
- parentBoneCube.addChild(boneCube);
- }
- );
- const rootBoneName = bones.find(({ parent }: any) => !parent).name;
- const rootBone = boneCubes[rootBoneName];
- // Initialize userData
- rootBone.userData = rootBone.userData || {};
- rootBone.userData.anims = anims;
- rootBone.userData.boneCubes = boneCubes;
- // Store base positions and rotations for each bone
- Object.entries(boneCubes).forEach(([name, bone]) => {
- bone.userData = bone.userData || {};
- bone.userData.basePos = [
- bone.position.x,
- bone.position.y,
- bone.position.z,
- ];
- bone.userData.baseRot = [
- bone.rotation.x,
- bone.rotation.y,
- bone.rotation.z,
- ];
- });
- return rootBone;
- };
- const model = buildModel(geoData, animData, texture);
- scene.addMesh(model);
- model.position.y = -15;
- // Force update bounding info after model is fully built
- model.refreshBoundingInfo();
- scene.registerBeforeRender(() => {}); // Force one frame to ensure everything is calculated
- // Calculate optimal camera distance based on model size
- const boundingInfo = model.getBoundingInfo();
- const boundingBox = boundingInfo.boundingBox;
- const modelSize = boundingBox.maximumWorld.subtract(
- boundingBox.minimumWorld
- );
- const maxDimension = Math.max(modelSize.x, modelSize.y, modelSize.z);
- // Much more aggressive zoom out calculation
- const baseDistance = maxDimension * 8; // Simple multiplier approach
- const finalDistance = Math.max(baseDistance, 50); // Much higher minimum distance
- console.log("🔍 Model sizing debug:", {
- boundingBox: {
- min: boundingBox.minimumWorld.toString(),
- max: boundingBox.maximumWorld.toString(),
- size: modelSize.toString(),
- },
- maxDimension,
- baseDistance,
- finalDistance,
- modelPosition: model.position.toString(),
- });
- // Create camera centered on the model's position with calculated distance
- const camera = new ArcRotateCamera(
- "Camera",
- -Math.PI / 2,
- Math.PI / 3,
- finalDistance, // Use our calculated distance without any override
- new Vector3(0, -1, 0), // Target where the model actually is
- scene
- );
- camera.attachControl(canvasRef.current, true);
- // Animation loop with slower timing
- let time = 0;
- let currentAnimationIndex = 0;
- let animationDuration = 8; // Increased from 5 to 8 seconds per animation
- let availableAnimations: string[] = [];
- scene.registerBeforeRender(() => {
- if (model?.userData?.anims) {
- if (availableAnimations.length === 0) {
- availableAnimations = Object.keys(
- model.userData.anims.animations
- ).map((name) => {
- return name.split(".").pop() || name;
- });
- console.log("Available animations:", availableAnimations);
- }
- const currentAnimTime =
- time % (animationDuration * availableAnimations.length);
- const newAnimIndex = Math.floor(currentAnimTime / animationDuration);
- if (newAnimIndex !== currentAnimationIndex) {
- currentAnimationIndex = newAnimIndex;
- console.log(
- `🎬 Now playing: ${availableAnimations[currentAnimationIndex]}`
- );
- }
- const currentAnim = availableAnimations[currentAnimationIndex];
- // Apply the animation
- const molang = new Molang();
- applyAnimation(model, currentAnim, molang, boneCubes, time);
- // Slower rotation for easier observation
- model.rotation.y += 0.001;
- time += 0.000125; // Ultra slow - 1/8 of previous speed (128x slower than original)
- }
- });
- engine.runRenderLoop(() => {
- scene.render();
- });
- const handleResize = () => {
- engine.resize();
- };
- window.addEventListener("resize", handleResize);
- return () => {
- window.removeEventListener("resize", handleResize);
- engine.dispose();
- scene.dispose();
- };
- }, [pokemonName, geoData, animData]);
- return (
- <canvas
- ref={canvasRef}
- style={{
- width: `${size.width}px`,
- height: `${size.height}px`,
- display: "block",
- }}
- />
- );
- }
Add Comment
Please, Sign In to add comment