Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recreate canvas and renderer when component remount #38

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
15 changes: 7 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
1 change: 0 additions & 1 deletion src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ export default {
avatarConfigChanged: "avatarConfigChanged",
exportAvatar: "exportAvatar",
resetView: "resetView",
reactIsLoaded: "reactIsLoaded",
renderThumbnail: "renderThumbnail",
thumbnailResult: "thumbnailResult",
combinedMeshName: "CombinedMesh",
Expand Down
2 changes: 1 addition & 1 deletion src/create-texture-atlas.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
12 changes: 10 additions & 2 deletions src/export.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,16 @@ export function combineHubsComponents(a, b) {
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 });
return new Promise((resolve, reject) => {
exporter.parse(
object3D,
(gltf) => resolve({ gltf }),
(error) => {
console.error(error);
reject("Error exporting the avatar");
},
{ binary, animations }
);
});
};
})();
Expand Down
101 changes: 51 additions & 50 deletions src/game.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ window.combineCurrentAvatar = async function () {
};

const state = {
reactIsLoaded: false,
shouldResize: true,
didInit: false,
scene: null,
Expand Down Expand Up @@ -57,11 +56,8 @@ const state = {
};
window.gameState = state;

window.onresize = () => {
window.addEventListener("resize", () => {
state.shouldResize = true;
};
document.addEventListener(constants.reactIsLoaded, () => {
state.reactIsLoaded = true;
});
document.addEventListener(constants.avatarConfigChanged, (e) => {
state.newAvatarConfig = e.detail.avatarConfig;
Expand Down Expand Up @@ -107,48 +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.physicallyCorrectLights = true;
renderer.gammaOutput = true;
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();

// TODO Remove this test code
state.testExportGroup = new THREE.Group();
scene.add(state.testExportGroup);

state.clock = new THREE.Clock();
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) {
Expand All @@ -157,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();
Expand All @@ -179,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();
Expand Down Expand Up @@ -256,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();
}
Expand Down Expand Up @@ -354,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);
});

Expand Down Expand Up @@ -430,10 +434,6 @@ function tick(time) {
}
}

{
window.requestAnimationFrame(tick);
}

{
const { renderer, scene, camera, controls } = state;
if (!state.quietMode || state.shouldRenderInQuietMode) {
Expand All @@ -443,4 +443,5 @@ function tick(time) {
}
}

window.requestAnimationFrame(tick);
window.gameState.initRenderer = initRenderer;
window.gameState.disposeRenderer = disposeRenderer;
6 changes: 3 additions & 3 deletions src/merge-geometry.js
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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()
);
});
Expand Down Expand Up @@ -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]);
});
});

Expand Down
15 changes: 15 additions & 0 deletions src/mesh-combination.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
10 changes: 6 additions & 4 deletions src/react-components/AvatarEditorContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,16 @@ 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.
useEffect(async () => {
if (canvasUrl === null) {
setCanvasUrl(await generateWave());
useEffect(() => {
async function init() {
if (canvasUrl === null) {
setCanvasUrl(await generateWave());
}
}
init();
});

function updateAvatarConfig(newConfig) {
Expand Down
12 changes: 10 additions & 2 deletions src/react-components/AvatarPreviewContainer.js
Original file line number Diff line number Diff line change
@@ -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 (
<div id="sceneContainer">
{!thumbnailMode && (
<div className="waveContainer" style={{ backgroundImage: canvasUrl ? `url("${canvasUrl}")` : "none" }}></div>
)}
<canvas id="scene"></canvas>
<canvas ref={canvasRef}></canvas>
</div>
);
}
4 changes: 2 additions & 2 deletions src/react-components/ToolbarContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ export function ToolbarContainer({ onGLBUploaded, randomizeConfig }) {
</button>
</div>
<div className="toolbarNotice">
<span>The 3D models used in this app are ©2020-2022 by individual <a href="https://www.mozilla.org" target="_blank" noreferrer>mozilla.org</a> contributors.
Content available under a <a href="https://www.mozilla.org/en-US/foundation/licensing/website-content/" target="_blank" noreferrer>Creative Commons license</a>.</span>
<span>The 3D models used in this app are ©2020-2022 by individual <a href="https://www.mozilla.org" target="_blank" rel="noreferrer">mozilla.org</a> contributors.
Content available under a <a href="https://www.mozilla.org/en-US/foundation/licensing/website-content/" target="_blank" rel="noreferrer">Creative Commons license</a>.</span>
</div>
</Toolbar>
);
Expand Down