Step-by-Step Tutorial — Build Your Own Virtual Art Gallery
📋 Table of Contents
1) Set up your project (2 min)
- Create a folder named
webvr-gallery - Inside it, create a subfolder named
images - Put 9 images in
images/named1_img.jpg…9_img.jpg(or rename later)
Your structure should look like:
webvr-gallery/
├─ index.html
└─ images/
├─ 1_img.jpg … 9_img.jpg
2) Make the HTML file (2 min)
Create index.html (empty file). Open it in VS Code or your editor.
3) Paste the minimal A-Frame skeleton (1 min)
Copy this into index.html (this gives you a working empty scene):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>A-Frame Gallery (No JS)</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="https://aframe.io/releases/1.6.0/aframe.min.js"></script>
<style>html, body { margin:0; height:100%; background:#0b0b12; }</style>
</head>
<body>
<a-scene renderer="colorManagement: true" background="color: #101018">
</a-scene>
</body>
</html>
Open the file in your browser (double-click or use VS Code Live Server). You should see a blank A-Frame canvas.
4) Preload the gallery images (2 min)
Inside <a-scene> ... </a-scene>, add an assets block:
<a-assets timeout="30000">
<img id="img1" src="images/1_img.jpg">
<img id="img2" src="images/2_img.jpg">
<img id="img3" src="images/3_img.jpg">
<img id="img4" src="images/4_img.jpg">
<img id="img5" src="images/5_img.jpg">
<img id="img6" src="images/6_img.jpg">
<img id="img7" src="images/7_img.jpg">
<img id="img8" src="images/8_img.jpg">
<img id="img9" src="images/9_img.jpg">
</a-assets>
Why:
<a-assets> preloads textures so they don't pop in late.
5) Add lights (1 min)
Still inside <a-scene> add:
<a-entity light="type: ambient; intensity: 0.55; color: #fff"></a-entity>
<a-entity position="0 3 2" light="type: directional; intensity: 0.9;
castShadow: true"></a-entity>
6) Build the room (floor + 3 walls) (2 min)
<!-- Floor -->
<a-plane rotation="-90 0 0" width="20" height="20"
color="#2a2a3a" material="roughness:0.9; metalness:0; repeat:4 4"></a-plane>
<!-- Walls -->
<a-plane position="0 2.5 -6" rotation="0 0 0" width="20" height="5"
color="#171725"></a-plane>
<a-plane position="-10 2.5 0" rotation="0 90 0" width="12" height="5"
color="#141422"></a-plane>
<a-plane position="10 2.5 0" rotation="0 -90 0" width="12" height="5"
color="#141422"></a-plane>
Tip: Y-axis is up, Z goes forward/back, X is left/right.
7) Add camera + movement + cursor (2 min)
<a-text value="WASD/Arrows to move • Hover images"
position="0 4.6 -4" align="center" color="#eaeaf7" width="6"></a-text>
<a-entity id="rig" position="0 1.6 4">
<a-camera wasd-controls="acceleration: 20">
<!-- mouse hover (desktop) + gaze (VR) -->
<a-cursor fuse="false"
raycaster="objects: .interactable; rayOrigin: mouse; showLine: false">
</a-cursor>
</a-camera>
</a-entity>
Important: The cursor raycasts only against elements with
class="interactable".
8) Build one framed image (the template) (3 min)
We'll assemble frame + mat + image + caption in a group and position it on the center wall (z ≈ -6):
<a-entity position="-6 3.6 -5.99">
<!-- Frame -->
<a-plane width="2.2" height="1.6" color="#0e0e18"></a-plane>
<!-- Mat -->
<a-plane width="2.06" height="1.46" color="#e7e7ef" position="0 0 0.005"></a-plane>
<!-- Image (interactive, no JS) -->
<a-image class="interactable" src="#img1" width="1.9" height="1.2"
position="0 0 0.01"
material="shader: standard; roughness:0.7; metalness:0.05"
animation__enter="property: scale; to: 1.2 1.2 1; startEvents:
mouseenter; dur: 180; easing: easeOutQuad"
animation__leave="property: scale; to: 1 1 1; startEvents:
mouseleave; dur: 180; easing: easeOutQuad"
animation__enter_pos="property: position; to: 0 0 0.06;
startEvents: mouseenter; dur: 180; easing: easeOutQuad"
animation__leave_pos="property: position; to: 0 0 0.01;
startEvents: mouseleave; dur: 180; easing: easeOutQuad">
</a-image>
<!-- Caption -->
<a-text value="Landscape" color="#cfcfe6" width="2.4"
position="0 -1.0 0.02" align="center"></a-text>
</a-entity>
Test: Reload the page. Move close to the frame and hover — the image should scale forward slightly.
9) Duplicate to make a 3×3 grid (5–7 min)
Copy your entire <a-entity>...</a-entity> block 8 times and change:
position="X Y Z"(grid coordinates)src="#imgN"(pick the right photo)- Optional: change the caption text
Use these suggested positions (on the same wall Z = -5.99):
| Row (Y) | Left X | Center X | Right X |
|---|---|---|---|
| 3.6 | -6 | -2 | 2 |
| 2.1 | -6 | -2 | 2 |
| 0.6 | -6 | -2 | 2 |
Remember to switch each image's src: #img1 … #img9.
Optional wall accents (purely decorative):
<a-plane position="-9.7 2.5 -2" rotation="0 90 0" width="4" height="3"
color="#1b1b2b"></a-plane>
<a-plane position="9.7 2.5 -2" rotation="0 -90 0" width="4" height="3"
color="#1b1b2b"></a-plane>
10) Test on desktop + mobile (2–3 min)
- Desktop: Move with WASD/Arrows, look around with mouse, hover images.
- Mobile: Open the file via a local web server (Live Server). Tap the VR goggles button to enter XR if your device supports it.
11) Troubleshooting (quick fixes)
Nothing shows / black scene
- Check for duplicated
<html>/<head>/<body>tags. You should have one of each. - Check image paths: the console (F12) will show 404s if paths are wrong.
Hover doesn't work
- Ensure images have
class="interactable". - Cursor must include
rayOrigin: mouse.
Too dark
- Increase ambient or directional light
intensity.
Off-by-one Z values
- Keep frames slightly in front of the wall (e.g., -5.99) so they're visible.
12) Stretch goals (no JS required)
- Add a sky:
<a-sky color="#0a0a14"></a-sky>or a 360° src. - Add subtle rotation on hover:
animation__rot_in="property: rotation; to: 0 0 2; startEvents: mouseenter; dur: 180" animation__rot_out="property: rotation; to: 0 0 0; startEvents: mouseleave; dur: 180" - Add floor tiles by using a textured image in assets and
material="src:#tile; repeat: 10 10".
✅ Final Checklist
- 9 images inside /images and all src IDs match
- Frames arranged in 3×3 grid, hover animation works
- WASD/Arrows movement + cursor hover on desktop
- Clean single HTML document (no duplicate <html> or <body>)
