Skip to content

Commit

Permalink
Merge pull request #10 from readyplayerme/feature/canvas-addons
Browse files Browse the repository at this point in the history
Feature/canvas addons
  • Loading branch information
BDenysovets authored Jul 31, 2022
2 parents fce3164 + 6bc9229 commit e9e7677
Show file tree
Hide file tree
Showing 9 changed files with 137 additions and 21 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ coverage
# Library
/dist

# IDE Logs
.idea

# node-waf configuration
.lock-wscript
Expand Down
11 changes: 9 additions & 2 deletions src/App/App.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,17 @@ import './App.scss';
function App() {
return (
<div className="App">
<div>localhost playground</div>
<div className="settings">
<div className="wrapper">
<h3 className="title">localhost playground</h3>
<div className="content">
Paste your content in there to test the avatar props without shrinking the canvas
</div>
</div>
</div>
<div className="container">
<div className="card" style={{ width: '100%' }}>
<Avatar modelUrl="/male.glb" poseUrl="/male-pose-standing.glb" backgroundColor="#fafafa" shadows />
<Avatar modelUrl="/male.glb" poseUrl="/male-pose-standing.glb" backgroundColor="#fafafa" shadows={false} />
</div>
</div>
</div>
Expand Down
42 changes: 42 additions & 0 deletions src/App/App.scss
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,45 @@ body,
width: 50%;
}
}

.App {
position: relative;

.settings {
z-index: 2;
position: fixed;
top: 24px;
right: 24px;
width: 100%;
max-width: 320px;
background-color: rgba(243, 243, 243, 0.9);
border: 1px solid #b9b9b9;
border-radius: 4px;
overflow: hidden;

.wrapper {
position: relative;
height: 100%;
width: 100%;
}

.title {
font-size: 18px;
font-weight: 600;
font-family: Arial, sans-serif;
color: black;
display: block;
padding: 8px;
margin: 0;
border-bottom: 1px solid #b9b9b9;
}

.content {
position: relative;
padding: 8px;
display: flex;
flex-direction: column;
gap: 12px;
}
}
}
20 changes: 17 additions & 3 deletions src/components/Avatar/Avatar.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import { PresetsType } from '@react-three/drei/helpers/environment-assets';
import { Vector3 } from 'three';
import { CameraLighting } from 'src/components/SceneControls/CameraLighting.component';
import { AnimationModel } from 'src/components/Models/AnimationModel/AnimationModel.component';
import { HeadBlendShapeType, LightingProps } from 'src/types';
import { LightingProps } from 'src/types';
import { BaseCanvas } from 'src/components/BaseCanvas';
import { HalfBodyModel, StaticModel, PoseModel } from 'src/components/Models';
import { isValidGlbUrl } from 'src/services';
import Capture, { CaptureType } from '../Capture/Capture.component';
import Box, { Background } from '../Background/Box/Box.component';

export const CAMERA = {
TARGET: {
Expand All @@ -32,7 +34,7 @@ export const CAMERA = {
}
};

export type Emotion = Record<HeadBlendShapeType, number>;
export type Emotion = Record<string, number>;

export interface AvatarProps extends LightingProps {
/**
Expand Down Expand Up @@ -88,6 +90,14 @@ export interface AvatarProps extends LightingProps {
* Applies a face emotion of the model.
*/
emotion?: Emotion;
/**
* Applies Box background for canvas.
*/
background?: Background;
/**
* Return base64 image after making screenshot of the canvas.
*/
onCapture?: CaptureType;
}

/**
Expand All @@ -114,7 +124,9 @@ export const Avatar: FC<AvatarProps> = ({
cameraInitialDistance = CAMERA.INITIAL_DISTANCE.FULL_BODY,
style,
emotion,
idleRotation = false
idleRotation = false,
onCapture,
background
}) => {
const AvatarModel = useMemo(() => {
if (!isValidGlbUrl(modelUrl)) {
Expand Down Expand Up @@ -170,6 +182,8 @@ export const Avatar: FC<AvatarProps> = ({
</mesh>
</group>
)}
{background?.src && <Box {...background} />}
{onCapture && <Capture onCapture={onCapture} />}
</Suspense>
</BaseCanvas>
);
Expand Down
27 changes: 27 additions & 0 deletions src/components/Background/Box/Box.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React, { FC, useRef } from 'react';
import { Mesh, TextureLoader } from 'three';
import { useFrame, useLoader } from '@react-three/fiber';
import { MeshProps } from '@react-three/fiber/dist/declarations/src/three-types';

export type Background = { src?: string } & MeshProps;

const Box: FC<Background> = ({ src = '', ...baseProps }) => {
const ref = useRef<Mesh>();
const texture = useLoader(TextureLoader, src);

useFrame(() => {
if (ref.current?.rotation.y) {
ref.current.rotation.y += 0.01;
}
});

return (
<mesh ref={ref} castShadow receiveShadow {...baseProps}>
<boxBufferGeometry />
<meshPhysicalMaterial map={texture} />
<axesHelper />
</mesh>
);
};

export default Box;
1 change: 1 addition & 0 deletions src/components/BaseCanvas/BaseCanvas.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const BaseCanvas: FC<BaseCanvasProps> = ({
<Canvas
className={styles['base-canvas']}
shadows
gl={{ preserveDrawingBuffer: true }}
dpr={[1, 4]}
camera={{ fov, position }}
resize={{ scroll: true, debounce: { scroll: 50, resize: 0 } }}
Expand Down
21 changes: 21 additions & 0 deletions src/components/Capture/Capture.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React, { FC } from 'react';
import { useThree } from '@react-three/fiber';

export type CaptureType = (image?: string) => void;

type CaptureProps = {
onCapture?: CaptureType;
};

const Capture: FC<CaptureProps> = ({ onCapture }) => {
const gl = useThree((state) => state.gl);

if (onCapture) {
onCapture(gl.domElement.toDataURL('image/png', 0.1));
}

// eslint-disable-next-line react/jsx-no-useless-fragment
return <></>;
};

export default Capture;
2 changes: 1 addition & 1 deletion src/components/Models/PoseModel/PoseModel.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const PoseModel: FC<PoseModelProps> = ({ modelUrl, poseUrl, modelRef, sca
const pose = useLoader(GLTFLoader, poseUrl);
const { nodes: sourceNodes } = useGraph(pose.scene);

mutatePose(nodes, sourceNodes);
mutatePose(sourceNodes, nodes);
useEmotion(nodes, emotion);

return <Model modelRef={modelRef} scene={scene} scale={scale} />;
Expand Down
32 changes: 17 additions & 15 deletions src/services/Models.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,30 +114,32 @@ export const mutatePose = (targetNodes?: ObjectMap['nodes'], sourceNodes?: Objec
};

export const useEmotion = (nodes: ObjectMap['nodes'], emotion?: Emotion) => {
const headMesh = (nodes.Wolf3D_Head || nodes.Wolf3D_Avatar) as SkinnedMesh;
// @ts-ignore
const meshes = Object.values(nodes).filter((item: SkinnedMesh) => item?.morphTargetInfluences) as SkinnedMesh[];

const resetEmotions = () =>
headMesh?.morphTargetInfluences?.forEach((_, index) => {
headMesh!.morphTargetInfluences![index] = 0;
const resetEmotions = (resetMeshes: Array<SkinnedMesh>) => {
resetMeshes.forEach((mesh) => {
mesh?.morphTargetInfluences?.forEach((_, index) => {
mesh!.morphTargetInfluences![index] = 0;
});
});
};

useFrame(() => {
if (!headMesh) {
return;
}

if (emotion) {
resetEmotions();
resetEmotions(meshes);

Object.entries(emotion).forEach(([shape, value]) => {
const shapeId = headMesh!.morphTargetDictionary?.[shape];
meshes.forEach((mesh) => {
Object.entries(emotion).forEach(([shape, value]) => {
const shapeId = mesh!.morphTargetDictionary?.[shape];

if (shapeId) {
headMesh!.morphTargetInfluences![shapeId] = value;
}
if (shapeId) {
mesh!.morphTargetInfluences![shapeId] = value;
}
});
});
} else {
resetEmotions();
resetEmotions(meshes);
}
});
};

0 comments on commit e9e7677

Please sign in to comment.