Skip to content
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