How I Built WebXR using A-Frame, Preact, & Snowpack
Untuk pembaca berbahasa Indonesia periksa link ini.
Since the end of last year I was working for webXR using A-Frame to create applications for Virtual Reality device. While working for those applications, I did some research how to build WebXR using A-Frame in NodeJs stacks.
And Today, I would like to share my experiment through a short tutorial on creating your own WebXR application using A-Frame, Preact (instead of React), and Snowpack.
What’s included in this tutorial?
- What is A-Frame?
- Why Preact?
- Why Snowpack?
- Into to the tutorial: Setup!
- Setting up the Environment
- Add Your First Object
- Make it Interactive
- Deployment (bonus steps)
- Wrap it up!
What is A-Frame?
A-Frame is a web framework for building 3D/Augmenter Reality (AR)/Virtual Reality (VR) experiences based on top of HTML. A-Frame supports most VR headsets such as Vive, Rift, Windows Mixed Reality, Daydream, GearVR, Cardboard, Oculus Go, Oculus Quest, and can even be used for augmented reality. Although A-Frame supports the whole spectrum, A-Frame aims to define fully immersive interactive VR experiences that go beyond basic 360° content, making full use of positional tracking and controllers.
The cool thing in A-Frame, it let you build WebXR in a few minutes writing just a few lines of HTML. It provides an excellent HTML API for you to scaffold out the scene, while still giving you full flexibility by letting you access the rich three.js API that powers it. Yup, A-Frame is built on top of three.js, an advanced 3D JavaScript library that makes working with WebGL. Furthermore, I think the documentation is an excellent place to learn more about it in detail.
A-Frame is built on the Entity-component-system (ECS) architecture, a very common pattern utilized in 3D and game development most notably popularized by Unity, a game engine. What ECS means in the context of an A-Frame app is that we create a bunch of Entities that quite literally do nothing, and attach components to them to describe their behavior and appearance.
Why Preact?
Preact or PreactJs is alternative to React with the same modern API. Preact describes itself as a fast 3kB so that Preact aims to deliver on a few key goals on performance, size, efficiency memory usage, and compability.
Meanwhile, the advantage for using React is because it using Component-Based Architecture which is similar to the architecture of the A-Frame (ECS Architecture as mentioned above). And because we’re using whatever Preact or React, this means that we’ll be passing props into our Entity
to tell it what to render. For example, passing in a-box
as the value of the prop primitive
will render a box for us. Same goes for a-sphere
, or a-cylinder
. Kemudian kita dapat memasukkan nilai lain pada atribut lain seperti position
, rotation
, material
, height
, dll.
Why Snowpack?
Snowpack describes itself as a lightning-fast frontend build tool, designed for the modern web. It is an alternative to heavier, more complex bundlers like webpack or Parcel in your development workflow. Because we will build this WebXR app in a NodeJs full-stacks so we need a javascript bundler or build tool. In this case, I would choose snowpack and I would start with it.
Into the Tutorial: Setup!
In this part I will show you how I setup everything before we can create the WebXR app. I will share the boilerplate on the bottom of this part.
First of all, generate a nodejs project. I love to use yarn instead of npm.
yarn init
after you finished generate your nodejs project. Add snowpack and the others. For snowpack. In this case, I also add prefresh, it is for enabling the HMR and Fast Refresh:
yarn add --dev snowpack @prefresh/snowpack
For A-Frame. Don’t forget to add aframe-react either, it will be used for bridging the aframe into react components:
yarn add aframe aframe-react
For Preact, just type it:
yarn add preact
After you finished to install the dependencies, add some configuration (especially the scripts) on your package.json. And it will look like this:
{
"name": "hello-virtualworld",
"version": "1.0.0",
"description": "Getting into WebXR using A-Frame in NodeJs stacks",
"main": "index.js",
"author": "rizanw",
"license": "MIT",
"scripts": {
"start": "snowpack dev --port 3000",
"build": "snowpack build"
},
"dependencies": {
"aframe": "^1.2.0",
"aframe-react": "^4.4.0",
"preact": "^10.5.14"
},
"devDependencies": {
"@prefresh/snowpack": "^3.1.2",
"snowpack": "^3.8.2"
}
}
After that, we need to generate the snowpack config file by typing:
snowpack init
It will generate snowpack.config.js
file. Then make the configuration like this:
/** @type {import("snowpack").SnowpackUserConfig } */
module.exports = {
mount: {
public: { url: "/", static: true },
src: { url: "/dist" },
},
plugins: ["@prefresh/snowpack"],
optimize: {
minify: true,
},
packageOptions: {
/* ... */
},
devOptions: {
/* ... */
},
buildOptions: {
/* ... */
},
alias: {
react: "preact/compat",
"react-dom": "preact/compat",
},
};
Then we need to create a public
folder and src
folder. The public
folder is used for our html file and the necessary. And the src
folder is our javascript code, in this case is the A-Frame with Preact.
The required files inside the public
folder is index.html
. While inside the src
folder we will need index.jsx
as the endpoint of our react components.
After the necessary files filled, we can add some supported files. And our project will look like this.
index.tsx
will be the entry point of our javascript source, while the App.tsx
is our React application. For the complete code and the boilerplate you can clone this repo. Then you can follow the basic tutorial below.
Setting Up the Environment
Because it will be 3D world. We will need establish the environtment. We can start it with a blank slate by deleting everything inside the Scene
component. The environment that we can set basically is sky and the ground. Or the most easiest, you can aframe-environtment-component by the community.
First of all download the environment component by the community. You can put it anywhere, I will put it in assets/js
folder. After it we can import it on the index.js
:
import "./assets/js/aframe-environment";
Inside the Scene
component add environment
like this:
<Scene
environment={{
preset: "starry",
seed: 1,
lightPosition: { x: 200.0, y: 1.0, z: -50.0 },
fog: 0.8,
ground: "hills",
groundYScale: 5.0,
groundTexture: "none",
groundColor: "#242444",
grid: "1x1",
}}
>...</Scene>
Done! in this step actually, you can start your app and it will look like this:
If you feel it’s too dark add some light source. A-Frame has 5 types of lights from ambient, directional, hemisphere, point, and spot light. For the example, I will add the ambient, directional light, and the point light :
<Entity light={{ type: “ambient”, color: “#BBB” }} />
<Entity
light={{ type: “directional”, color: “#EEE”, intensity: “0.6” }}
position=”0 -0.4 1"
/>
<Entity
light={{ type: “point”, intensity: “0.4”, distance: 40, decay: 2 }}
position=”0 10 0"
/>
Then you will see the differences:
It’s still nothing inside. So add a component inside by following the next tutorial
Add Your First Object
Here we go! I will add a box
component on it includes the props. For the example:
<Entity
primitive=”a-box”
color="tomato"
depth="4"
height="4"
width="4"
position={{ x: 0.0, y: 4, z: -10.0 }}
/>
Now it’s just a box in front of you now. You can add some animation on this box, like this:
<Entity
primitive=”a-box”
color="tomato"
depth="4"
height="4"
width="4"
position={{ x: 0.0, y: 4, z: -10.0 }}
animation__rotate={{
property: 'rotation',
dur: 20000,
easing: 'linear',
loop: true,
to: { x: 360, y: 360, z: 0 }
}}
/>
And the simple tomato box will rotating itself.
Make it Interactive
We have the scene and the environment. we have placed an object. we have animate the object. But, could we interact with the object? yes, we can! Let’s add some user input by changing the color of the box every time it gets touched on it.
A-Frame has fully functional raycaster out of the box. The raycaster checks for intersections at a certain interval against a list of objects, and will emit events on the entity when it detects intersections or clearing of intersections. To do the interaction, A-Frame has cursor component and laser-controls components both build on top of the raycaster component. To add a raycaster using cursor component, we provide the raycaster
props to the camera with the class of objects which we want to be clickable. Our camera component should now look like:
<Entity primitive="a-camera">
<Entity
cursor={{ fuse: true, fuseTimeout: 500 }}
geometry={{
primitive: “ring”,
radiusInner: 0.006,
radiusOuter: 0.01,
}}
material={{ color: “white”, shader: “flat” }}
position=”0 0 -1"
raycaster="objects: .raycasterable"
/>
</Entity>
We need the aframe-event-set-component from the community. It lets us define events and their effects accordingly. To install it we can just type:
yarn add aframe-event-set-component
Then import it on the index.tsx
after the aframe imported:
...
import “aframe”;
import “aframe-event-set-component”
...
So now we can use the event-set component as a props. But before that, we need to set the box as raycasterable
object by add class props on it. We also need to replace the color string value with our array of colors.
<Entity
primitive="a-box"
color={COLORS[colorIndex]}
...
class=”raycasterable”
events={{
click: () => {
setColorIndex((colorIndex + 1) % COLORS.length);
},
}}
animation__click={{
property: "scale",
startEvents: "click",
easing: "easeInCubic",
dur: 200,
from: "0.1 0.1 0.1",
to: "1 1 1",
}}
/>
Because I use setColorIndex
using hooks. Do not forget to add it on the top inside the App
function component:
const [colorIndex, setColorIndex] = useState(0);
And add your color array outside the App
function component:
const COLORS = [“tomato”, “#D92B6A”, “#9564F2”, “#B4C1B3”];
Here’s the final result will be:
Deployment (bonus steps)
After you finished with the project, It’s time to deploy. We can use the awesome tool called surge.sh. First, we need a production build of our app:
yarn build
It will output final build to the build/
directory. After that we need to install surge.sh by running:
npm install -g surge
Yup, NPM! I use npm for install it globally only. After the surge is installed. Now we can publish it:
surge build/
It should prompt you to log in or create an account and you'll have the choice to change your domain name. The rest should be very straightforward, and you will get a URL of your deployed site at the end.
That's it. You can check the live demo here: https://hello-virtualworld.surge.sh. While for the complete code you can check this repo.
Wrap it up!
Now you see how this is pretty much like creating ordinary React app, isn't it? Since React is excellent at binding state to markup, diffing it for you, and re-rendering, we’ll be taking advantage of that. Keep in mind that we won’t be handling any WebGL render calls or manipulating the animation loop with React. So that A-Frame has a built in animation engine that handles that for us. We just need to pass in the appropriate props and let it do the hard work for us.
References:
- https://aframe.io/docs/1.2.0/
- https://preactjs.com/guide/v10/getting-started
- https://www.snowpack.dev/tutorials/quick-start
- https://aframe.io/aframe-school/#/
- https://aframe.io/blog/awoa-75/
- https://medium.com/@necsoft/three-js-101-hello-world-part-1-443207b1ebe1
- https://github.com/supermedium/aframe-react