Download sd-3dmodel-loader/src/ThreeJsScene.js from toto10/extensions: direct link, hf CLI and curl.
- Browser
- Download file 21.8 kB
-
https://huggingface.co/toto10/extensions/resolve/refs%2Fpr%2F4/sd-3dmodel-loader/src/ThreeJsScene.js
- Command line
-
hf download hf://toto10/extensions@refs/pr/4/sd-3dmodel-loader/src/ThreeJsScene.js
-
curl -L -o ThreeJsScene.js https://huggingface.co/toto10/extensions/resolve/refs%2Fpr%2F4/sd-3dmodel-loader/src/ThreeJsScene.js
21.8 kB
| import React, {useRef, useEffect} from 'react'; | |
| import {OrbitControls} from 'three/addons/controls/OrbitControls.js'; | |
| import * as THREE from 'three'; | |
| import {TransformControls} from 'three/examples/jsm/controls/TransformControls'; | |
| import {OBJLoader} from 'three/addons/loaders/OBJLoader.js'; | |
| import {STLLoader} from 'three/addons/loaders/STLLoader.js'; | |
| import {FBXLoader} from 'three/addons/loaders/FBXLoader.js'; | |
| import {DRACOLoader} from 'three/addons/loaders/DRACOLoader.js'; | |
| import {GLTFLoader} from 'three/addons/loaders/GLTFLoader.js'; | |
| import {ColladaLoader} from 'three/addons/loaders/ColladaLoader.js'; | |
| import {VRMLoaderPlugin, VRMUtils} from "@pixiv/three-vrm"; | |
| import {setAnimationProgress} from './AnimationPanel' | |
| let _playing = true; | |
| let _renderer; | |
| let _container; | |
| let _camera; | |
| let _groundMesh; | |
| let _groundGrid; | |
| let _axis; | |
| let _action; | |
| let _mixer; | |
| let _totalTime; | |
| let _currentTime; | |
| let _progress; | |
| let _controlByProgressBar; | |
| let _secondCamera; | |
| let _scene; | |
| let previewWidth = 300; | |
| let previewHeight = 300; | |
| let _width; | |
| let _height; | |
| let _transformControls; | |
| let _orbitController; | |
| let _selectedObject; | |
| let _currentVRM; | |
| let _mainObjectCounter = 1; | |
| export function setPreviewSize(previewSize) { | |
| if (previewSize === "1:1") { | |
| previewWidth = 300; | |
| previewHeight = 300; | |
| } else if (previewSize === "2:3") { | |
| previewWidth = 300; | |
| previewHeight = 450; | |
| } else if (previewSize === "3:2") { | |
| previewWidth = 450; | |
| previewHeight = 300; | |
| } | |
| } | |
| export function removeObject(objName) { | |
| const object = _scene.getObjectByName(objName); | |
| if (object) { | |
| _scene.remove(object); | |
| _transformControls.detach(); | |
| } | |
| if (_currentVRM && _currentVRM.scene.name === objName) { | |
| _currentVRM = undefined; | |
| } | |
| } | |
| export function handlePoseSelectedObject(objName, transformControlsMode) { | |
| if (transformControlsMode && transformControlsMode !== "none") { | |
| if (_currentVRM) { | |
| const boneNode = _currentVRM.humanoid.getNormalizedBoneNode(objName); | |
| if (boneNode) { | |
| _transformControls.setMode(transformControlsMode); | |
| _transformControls.attach(boneNode); | |
| } | |
| } | |
| } else { | |
| _transformControls.detach(); | |
| } | |
| } | |
| export function handleSelectedObject(objName, transformControlsMode) { | |
| if (objName !== "Scene" && transformControlsMode && transformControlsMode !== "none") { | |
| _selectedObject = _scene.getObjectByName(objName); | |
| _transformControls.setMode(transformControlsMode); | |
| _transformControls.attach(_selectedObject); | |
| } else { | |
| _transformControls.detach(); | |
| } | |
| } | |
| export function refreshSceneTree() { | |
| window.updateObjects(convertThreeJsObjects()); | |
| } | |
| export function setFar(far) { | |
| _camera.far = far; | |
| _camera.updateProjectionMatrix(); | |
| _secondCamera.far = far; | |
| _secondCamera.updateProjectionMatrix(); | |
| } | |
| export function setNear(near) { | |
| _camera.near = near; | |
| _camera.updateProjectionMatrix(); | |
| _secondCamera.near = near; | |
| _secondCamera.updateProjectionMatrix(); | |
| } | |
| export function setFOV(fov) { | |
| _camera.fov = fov; | |
| _camera.updateProjectionMatrix(); | |
| _secondCamera.fov = fov; | |
| _secondCamera.updateProjectionMatrix(); | |
| } | |
| const observer = new IntersectionObserver((entries, observer) => { | |
| entries.forEach(entry => { | |
| if (entry.isIntersecting) { | |
| onWindowResize(); | |
| } | |
| }); | |
| }); | |
| const onWindowResize = () => { | |
| _width = _container.clientWidth; | |
| _height = _container.clientHeight; | |
| _camera.aspect = _width / _height; | |
| _camera.updateProjectionMatrix(); | |
| _renderer.setSize(_width, _height); | |
| }; | |
| function checkDivVisible(div) { | |
| if ((div.offsetWidth > 0) && (div.offsetHeight > 0)) { | |
| return true; | |
| } | |
| return false; | |
| } | |
| function ThreeJsScene({uploadedModelFile}) { | |
| const containerRef = useRef(); | |
| const managerRef = useRef(); | |
| const sceneRef = useRef(); | |
| useEffect(() => { | |
| _container = containerRef.current; | |
| _renderer = new THREE.WebGLRenderer({antialias: true, preserveDrawingBuffer: true,}); | |
| _renderer.domElement.style.width = '100%'; | |
| _renderer.domElement.style.height = '100%'; | |
| _container.appendChild(_renderer.domElement); | |
| observer.observe(_container); | |
| _scene = new THREE.Scene(); | |
| _scene.name = "Scene"; | |
| sceneRef.current = _scene; | |
| _width = _container.clientWidth; | |
| _height = _container.clientHeight; | |
| managerRef.current = new THREE.LoadingManager(); | |
| const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x444444); | |
| hemisphereLight.name = "Hemisphere Light"; | |
| hemisphereLight.position.set(0, 200, 0); | |
| _scene.add(hemisphereLight); | |
| const directionalLight = new THREE.DirectionalLight(0xffffff); | |
| directionalLight.name = "Directional Light"; | |
| directionalLight.position.set(0, 200, 100); | |
| directionalLight.castShadow = true; | |
| directionalLight.shadow.camera.top = 180; | |
| directionalLight.shadow.camera.bottom = -100; | |
| directionalLight.shadow.camera.left = -120; | |
| directionalLight.shadow.camera.right = 120; | |
| _scene.add(directionalLight); | |
| _camera = new THREE.PerspectiveCamera(45, _width / _height, 0.1, 1000); | |
| _camera.position.x = -30; | |
| _camera.position.y = 40; | |
| _camera.position.z = 30; | |
| _camera.lookAt(_scene.position); | |
| _renderer.setSize(_width, _height); | |
| const groundMaterial = new THREE.MeshBasicMaterial({color: 0xbebebe}); | |
| _groundMesh = new THREE.Mesh(new THREE.PlaneBufferGeometry(2000, 2000), groundMaterial); | |
| _groundMesh.name = "Ground"; | |
| _groundMesh.rotation.x = -Math.PI / 2; | |
| _groundMesh.receiveShadow = true; | |
| _scene.add(_groundMesh); | |
| _groundGrid = new THREE.GridHelper(2000, 20, 0x000000, 0x000000); | |
| _groundGrid.name = "Grid"; | |
| _groundGrid.material.opacity = 0.2; | |
| _groundGrid.material.transparent = true; | |
| _scene.add(_groundGrid); | |
| _axis = new THREE.AxesHelper(2000); | |
| _axis.name = "Axis"; | |
| _scene.add(_axis); | |
| _orbitController = new OrbitControls(_camera, _renderer.domElement); | |
| _orbitController.mouseButtons = {LEFT: THREE.MOUSE.ROTATE, MIDDLE: THREE.MOUSE.PAN, RIGHT: THREE.MOUSE.PAN}; | |
| const clock = new THREE.Clock(); | |
| _secondCamera = new THREE.PerspectiveCamera(45, previewWidth / previewHeight, 0.1, 1000); | |
| _secondCamera.name = "Preview Camera"; | |
| _secondCamera.position.copy(_camera.position); | |
| _secondCamera.quaternion.copy(_camera.quaternion); | |
| const orbitController2 = new OrbitControls(_secondCamera, _renderer.domElement); | |
| orbitController2.mouseButtons = {LEFT: THREE.MOUSE.ROTATE, MIDDLE: THREE.MOUSE.PAN, RIGHT: THREE.MOUSE.PAN}; | |
| _scene.add(_secondCamera); | |
| _transformControls = new TransformControls(_camera, _renderer.domElement); | |
| _transformControls.name = "Transform Controls"; | |
| _transformControls.addEventListener('dragging-changed', (event) => { | |
| _orbitController.enabled = !event.value; | |
| orbitController2.enabled = !event.value; | |
| }); | |
| _scene.add(_transformControls); | |
| const animate = () => { | |
| requestAnimationFrame(animate); | |
| if (checkDivVisible(_container)) { | |
| _orbitController.update(); | |
| let delta = clock.getDelta(); | |
| if (_mixer && _playing) { | |
| _currentTime = _mixer.time % _totalTime; | |
| _progress = ((_currentTime / _totalTime) * 100).toFixed(1); | |
| setAnimationProgress(_progress); | |
| } | |
| if (_mixer && (_playing || _controlByProgressBar)) { | |
| _mixer.update(delta); | |
| if (!_currentVRM) { | |
| _controlByProgressBar = false; | |
| } | |
| } | |
| if ((_currentVRM && _playing) || (_currentVRM && _controlByProgressBar)) { | |
| _currentVRM.update(delta); | |
| _controlByProgressBar = false; | |
| } | |
| _renderer.setViewport(0, 0, _container.clientWidth, _container.clientHeight); | |
| _renderer.setScissor(0, 0, _container.clientWidth, _container.clientHeight); | |
| _renderer.setScissorTest(true); | |
| _camera.updateProjectionMatrix(); | |
| _renderer.render(_scene, _camera); | |
| orbitController2.update() | |
| _renderer.setViewport(0, 0, previewWidth, previewHeight); | |
| _renderer.setScissor(0, 0, previewWidth, previewHeight); | |
| _renderer.setScissorTest(true); | |
| _secondCamera.aspect = previewWidth / previewHeight; | |
| _secondCamera.updateProjectionMatrix(); | |
| _renderer.render(_scene, _secondCamera); | |
| } | |
| }; | |
| if (window.updateObjects) { | |
| window.updateObjects(convertThreeJsObjects()); | |
| } | |
| window.addEventListener('resize', onWindowResize); | |
| animate(); | |
| return () => { | |
| // 组件卸载时的清理操作 | |
| window.removeEventListener('resize', onWindowResize); | |
| }; | |
| }, []); | |
| function isGLTF1(contents) { | |
| let resultContent; | |
| if (typeof contents === 'string') { | |
| resultContent = contents; | |
| } else { | |
| const magic = THREE.LoaderUtils.decodeText(new Uint8Array(contents, 0, 4)); | |
| if (magic === 'glTF') { | |
| const version = new DataView(contents).getUint32(4, true); | |
| return version < 2; | |
| } else { | |
| resultContent = THREE.LoaderUtils.decodeText(new Uint8Array(contents)); | |
| } | |
| } | |
| const json = JSON.parse(resultContent); | |
| return (json.asset !== undefined && json.asset.version[0] < 2); | |
| } | |
| function loadOBJ(event) { | |
| const contents = event.target.result; | |
| const mainObject = new OBJLoader().parse(contents); | |
| mainObject.name = "mainObject" + _mainObjectCounter.toString(); | |
| _mainObjectCounter++; | |
| scaleObjectToProper(mainObject); | |
| _scene.add(mainObject); | |
| window.updateObjects(convertThreeJsObjects()); | |
| } | |
| function loadSTL(event) { | |
| const contents = event.target.result; | |
| const geometry = new STLLoader().parse(contents); | |
| const material = new THREE.MeshStandardMaterial(); | |
| geometry.sourceType = "stl"; | |
| //geometry.sourceFile = file.name; | |
| const mainObject = new THREE.Mesh(geometry, material); | |
| mainObject.name = "mainObject" + _mainObjectCounter.toString(); | |
| _mainObjectCounter++; | |
| scaleObjectToProper(mainObject); | |
| _scene.add(mainObject); | |
| window.updateObjects(convertThreeJsObjects()); | |
| } | |
| // eslint-disable-next-line react-hooks/exhaustive-deps | |
| function loadFBX(event) { | |
| const contents = event.target.result; | |
| const loader = new FBXLoader(managerRef.current); | |
| const mainObject = loader.parse(contents); | |
| _mixer = new THREE.AnimationMixer(mainObject); | |
| if (mainObject.animations[0]) { | |
| _action = _mixer.clipAction(mainObject.animations[0]); | |
| if (_playing) { | |
| _action.play(); | |
| } | |
| const clip = _action.getClip(); | |
| _totalTime = clip.duration; | |
| } | |
| mainObject.traverse(function (child) { | |
| if (child.isMesh) { | |
| child.castShadow = true; | |
| child.receiveShadow = true; | |
| } | |
| }); | |
| mainObject.name = "mainObject" + _mainObjectCounter.toString(); | |
| _mainObjectCounter++; | |
| scaleObjectToProper(mainObject); | |
| _scene.add(mainObject); | |
| window.updateObjects(convertThreeJsObjects()); | |
| } | |
| // eslint-disable-next-line react-hooks/exhaustive-deps | |
| function loadGLTF(event) { | |
| const contents = event.target.result; | |
| let loader; | |
| if (isGLTF1(contents)) { | |
| alert('Import of glTF asset not possible. Only versions >= 2.0 are supported. Please try to upgrade the file to glTF 2.0 using glTF-Pipeline.'); | |
| } else { | |
| const dracoLoader = new DRACOLoader(); | |
| dracoLoader.setDecoderPath('/file=extensions/sd-3dmodel-loader/js/'); | |
| loader = new GLTFLoader(managerRef.current); | |
| loader.setDRACOLoader(dracoLoader); | |
| } | |
| loader.parse(contents, '', function (result) { | |
| const resultScene = result.scene; | |
| resultScene.name = "mainObject" + _mainObjectCounter.toString(); | |
| _mainObjectCounter++; | |
| resultScene.animations.push(...result.animations); | |
| _scene.add(resultScene); | |
| window.updateObjects(convertThreeJsObjects()); | |
| }); | |
| } | |
| function loadGLB(event) { | |
| const contents = event.target.result; | |
| const dracoLoader = new DRACOLoader(); | |
| dracoLoader.setDecoderPath('/file=extensions/sd-3dmodel-loader/js/'); | |
| const loader = new GLTFLoader(); | |
| loader.setDRACOLoader(dracoLoader); | |
| loader.parse(contents, '', function (result) { | |
| const resultScene = result.scene; | |
| resultScene.name = "mainObject" + _mainObjectCounter.toString(); | |
| _mainObjectCounter++; | |
| resultScene.animations.push(...result.animations); | |
| _scene.add(resultScene); | |
| window.updateObjects(convertThreeJsObjects()); | |
| }); | |
| } | |
| // eslint-disable-next-line react-hooks/exhaustive-deps | |
| function loadDAE(event) { | |
| const contents = event.target.result; | |
| const loader = new ColladaLoader(managerRef.current); | |
| const collada = loader.parse(contents); | |
| collada.scene.name = "mainObject" + _mainObjectCounter.toString(); | |
| _mainObjectCounter++; | |
| _scene.add(collada.scene); | |
| window.updateObjects(convertThreeJsObjects()); | |
| } | |
| // eslint-disable-next-line react-hooks/exhaustive-deps | |
| function loadVRM(event) { | |
| const contents = event.target.result; | |
| const loader = new GLTFLoader(managerRef.current); | |
| loader.register((parser) => { | |
| return new VRMLoaderPlugin(parser); | |
| }); | |
| loader.parse(contents, '', function (result) { | |
| const vrm = result.userData.vrm; | |
| if (!_currentVRM) { | |
| _currentVRM = vrm; | |
| } | |
| const resultScene = vrm.scene; | |
| resultScene.name = "mainObject" + _mainObjectCounter.toString(); | |
| _mainObjectCounter++; | |
| scaleObjectToProper(resultScene); | |
| _scene.add(resultScene); | |
| window.updateObjects(convertThreeJsObjects()); | |
| }); | |
| } | |
| useEffect(() => { | |
| if (uploadedModelFile) { | |
| const filename = uploadedModelFile.name; | |
| const extension = filename.split('.').pop().toLowerCase(); | |
| const reader = new FileReader(); | |
| //removeMainObject(); | |
| switch (extension) { | |
| case 'obj': | |
| reader.addEventListener('load', loadOBJ, false); | |
| reader.readAsText(uploadedModelFile); | |
| break; | |
| case 'stl': | |
| reader.addEventListener('load', loadSTL, false); | |
| if (reader.readAsBinaryString !== undefined) { | |
| reader.readAsBinaryString(uploadedModelFile); | |
| } else { | |
| reader.readAsArrayBuffer(uploadedModelFile); | |
| } | |
| break; | |
| case 'fbx': | |
| reader.addEventListener('load', loadFBX, false); | |
| reader.readAsArrayBuffer(uploadedModelFile); | |
| break; | |
| case 'gltf': | |
| reader.addEventListener('load', loadGLTF, false); | |
| reader.readAsArrayBuffer(uploadedModelFile); | |
| break; | |
| case 'glb': | |
| reader.addEventListener('load', loadGLB, false); | |
| reader.readAsArrayBuffer(uploadedModelFile); | |
| break; | |
| case 'dae': | |
| reader.addEventListener('load', loadDAE, false); | |
| reader.readAsText(uploadedModelFile); | |
| break; | |
| case 'vrm': | |
| reader.addEventListener('load', loadVRM, false); | |
| reader.readAsArrayBuffer(uploadedModelFile); | |
| break; | |
| } | |
| } | |
| }, [uploadedModelFile, loadOBJ, loadSTL, loadFBX, loadGLTF, loadDAE, loadVRM]); | |
| return ( | |
| <div ref={containerRef} style={{width: '100%', height: '100%'}}></div> | |
| ); | |
| } | |
| function scaleObjectToProper(object) { | |
| const boundingBox = new THREE.Box3(); | |
| boundingBox.setFromObject(object); | |
| if (object.geometry) { | |
| const center = new THREE.Vector3(); | |
| boundingBox.getCenter(center); | |
| object.geometry.translate(-center.x, -center.y, -center.z); | |
| } | |
| const expectRadius = 20; | |
| const radius = boundingBox.getBoundingSphere(new THREE.Sphere()).radius; | |
| const modelScale = expectRadius / radius; | |
| object.scale.set(modelScale, modelScale, modelScale); | |
| } | |
| export function loadPoseModel(poseModelFileName) { | |
| if (poseModelFileName && !_currentVRM) { | |
| let manager = new THREE.LoadingManager(); | |
| let path = "/file=extensions/sd-3dmodel-loader/models/" + poseModelFileName; | |
| //let path = "/file=/modelss/" + poseModelFileName; | |
| const ext = poseModelFileName.split('.').pop().toLowerCase() | |
| switch (ext) { | |
| case "vrm": { | |
| const loader = new GLTFLoader(manager); | |
| loader.crossOrigin = 'anonymous'; | |
| loader.register((parser) => { | |
| return new VRMLoaderPlugin(parser); | |
| }); | |
| loader.load( | |
| path, | |
| (gltf) => { | |
| const vrm = gltf.userData.vrm; | |
| const resultScene = vrm.scene; | |
| resultScene.name = "mainObject" + _mainObjectCounter.toString(); | |
| _mainObjectCounter++; | |
| scaleObjectToProper(resultScene); | |
| _scene.add(resultScene); | |
| _currentVRM = vrm; | |
| vrm.scene.traverse((obj) => { | |
| obj.frustumCulled = false; | |
| }); | |
| VRMUtils.rotateVRM0(vrm); | |
| } | |
| ) | |
| break; | |
| } | |
| case "fbx": { | |
| const loader = new FBXLoader(manager); | |
| loader.load(path, (object) => { | |
| object.traverse(function (child) { | |
| if (child.isMesh) { | |
| child.castShadow = true; | |
| child.receiveShadow = true; | |
| } | |
| }); | |
| object.name = "mainObject"; | |
| scaleObjectToProper(object); | |
| _scene.add(object); | |
| }); | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| export function convertThreeJsObjects() { | |
| return _scene.toJSON(); | |
| } | |
| export default ThreeJsScene; | |
| export function setBgColor(bgColor) { | |
| const backgroundColor = new THREE.Color(bgColor.hex); | |
| _renderer.setClearColor(backgroundColor); | |
| } | |
| export function setPlaying() { | |
| _playing = !_playing; | |
| if (_action && _playing) { | |
| _action.play(); | |
| } | |
| } | |
| export function setStopPlaying() { | |
| if (_action) { | |
| _action.stop(); | |
| } | |
| _playing = false; | |
| } | |
| export function setVisible(objName, visible) { | |
| if (_scene) { | |
| const selectedObject = _scene.getObjectByName(objName); | |
| selectedObject.visible = visible; | |
| } | |
| } | |
| export function controlAnimationProgress(e) { | |
| if (_action && !_playing) { | |
| _currentTime = e / 100 * _totalTime; | |
| _action.time = _currentTime; | |
| _mixer.time = _currentTime; | |
| _controlByProgressBar = true; | |
| } | |
| } | |
| export function setRendererImage(sendImage, controlNetIndex, type) { | |
| if (controlNetIndex !== '') { | |
| _renderer.domElement.toBlob((blob) => { | |
| const image = new Image(); | |
| image.onload = function () { | |
| const canvas = document.createElement('canvas'); | |
| const canvas2 = document.createElement('canvas'); | |
| canvas.width = _width; | |
| canvas.height = _height; | |
| const ctx = canvas.getContext('2d'); | |
| ctx.drawImage(image, 0, 0); | |
| const imageData = ctx.getImageData(0, image.height - previewHeight, previewWidth, previewHeight); | |
| canvas2.width = previewWidth; | |
| canvas2.height = previewHeight; | |
| const ctx2 = canvas2.getContext('2d'); | |
| canvas2.width = previewWidth; | |
| canvas2.height = previewHeight; | |
| ctx2.putImageData(imageData, 0, 0); | |
| canvas2.toBlob((blob2) => { | |
| const file = new File([blob2], "pose.png") | |
| const dt = new DataTransfer(); | |
| dt.items.add(file); | |
| sendImage(type, controlNetIndex, dt); | |
| }); | |
| }; | |
| image.src = URL.createObjectURL(blob); | |
| }); | |
| } else { | |
| alert('No ControlNet Selected'); | |
| } | |
| } |