Added babylonjs, ammojs, dice models and simple dice interaction

This commit is contained in:
Mitchell McCaffrey 2020-05-08 12:56:36 +10:00
parent e73f64a3b5
commit 0a71609105
34 changed files with 632 additions and 8 deletions

View File

@ -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",

View File

@ -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={
<>
<MapDice />
{mapControls}
{loading && <LoadingOverlay />}
</>
}
>
{map && mapImage}
{map && mapDrawing}

View File

@ -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 (
<Box
sx={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
}}
>
<Scene onSceneMount={handleSceneMount} />
<div
style={{
position: "absolute",
top: "8px",
left: "50%",
transform: "translateX(-50%)",
display: "flex",
color: "white",
}}
>
{dieNumbers.map((num, index) => (
<h3 key={index}>
{num || "?"}
{index === dieNumbers.length - 1 ? "" : "+"}
</h3>
))}
<h3>
{dieNumbers.length > 0 &&
`= ${dieNumbers.reduce((a, b) => (a || 0) + (b || 0))}`}
</h3>
</div>
<div
style={{
position: "absolute",
bottom: "24px",
left: "50%",
transform: "translateX(-50%)",
}}
>
<button onClick={() => handleAddDice(ColorDice, "d20")}>
Add color d20
</button>
<button onClick={() => handleAddDice(GemStoneDice, "d20")}>
Add gem d20
</button>
<button onClick={() => handleAddDice(GlassDice, "d20")}>
Add glass d20
</button>
<button onClick={() => handleAddDice(MetalDice, "d20")}>
Add metal d20
</button>
<button onClick={() => handleAddDice(MetalStoneDice, "d20")}>
Add metal stone d20
</button>
<button onClick={() => handleAddDice(WoodDice, "d20")}>
Add wood d20
</button>
</div>
</Box>
);
}
export default MapDice;

View File

@ -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 (
<div
style={{ width: "100%", height: "100%", overflow: "hidden" }}
ref={containerRef}
>
<canvas
onPointerDown={handlePointerDown}
onPointerUp={handlePointerUp}
ref={canvasRef}
/>
</div>
);
}
export default MapDiceScene;

View File

@ -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({
</MapInteractionProvider>
</Box>
</Box>
{controls}
{loading && <LoadingOverlay />}
{sideContent}
</Box>
);
}

74
src/dice/Dice.js Normal file
View File

@ -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;

View File

@ -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;

BIN
src/dice/color/albedo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 673 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 KiB

BIN
src/dice/color/normal.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

BIN
src/dice/environment.dds Executable file

Binary file not shown.

View File

@ -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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 493 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 KiB

View File

@ -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;

BIN
src/dice/glass/albedo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

BIN
src/dice/glass/mask.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

BIN
src/dice/glass/normal.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

Binary file not shown.

View File

@ -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;

BIN
src/dice/metal/albedo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 KiB

BIN
src/dice/metal/normal.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

View File

@ -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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 943 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 776 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

27
src/dice/wood/WoodDice.js Normal file
View File

@ -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;

BIN
src/dice/wood/albedo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 911 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

BIN
src/dice/wood/normal.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

View File

@ -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"