Code: Select all
index.html
Code: Select all
default loading
Web Worker loading
{
"imports": {
"three": "https://esm.sh/three@0.168.0/build/three.module.js",
"three/addons/": "https://esm.sh/three@0.168.0/examples/jsm/"
}
}
// base libs
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
import * as SkeletonUtils from 'three/addons/utils/SkeletonUtils.js';
// dependencies
let _glbLoader = new GLTFLoader();
// state
let _isSceneInstantiated = false;
// models
let _itemsContainer = undefined;
let _baseSkeleton = undefined;
// animation
let _clock = new THREE.Clock();
let _skeletonMixer = undefined;
// parts
let _scene = undefined;
let _camera = undefined;
let _renderer = undefined;
let InstantiateScene = (hasToLoadWithWebWorker = true) =>
{
if (!_isSceneInstantiated)
{
_isSceneInstantiated = true;
let canvas = document.getElementById('canvas-container');
canvas.classList.remove('hidden');
let rect = canvas.getBoundingClientRect();
_scene = new THREE.Scene();
_camera = new THREE.PerspectiveCamera(30, rect.width / rect.height, 0.1, 1000);
_renderer = new THREE.WebGLRenderer({ canvas: canvas, });
_renderer.setSize(rect.width, rect.height);
_renderer.setAnimationLoop(animate);
const geometry = new THREE.BoxGeometry(0.4, 0.4, 0.4);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);
_scene.add(cube);
_camera.position.z = 5;
function animate()
{
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
let deltaTime = (60.0 / 1000.0);
if (_clock)
deltaTime = _clock.getDelta();
if (_skeletonMixer)
_skeletonMixer.update(deltaTime);
if (_renderer)
_renderer.render(_scene, _camera);
}
_scene.background = new THREE.Color("rgb(10, 80, 10)");
SetupInitialState(hasToLoadWithWebWorker);
}
}
let DestroyScene = () =>
{
if (_isSceneInstantiated)
{
_isSceneInstantiated = false;
_scene = undefined;
_camera = undefined;
_renderer = undefined;
let canvas = document.getElementById('canvas-container');
canvas.classList.add('hidden');
}
}
let SetupInitialState = (hasToLoadWithWebWorker = true) =>
{
_itemsContainer = new THREE.Group();
_scene.add(_itemsContainer);
_itemsContainer.position.set(0, -1, 0);
InstantiateLights();
let onFinishCallback = () =>
{
let blueMaterial = new THREE.MeshStandardMaterial({ color: 0x0000ff });
let redMaterial = new THREE.MeshStandardMaterial({ color: 0xff0000 });
InstantiateDynamicBodyPart('default-male-body-01-model-01.glb', blueMaterial, hasToLoadWithWebWorker);
InstantiateDynamicBodyPart('default-male-shirt-01-model-01.glb', redMaterial, hasToLoadWithWebWorker);
}
InstantiateBaseSkeleton(onFinishCallback, hasToLoadWithWebWorker)
}
let InstantiateLights = () =>
{
const mainUpLight = new THREE.DirectionalLight(0xffffff, 2);
const secondaryUpLight = new THREE.DirectionalLight(0xffffff, 1);
mainUpLight.position.set(20, 30, 10);
mainUpLight.target.position.set(0, 0, 0);
mainUpLight.castShadow = true;
secondaryUpLight.position.set(-20, 30, -10);
secondaryUpLight.target.position.set(0, 0, 0);
secondaryUpLight.castShadow = true;
const reverseLight1 = new THREE.DirectionalLight(0xffffff, 1);
const reverseLight2 = new THREE.DirectionalLight(0xffffff, 1);
reverseLight1.position.set(20, -30, -10);
reverseLight1.target.position.set(0, 0, 0);
reverseLight1.castShadow = false;
reverseLight2.position.set(-20, -30, 10);
reverseLight2.target.position.set(0, 0, 0);
reverseLight2.castShadow = false;
const ambientLight = new THREE.AmbientLight(0xaaaaaa); // soft white light
_scene.add(ambientLight);
_scene.add(mainUpLight);
}
let InstantiateBaseSkeleton = async (onFinishCallback, hasToLoadWithWebWorker = true) =>
{
let onFinishCallback2 = (baseModel) =>
{
if (baseModel)
{
_itemsContainer.add(baseModel);
let skeletonHelper = new THREE.SkeletonHelper(_scene);
_scene.add(skeletonHelper)
let baseBones = [];
baseModel.traverse(
(object) =>
{
switch (object.type)
{
case 'Bone':
baseBones.push(object);
break;
}
});
_baseSkeleton = new THREE.Skeleton(baseBones);
let charAnimationGroup = new THREE.AnimationObjectGroup(baseModel);
_skeletonMixer = new THREE.AnimationMixer(charAnimationGroup);
if (baseModel.animations.length > 0)
{
let currentAnimation = _skeletonMixer.clipAction(baseModel.animations[1]);
currentAnimation.play();
}
let finalScale = 1;
let finalPositionX = 0;
let finalPositionY = 0;
baseModel.scale.set(finalScale, finalScale, finalScale);
baseModel.position.set(finalPositionX, 0, finalPositionY);
}
onFinishCallback();
};
let baseModel = await LoadObject3D('default-skeleton-01.glb', onFinishCallback2, hasToLoadWithWebWorker);
}
let InstantiateDynamicBodyPart = (
modelFileName,
baseMaterial,
hasToLoadWithWebWorker = true) =>
{
let onFinishCallback = (baseModel) =>
{
if (baseModel)
{
let skinnedMeshesList = [];
baseModel.traverse(
(object) =>
{
switch (object.type)
{
case 'SkinnedMesh':
skinnedMeshesList.push(object);
break;
}
});
InjectMaterial(baseModel, baseMaterial);
skinnedMeshesList.map(
(skinnedMesh) =>
{
_itemsContainer.add(skinnedMesh);
skinnedMesh.skeleton = _baseSkeleton;
});
}
}
LoadObject3D(modelFileName, onFinishCallback, hasToLoadWithWebWorker);
}
let InjectMaterial = (baseModel, material) =>
{
if (baseModel)
{
baseModel.traverse(
(object) =>
{
if (object.isMesh)
object.material = material;
});
}
}
let GetRandomId = (length) =>
{
let result = '';
const characters = 'abcdefghijklmnopqrstuvwxyz0123456789';
const charactersLength = characters.length;
let counter = 0;
while (counter < length)
{
result += characters.charAt(Math.floor(Math.random() * charactersLength));
counter += 1;
}
return result;
}
let LoadObject3D = (assetSubPath, onFinishCallback, hasToLoadWithWebWorker = true) =>
{
let value = undefined;
let assetsRootPath = 'https://y87vj9.csb.app/public/';
let assetPath = assetsRootPath + assetSubPath;
if (!hasToLoadWithWebWorker)
{
if (!value)
{
console.log('[default loading] > loading model... | assetPath =', assetPath);
let onSuccessCallback =
(modelGLTF) =>
{
console.log('[default loading] >>> model loaded! | assetPath =', assetPath);
let moodelObject3D = SkeletonUtils.clone(modelGLTF.scene.clone());
moodelObject3D.animations = modelGLTF.animations;
onFinishCallback(moodelObject3D);
};
_glbLoader.load(assetPath, onSuccessCallback);
};
}
else
{
if (typeof Worker !== 'undefined')
{
let requestId = GetRandomId(16);
_webworkersCallbacksById[requestId] = onFinishCallback;
let webworkerRequest =
{
RequestId: requestId,
ProcessName: 'LoadModel',
Params: [assetPath],
}
_worker.postMessage(JSON.stringify(webworkerRequest));
}
else
{
// Web workers are not supported in this environment.
// You should add a fallback so that your program still executes correctly.
console.error('$$q> AssetsService.GetAsset | Web workers are not supported in this environment!');
}
}
}
// region Worker
// state
let _webworkersCallbacksById = {};
let HandleWebworkerMessage = (data) =>
{
// let webworkerResponse = JSON.parse(data);
let webworkerResponse = JSON.parse(data.data);
let requestId = webworkerResponse.RequestId;
let baseModel = undefined;
if (webworkerResponse.RawResponse)
{
let object3DJSON = webworkerResponse.RawResponse;
const loader = new THREE.ObjectLoader();
baseModel = loader.parse(object3DJSON);
}
else
console.error('$> webworkerResponse.RawResponse is undefined! ');
let isCallbackRegistered = requestId in _webworkersCallbacksById;
if (isCallbackRegistered)
{
let callback = _webworkersCallbacksById[requestId];
callback(baseModel);
delete _webworkersCallbacksById[requestId];
}
else
console.error('$> Unexpected requestId! requestId = ', requestId);
}
const _worker = new Worker("/model-loading-worker.js", { type: 'module' });
_worker.onmessage = HandleWebworkerMessage;
//end region Worker
let DefaultIntantiatingButton = () =>
{
DestroyScene();
InstantiateScene(false);
}
let WebWorkerIntantiatingButton = () =>
{
DestroyScene();
InstantiateScene(true);
}
let SetupButtons = () =>
{
let defaultIntantiatingButton = document.querySelector('#default-intantiating-button');
let webWorkerIntantiatingButton = document.querySelector('#web-worker-intantiating-button');
defaultIntantiatingButton.onclick = DefaultIntantiatingButton;
webWorkerIntantiatingButton.onclick = WebWorkerIntantiatingButton;
}
SetupButtons();
DefaultIntantiatingButton();
< /code>
model-loading-worker.js
// dependencies
let _glbLoader = new GLTFLoader();
let HandleMessage = async (request) =>
{
let response = {};
response.RequestId = request.RequestId;
switch (request.ProcessName)
{
case 'LoadModel':
{
let assetPath = request.Params[0];
console.log('[web worker] > loading model... | assetPath =', assetPath);
let onSuccessCallback =
(modelGLTF) =>
{
console.log('[web worker] >>> model loaded! | assetPath =', assetPath);
let finalAssetValue = modelGLTF.scene;
finalAssetValue.animations = modelGLTF.animations;
let finalAssetValueObject3DJSON = finalAssetValue.toJSON()
response.RawResponse = finalAssetValueObject3DJSON;
}
await _glbLoader.loadAsync(assetPath).then(onSuccessCallback);
break
}
default:
{
console.error('Unexpected request.ProcessName! | request.ProcessName =', request.ProcessName);
break;
}
}
return response;
}
addEventListener(
'message',
async ({ data }) =>
{
let webworkerRequest = JSON.parse(data, function (key, value)
{
return value;
});
let response = await HandleMessage(webworkerRequest);
data = JSON.stringify(response);
postMessage(data);
});
< /code>
I'll leave a link below to a practical example on codesandbox with reproduction (change between loading modes by changing the state of the checkbox in the lower right corner and then press "Instatiate Scene". The buttons have a yellow hover to test with the mouse if the page is really stuck during loading, as well as the cube animation)
https://codesandbox.io/p/sandbox/y87vj9
Any help or guidance will be very welcome, I've been trying to find a way to improve the optimization of this page for about 2 weeks, and this was the only thing that had a good result in performance, however, it's not working.