From 53fc58818ff7e64af3e3288e0778073cab0c3fb5 Mon Sep 17 00:00:00 2001 From: Vincent Fretin Date: Tue, 28 Jun 2022 17:38:20 +0200 Subject: [PATCH 01/10] modify export code to work with threejs r141 --- src/create-texture-atlas.js | 2 +- src/merge-geometry.js | 6 +++--- src/mesh-combination.js | 15 +++++++++++++++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/create-texture-atlas.js b/src/create-texture-atlas.js index c693dec..67718ab 100644 --- a/src/create-texture-atlas.js +++ b/src/create-texture-atlas.js @@ -84,7 +84,7 @@ export const createTextureAtlas = (function () { context.globalCompositeOperation = image ? "multiply" : "source-over"; const colorClone = mesh.material.color.clone(); - colorClone.convertLinearToGamma(); + colorClone.convertLinearToSRGB(); context.fillStyle = `#${colorClone.getHexString()}`; context.fillRect(min.x * ATLAS_SIZE_PX, min.y * ATLAS_SIZE_PX, tileSize, tileSize); diff --git a/src/merge-geometry.js b/src/merge-geometry.js index 817da36..33d5ec1 100644 --- a/src/merge-geometry.js +++ b/src/merge-geometry.js @@ -1,5 +1,5 @@ import * as THREE from "three"; -import { BufferGeometryUtils } from "three/examples/jsm/utils/BufferGeometryUtils"; +import { mergeBufferAttributes } from "three/examples/jsm/utils/BufferGeometryUtils"; import constants from "./constants"; import { GLTFCubicSplineInterpolant } from "./gltf-cubic-spline-interpolant"; @@ -39,7 +39,7 @@ function mergeSourceAttributes({ sourceAttributes }) { const destAttributes = {}; Array.from(propertyNames.keys()).map((name) => { - destAttributes[name] = BufferGeometryUtils.mergeBufferAttributes( + destAttributes[name] = mergeBufferAttributes( allSourceAttributes.map((sourceAttributes) => sourceAttributes[name]).flat() ); }); @@ -103,7 +103,7 @@ function mergeSourceMorphAttributes({ propertyNames.forEach((propName) => { merged[propName] = []; Object.entries(destMorphTargetDictionary).forEach(([morphName, destMorphIndex]) => { - merged[propName][destMorphIndex] = BufferGeometryUtils.mergeBufferAttributes(unmerged[propName][destMorphIndex]); + merged[propName][destMorphIndex] = mergeBufferAttributes(unmerged[propName][destMorphIndex]); }); }); diff --git a/src/mesh-combination.js b/src/mesh-combination.js index 2459d47..81d16d9 100644 --- a/src/mesh-combination.js +++ b/src/mesh-combination.js @@ -73,6 +73,21 @@ export async function combine({ avatar }) { delete geometry.attributes[`morphTarget${i}`]; delete geometry.attributes[`morphNormal${i}`]; } + // Computing tangents that was done in GLTFLoader in threejs 0.125.2 was removed in threejs r126 (https://github.com/mrdoob/three.js/pull/21186) + // The mergeSourceAttributes function will crash because it can't find the tangent attribute on some geometry. + // So putting back here the code that was executed in GLTFLoader 0.125.2: + const material = mesh.material; + if ( + material.isMeshStandardMaterial === true && + material.side === THREE.DoubleSide && + geometry.getIndex() !== null && + geometry.hasAttribute("position") === true && + geometry.hasAttribute("normal") === true && + geometry.hasAttribute("uv") === true && + geometry.hasAttribute("tangent") === false + ) { + geometry.computeTangents(); + } }); const { source, dest } = mergeGeometry({ meshes }); From b4eab502f9ef7eb38d838f7a046454e6448fd812 Mon Sep 17 00:00:00 2001 From: Vincent Fretin Date: Tue, 28 Jun 2022 17:38:48 +0200 Subject: [PATCH 02/10] fix react errors --- src/react-components/AvatarEditorContainer.js | 9 ++++++--- src/react-components/ToolbarContainer.js | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/react-components/AvatarEditorContainer.js b/src/react-components/AvatarEditorContainer.js index 8fae5f9..fbac7de 100644 --- a/src/react-components/AvatarEditorContainer.js +++ b/src/react-components/AvatarEditorContainer.js @@ -33,10 +33,13 @@ export function AvatarEditorContainer() { }); // TODO: Save the wave to a static image, or actually do some interesting animation with it. - useEffect(async () => { - if (canvasUrl === null) { - setCanvasUrl(await generateWave()); + useEffect(() => { + async function init() { + if (canvasUrl === null) { + setCanvasUrl(await generateWave()); + } } + init(); }); function updateAvatarConfig(newConfig) { diff --git a/src/react-components/ToolbarContainer.js b/src/react-components/ToolbarContainer.js index 275433e..766d1d0 100644 --- a/src/react-components/ToolbarContainer.js +++ b/src/react-components/ToolbarContainer.js @@ -35,8 +35,8 @@ export function ToolbarContainer({ onGLBUploaded, randomizeConfig }) {
- The 3D models used in this app are ©2020-2022 by individual mozilla.org contributors. - Content available under a Creative Commons license. + The 3D models used in this app are ©2020-2022 by individual mozilla.org contributors. + Content available under a Creative Commons license.
); From be1ad4a34d8a2489cc6362a6e050bb05aeb755c6 Mon Sep 17 00:00:00 2001 From: Vincent Fretin Date: Tue, 28 Jun 2022 17:41:09 +0200 Subject: [PATCH 03/10] update to three r141 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e5942c8..691e5ec 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,6 @@ "react": "^17.0.1", "react-dom": "^17.0.1", "simplebar-react": "^2.3.0", - "three": "^0.125.2" + "three": "^0.141.0" } } From e271e232cbd21f92245977d4c5273b500db1acd9 Mon Sep 17 00:00:00 2001 From: Vincent Fretin Date: Tue, 28 Jun 2022 17:45:56 +0200 Subject: [PATCH 04/10] update package-lock.json --- package-lock.json | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index f0c6b27..638ceb7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,7 +4,6 @@ "requires": true, "packages": { "": { - "name": "hackweek-avatar-maker", "dependencies": { "@fortawesome/fontawesome-svg-core": "^1.2.34", "@fortawesome/free-solid-svg-icons": "^5.15.2", @@ -13,7 +12,7 @@ "react": "^17.0.1", "react-dom": "^17.0.1", "simplebar-react": "^2.3.0", - "three": "^0.125.2" + "three": "^0.141.0" }, "devDependencies": { "@babel/core": "^7.12.10", @@ -4514,9 +4513,9 @@ } }, "node_modules/three": { - "version": "0.125.2", - "resolved": "https://registry.npmjs.org/three/-/three-0.125.2.tgz", - "integrity": "sha512-7rIRO23jVKWcAPFdW/HREU2NZMGWPBZ4XwEMt0Ak0jwLUKVJhcKM55eCBWyGZq/KiQbeo1IeuAoo/9l2dzhTXA==" + "version": "0.141.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.141.0.tgz", + "integrity": "sha512-JaSDAPWuk4RTzG5BYRQm8YZbERUxTfTDVouWgHMisS2to4E5fotMS9F2zPFNOIJyEFTTQDDKPpsgZVThKU3pXA==" }, "node_modules/thunky": { "version": "1.1.0", @@ -8809,9 +8808,9 @@ } }, "three": { - "version": "0.125.2", - "resolved": "https://registry.npmjs.org/three/-/three-0.125.2.tgz", - "integrity": "sha512-7rIRO23jVKWcAPFdW/HREU2NZMGWPBZ4XwEMt0Ak0jwLUKVJhcKM55eCBWyGZq/KiQbeo1IeuAoo/9l2dzhTXA==" + "version": "0.141.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.141.0.tgz", + "integrity": "sha512-JaSDAPWuk4RTzG5BYRQm8YZbERUxTfTDVouWgHMisS2to4E5fotMS9F2zPFNOIJyEFTTQDDKPpsgZVThKU3pXA==" }, "thunky": { "version": "1.1.0", From d328098f44f1653c76032239bb1e18f1e40b617e Mon Sep 17 00:00:00 2001 From: Vincent Fretin Date: Tue, 28 Jun 2022 17:59:20 +0200 Subject: [PATCH 05/10] GLTFExporter.parse has four arguments now --- src/export.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/export.js b/src/export.js index d4d1971..e708032 100644 --- a/src/export.js +++ b/src/export.js @@ -54,7 +54,14 @@ export const exportGLTF = (function () { const exporter = new GLTFExporter(); return function exportGLTF(object3D, { binary, animations }) { return new Promise((resolve) => { - exporter.parse(object3D, (gltf) => resolve({ gltf }), { binary, animations }); + exporter.parse( + object3D, + (gltf) => resolve({ gltf }), + (error) => { + console.error(error); + }, + { binary, animations } + ); }); }; })(); From 1e0b33d4c3eefab0e3c0b12fb3f2c00f7975c869 Mon Sep 17 00:00:00 2001 From: Vincent Fretin Date: Wed, 29 Jun 2022 09:49:33 +0200 Subject: [PATCH 06/10] properly reject promise in case of error in GLTFExporter.parse, even if we don't catch it now --- src/export.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/export.js b/src/export.js index e708032..ae4ea86 100644 --- a/src/export.js +++ b/src/export.js @@ -53,12 +53,13 @@ export function combineHubsComponents(a, b) { export const exportGLTF = (function () { const exporter = new GLTFExporter(); return function exportGLTF(object3D, { binary, animations }) { - return new Promise((resolve) => { + return new Promise((resolve, reject) => { exporter.parse( object3D, (gltf) => resolve({ gltf }), (error) => { console.error(error); + reject("Error exporting the avatar"); }, { binary, animations } ); From a1acc0bbf4404b68b1dbb8fe82ae62a9565c354b Mon Sep 17 00:00:00 2001 From: Vincent Fretin Date: Wed, 29 Jun 2022 11:49:15 +0200 Subject: [PATCH 07/10] replace gammaOutput=true by outputEncoding=THREE.sRGBEncoding --- src/game.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game.js b/src/game.js index d2c7832..71fc2bf 100644 --- a/src/game.js +++ b/src/game.js @@ -127,7 +127,7 @@ function init() { // TODO: Square this with react const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById("scene"), antialias: true }); renderer.physicallyCorrectLights = true; - renderer.gammaOutput = true; + renderer.outputEncoding = THREE.sRGBEncoding; state.renderer = renderer; state.clock = new THREE.Clock(); From bb46186bc3d3ce1d2bbc092b3cef3268b7b4f054 Mon Sep 17 00:00:00 2001 From: Vincent Fretin Date: Wed, 29 Jun 2022 11:50:45 +0200 Subject: [PATCH 08/10] setPixelRatio on renderer --- src/game.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/game.js b/src/game.js index 71fc2bf..36220ac 100644 --- a/src/game.js +++ b/src/game.js @@ -126,6 +126,7 @@ function init() { // TODO: Square this with react const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById("scene"), antialias: true }); + renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); renderer.physicallyCorrectLights = true; renderer.outputEncoding = THREE.sRGBEncoding; state.renderer = renderer; From 319678aa7c8b9923913b08548e9a277adbdf3d34 Mon Sep 17 00:00:00 2001 From: Vincent Fretin Date: Wed, 29 Jun 2022 11:52:35 +0200 Subject: [PATCH 09/10] register the resize callback with addEventListener --- src/game.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/game.js b/src/game.js index 36220ac..21199e2 100644 --- a/src/game.js +++ b/src/game.js @@ -57,9 +57,9 @@ const state = { }; window.gameState = state; -window.onresize = () => { +window.addEventListener("resize", () => { state.shouldResize = true; -}; +}); document.addEventListener(constants.reactIsLoaded, () => { state.reactIsLoaded = true; }); From 0137502c0faa9a08e8f8ecbfac7bcfc328c22cd9 Mon Sep 17 00:00:00 2001 From: Vincent Fretin Date: Wed, 29 Jun 2022 14:04:50 +0200 Subject: [PATCH 10/10] recreate renderer when the react component remount --- src/constants.js | 1 - src/game.js | 100 +++++++++--------- src/react-components/AvatarEditorContainer.js | 1 - .../AvatarPreviewContainer.js | 12 ++- 4 files changed, 60 insertions(+), 54 deletions(-) diff --git a/src/constants.js b/src/constants.js index 05766c4..4c4dbd1 100644 --- a/src/constants.js +++ b/src/constants.js @@ -2,7 +2,6 @@ export default { avatarConfigChanged: "avatarConfigChanged", exportAvatar: "exportAvatar", resetView: "resetView", - reactIsLoaded: "reactIsLoaded", renderThumbnail: "renderThumbnail", thumbnailResult: "thumbnailResult", combinedMeshName: "CombinedMesh", diff --git a/src/game.js b/src/game.js index 21199e2..0c4bcc7 100644 --- a/src/game.js +++ b/src/game.js @@ -27,7 +27,6 @@ window.combineCurrentAvatar = async function () { }; const state = { - reactIsLoaded: false, shouldResize: true, didInit: false, scene: null, @@ -60,9 +59,6 @@ window.gameState = state; window.addEventListener("resize", () => { state.shouldResize = true; }); -document.addEventListener(constants.reactIsLoaded, () => { - state.reactIsLoaded = true; -}); document.addEventListener(constants.avatarConfigChanged, (e) => { state.newAvatarConfig = e.detail.avatarConfig; state.shouldApplyNewAvatarConfig = true; @@ -107,49 +103,67 @@ function resetView() { state.controls.reset(); } -function init() { - THREE.Cache.enabled = !isThumbnailMode(); +function initRenderer(canvas) { + const renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true }); + renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); + renderer.physicallyCorrectLights = true; + renderer.outputEncoding = THREE.sRGBEncoding; + state.renderer = renderer; + state.shouldResize = true; + init(renderer); + renderer.setAnimationLoop(tick); +} - const scene = new THREE.Scene(); - state.scene = scene; +function disposeRenderer() { + state.renderer.dispose(); + state.renderer = null; + state.scene.environment = null; + state.envMap.dispose(); + state.controls = null; +} - const skydome = createSkydome(isThumbnailMode() ? 2 : 400); - scene.add(skydome); +function init(renderer) { + if (!state.didInit) { + state.didInit = true; + THREE.Cache.enabled = !isThumbnailMode(); - const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); - camera.position.set(0, 0.6, 1); - state.camera = camera; + const scene = new THREE.Scene(); + state.scene = scene; - const directionalLight = new THREE.DirectionalLight(0xffffff, 4.0); - directionalLight.position.set(10, 20, 5); - scene.add(directionalLight); + const skydome = createSkydome(isThumbnailMode() ? 2 : 400); + scene.add(skydome); - // TODO: Square this with react - const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById("scene"), antialias: true }); - renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); - renderer.physicallyCorrectLights = true; - renderer.outputEncoding = THREE.sRGBEncoding; - state.renderer = renderer; + const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); + camera.position.set(0, 0.6, 1); + state.camera = camera; + + const directionalLight = new THREE.DirectionalLight(0xffffff, 4.0); + directionalLight.position.set(10, 20, 5); + scene.add(directionalLight); - state.clock = new THREE.Clock(); + state.clock = new THREE.Clock(); + + // TODO Remove this test code + state.testExportGroup = new THREE.Group(); + scene.add(state.testExportGroup); + + state.avatarGroup = new THREE.Group(); + scene.add(state.avatarGroup); + } const sky = createSky(); state.envMap = generateEnvironmentMap(sky, renderer); + sky.geometry.dispose(); + sky.material.dispose(); + state.scene.environment = state.envMap; - const controls = new OrbitControls(camera, renderer.domElement); + const controls = new OrbitControls(state.camera, renderer.domElement); controls.target = new THREE.Vector3(0, 0.5, 0); controls.update(); controls.saveState(); state.controls = controls; state.currentCameraPosition = new THREE.Vector3(); state.prevCameraPosition = new THREE.Vector3(); - - // TODO Remove this test code - state.testExportGroup = new THREE.Group(); - scene.add(state.testExportGroup); - - state.avatarGroup = new THREE.Group(); - scene.add(state.avatarGroup); } function playClips(scene, clips) { @@ -158,7 +172,7 @@ function playClips(scene, clips) { const mixer = new THREE.AnimationMixer(scene); for (const clip of clips) { - const animation = scene.animations.find(a => a.name === clip) + const animation = scene.animations.find((a) => a.name === clip); if (animation) { const action = mixer.clipAction(animation); action.play(); @@ -180,7 +194,7 @@ function initializeGltf(key, gltf) { gltf.scene.traverse((obj) => { forEachMaterial(obj, (material) => { if (material.isMeshStandardMaterial) { - material.envMap = state.envMap; + // material.envMap = state.envMap; // this is now set on state.scene.environment material.envMapIntensity = 0.4; if (material.map) { material.map.anisotropy = state.renderer.capabilities.getMaxAnisotropy(); @@ -257,17 +271,6 @@ async function loadIntoGroup({ category, part, group, cached = true }) { } function tick(time) { - { - if (state.reactIsLoaded && !state.didInit) { - state.didInit = true; - init(); - } - if (!state.didInit) { - requestAnimationFrame(tick); - return; - } - } - { state.delta = state.clock.getDelta(); } @@ -355,7 +358,7 @@ function tick(time) { // Reset all idle eyes animations before cloning or exporting the avatar // so that we don't export it mid-blink. - Object.values(state.idleEyesMixers).forEach(mixer => { + Object.values(state.idleEyesMixers).forEach((mixer) => { mixer.setTime(0); }); @@ -431,10 +434,6 @@ function tick(time) { } } - { - window.requestAnimationFrame(tick); - } - { const { renderer, scene, camera, controls } = state; if (!state.quietMode || state.shouldRenderInQuietMode) { @@ -444,4 +443,5 @@ function tick(time) { } } -window.requestAnimationFrame(tick); +window.gameState.initRenderer = initRenderer; +window.gameState.disposeRenderer = disposeRenderer; diff --git a/src/react-components/AvatarEditorContainer.js b/src/react-components/AvatarEditorContainer.js index fbac7de..f283f27 100644 --- a/src/react-components/AvatarEditorContainer.js +++ b/src/react-components/AvatarEditorContainer.js @@ -29,7 +29,6 @@ export function AvatarEditorContainer() { if (!thumbnailMode) { dispatch(constants.avatarConfigChanged, { avatarConfig: { ...avatarConfig, ...hoveredConfig } }); } - dispatch(constants.reactIsLoaded); }); // TODO: Save the wave to a static image, or actually do some interesting animation with it. diff --git a/src/react-components/AvatarPreviewContainer.js b/src/react-components/AvatarPreviewContainer.js index bd2b5a6..ad90ae2 100644 --- a/src/react-components/AvatarPreviewContainer.js +++ b/src/react-components/AvatarPreviewContainer.js @@ -1,12 +1,20 @@ -import React from "react"; +import React, { useEffect, useRef } from "react"; export function AvatarPreviewContainer({ thumbnailMode, canvasUrl }) { + const canvasRef = useRef(null); + useEffect(() => { + const canvasEl = canvasRef.current; + window.gameState.initRenderer(canvasEl); + return () => { + window.gameState.disposeRenderer(); + }; + }, [canvasRef]); return (
{!thumbnailMode && (
)} - +
); }