How to develop a Web AR Facefilter with React and ThreeJS / React Three Fiber in 2021
In this post i'll walk through how we built a small, simple Web AR Facefilter with free resources from Jeeliz and Sketchfab.
For the 3 year anniversary of 🧙🏼♂️ Level30Wizards we wanted to build something that's related to our work, brand and interests. We finally ended up creating a Snapchat like facefilter for Web AR or Web XR.
You can try the final product here: Facefilter demo
Shameless plug
Hey, are you looking to build a Facefilter for a client or project? We'd love to help.
Intro
Mobile web browsing is advancing fast. We are able to get data from the NFC reader, Storage and also camera. Sadly, as of now, we are not allowed to use the ARKit on iPhones or iPads.
There are a lot of great libraries like 8th Wall or Zappar, but they usually cost a pretty penny.
Luckily, lots of smart people build neural networks that can recognize faces from 2D screens.
WebAR allows you to utilize computer or data visualization without the need of downloading an app, besides your default web browser. Web AR can use both SLAM (Simultaneous Localization and Mapping) and Image Recognition technologies to digitally visualize your content. Since it's still a webpage, we can add a variety of web features and functionality to the AR experience. This opens up loads of opportunities on the web.
Augmented reality will be a valuable addition to a lot of existing web pages. For example, it could help people learn on education sites, or allow potential buyers to visualize objects in their home while shopping.
Goal
The goal is to project a 3D model on top of our face.
Tools
Prototyping disclaimer
This experiment we built, is a complete prototype. We do not recommend you use this in a production environment. This example needs a lot of tweaks before it's viable to use in production.
Code: Introduction
Before we show the camera, we need some time to load the 3D model and calculate some positions. That's why we opted for an introduction screen that add some suspense.
The pretty wavy text is from a previous tutorial! Wavy Text Animation using React Hooks with GSAP v3
1import gsap from 'gsap';2import React, { useEffect } from 'react';34export const Intro = () => {5 useEffect(() => {6 gsap.set('.wavy', { perspective: 400 });78 const sequence = (id, reverse) => {9 const tl = gsap.timeline({ delay: 0.5 });1011 tl.from(`.wavy[data-id="${id}"]`, {12 duration: 0.5,13 autoAlpha: 0,14 ease: 'back',15 stagger: 1,16 })17 .from(`.wavy[data-id="${id}"] [data-letter]`, {18 duration: 0.5,19 autoAlpha: 0,20 scale: 1,21 y: -30,22 rotationX: -90,23 transformOrigin: '0% 50% -50',24 ease: 'back',25 stagger: 0.025,26 })27 .to(`.wavy[data-id="${id}"] [data-letter]`, {28 duration: 0.5,29 delay: 1.25,30 autoAlpha: 0,31 scale: 1,32 y: -30,33 rotationX: -90,34 transformOrigin: '0% 50% -50',35 ease: 'back',36 stagger: 0.025,37 })38 .to(`.wavy[data-id="${id}"]`, {39 duration: 0.25,40 autoAlpha: 0,41 ease: 'back',42 stagger: 1,43 });4445 return tl;46 };4748 const tl = gsap.timeline();4950 tl.add(sequence('one'))51 .add(sequence('two'))52 .add(sequence('three'))53 .to('.intro', {54 duration: 1,55 autoAlpha: 0,56 });5758 tl.timeScale(1.2);5960 tl.play();61 return () => {62 tl.kill();63 };64 }, []);6566 return (67 <div className="intro">68 <AnimatedText id="one" text={'Level30Wizards Presents...'} />69 <AnimatedText id="two" text={'Are you a true wizard?'} />70 <AnimatedText id="three" text={"Let's find out!"} />71 <img72 src="https://level30wizards.com/images/branded/brand-image-6.png"73 alt="Dragon"74 className="dragon"75 />76 <a href="https://level30wizards.com" aria-label="level30wizards">77 <img78 src="https://level30wizards.com/images/svg/logo.svg"79 alt="Level30Wizards"80 className="logo"81 />82 </a>83 </div>84 );85};
Wizards hat
To load in a 3D model, we used GLTFJSX.
1npx gltfjsx model.gltf -t
This results in this:
1// @ts-nocheck2/*3Auto-generated by: https://github.com/pmndrs/gltfjsx4author: Paulina (https://sketchfab.com/Byakko)5license: CC-BY-NC-4.0 (http://creativecommons.org/licenses/by-nc/4.0/)6source: https://sketchfab.com/3d-models/wizards-hat-68a9fb2dbd8442a5bacf9c01413203087title: Wizard's hat8*/910import { useGLTF } from '@react-three/drei';11import React, { useRef } from 'react';1213export default function Model(props) {14 const group = useRef();15 const { nodes, materials } = useGLTF('/models/wizards_hat/scene.gltf');16 return (17 <group ref={group} {...props} dispose={null}>18 <group rotation={[-Math.PI / 2, 0, 0]}>19 <group rotation={[Math.PI / 2, 0, 0]}>20 <mesh21 geometry={nodes.defaultMaterial.geometry}22 material={nodes.defaultMaterial.material}23 />24 <mesh25 geometry={nodes.defaultMaterial_1.geometry}26 material={nodes.defaultMaterial_1.material}27 />28 <mesh29 geometry={nodes.defaultMaterial_2.geometry}30 material={nodes.defaultMaterial_2.material}31 />32 <mesh33 geometry={nodes.defaultMaterial_3.geometry}34 material={nodes.defaultMaterial_3.material}35 />36 <mesh37 geometry={nodes.defaultMaterial_4.geometry}38 material={nodes.defaultMaterial_4.material}39 />40 <mesh41 geometry={nodes.defaultMaterial_5.geometry}42 material={nodes.defaultMaterial_5.material}43 />44 </group>45 </group>46 </group>47 );48}4950useGLTF.preload('/models/wizards_hat/scene.gltf');
Web AR Library
We used Jeeliz R3F Demo and combined that with some logic from their Matrix Demo. If you want to, without any shame, copy paste this blogpost to create a working product... You're out of luck. Refer to the demos and tinker yourself, you'll probably learn more.
The resulting code that did most of the work looked a bit like this:
1const FaceFollower = props => {2 const objRef = useRef();3 useEffect(() => {4 const threeObject3D = objRef.current;5 _faceFollowers[props.faceIndex] = threeObject3D;6 });78 return (9 <object3D ref={objRef}>10 <Suspense fallback={null}>11 <WizardsHat12 rotation={[0, -Math.PI, 0]}13 position={[0, 1.825, 0]}14 scale={[1.5, 1.5, 1.5]}15 renderOrder={2}16 />17 <Head position={[0, -0.1435, 0]} scale={[1.125, 1, 1.125]} />18 </Suspense>19 </object3D>20 );21};
We used a model of a human's head and used a trick with colorWrite
and renderOrder
.
We just used some human head from SketchFab, low-poly is fine.
1const hiderMat = new THREE.MeshPhongMaterial({2 attach: 'material',3 color: 'hotpink',4 colorWrite: false,5 renderOrder: 1,6});
If we didn't do this, the hat would clip your face when you throw your head back in laughter, because the web ar experience is so amazing.
Snapshot!
It wouldn't be a facefilter if you couldn't take a photo while making a peace-sign and touting your lips like a duck.
For our next trick, I used 3 canvas elements. Yes, we know.
We combine the camera canvas, the canvas containing the "hat" and "head" and the canvas on which we project our logo.
1const faceFilterCanvasRef = useRef(null);2const canvasRef = useRef(null);3const pictureCanvasRef = useRef(null);4const logoCanvasRef = useRef(null);56const snapshot = useCallback(() => {7 const canvas = pictureCanvasRef.current;8 canvas.getContext('2d').drawImage(faceFilterCanvasRef.current, 0, 0);910 const img = new Image();11 img.src = '/images/logo.png';1213 img.addEventListener('error', e => {14 console.error(e);15 });16 img.addEventListener('load', e => {17 logoCanvasRef.current18 .getContext('2d')19 .drawImage(20 img,21 sizing.width - (144 + 24),22 sizing.height - (42.38 + 24),23 144,24 42.3825 );2627 mergeImages([28 canvas.toDataURL('image/png'),29 canvasRef.current.toDataURL('image/png'),30 logoCanvasRef.current.toDataURL('image/png'),31 ]).then(b64 => {32 setDownloadUrl(b64);33 setShareData(b64);34 });35 });36}, []);
Showing the final image
To download and share your photo, we tried to implement the Native Share API. But, that API required us to save the images, which we didn't like since that's a privacy concern. Please take that into consideration if you build this yourself. Use a token or some way to verify the user looking at the image is from a shared link or the current user.
1<div2 style={{3 display: shareData ? 'block' : 'none',4 position: 'fixed',5 zIndex: 3,6 borderRadius: '50%',7 overflow: 'hidden',8 }}9>10 <img11 style={{12 position: 'fixed',13 zIndex: 4,14 width: 'auto',15 height: '100%',16 top: '50%',17 left: '50%',18 transform: 'translate(-50%,-50%)',19 }}20 src={shareData}21 alt="generated"22 />23</div>
PS.
See an example in action!.
We're working on some magical things at 🧙🏼♂️ Level30Wizards, be sure to follow us on socials to keep an eye out for new releases.
Thanks for reading!
We hope you've learned something about augmented reality in web browsers from this article.
If you use this tech, please do so responsibly.
Other posts you might like
How I built the Level30Wizards page transition with Gatsby and Framer Motion
July 02, 2020Tech stack The Level30Wizards website is built with: Gatsby for templating, routing, etc. Framer…
Creating scroll snapping blocks with GSAP v3 animations
July 01, 2020Setting up some HTML So firstly I want to say something about using a lot of div elements. Stop…
Beginner guide to web animation to get started
June 30, 2020A Creative Front-End Developer or Motion Developer will make a UI or concept come to life on the web…