diff --git a/package.json b/package.json
index 341f414..d5b1828 100644
--- a/package.json
+++ b/package.json
@@ -8,6 +8,9 @@
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
+ "ammo.js": "kripken/ammo.js#aab297a4164779c3a9d8dc8d9da26958de3cb778",
+ "babylonjs": "^4.1.0",
+ "babylonjs-loaders": "^4.1.0",
"dexie": "^2.0.4",
"fake-indexeddb": "^3.0.0",
"interactjs": "^1.9.7",
diff --git a/src/components/map/Map.js b/src/components/map/Map.js
index 965afef..2cd5170 100644
--- a/src/components/map/Map.js
+++ b/src/components/map/Map.js
@@ -7,6 +7,8 @@ import MapToken from "./MapToken";
import MapDrawing from "./MapDrawing";
import MapFog from "./MapFog";
import MapControls from "./MapControls";
+import MapDice from "./MapDice";
+import LoadingOverlay from "../LoadingOverlay";
import { omit } from "../../helpers/shared";
import useDataSource from "../../helpers/useDataSource";
@@ -296,8 +298,13 @@ function Map({
map={map}
aspectRatio={aspectRatio}
isEnabled={selectedToolId === "pan"}
- controls={mapControls}
- loading={loading}
+ sideContent={
+ <>
+
+ {mapControls}
+ {loading && }
+ >
+ }
>
{map && mapImage}
{map && mapDrawing}
diff --git a/src/components/map/MapDice.js b/src/components/map/MapDice.js
new file mode 100644
index 0000000..bbcbba3
--- /dev/null
+++ b/src/components/map/MapDice.js
@@ -0,0 +1,194 @@
+import React, { useRef, useState, useCallback } from "react";
+import * as BABYLON from "babylonjs";
+import { Box } from "theme-ui";
+
+import environment from "../../dice/environment.dds";
+
+import ColorDice from "../../dice/color/ColorDice";
+import GemStoneDice from "../../dice/gemStone/GemStoneDice";
+import GlassDice from "../../dice/glass/GlassDice";
+import MetalDice from "../../dice/metal/MetalDice";
+import MetalStoneDice from "../../dice/metalStone/MetalStoneDice";
+import WoodDice from "../../dice/wood/WoodDice";
+
+import Scene from "./MapDiceScene";
+
+function MapDice() {
+ const sceneRef = useRef();
+ const dieRef = useRef([]);
+ const dieSleepRef = useRef([]);
+ const [dieNumbers, setDieNumbers] = useState([]);
+
+ const handleSceneMount = useCallback(({ scene, engine }) => {
+ sceneRef.current = scene;
+ initializeScene(scene);
+ engine.runRenderLoop(() => update(scene));
+ }, []);
+
+ async function initializeScene(scene) {
+ var ground = BABYLON.Mesh.CreateGround("ground", 100, 100, 2, scene);
+ ground.physicsImpostor = new BABYLON.PhysicsImpostor(
+ ground,
+ BABYLON.PhysicsImpostor.BoxImpostor,
+ { mass: 0, friction: 100.0 },
+ scene
+ );
+ ground.isVisible = false;
+
+ function createWall(name, x, z, yaw) {
+ let wall = BABYLON.Mesh.CreateBox(
+ name,
+ 50,
+ scene,
+ true,
+ BABYLON.Mesh.DOUBLESIDE
+ );
+ wall.rotation = new BABYLON.Vector3(0, yaw, 0);
+ wall.position.z = z;
+ wall.position.x = x;
+ wall.physicsImpostor = new BABYLON.PhysicsImpostor(
+ wall,
+ BABYLON.PhysicsImpostor.BoxImpostor,
+ { mass: 0, friction: 1.0 },
+ scene
+ );
+ wall.isVisible = false;
+ }
+
+ createWall("wallTop", 0, -35, 0);
+ createWall("wallRight", -39, 0, Math.PI / 2);
+ createWall("wallBottom", 0, 35, Math.PI);
+ createWall("wallLeft", 39, 0, -Math.PI / 2);
+
+ var roof = BABYLON.Mesh.CreateGround("roof", 100, 100, 2, scene);
+ roof.physicsImpostor = new BABYLON.PhysicsImpostor(
+ roof,
+ BABYLON.PhysicsImpostor.BoxImpostor,
+ { mass: 0, friction: 1.0 },
+ scene
+ );
+ roof.position.y = 10;
+ roof.isVisible = false;
+
+ scene.environmentTexture = BABYLON.CubeTexture.CreateFromPrefilteredData(
+ environment,
+ scene
+ );
+ scene.environmentIntensity = 1.5;
+ }
+
+ function update(scene) {
+ const die = dieRef.current;
+ for (let i = 0; i < die.length; i++) {
+ const dice = die[i];
+ const diceAsleep = dieSleepRef.current[i];
+ const speed = dice.physicsImpostor.getLinearVelocity().length();
+ if (speed < 0.01 && !diceAsleep) {
+ let highestDot = -1;
+ let highestLocator;
+ for (let locator of dice.getChildTransformNodes()) {
+ let dif = locator
+ .getAbsolutePosition()
+ .subtract(dice.getAbsolutePosition());
+ let direction = dif.normalize();
+ const dot = BABYLON.Vector3.Dot(direction, BABYLON.Vector3.Up());
+ if (dot > highestDot) {
+ highestDot = dot;
+ highestLocator = locator;
+ }
+ }
+ dieSleepRef.current[i] = true;
+ const newNumber = parseInt(highestLocator.name.slice(8));
+ setDieNumbers((prevNumbers) => {
+ let newNumbers = [...prevNumbers];
+ newNumbers[i] = newNumber;
+ return newNumbers;
+ });
+ } else if (speed > 0.5 && diceAsleep) {
+ dieSleepRef.current[i] = false;
+ setDieNumbers((prevNumbers) => {
+ let newNumbers = [...prevNumbers];
+ newNumbers[i] = null;
+ return newNumbers;
+ });
+ }
+ }
+ if (scene) {
+ scene.render();
+ }
+ }
+
+ async function handleAddDice(style, type) {
+ const scene = sceneRef.current;
+ if (scene) {
+ const instance = await style.createInstance(type, scene);
+ dieRef.current.push(instance);
+ dieSleepRef.current.push(false);
+ setDieNumbers((prevNumbers) => [...prevNumbers, null]);
+ }
+ }
+
+ return (
+
+
+
+ {dieNumbers.map((num, index) => (
+
+ {num || "?"}
+ {index === dieNumbers.length - 1 ? "" : "+"}
+
+ ))}
+
+ {dieNumbers.length > 0 &&
+ `= ${dieNumbers.reduce((a, b) => (a || 0) + (b || 0))}`}
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default MapDice;
diff --git a/src/components/map/MapDiceScene.js b/src/components/map/MapDiceScene.js
new file mode 100644
index 0000000..07c8780
--- /dev/null
+++ b/src/components/map/MapDiceScene.js
@@ -0,0 +1,124 @@
+import React, { useRef, useEffect } from "react";
+import * as BABYLON from "babylonjs";
+import * as AMMO from "ammo.js";
+import "babylonjs-loaders";
+
+function MapDiceScene({ onSceneMount }) {
+ const sceneRef = useRef();
+ const engineRef = useRef();
+ const canvasRef = useRef();
+ const containerRef = useRef();
+
+ useEffect(() => {
+ const canvas = canvasRef.current;
+ const engine = new BABYLON.Engine(canvas, true, {
+ preserveDrawingBuffer: true,
+ stencil: true,
+ });
+ const scene = new BABYLON.Scene(engine);
+ scene.clearColor = new BABYLON.Color4(0, 0, 0, 0);
+ // Enable physics
+ scene.enablePhysics(
+ new BABYLON.Vector3(0, -98, 0),
+ new BABYLON.AmmoJSPlugin(true, AMMO)
+ );
+
+ let camera = new BABYLON.TargetCamera(
+ "camera",
+ new BABYLON.Vector3(0, 85, 0),
+ scene
+ );
+ camera.fov = 0.25;
+ camera.setTarget(BABYLON.Vector3.Zero());
+
+ onSceneMount && onSceneMount({ scene, engine, canvas });
+
+ engineRef.current = engine;
+ sceneRef.current = scene;
+
+ engine.runRenderLoop(() => {
+ const scene = sceneRef.current;
+ const selectedMesh = selectedMeshRef.current;
+ if (selectedMesh && scene) {
+ const ray = scene.createPickingRay(
+ scene.pointerX,
+ scene.pointerY,
+ BABYLON.Matrix.Identity(),
+ camera
+ );
+ const currentPosition = selectedMesh.getAbsolutePosition();
+ let newPosition = ray.origin.scale(camera.globalPosition.y);
+ newPosition.y = currentPosition.y;
+ const delta = newPosition.subtract(currentPosition);
+ selectedMesh.setAbsolutePosition(newPosition);
+ selectedMeshDeltaPositionRef.current = delta;
+ }
+ });
+
+ const resizeObserver = new ResizeObserver((entries) => {
+ engine.resize();
+ for (let entry of entries) {
+ canvasRef.current.width = entry.contentRect.width;
+ canvasRef.current.height = entry.contentRect.height;
+ }
+ });
+
+ resizeObserver.observe(containerRef.current);
+
+ return () => {
+ resizeObserver.disconnect();
+ };
+ }, [onSceneMount]);
+
+ const selectedMeshRef = useRef();
+ const selectedMeshDeltaPositionRef = useRef();
+ function handlePointerDown() {
+ const scene = sceneRef.current;
+ if (scene) {
+ const pickInfo = scene.pick(scene.pointerX, scene.pointerY);
+ if (pickInfo.hit) {
+ pickInfo.pickedMesh.physicsImpostor.setLinearVelocity(
+ new BABYLON.Vector3(0, 0, 0)
+ );
+ pickInfo.pickedMesh.physicsImpostor.setAngularVelocity(
+ new BABYLON.Vector3(0, 0, 0)
+ );
+ selectedMeshRef.current = pickInfo.pickedMesh;
+ }
+ }
+ }
+
+ function handlePointerUp() {
+ const selectedMesh = selectedMeshRef.current;
+ const deltaPosition = selectedMeshDeltaPositionRef.current;
+ const scene = sceneRef.current;
+ if (selectedMesh && scene && deltaPosition) {
+ let impulse = deltaPosition.scale(1000 / scene.deltaTime);
+ impulse.scale(5);
+ impulse.y = Math.max(impulse.length() * 0.1, 0.5);
+ selectedMesh.physicsImpostor.applyImpulse(
+ impulse,
+ selectedMesh
+ .getAbsolutePosition()
+ .add(new BABYLON.Vector3(0, Math.random() * 0.5 + 0.5, 0))
+ );
+ }
+ selectedMeshRef.current = null;
+ selectedMeshDeltaPositionRef.current = null;
+ }
+
+ return (
+
+
+
+ );
+}
+
+export default MapDiceScene;
diff --git a/src/components/map/MapInteraction.js b/src/components/map/MapInteraction.js
index 06e0cd0..af60f34 100644
--- a/src/components/map/MapInteraction.js
+++ b/src/components/map/MapInteraction.js
@@ -5,8 +5,6 @@ import normalizeWheel from "normalize-wheel";
import { MapInteractionProvider } from "../../contexts/MapInteractionContext";
-import LoadingOverlay from "../LoadingOverlay";
-
const zoomSpeed = -0.001;
const minZoom = 0.1;
const maxZoom = 5;
@@ -16,8 +14,7 @@ function MapInteraction({
aspectRatio,
isEnabled,
children,
- controls,
- loading,
+ sideContent,
}) {
const mapContainerRef = useRef();
const mapMoveContainerRef = useRef();
@@ -159,8 +156,7 @@ function MapInteraction({
- {controls}
- {loading && }
+ {sideContent}
);
}
diff --git a/src/dice/Dice.js b/src/dice/Dice.js
new file mode 100644
index 0000000..b953ccc
--- /dev/null
+++ b/src/dice/Dice.js
@@ -0,0 +1,74 @@
+import * as BABYLON from "babylonjs";
+
+import d20SharpSource from "./meshes/d20Sharp.glb";
+import d20RoundSource from "./meshes/d20Round.glb";
+
+class Dice {
+ static instanceCount = 0;
+
+ static async loadMeshes(diceStyle, material, scene) {
+ let meshes = {};
+ const d20Source = diceStyle === "round" ? d20RoundSource : d20SharpSource;
+ const d20Mesh = await this.loadMesh(d20Source, material, scene);
+ meshes.d20 = d20Mesh;
+ return meshes;
+ }
+
+ static async loadMesh(source, material, scene) {
+ let mesh = (
+ await BABYLON.SceneLoader.ImportMeshAsync("", source, "", scene)
+ ).meshes[1];
+ mesh.setParent(null);
+
+ mesh.material = material;
+
+ mesh.isVisible = false;
+ return mesh;
+ }
+
+ static loadMaterial(materialName, textures, scene) {
+ let pbr = new BABYLON.PBRMaterial(materialName, scene);
+ pbr.albedoTexture = new BABYLON.Texture(textures.albedo);
+ pbr.normalTexture = new BABYLON.Texture(textures.normal);
+ pbr.metallicTexture = new BABYLON.Texture(textures.metalRoughness);
+ pbr.useRoughnessFromMetallicTextureAlpha = false;
+ pbr.useRoughnessFromMetallicTextureGreen = true;
+ pbr.useMetallnessFromMetallicTextureBlue = true;
+ return pbr;
+ }
+
+ static createInstanceFromMesh(mesh, name, scene) {
+ let instance = mesh.createInstance(name);
+ instance.position = mesh.position;
+ for (let child of mesh.getChildTransformNodes()) {
+ const locator = child.clone();
+ locator.setAbsolutePosition(child.getAbsolutePosition());
+ locator.name = child.name;
+ instance.addChild(locator);
+ }
+
+ instance.physicsImpostor = new BABYLON.PhysicsImpostor(
+ instance,
+ BABYLON.PhysicsImpostor.ConvexHullImpostor,
+ { mass: 1, friction: 50 },
+ scene
+ );
+
+ // TODO: put in random start position
+ instance.position.y = 5;
+
+ return instance;
+ }
+
+ static async createInstance(mesh, scene) {
+ this.instanceCount++;
+
+ return this.createInstanceFromMesh(
+ mesh,
+ `dice_instance_${this.instanceCount}`,
+ scene
+ );
+ }
+}
+
+export default Dice;
diff --git a/src/dice/color/ColorDice.js b/src/dice/color/ColorDice.js
new file mode 100644
index 0000000..672355b
--- /dev/null
+++ b/src/dice/color/ColorDice.js
@@ -0,0 +1,27 @@
+import Dice from "../Dice";
+
+import albedo from "./albedo.jpg";
+import metalRoughness from "./metalRoughness.jpg";
+import normal from "./normal.jpg";
+
+class ColorDice extends Dice {
+ static meshes;
+ static material;
+
+ static async createInstance(diceType, scene) {
+ if (!this.material) {
+ this.material = this.loadMaterial(
+ "color_pbr",
+ { albedo, metalRoughness, normal },
+ scene
+ );
+ }
+ if (!this.meshes) {
+ this.meshes = await this.loadMeshes("round", this.material, scene);
+ }
+
+ return Dice.createInstance(this.meshes[diceType], scene);
+ }
+}
+
+export default ColorDice;
diff --git a/src/dice/color/albedo.jpg b/src/dice/color/albedo.jpg
new file mode 100644
index 0000000..f26d42a
Binary files /dev/null and b/src/dice/color/albedo.jpg differ
diff --git a/src/dice/color/metalRoughness.jpg b/src/dice/color/metalRoughness.jpg
new file mode 100644
index 0000000..b749395
Binary files /dev/null and b/src/dice/color/metalRoughness.jpg differ
diff --git a/src/dice/color/normal.jpg b/src/dice/color/normal.jpg
new file mode 100644
index 0000000..0f57fc7
Binary files /dev/null and b/src/dice/color/normal.jpg differ
diff --git a/src/dice/environment.dds b/src/dice/environment.dds
new file mode 100755
index 0000000..e8c89af
Binary files /dev/null and b/src/dice/environment.dds differ
diff --git a/src/dice/gemStone/GemStoneDice.js b/src/dice/gemStone/GemStoneDice.js
new file mode 100644
index 0000000..f7c6993
--- /dev/null
+++ b/src/dice/gemStone/GemStoneDice.js
@@ -0,0 +1,48 @@
+import * as BABYLON from "babylonjs";
+
+import Dice from "../Dice";
+
+import albedo from "./albedo.jpg";
+import metalRoughness from "./metalRoughness.jpg";
+import normal from "./normal.jpg";
+
+class GemStoneDice extends Dice {
+ static meshes;
+ static material;
+
+ static loadMaterial(materialName, textures, scene) {
+ let pbr = new BABYLON.PBRMaterial(materialName, scene);
+
+ pbr.albedoTexture = new BABYLON.Texture(textures.albedo);
+ pbr.normalTexture = new BABYLON.Texture(textures.normal);
+ pbr.metallicTexture = new BABYLON.Texture(textures.metalRoughness);
+ pbr.useRoughnessFromMetallicTextureAlpha = false;
+ pbr.useRoughnessFromMetallicTextureGreen = true;
+ pbr.useMetallnessFromMetallicTextureBlue = true;
+
+ pbr.subSurface.isTranslucencyEnabled = true;
+ pbr.subSurface.translucencyIntensity = 1.0;
+ pbr.subSurface.minimumThickness = 5;
+ pbr.subSurface.maximumThickness = 10;
+ pbr.subSurface.tintColor = new BABYLON.Color3(0, 1, 0);
+
+ return pbr;
+ }
+
+ static async createInstance(diceType, scene) {
+ if (!this.material) {
+ this.material = this.loadMaterial(
+ "gem_stone_pbr",
+ { albedo, metalRoughness, normal },
+ scene
+ );
+ }
+ if (!this.meshes) {
+ this.meshes = await this.loadMeshes("round", this.material, scene);
+ }
+
+ return Dice.createInstance(this.meshes[diceType], scene);
+ }
+}
+
+export default GemStoneDice;
diff --git a/src/dice/gemStone/albedo.jpg b/src/dice/gemStone/albedo.jpg
new file mode 100644
index 0000000..dfd2132
Binary files /dev/null and b/src/dice/gemStone/albedo.jpg differ
diff --git a/src/dice/gemStone/metalRoughness.jpg b/src/dice/gemStone/metalRoughness.jpg
new file mode 100644
index 0000000..7512e39
Binary files /dev/null and b/src/dice/gemStone/metalRoughness.jpg differ
diff --git a/src/dice/gemStone/normal.jpg b/src/dice/gemStone/normal.jpg
new file mode 100644
index 0000000..1290434
Binary files /dev/null and b/src/dice/gemStone/normal.jpg differ
diff --git a/src/dice/glass/GlassDice.js b/src/dice/glass/GlassDice.js
new file mode 100644
index 0000000..961b9be
--- /dev/null
+++ b/src/dice/glass/GlassDice.js
@@ -0,0 +1,49 @@
+import * as BABYLON from "babylonjs";
+
+import Dice from "../Dice";
+
+import albedo from "./albedo.png";
+import mask from "./mask.png";
+import normal from "./normal.jpg";
+
+class GlassDice extends Dice {
+ static meshes;
+ static material;
+
+ static loadMaterial(materialName, textures, scene) {
+ let pbr = new BABYLON.PBRMaterial(materialName, scene);
+ pbr.albedoTexture = new BABYLON.Texture(textures.albedo);
+ pbr.normalTexture = new BABYLON.Texture(textures.normal);
+
+ pbr.roughness = 0.25;
+ pbr.metallic = 0;
+
+ pbr.subSurface.isRefractionEnabled = true;
+ pbr.subSurface.indexOfRefraction = 1.0;
+ pbr.subSurface.isTranslucencyEnabled = true;
+ pbr.subSurface.translucencyIntensity = 1.0;
+ pbr.subSurface.minimumThickness = 10;
+ pbr.subSurface.maximumThickness = 10;
+ pbr.subSurface.tintColor = new BABYLON.Color3(43 / 255, 1, 115 / 255);
+ pbr.subSurface.thicknessTexture = new BABYLON.Texture(textures.mask);
+ pbr.subSurface.useMaskFromThicknessTexture = true;
+
+ return pbr;
+ }
+
+ static async createInstance(diceType, scene) {
+ if (!this.material) {
+ this.material = this.loadMaterial(
+ "glass_pbr",
+ { albedo, mask, normal },
+ scene
+ );
+ }
+ if (!this.meshes) {
+ this.meshes = await this.loadMeshes("round", this.material, scene);
+ }
+ return Dice.createInstance(this.meshes[diceType], scene);
+ }
+}
+
+export default GlassDice;
diff --git a/src/dice/glass/albedo.png b/src/dice/glass/albedo.png
new file mode 100644
index 0000000..50c48bd
Binary files /dev/null and b/src/dice/glass/albedo.png differ
diff --git a/src/dice/glass/mask.png b/src/dice/glass/mask.png
new file mode 100644
index 0000000..27eb660
Binary files /dev/null and b/src/dice/glass/mask.png differ
diff --git a/src/dice/glass/normal.jpg b/src/dice/glass/normal.jpg
new file mode 100644
index 0000000..5f0b6d6
Binary files /dev/null and b/src/dice/glass/normal.jpg differ
diff --git a/src/dice/meshes/d20Round.glb b/src/dice/meshes/d20Round.glb
new file mode 100644
index 0000000..5451233
Binary files /dev/null and b/src/dice/meshes/d20Round.glb differ
diff --git a/src/dice/meshes/d20Sharp.glb b/src/dice/meshes/d20Sharp.glb
new file mode 100644
index 0000000..0749d5b
Binary files /dev/null and b/src/dice/meshes/d20Sharp.glb differ
diff --git a/src/dice/metal/MetalDice.js b/src/dice/metal/MetalDice.js
new file mode 100644
index 0000000..7613d89
--- /dev/null
+++ b/src/dice/metal/MetalDice.js
@@ -0,0 +1,26 @@
+import Dice from "../Dice";
+
+import albedo from "./albedo.jpg";
+import metalRoughness from "./metalRoughness.jpg";
+import normal from "./normal.jpg";
+
+class MetalDice extends Dice {
+ static meshes;
+ static material;
+
+ static async createInstance(diceType, scene) {
+ if (!this.material) {
+ this.material = this.loadMaterial(
+ "metal_pbr",
+ { albedo, metalRoughness, normal },
+ scene
+ );
+ }
+ if (!this.meshes) {
+ this.meshes = await this.loadMeshes("round", this.material, scene);
+ }
+ return Dice.createInstance(this.meshes[diceType], scene);
+ }
+}
+
+export default MetalDice;
diff --git a/src/dice/metal/albedo.jpg b/src/dice/metal/albedo.jpg
new file mode 100644
index 0000000..57b6df4
Binary files /dev/null and b/src/dice/metal/albedo.jpg differ
diff --git a/src/dice/metal/metalRoughness.jpg b/src/dice/metal/metalRoughness.jpg
new file mode 100644
index 0000000..5fddf0d
Binary files /dev/null and b/src/dice/metal/metalRoughness.jpg differ
diff --git a/src/dice/metal/normal.jpg b/src/dice/metal/normal.jpg
new file mode 100644
index 0000000..1be1284
Binary files /dev/null and b/src/dice/metal/normal.jpg differ
diff --git a/src/dice/metalStone/MetalStoneDice.js b/src/dice/metalStone/MetalStoneDice.js
new file mode 100644
index 0000000..5662a5d
--- /dev/null
+++ b/src/dice/metalStone/MetalStoneDice.js
@@ -0,0 +1,27 @@
+import Dice from "../Dice";
+
+import albedo from "./albedo.jpg";
+import metalRoughness from "./metalRoughness.jpg";
+import normal from "./normal.jpg";
+
+class MetalStoneDice extends Dice {
+ static meshes;
+ static material;
+
+ static async createInstance(diceType, scene) {
+ if (!this.material) {
+ this.material = this.loadMaterial(
+ "metal_stone_pbr",
+ { albedo, metalRoughness, normal },
+ scene
+ );
+ }
+ if (!this.meshes) {
+ this.meshes = await this.loadMeshes("round", this.material, scene);
+ }
+
+ return Dice.createInstance(this.meshes[diceType], scene);
+ }
+}
+
+export default MetalStoneDice;
diff --git a/src/dice/metalStone/albedo.jpg b/src/dice/metalStone/albedo.jpg
new file mode 100644
index 0000000..62e3ea2
Binary files /dev/null and b/src/dice/metalStone/albedo.jpg differ
diff --git a/src/dice/metalStone/metalRoughness.jpg b/src/dice/metalStone/metalRoughness.jpg
new file mode 100644
index 0000000..eb10923
Binary files /dev/null and b/src/dice/metalStone/metalRoughness.jpg differ
diff --git a/src/dice/metalStone/normal.jpg b/src/dice/metalStone/normal.jpg
new file mode 100644
index 0000000..821e245
Binary files /dev/null and b/src/dice/metalStone/normal.jpg differ
diff --git a/src/dice/wood/WoodDice.js b/src/dice/wood/WoodDice.js
new file mode 100644
index 0000000..6a26ed0
--- /dev/null
+++ b/src/dice/wood/WoodDice.js
@@ -0,0 +1,27 @@
+import Dice from "../Dice";
+
+import albedo from "./albedo.jpg";
+import metalRoughness from "./metalRoughness.jpg";
+import normal from "./normal.jpg";
+
+class WoodDice extends Dice {
+ static meshes;
+ static material;
+
+ static async createInstance(diceType, scene) {
+ if (!this.material) {
+ this.material = this.loadMaterial(
+ "wood_pbr",
+ { albedo, metalRoughness, normal },
+ scene
+ );
+ }
+ if (!this.meshes) {
+ this.meshes = await this.loadMeshes("sharp", this.material, scene);
+ }
+
+ return Dice.createInstance(this.meshes[diceType], scene);
+ }
+}
+
+export default WoodDice;
diff --git a/src/dice/wood/albedo.jpg b/src/dice/wood/albedo.jpg
new file mode 100644
index 0000000..8df9e7d
Binary files /dev/null and b/src/dice/wood/albedo.jpg differ
diff --git a/src/dice/wood/metalRoughness.jpg b/src/dice/wood/metalRoughness.jpg
new file mode 100644
index 0000000..eee1bda
Binary files /dev/null and b/src/dice/wood/metalRoughness.jpg differ
diff --git a/src/dice/wood/normal.jpg b/src/dice/wood/normal.jpg
new file mode 100644
index 0000000..a236336
Binary files /dev/null and b/src/dice/wood/normal.jpg differ
diff --git a/yarn.lock b/yarn.lock
index 9cb4b13..2db3f6f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2178,6 +2178,10 @@ alphanum-sort@^1.0.0:
resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=
+ammo.js@kripken/ammo.js#aab297a4164779c3a9d8dc8d9da26958de3cb778:
+ version "0.0.2"
+ resolved "https://codeload.github.com/kripken/ammo.js/tar.gz/aab297a4164779c3a9d8dc8d9da26958de3cb778"
+
ansi-colors@^3.0.0:
version "3.2.4"
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf"
@@ -2642,6 +2646,24 @@ babylon@^6.18.0:
resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3"
integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==
+babylonjs-gltf2interface@4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/babylonjs-gltf2interface/-/babylonjs-gltf2interface-4.1.0.tgz#95ec994e352ac5cb74869e238218a1b4df18e2f4"
+ integrity sha512-H2obg+4t8bcmLyzGiOQqmUaTQqTu+6mJUlsMWZvmRBf0k2fQVeTdAkH7aDy6HVIz/THvpIx4ntG1Lsyquvmc5Q==
+
+babylonjs-loaders@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/babylonjs-loaders/-/babylonjs-loaders-4.1.0.tgz#b056423b98c1e3a3962491ec72dce5e0b8852295"
+ integrity sha512-gNC+XEVI5cLJLVRTlkFHVfSY4EZS0VzWzEmNb8M49ZMFNuqOuHsVnQZg4Vms9e4LgvNtws4Z0SWrRanZnkIX5g==
+ dependencies:
+ babylonjs "4.1.0"
+ babylonjs-gltf2interface "4.1.0"
+
+babylonjs@4.1.0, babylonjs@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/babylonjs/-/babylonjs-4.1.0.tgz#a2d1d6765795e9d44f002831554d63d6275394bd"
+ integrity sha512-MnaH1BQIL+PYgqGaAvGVdP8yd7nM1j6sbQi/K/6+RlkHPxIETW2NbjqxiW7Sywgy7r3PwqWT/gxG4Bz95Z6/yA==
+
backo2@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"