mirror of
https://github.com/storytold/spark.git
synced 2026-10-17 00:12:59 +00:00
Initial commit
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
<!DOCTYPE html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Forge • Animation • Skinning</title>
|
||||
<style> body {margin: 0;} </style>
|
||||
</head>
|
||||
<body>
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"three": "/examples/js/vendor/three/build/three.module.js",
|
||||
"@forge-gfx/forge": "/dist/forge.module.js"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script type="module">
|
||||
import * as THREE from "three";
|
||||
import { SplatMesh, SplatSkinning } from "@forge-gfx/forge";
|
||||
import { getAssetFileURL } from "/examples/js/get-asset-url.js";
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||||
const renderer = new THREE.WebGLRenderer();
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
document.body.appendChild(renderer.domElement)
|
||||
|
||||
function gridToBoneIndex(x, y) {
|
||||
return 15 * (y + 7) + (x + 7);
|
||||
}
|
||||
|
||||
const splatURL = await getAssetFileURL("butterfly.spz");
|
||||
const butterfly = new SplatMesh({
|
||||
url: splatURL,
|
||||
onLoad: (mesh) => {
|
||||
// Make a 15x15 uniform grid of bones on the XY plane
|
||||
const skinning = new SplatSkinning({ mesh, numBones: 15 * 15 });
|
||||
const position = new THREE.Vector3();
|
||||
const quaternion = new THREE.Quaternion();
|
||||
for (let y = -7; y <= 7; y++) {
|
||||
for (let x = -7; x <= 7; x++) {
|
||||
position.set(x * 0.05, y * 0.05, 0);
|
||||
skinning.setRestQuatPos(gridToBoneIndex(x, y), quaternion, position);
|
||||
}
|
||||
}
|
||||
|
||||
// Compute bone indices and weights for each Gsplat such that
|
||||
// it uses the 4 nearest bones in a bilinear manner.
|
||||
const boneIndices = new THREE.Vector4();
|
||||
const weights = new THREE.Vector4();
|
||||
mesh.forEachSplat((index, center) => {
|
||||
const x = center.y / 0.05;
|
||||
const y = center.x / 0.05;
|
||||
const gridX = Math.max(-7, Math.min(6, Math.floor(x)));
|
||||
const gridY = Math.max(-7, Math.min(6, Math.floor(y)));
|
||||
const fractX = Math.max(0, Math.min(1, x - gridX));
|
||||
const fractY = Math.max(0, Math.min(1, y - gridY));
|
||||
boneIndices.set(
|
||||
gridToBoneIndex(gridX, gridY),
|
||||
gridToBoneIndex(gridX + 1, gridY),
|
||||
gridToBoneIndex(gridX, gridY + 1),
|
||||
gridToBoneIndex(gridX + 1, gridY + 1),
|
||||
);
|
||||
weights.set(
|
||||
(1 - fractX) * (1 - fractY),
|
||||
fractX * (1 - fractY),
|
||||
(1 - fractX) * fractY,
|
||||
fractX * fractY,
|
||||
);
|
||||
skinning.setSplatBones(index, boneIndices, weights);
|
||||
});
|
||||
|
||||
mesh.skinning = skinning;
|
||||
mesh.updateGenerator();
|
||||
},
|
||||
});
|
||||
butterfly.quaternion.set(1, 0, 0, 0);
|
||||
butterfly.position.set(0, 0, -1);
|
||||
scene.add(butterfly);
|
||||
|
||||
renderer.setAnimationLoop(function animate(time) {
|
||||
renderer.render(scene, camera);
|
||||
if (butterfly.skinning) {
|
||||
// Making sure async loading and initialization is complete
|
||||
const position = new THREE.Vector3();
|
||||
const quaternion = new THREE.Quaternion();
|
||||
for (let y = -7; y <= 7; y++) {
|
||||
for (let x = -7; x <= 7; x++) {
|
||||
position.set(x * 0.05, y * 0.05, 0);
|
||||
// Rotate Gsplats to flap the wings
|
||||
quaternion.setFromEuler(
|
||||
new THREE.Euler(
|
||||
x * 0.09 * Math.sin(time * 0.0011),
|
||||
y * 0.05 * Math.cos(time * 0.0071),
|
||||
0.2 * Math.sin(time * 0.0011),
|
||||
),
|
||||
);
|
||||
butterfly.skinning.setBoneQuatPos(gridToBoneIndex(x, y), quaternion, position);
|
||||
}
|
||||
}
|
||||
butterfly.skinning.updateBones();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"robot-head.spz": {
|
||||
"url": "https://forge-gfx.github.io/assets/splats/robot-head.spz",
|
||||
"directory": "splats"
|
||||
},
|
||||
"forge.spz": {
|
||||
"url": "https://forge-gfx.github.io/assets/splats/forge.spz",
|
||||
"directory": "splats"
|
||||
},
|
||||
"butterfly-wings-closed.spz": {
|
||||
"url": "https://forge-gfx.github.io/assets/splats/butterfly-wings-closed.spz",
|
||||
"directory": "splats"
|
||||
},
|
||||
"butterfly.spz": {
|
||||
"url": "https://forge-gfx.github.io/assets/splats/butterfly.spz",
|
||||
"directory": "splats"
|
||||
},
|
||||
"fireplace.spz": {
|
||||
"url": "https://forge-gfx.github.io/assets/splats/fireplace.spz",
|
||||
"directory": "splats"
|
||||
},
|
||||
"branzino-amarin.spz": {
|
||||
"url": "https://forge-gfx.github.io/assets/splats/food/branzino-amarin.spz",
|
||||
"directory": "splats/food"
|
||||
},
|
||||
"burger-from-amboy.spz": {
|
||||
"url": "https://forge-gfx.github.io/assets/splats/food/burger-from-amboy.spz",
|
||||
"directory": "splats/food"
|
||||
},
|
||||
"clams-and-caviar-by-ikoyi.spz": {
|
||||
"url": "https://forge-gfx.github.io/assets/splats/food/clams-and-caviar-by-ikoyi.spz",
|
||||
"directory": "splats/food"
|
||||
},
|
||||
"coral-caviar.spz": {
|
||||
"url": "https://forge-gfx.github.io/assets/splats/food/coral-caviar.spz",
|
||||
"directory": "splats/food"
|
||||
},
|
||||
"double-double-from-InNOut.spz": {
|
||||
"url": "https://forge-gfx.github.io/assets/splats/food/double-Double-from-InNOut.spz",
|
||||
"directory": "splats/food"
|
||||
},
|
||||
"gyro.spz": {
|
||||
"url": "https://forge-gfx.github.io/assets/splats/food/gyro.spz",
|
||||
"directory": "splats/food"
|
||||
},
|
||||
"iberico-sandwich-by-reserve.spz": {
|
||||
"url": "https://forge-gfx.github.io/assets/splats/food/iberico-sandwich-by-reserve.spz",
|
||||
"directory": "splats/food"
|
||||
},
|
||||
"primerib-tamos.spz": {
|
||||
"url": "https://forge-gfx.github.io/assets/splats/food/primerib-tamos.spz",
|
||||
"directory": "splats/food"
|
||||
},
|
||||
"steaksandwich-mels.spz": {
|
||||
"url": "https://forge-gfx.github.io/assets/splats/food/steaksandwich-mels.spz",
|
||||
"directory": "splats/food"
|
||||
},
|
||||
"pad-thai.spz": {
|
||||
"url": "https://forge-gfx.github.io/assets/splats/food/pad-thai.spz",
|
||||
"directory": "splats/food"
|
||||
},
|
||||
"tomahawk-niku.spz": {
|
||||
"url": "https://forge-gfx.github.io/assets/splats/food/tomahawk-niku.spz",
|
||||
"directory": "splats/food"
|
||||
},
|
||||
"rubberduck.glb": {
|
||||
"url": "https://forge-gfx.github.io/assets/models/rubberduck.glb",
|
||||
"directory": "models"
|
||||
},
|
||||
"table.glb": {
|
||||
"url": "https://forge-gfx.github.io/assets/models/table.glb",
|
||||
"directory": "models"
|
||||
},
|
||||
"butterfly.png": {
|
||||
"url": "https://forge-gfx.github.io/assets/images/butterfly.png",
|
||||
"directory": "images"
|
||||
},
|
||||
"furry.spz": {
|
||||
"url": "https://forge-gfx.github.io/assets/splats/furry.spz",
|
||||
"directory": "splats"
|
||||
},
|
||||
"pedestal.spz": {
|
||||
"url": "https://forge-gfx.github.io/assets/splats/pedestal.spz",
|
||||
"directory": "splats"
|
||||
},
|
||||
"cat.spz": {
|
||||
"url": "https://forge-gfx.github.io/assets/splats/cat.spz",
|
||||
"directory": "splats"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Forge • Environment Mapping</title>
|
||||
<style>
|
||||
label {
|
||||
font-family: sans-serif;
|
||||
font-size: 20px;
|
||||
position: absolute;
|
||||
margin: 1rem auto;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<label><input type="checkbox" onchange="toggleMetal(this)" autocomplete="off"> Metal</label>
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"three": "/examples/js/vendor/three/build/three.module.js",
|
||||
"three/addons/": "/examples/js/vendor/three/examples/jsm/",
|
||||
"@forge-gfx/forge": "/dist/forge.module.js"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script type="module">
|
||||
import * as THREE from "three";
|
||||
import { SplatMesh, ForgeControls, ForgeRenderer } from "@forge-gfx/forge";
|
||||
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
|
||||
import { getAssetFileURL } from "/examples/js/get-asset-url.js";
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||||
|
||||
const renderer = new THREE.WebGLRenderer();
|
||||
renderer.setClearColor(new THREE.Color(0x3b5272), 1.0);
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
document.body.appendChild(renderer.domElement)
|
||||
|
||||
// Explicitly create a ForgeRenderer to render environment maps
|
||||
const forge = new ForgeRenderer({ renderer });
|
||||
|
||||
const background = new SplatMesh({ url: "https://d28zzqy0iyovbz.cloudfront.net/a58b478d/scene.compressed.ply" });
|
||||
|
||||
background.position.set(5, -10, -10);
|
||||
background.quaternion.set(1, 0, 0, 0);
|
||||
scene.add(background);
|
||||
// Make sure background is loaded before rendering env map
|
||||
await background.initialized;
|
||||
|
||||
// Add light to make the duck pop out a bit more
|
||||
const rectLight = new THREE.RectAreaLight(0xffffff, 3, 2, 2);
|
||||
rectLight.position.set(0, 1, 1);
|
||||
rectLight.lookAt(0, 0, 0);
|
||||
scene.add(rectLight)
|
||||
|
||||
// Add rubber duck
|
||||
const gltfLoader = new GLTFLoader();
|
||||
const modelURL = await getAssetFileURL('rubberduck.glb');
|
||||
const gltf = await gltfLoader.loadAsync(modelURL);
|
||||
const duck = gltf.scene;
|
||||
duck.position.set(0, -0.2, -1);
|
||||
duck.scale.set(3, 3, 3);
|
||||
scene.add(duck);
|
||||
|
||||
let renderedEnvMap = false;
|
||||
|
||||
const controls = new ForgeControls({ canvas: renderer.domElement });
|
||||
|
||||
renderer.setAnimationLoop(function animate(time) {
|
||||
controls.update(camera);
|
||||
renderer.render(scene, camera);
|
||||
duck.rotation.y += 0.01;
|
||||
|
||||
// Make sure we only launch async renderEnvMap once
|
||||
if (!renderedEnvMap) {
|
||||
renderedEnvMap = true;
|
||||
forge.renderEnvMap({
|
||||
scene,
|
||||
// World center to render from
|
||||
worldCenter: duck.position,
|
||||
// Hide the donut so we don't obscure the environment
|
||||
hideObjects: [duck],
|
||||
// Not needed in normal use, guards against "first render" issues
|
||||
update: true,
|
||||
})
|
||||
.then((envMap) => {
|
||||
for (let obj of duck.children) {
|
||||
// Set some reflectivity in the materials of the duck
|
||||
obj.material.envMap = envMap;
|
||||
obj.material.original_color = obj.material.color.clone();
|
||||
obj.material.metalness = 0.2;
|
||||
obj.material.roughness = 0.1;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Toggles between normal duck and metal duck
|
||||
window.toggleMetal = function toggleMetal(el) {
|
||||
const metal = el.checked;
|
||||
for (let obj of duck.children) {
|
||||
obj.material.color.set(metal ? 0xAAAAAA : obj.material.original_color);
|
||||
obj.material.metalness = metal ? 1.0 : 0.2;
|
||||
obj.material.roughness = metal ? 0.02 : 0.1;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,121 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Forge • Carousel</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"three": "/examples/js/vendor/three/build/three.module.js",
|
||||
"@forge-gfx/forge": "/dist/forge.module.js"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script type="module">
|
||||
import * as THREE from "three";
|
||||
import { SplatMesh } from "@forge-gfx/forge";
|
||||
import { getAssetFileURL } from "/examples/js/get-asset-url.js";
|
||||
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||||
const renderer = new THREE.WebGLRenderer();
|
||||
renderer.setClearColor(0x000000, 1);
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
document.body.appendChild(renderer.domElement);
|
||||
|
||||
// Setup mouse controls to orbit the camera around
|
||||
const controls = new OrbitControls(camera, renderer.domElement);
|
||||
controls.target.set(0, 0, -3);
|
||||
controls.minDistance = 2;
|
||||
controls.maxDistance = 3;
|
||||
controls.enablePan = false;
|
||||
controls.enabled = true;
|
||||
controls.update();
|
||||
|
||||
const ASSETS = [
|
||||
{ file: "butterfly.spz", scale: 1.2 },
|
||||
{ file: "furry.spz", scale: 0.45 },
|
||||
{ file: "forge.spz", scale: 1 },
|
||||
];
|
||||
const assets = [];
|
||||
|
||||
const pedestalURL = await getAssetFileURL("pedestal.spz");
|
||||
const pedestalSplat = new SplatMesh({ url: pedestalURL });
|
||||
pedestalSplat.scale.set(0.4, 0.4, 0.4);
|
||||
pedestalSplat.position.set(0, -2.25, -3);
|
||||
pedestalSplat.opacity = 0.0;
|
||||
scene.add(pedestalSplat);
|
||||
|
||||
for (let asset of ASSETS) {
|
||||
const splatURL = await getAssetFileURL(asset.file);
|
||||
const splat = new SplatMesh({ url: splatURL });
|
||||
splat.quaternion.set(1, 0, 0, 0);
|
||||
splat.position.set(0, 0, -3);
|
||||
splat.scale.setScalar(asset.scale);
|
||||
assets.push(splat);
|
||||
}
|
||||
|
||||
let current = 0;
|
||||
scene.add(assets[current]);
|
||||
|
||||
const FADE_TIME = 1000; //ms
|
||||
let fadeOut = null;
|
||||
let fadeIn = null;
|
||||
|
||||
let prevTime;
|
||||
|
||||
renderer.setAnimationLoop(function animate(time) {
|
||||
const dt = time - (prevTime ?? time);
|
||||
prevTime = time;
|
||||
|
||||
renderer.render(scene, camera);
|
||||
assets[current].rotation.y = time / 1500;
|
||||
controls.update();
|
||||
|
||||
// fade out
|
||||
if (fadeOut !== null) {
|
||||
fadeOut += dt;
|
||||
|
||||
if (fadeOut < FADE_TIME) {
|
||||
assets[current].opacity = 1 - easeInOutSine(fadeOut / FADE_TIME);
|
||||
} else {
|
||||
fadeOut = null;
|
||||
fadeIn = 0;
|
||||
scene.remove(assets[current]);
|
||||
current = (current + 1) % assets.length;
|
||||
scene.add(assets[current]);
|
||||
}
|
||||
}
|
||||
|
||||
// fade in
|
||||
if (fadeIn !== null) {
|
||||
fadeIn += dt;
|
||||
pedestalSplat.opacity = ASSETS[current].file === "furry.spz" ? 1 : 0;
|
||||
if (fadeIn < FADE_TIME) {
|
||||
assets[current].opacity = easeInOutSine(fadeIn / FADE_TIME);
|
||||
} else {
|
||||
fadeIn = null;
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// start fadeout every x seconds
|
||||
setInterval(() => { fadeOut = 0 }, 5000);
|
||||
|
||||
function easeInOutSine(x) {
|
||||
return -(Math.cos(Math.PI * x) - 1) / 2;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Forge • Hello World</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"three": "/examples/js/vendor/three/build/three.module.js",
|
||||
"@forge-gfx/forge": "/dist/forge.module.js"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script type="module">
|
||||
import * as THREE from "three";
|
||||
import { SplatMesh } from "@forge-gfx/forge";
|
||||
import { getAssetFileURL } from "/examples/js/get-asset-url.js";
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||||
const renderer = new THREE.WebGLRenderer();
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
document.body.appendChild(renderer.domElement)
|
||||
|
||||
const splatURL = await getAssetFileURL("butterfly.spz");
|
||||
const butterfly = new SplatMesh({ url: splatURL });
|
||||
butterfly.quaternion.set(1, 0, 0, 0);
|
||||
butterfly.position.set(0, 0, -3);
|
||||
scene.add(butterfly);
|
||||
|
||||
renderer.setAnimationLoop(function animate(time) {
|
||||
renderer.render(scene, camera);
|
||||
butterfly.rotation.y += 0.01;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="refresh" content="0;url=/" />
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,75 @@
|
||||
// Definition of all the food assets and some attributes for each
|
||||
export const FOOD_ASSETS = [
|
||||
{
|
||||
name: "Branzino Amarin",
|
||||
file: "branzino-amarin.spz",
|
||||
scale: 0.3,
|
||||
shadowSize: 1.0,
|
||||
},
|
||||
{
|
||||
name: "Iberico Sandwich",
|
||||
file: "iberico-sandwich-by-reserve.spz",
|
||||
scale: 0.6,
|
||||
shadowSize: 0,
|
||||
offsetY: -0.05,
|
||||
},
|
||||
{
|
||||
name: "Coral Caviar",
|
||||
file: "coral-caviar.spz",
|
||||
scale: 0.4,
|
||||
shadowSize: 0.7,
|
||||
},
|
||||
{
|
||||
name: "Pad Thai",
|
||||
file: "pad-thai.spz",
|
||||
scale: 0.2,
|
||||
shadowSize: 1.0,
|
||||
},
|
||||
{
|
||||
name: "Prime Rib",
|
||||
file: "primerib-tamos.spz",
|
||||
scale: 0.3,
|
||||
shadowSize: 1.1,
|
||||
},
|
||||
{
|
||||
name: "Double-Double",
|
||||
file: "double-double-from-InNOut.spz",
|
||||
scale: 0.5,
|
||||
shadowSize: 0.6,
|
||||
offsetY: 0.03,
|
||||
},
|
||||
{
|
||||
name: "Gyro Kebab",
|
||||
file: "gyro.spz",
|
||||
scale: 0.13,
|
||||
shadowSize: 0,
|
||||
offsetY: -0.13,
|
||||
},
|
||||
{
|
||||
name: "Mels Steak Sandwich",
|
||||
file: "steaksandwich-mels.spz",
|
||||
scale: 0.3,
|
||||
shadowSize: 0,
|
||||
offsetY: 0.08,
|
||||
},
|
||||
{
|
||||
name: "Clams and Caviar",
|
||||
file: "clams-and-caviar-by-ikoyi.spz",
|
||||
scale: 0.4,
|
||||
shadowSize: 0.72,
|
||||
},
|
||||
{
|
||||
name: "Tomahawk Niku",
|
||||
file: "tomahawk-niku.spz",
|
||||
scale: 0.3,
|
||||
shadowSize: 1.0,
|
||||
},
|
||||
{
|
||||
name: "Dh Burger",
|
||||
file: "burger-from-amboy.spz",
|
||||
scale: 0.4,
|
||||
shadowSize: 0.6,
|
||||
offsetY: 0.04,
|
||||
},
|
||||
];
|
||||
export const FOOD_URL = "/examples/assets/splats/food/";
|
||||
@@ -0,0 +1,215 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Forge • Multiple Splats</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<img src="loading.gif" alt="loading" id="loading">
|
||||
<div id="menu">
|
||||
<div class="border">
|
||||
<div class="border">
|
||||
<h3>Tipatat's Splat Restaurant</h3>
|
||||
<h1> Menu </h1>
|
||||
<h2>CHEF SPECIALS</h2>
|
||||
<div id="menu_list"></div>
|
||||
<h2>Food scans by <a href="https://x.com/tipatat" target="_blank">Tipatat</a></h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"three": "/examples/js/vendor/three/build/three.module.js",
|
||||
"three/addons/": "/examples/js/vendor/three/examples/jsm/",
|
||||
"@forge-gfx/forge": "/dist/forge.module.js"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script type="module">
|
||||
import * as THREE from "three";
|
||||
import { SplatMesh } from "@forge-gfx/forge";
|
||||
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
|
||||
import { EXRLoader } from "three/addons/loaders/EXRLoader.js";
|
||||
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
||||
import { FOOD_ASSETS, FOOD_URL } from './food.js';
|
||||
import { getAssetFileURL } from "/examples/js/get-asset-url.js";
|
||||
|
||||
// Add food items to the menu
|
||||
FOOD_ASSETS.forEach((food, i) => {
|
||||
const el = document.createElement("a");
|
||||
el.textContent = food.name;
|
||||
el.href = 'javascript:;';
|
||||
el.addEventListener('click', async function () {
|
||||
await switchToFood(i);
|
||||
});
|
||||
document.getElementById('menu_list').appendChild(el);
|
||||
});
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
const renderer = new THREE.WebGLRenderer();
|
||||
renderer.shadowMap.enabled = true;
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
document.body.appendChild(renderer.domElement)
|
||||
|
||||
// Setup camera
|
||||
const camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||||
camera.position.set(0, 0.9, -1.2);
|
||||
|
||||
// Setup lighting
|
||||
const spotLight = new THREE.SpotLight(0xffcc88);
|
||||
spotLight.position.set(0, 1, 0);
|
||||
spotLight.castShadow = true;
|
||||
spotLight.shadow.mapSize.width = 1024;
|
||||
spotLight.shadow.mapSize.height = 1024;
|
||||
spotLight.shadow.camera.near = 0.1;
|
||||
spotLight.shadow.camera.far = 5;
|
||||
spotLight.angle = 0.9;
|
||||
spotLight.penumbra = 1;
|
||||
spotLight.intensity = 3;
|
||||
scene.add(spotLight);
|
||||
|
||||
const fillLight = new THREE.PointLight(0xffcc88, 0.2);
|
||||
fillLight.position.set(0, 0, -3);
|
||||
scene.add(fillLight);
|
||||
|
||||
// Splats don't project shadows, so we add a cylinder below the spotlight to fake one ;)
|
||||
const geometry = new THREE.CylinderGeometry(0.45, 0.45, 0.04, 40, 1);
|
||||
const material = new THREE.MeshPhongMaterial({ colorWrite: false, depthWrite: false });
|
||||
const shadow = new THREE.Mesh(geometry, material);
|
||||
shadow.visible = false;
|
||||
shadow.castShadow = true;
|
||||
shadow.position.set(0, 0.1, 0);
|
||||
scene.add(shadow);
|
||||
|
||||
// Add table
|
||||
const gltfLoader = new GLTFLoader();
|
||||
const modelURL = await getAssetFileURL("table.glb");
|
||||
const gltfTable = await gltfLoader.loadAsync(modelURL);
|
||||
const table = gltfTable.scene;
|
||||
// Set the table cloth to receive shadows
|
||||
const tableCloth = table.children.find(item => item.name == 'cover');
|
||||
tableCloth.receiveShadow = true;
|
||||
scene.add(table);
|
||||
|
||||
// Add floor
|
||||
const plane = new THREE.PlaneGeometry(10, 10);
|
||||
const floormat = new THREE.MeshPhongMaterial({ color: 0x777777 });
|
||||
const floor = new THREE.Mesh(plane, floormat);
|
||||
floor.rotation.x = -Math.PI / 2;
|
||||
floor.position.set(0, -1.397, 0);
|
||||
scene.add(floor);
|
||||
|
||||
// Setup mouse controls to orbit the camera around
|
||||
const controls = new OrbitControls(camera, renderer.domElement);
|
||||
controls.target.set(0.2, 0, 0);
|
||||
controls.minDistance = 0.8;
|
||||
controls.maxDistance = 2.3;
|
||||
controls.enablePan = false;
|
||||
controls.update();
|
||||
|
||||
// Current and next food splats
|
||||
let food, nextFood;
|
||||
|
||||
const loadingIcon = document.getElementById('loading');
|
||||
// Transition length in frames.
|
||||
// Two transitions: one for fading out the old food, another for fading in the next one
|
||||
const TRANSITION_LENGTH = 60;
|
||||
// Transition timers. `null` if transition is not active
|
||||
let fadeOutTime = null;
|
||||
let fadeInTime = null;
|
||||
|
||||
// Load first food by default
|
||||
await switchToFood(0);
|
||||
|
||||
renderer.setAnimationLoop(function animate(time) {
|
||||
controls.update();
|
||||
renderer.render(scene, camera);
|
||||
const rotation = time / 10000;
|
||||
table.rotation.y = rotation;
|
||||
if (food) food.rotation.y = -rotation;
|
||||
if (nextFood) nextFood.rotation.y = -rotation;
|
||||
|
||||
|
||||
// fade out
|
||||
if (fadeOutTime !== null) {
|
||||
fadeOutTime++;
|
||||
if (fadeOutTime < TRANSITION_LENGTH) {
|
||||
if (food) food.opacity = 1 - easeInOutSine(fadeOutTime / TRANSITION_LENGTH);
|
||||
} else {
|
||||
// Fade out finished
|
||||
if (food) food.dispose();
|
||||
fadeOutTime = null;
|
||||
// Fade in next food
|
||||
fadeInTime = 0;
|
||||
shadow.visible = true;
|
||||
shadow.scale.setScalar(shadow.nextScale);
|
||||
}
|
||||
}
|
||||
|
||||
// fade in
|
||||
if (fadeInTime != null && nextFood.isInitialized) {
|
||||
fadeInTime++;
|
||||
if (fadeInTime < TRANSITION_LENGTH) {
|
||||
nextFood.opacity = easeInOutSine(fadeInTime / TRANSITION_LENGTH);
|
||||
} else {
|
||||
|
||||
// Fade in finished
|
||||
food = nextFood;
|
||||
fadeInTime = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
// Change food from menu link
|
||||
async function switchToFood(foodIndex) {
|
||||
|
||||
// already transitioning
|
||||
if (fadeOutTime !== null || fadeInTime !== null) return;
|
||||
|
||||
const foodItem = FOOD_ASSETS[foodIndex];
|
||||
|
||||
const splatURL = await getAssetFileURL(foodItem.file);
|
||||
nextFood = new SplatMesh({ url: splatURL });
|
||||
nextFood.quaternion.set(1, 0, 0, 0);
|
||||
|
||||
// Customize splat depending on the settings set for this food
|
||||
if (foodItem['offsetY']) nextFood.position.set(0, foodItem.offsetY, 0);
|
||||
nextFood.scale.setScalar(foodItem.scale);
|
||||
shadow.nextScale = foodItem.shadowSize;
|
||||
|
||||
// Set opacity to 0 to prepare the fade in transition
|
||||
nextFood.opacity = 0;
|
||||
|
||||
// Setup shadow to the required size, and make it visible only when the splat is initialized
|
||||
nextFood.initialized.then(() => {
|
||||
shadow.visible = false;
|
||||
loadingIcon.style.display = 'none';
|
||||
fadeOutTime = 0; // Fade in next food
|
||||
});
|
||||
|
||||
scene.add(nextFood);
|
||||
|
||||
// toggle menu item
|
||||
const menu_items = document.getElementById('menu_list').children;
|
||||
for (let i = 0; i < menu_items.length; i++) {
|
||||
if (foodIndex == i) {
|
||||
loadingIcon.style.display = 'inline';
|
||||
menu_items[i].appendChild(loadingIcon);
|
||||
}
|
||||
menu_items[i].classList.toggle('active', foodIndex == i);
|
||||
}
|
||||
}
|
||||
|
||||
function easeInOutSine(x) {
|
||||
return -(Math.cos(Math.PI * x) - 1) / 2;
|
||||
}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
@@ -0,0 +1,91 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=Mea+Culpa&display=swap");
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: "Mea Culpa", cursive;
|
||||
font-weight: 500;
|
||||
font-size: 5rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-family: sans-serif;
|
||||
font-weight: normal;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-family: "Mea Culpa", cursive;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#menu {
|
||||
position: absolute;
|
||||
left: -12rem;
|
||||
display: inline-block;
|
||||
background-color: rgba(223, 203, 171, 0.8);
|
||||
padding: 2rem 2rem;
|
||||
margin-top: 4rem;
|
||||
text-align: center;
|
||||
font-size: 1.2rem;
|
||||
line-height: 1.7rem;
|
||||
left: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 0 15px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
#menu .border {
|
||||
border: 1.0px solid rgb(92, 92, 92);
|
||||
padding: 1rem 3rem;
|
||||
}
|
||||
|
||||
#menu > .border {
|
||||
border: 1.5px solid rgb(49, 49, 49);
|
||||
padding: 5px 6px;
|
||||
}
|
||||
|
||||
#menu a {
|
||||
font-style: italic;
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
color: rgb(30, 13, 140);
|
||||
}
|
||||
|
||||
#menu a:hover {
|
||||
/* border-bottom: 1px solid black; */
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#menu a.active {
|
||||
text-decoration: underline;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
#menu a.active::before {
|
||||
content: "🙜 ";
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
#menu a.active::after {
|
||||
content: " 🙟";
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
#menu_list {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
#menu h2 a {
|
||||
font-weight: normal;
|
||||
color: black;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
#loading {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 40vh;
|
||||
left: 45vw;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export async function getAssetFileURL(assetFile) {
|
||||
try {
|
||||
const response = await fetch("/examples/assets.json");
|
||||
const assetsDirectory = "/examples/assets/";
|
||||
const assetsInfo = await response.json();
|
||||
let url = assetsInfo[assetFile].url;
|
||||
if (window.forgeLocalAssets) {
|
||||
url = `${assetsDirectory}${assetsInfo[assetFile].directory}/${assetFile}`;
|
||||
}
|
||||
return url;
|
||||
} catch (error) {
|
||||
console.error("Failed to load asset file URL:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Forge • Multiple Splats</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
a,
|
||||
footer {
|
||||
color: #797979
|
||||
}
|
||||
|
||||
footer {
|
||||
position: absolute;
|
||||
margin: 10px;
|
||||
bottom: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<footer>
|
||||
<div>Food scans by <a href="https://x.com/tipatat">Tipatat</a></div>
|
||||
</footer>
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"three": "/examples/js/vendor/three/build/three.module.js",
|
||||
"three/addons/": "/examples/js/vendor/three/examples/jsm/",
|
||||
"@forge-gfx/forge": "/dist/forge.module.js"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script type="module">
|
||||
import * as THREE from "three";
|
||||
import { SplatMesh } from "@forge-gfx/forge";
|
||||
import { EXRLoader } from "three/addons/loaders/EXRLoader.js";
|
||||
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
||||
import { getAssetFileURL } from "/examples/js/get-asset-url.js";
|
||||
import { FOOD_ASSETS } from "/examples/interactivity/food.js";
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||||
camera.position.set(0, 0, 6.5);
|
||||
camera.lookAt(0, 0, 0);
|
||||
|
||||
const renderer = new THREE.WebGLRenderer();
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
renderer.setClearColor(new THREE.Color(0xFFFFFF), 1);
|
||||
document.body.appendChild(renderer.domElement)
|
||||
|
||||
// add food
|
||||
const foods = [];
|
||||
for (let i = 0; i < FOOD_ASSETS.length; i++) {
|
||||
let splatURL = await getAssetFileURL(FOOD_ASSETS[i].file);
|
||||
const splat = new SplatMesh({ url: splatURL });
|
||||
splat.quaternion.set(1, 0, 0, 0);
|
||||
splat.scale.setScalar(FOOD_ASSETS[i].scale);
|
||||
scene.add(splat);
|
||||
foods.push(splat);
|
||||
}
|
||||
|
||||
// add food master
|
||||
let splatURL = await getAssetFileURL("cat.spz");
|
||||
const master = new SplatMesh({ url: splatURL });
|
||||
master.quaternion.set(1, 0, 0, 0);
|
||||
master.scale.setScalar(0.5);
|
||||
scene.add(master);
|
||||
|
||||
// Setup mouse controls to orbit the camera around
|
||||
const controls = new OrbitControls(camera, renderer.domElement);
|
||||
controls.target.set(0, 0, 0);
|
||||
controls.minDistance = 0.3;
|
||||
controls.maxDistance = 20;
|
||||
controls.update();
|
||||
|
||||
const RADIUS = 2;
|
||||
|
||||
renderer.setAnimationLoop(function animate(time) {
|
||||
controls.update();
|
||||
|
||||
// Rotate food
|
||||
for (let i = 0; i < foods.length; i++) {
|
||||
const ang = (-time / 10000) + i / FOOD_ASSETS.length * Math.PI * 2;
|
||||
foods[i].position.set(Math.cos(ang) * RADIUS, Math.sin(ang) * RADIUS - 0.2, 0);
|
||||
foods[i].rotation.y = i + time / 4000;
|
||||
}
|
||||
|
||||
// Animate food master
|
||||
master.position.y = -0.8 + Math.sin(time / 1000) * 0.1;
|
||||
|
||||
renderer.render(scene, camera);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,94 @@
|
||||
<!DOCTYPE html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Forge • Multiple Viewpoints</title>
|
||||
<style> body {margin: 0;} </style>
|
||||
</head>
|
||||
<body>
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"three": "/examples/js/vendor/three/build/three.module.js",
|
||||
"@forge-gfx/forge": "/dist/forge.module.js"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script type="module">
|
||||
import * as THREE from "three";
|
||||
import { ForgeRenderer, ForgeViewpoint, SplatMesh } from "@forge-gfx/forge";
|
||||
import { getAssetFileURL } from "/examples/js/get-asset-url.js";
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color(0x000000);
|
||||
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||||
const renderer = new THREE.WebGLRenderer();
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
document.body.appendChild(renderer.domElement)
|
||||
|
||||
// Explicitly create a ForgeRenderer in the scene to spawn new viewpoints
|
||||
const forge = new ForgeRenderer({ renderer });
|
||||
|
||||
const splatURL = await getAssetFileURL("butterfly.spz");
|
||||
const butterfly = new SplatMesh({ url: splatURL});
|
||||
butterfly.quaternion.set(1, 0, 0, 0);
|
||||
butterfly.position.set(0, 0, -1);
|
||||
scene.add(butterfly);
|
||||
|
||||
function createViewpoint(origin, screenPos) {
|
||||
// Create a textured rectangle at the near plane to show the viewpoint
|
||||
const width = 320;
|
||||
const height = 240;
|
||||
const screen = new THREE.Mesh(
|
||||
new THREE.PlaneGeometry(0.5 * camera.near, 0.5 * camera.near * height / width),
|
||||
new THREE.MeshBasicMaterial({ map: ForgeViewpoint.EMPTY_TEXTURE }),
|
||||
);
|
||||
screen.position.set(screenPos.x, screenPos.y, -1).multiplyScalar(camera.near);
|
||||
scene.add(screen);
|
||||
|
||||
const viewCamera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
|
||||
viewCamera.position.copy(origin);
|
||||
viewCamera.lookAt(butterfly.position);
|
||||
|
||||
// Create a cone to show the viewpoint camera
|
||||
const cone = new THREE.Mesh(
|
||||
new THREE.ConeGeometry(0.1, 0.1, 32),
|
||||
new THREE.MeshNormalMaterial(),
|
||||
);
|
||||
cone.position.copy(viewCamera.position);
|
||||
cone.quaternion.copy(viewCamera.quaternion);
|
||||
cone.rotateX(Math.PI / 2);
|
||||
scene.add(cone);
|
||||
|
||||
// Spawn a new viewpoint that auto-updates its sort order, double
|
||||
// buffered in case the texture is used recursively in the scene
|
||||
const viewpoint = forge.newViewpoint({
|
||||
autoUpdate: true,
|
||||
camera: viewCamera,
|
||||
target: { width, height, doubleBuffer: true },
|
||||
onTextureUpdated: (texture) => {
|
||||
// Update the view screen with the rendered viewpoint
|
||||
screen.material.map = texture;
|
||||
},
|
||||
});
|
||||
return viewpoint;
|
||||
}
|
||||
|
||||
const viewpoints = [
|
||||
createViewpoint(new THREE.Vector3(0, 0.5, -1), new THREE.Vector2(-0.5, 0.5)),
|
||||
createViewpoint(new THREE.Vector3(0.5, -0.5, -1), new THREE.Vector2(-0.5, -0.5)),
|
||||
];
|
||||
|
||||
renderer.setAnimationLoop(function animate(time) {
|
||||
scene.background.set(0x000000);
|
||||
renderer.render(scene, camera);
|
||||
butterfly.rotation.y += 0.01;
|
||||
|
||||
// Adjust the background for visibility and render the viewpoints
|
||||
scene.background.set(0x000040);
|
||||
for (const viewpoint of viewpoints) {
|
||||
viewpoint.renderTarget({ scene, camera: viewpoint.camera });
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,141 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Forge • Procedural Splats</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"three": "/examples/js/vendor/three/build/three.module.js",
|
||||
"@forge-gfx/forge": "/dist/forge.module.js"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script type="module">
|
||||
import * as THREE from "three";
|
||||
import { ForgeControls, SplatMesh, constructGrid, constructAxes, textSplats, imageSplats } from "@forge-gfx/forge";
|
||||
import { getAssetFileURL } from "/examples/js/get-asset-url.js";
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||||
const renderer = new THREE.WebGLRenderer();
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
document.body.appendChild(renderer.domElement)
|
||||
|
||||
// Generate a Sierpinski pyramid one splat at a time via iteration
|
||||
const pyramid = new SplatMesh({
|
||||
constructSplats: (splats) => {
|
||||
const NUM_SPLATS = 500000;
|
||||
const ITERATIONS = 15;
|
||||
const CORNERS = [
|
||||
new THREE.Vector3(0, 1, -0.7),
|
||||
new THREE.Vector3(0, 0, 0),
|
||||
new THREE.Vector3(0.6, 1, 0.5),
|
||||
new THREE.Vector3(-0.6, 1, 0.5),
|
||||
];
|
||||
|
||||
const center = new THREE.Vector3();
|
||||
const scales = new THREE.Vector3().setScalar(0.0005);
|
||||
const quaternion = new THREE.Quaternion();
|
||||
const opacity = 1;
|
||||
const color = new THREE.Color(1, 1, 1);
|
||||
|
||||
for (let i = 0; i < NUM_SPLATS; i++) {
|
||||
center.set(0, 0, 0);
|
||||
for (let iter = 0; iter < ITERATIONS; iter++) {
|
||||
const corner = CORNERS[Math.floor(Math.random() * 4)];
|
||||
center.add(corner).multiplyScalar(0.5);
|
||||
}
|
||||
color.set(0.3 + center.x - center.z * 0.5, center.z * 1.5, 0.5 + center.z * 2);
|
||||
splats.pushSplat(center, scales, quaternion, opacity, color);
|
||||
}
|
||||
},
|
||||
});
|
||||
pyramid.position.set(0, -0.5, -2);
|
||||
scene.add(pyramid);
|
||||
|
||||
const stars = new SplatMesh({
|
||||
constructSplats: (splats) => {
|
||||
const NUM_SPLATS = 100000;
|
||||
|
||||
const center = new THREE.Vector3();
|
||||
const scales = new THREE.Vector3().setScalar(0.008);
|
||||
const quaternion = new THREE.Quaternion();
|
||||
const opacity = 1;
|
||||
const color = new THREE.Color(1, 1, 1);
|
||||
|
||||
for (let i = 0; i < NUM_SPLATS; i++) {
|
||||
// Distribute stars on a 50x50x50 cube
|
||||
center.set(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5).multiplyScalar(50);
|
||||
// Random purple-ish color
|
||||
color.set(0.4 + Math.random() * 0.3, 0.5, 0.7 + Math.random() * 0.3);
|
||||
splats.pushSplat(center, scales, quaternion, opacity, color);
|
||||
}
|
||||
},
|
||||
});
|
||||
stars.position.set(0, -0.5, -2);
|
||||
scene.add(stars);
|
||||
|
||||
// Generate splats from rendered text
|
||||
const text = textSplats({
|
||||
text: "I GSplats!",
|
||||
font: "Arial",
|
||||
fontSize: 60,
|
||||
color: new THREE.Color(0xE420B7),
|
||||
});
|
||||
text.scale.setScalar(0.5 / 80);
|
||||
text.position.set(0.15, -1, -2);
|
||||
scene.add(text);
|
||||
|
||||
const text2 = textSplats({
|
||||
text: "WASD + mouse to move",
|
||||
font: "Verdana",
|
||||
fontSize: 30,
|
||||
color: new THREE.Color(0x49EAFF)
|
||||
});
|
||||
text2.scale.setScalar(0.2 / 40);
|
||||
text2.position.set(0, 1.0, -2);
|
||||
scene.add(text2);
|
||||
|
||||
// Generate splats from image pixels with alpha channel
|
||||
const imageURL = await getAssetFileURL("butterfly.png");
|
||||
const image = imageSplats({ url: imageURL });
|
||||
image.scale.setScalar(0.7 / 400);
|
||||
image.position.set(-0.5, -1, -2);
|
||||
scene.add(image);
|
||||
|
||||
const controls = new ForgeControls({ canvas: renderer.domElement });
|
||||
|
||||
renderer.setAnimationLoop(function animate(time) {
|
||||
controls.update(camera);
|
||||
renderer.render(scene, camera);
|
||||
|
||||
pyramid.rotation.y = time / 5000;
|
||||
|
||||
// Move stars back and forth
|
||||
stars.position.z = Math.sin(time / 10000) * 10;
|
||||
// Rotate butterfly at different rates on each axis
|
||||
image.rotation.set(
|
||||
-0.3 + 0.35 * Math.sin(time / 190),
|
||||
0.35 * Math.sin(time / 390),
|
||||
0.1 * Math.sin(time / 200)
|
||||
);
|
||||
// Oscilate butterfly y position
|
||||
image.position.y = -0.9 + 0.04 * Math.sin(time / 330);
|
||||
|
||||
// Animate opacity of top text
|
||||
text2.opacity = Math.abs(Math.sin(time / 1000) * 0.3);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,106 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Forge • Raycasting</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
header {
|
||||
position: absolute;
|
||||
color: silver;
|
||||
font-family: sans-serif;
|
||||
padding-top: 20px;
|
||||
text-align: center;
|
||||
width: 100vw;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>Click to select</header>
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"three": "/examples/js/vendor/three/build/three.module.js",
|
||||
"@forge-gfx/forge": "/dist/forge.module.js"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script type="module">
|
||||
import * as THREE from "three";
|
||||
import { SplatMesh, PackedSplats } from "@forge-gfx/forge";
|
||||
import { getAssetFileURL } from "/examples/js/get-asset-url.js";
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 10);
|
||||
camera.position.set(0, -0.25, -1.5);
|
||||
camera.lookAt(0, -0.15, 0);
|
||||
const renderer = new THREE.WebGLRenderer();
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
document.body.appendChild(renderer.domElement)
|
||||
|
||||
// Add robots
|
||||
const NUM_ROBOTS = 5;
|
||||
const robots = [];
|
||||
const splatURL = await getAssetFileURL("robot-head.spz");
|
||||
for (let i = 0; i < NUM_ROBOTS; i++) {
|
||||
const robot = new SplatMesh({ url: splatURL });
|
||||
robot.rotation.x = Math.PI;
|
||||
robot.position.set(0, 0, i);
|
||||
robot.speed = 600 + i * 10;
|
||||
robot.angle = 0;
|
||||
robot.clickedTimer = false;
|
||||
scene.add(robot);
|
||||
robots.push(robot);
|
||||
}
|
||||
|
||||
// Raycast logic
|
||||
const raycaster = new THREE.Raycaster();
|
||||
renderer.domElement.addEventListener("click", (event) => {
|
||||
const clickCoords = new THREE.Vector2(
|
||||
(event.clientX / renderer.domElement.width) * 2 - 1,
|
||||
-(event.clientY / renderer.domElement.height) * 2 + 1,
|
||||
);
|
||||
|
||||
// Run raycaster
|
||||
raycaster.setFromCamera(clickCoords, camera);
|
||||
const hits = raycaster.intersectObjects(scene.children);
|
||||
|
||||
// Find first SplatMesh
|
||||
const index = hits.findIndex(hit => hit.object instanceof SplatMesh);
|
||||
if (index >= 0) {
|
||||
const robot = hits[index].object;
|
||||
if (!robot.clickedTimer) {
|
||||
// Tint object and start countdown timer to remove tint
|
||||
robot.recolor.set(1, 0.6, 0.6);
|
||||
robot.clickedTimer = 40;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Main loop
|
||||
renderer.setAnimationLoop(function animate(time) {
|
||||
// Animate all robots, and check if it's time to remove tint
|
||||
for (let robot of robots) {
|
||||
// Pendulum movement
|
||||
robot.position.x = Math.cos(robot.angle) * 0.6;
|
||||
robot.position.y = Math.sin(robot.angle) * 0.6;
|
||||
robot.angle = Math.PI * 1.5 + Math.sin(time / robot.speed);
|
||||
|
||||
if (robot.clickedTimer > 0) {
|
||||
robot.clickedTimer -= 1;
|
||||
if (robot.clickedTimer == 0) {
|
||||
// Clicked timer expired, remove tint
|
||||
robot.recolor.set(1, 1, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
renderer.render(scene, camera);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,222 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Forge • VFX Dynamic Lighting</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
#canvas {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
header {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
margin: 1rem 2rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
label {
|
||||
font-family: sans-serif;
|
||||
font-size: 20px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
margin-right: 2rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<label><input type="checkbox" id="debug" autocomplete="off"> Debug Light SDFs</label>
|
||||
<label><input type="checkbox" id="daynight" checked="checked" autocomplete="off"> Enable Lighting</label>
|
||||
</header>
|
||||
|
||||
<canvas id="canvas"></canvas>
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"three": "/examples/js/vendor/three/build/three.module.js",
|
||||
"@forge-gfx/forge": "/dist/forge.module.js"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script type="module">
|
||||
import * as THREE from "three";
|
||||
import {
|
||||
SplatMesh,
|
||||
SplatEdit,
|
||||
ForgeRenderer,
|
||||
SplatEditSdf,
|
||||
SplatEditSdfType,
|
||||
SplatEditRgbaBlendMode,
|
||||
FpsMovement,
|
||||
PointerControls,
|
||||
} from "@forge-gfx/forge";
|
||||
import { getAssetFileURL } from "/examples/js/get-asset-url.js";
|
||||
|
||||
const canvas = document.getElementById("canvas");
|
||||
const scene = new THREE.Scene();
|
||||
|
||||
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100);
|
||||
camera.position.set(0, 0.3, -2.5);
|
||||
|
||||
const renderer = new THREE.WebGLRenderer();
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
document.body.appendChild(renderer.domElement)
|
||||
|
||||
// Create a ForgeRenderer and add it to the scene to render all the Gsplats.
|
||||
const forge = new ForgeRenderer({ renderer });
|
||||
scene.add(forge);
|
||||
|
||||
const splatURL = await getAssetFileURL("fireplace.spz");
|
||||
const fireplace = new SplatMesh({ url: splatURL });
|
||||
fireplace.quaternion.set(1, 0, 0, 0);
|
||||
fireplace.position.set(0, -1, -10);
|
||||
scene.add(fireplace);
|
||||
|
||||
const fpsMovement = new FpsMovement({ moveSpeed: 2 });
|
||||
const pointerControls = new PointerControls({ canvas });
|
||||
|
||||
let night = true;
|
||||
const lights = [];
|
||||
// Debug SDF spheres
|
||||
const helpers = [];
|
||||
|
||||
function createLight(scene, lightingLayer, position, color, radius, opacity) {
|
||||
const light = new SplatEditSdf({
|
||||
type: SplatEditSdfType.SPHERE,
|
||||
color: color,
|
||||
radius: radius,
|
||||
opacity: opacity,
|
||||
});
|
||||
light.position.copy(position);
|
||||
|
||||
// create a wireframe helper
|
||||
const helper = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(radius, 16, 16),
|
||||
new THREE.MeshBasicMaterial({ wireframe: true, color: color }),
|
||||
);
|
||||
helper.position.copy(light.position);
|
||||
helper.visible = false;
|
||||
scene.add(helper);
|
||||
helpers.push(helper);
|
||||
|
||||
lightingLayer.add(light);
|
||||
lights.push(light);
|
||||
}
|
||||
|
||||
// Create lighting layer
|
||||
const emberLayer = new SplatEdit({
|
||||
rgbaBlendMode: SplatEditRgbaBlendMode.ADD_RGBA,
|
||||
sdfSmooth: 0.1,
|
||||
softEdge: 0.8,
|
||||
});
|
||||
scene.add(emberLayer);
|
||||
|
||||
const lightingLayer = new SplatEdit({
|
||||
rgbaBlendMode: SplatEditRgbaBlendMode.ADD_RGBA,
|
||||
sdfSmooth: 0.1,
|
||||
softEdge: 1.4,
|
||||
});
|
||||
scene.add(lightingLayer);
|
||||
|
||||
const ambientLayer = new SplatEdit({
|
||||
rgbaBlendMode: SplatEditRgbaBlendMode.DARKEN,
|
||||
sdfSmooth: 0.1,
|
||||
softEdge: 0.05,
|
||||
});
|
||||
scene.add(ambientLayer);
|
||||
|
||||
// glowing embers at the base of the fire
|
||||
createLight(
|
||||
scene,
|
||||
emberLayer,
|
||||
new THREE.Vector3(0.5, -1.0, -10.5),
|
||||
new THREE.Color(1, 0.6, 0.4),
|
||||
0.75,
|
||||
1,
|
||||
);
|
||||
|
||||
// light in the fireplace
|
||||
createLight(
|
||||
scene,
|
||||
lightingLayer,
|
||||
new THREE.Vector3(0.3, -1.1, -10.6),
|
||||
new THREE.Color(1, 0.95, 0.2),
|
||||
1.6,
|
||||
0.9,
|
||||
);
|
||||
|
||||
// ambient light throughout the full room
|
||||
createLight(
|
||||
scene,
|
||||
ambientLayer,
|
||||
new THREE.Vector3(0, 1, -11),
|
||||
new THREE.Color(1, 0.8, 0.6),
|
||||
6,
|
||||
0.8,
|
||||
);
|
||||
console.log(helpers);
|
||||
|
||||
let lastTime;
|
||||
|
||||
renderer.setAnimationLoop(time => {
|
||||
const timeSeconds = time * 0.0005;
|
||||
const deltaTime = timeSeconds - (lastTime ?? timeSeconds);
|
||||
lastTime = timeSeconds;
|
||||
|
||||
// fpsMovement.update(deltaTime, camera);
|
||||
pointerControls.update(deltaTime, camera);
|
||||
camera.position.z = -2.5 - Math.sin(timeSeconds * 0.1) * 2;
|
||||
renderer.render(scene, camera);
|
||||
|
||||
// Create flickering fire effect
|
||||
const baseHue = 0.04; // Orange-red base hue
|
||||
const fireHue = 0.03; // Yellow base hue
|
||||
const hueVariation = 0.03; // Slight variation in hue
|
||||
|
||||
// Add some randomness to the flicker
|
||||
const randomFlicker = Math.sin(timeSeconds * 4) * 0.5 + 0.5;
|
||||
|
||||
// we'll combine these to make a more natural flickering effect
|
||||
const mediumFlicker = Math.sin(timeSeconds * 13) * 0.1 + 0.1; // Medium flicker
|
||||
const fastFlicker = Math.sin(timeSeconds * 20) * 0.1 + 0.1; // Fast flicker
|
||||
const slowFlicker = Math.sin(timeSeconds * 6) * 0.04 + 0.5; // Slow base flicker
|
||||
// Combine the flickers
|
||||
const combinedFlicker = (slowFlicker + mediumFlicker + fastFlicker) / 3;
|
||||
|
||||
for (let i = 0; i < lights.length - 1; i++) {
|
||||
const h = baseHue + combinedFlicker * hueVariation; // Slightly varying orange-red hue
|
||||
const s = 0.5 + randomFlicker * 0.3; // High saturation with slight variation
|
||||
const l = 0.5 + combinedFlicker * 0.2; // Varying brightness
|
||||
lights[i].color.setHSL(h, s, l);
|
||||
lights[i].visible = night;
|
||||
}
|
||||
|
||||
lights[lights.length - 1].visible = night;
|
||||
lights[lights.length - 1].color.setHSL(baseHue, 0.5, combinedFlicker);
|
||||
});
|
||||
|
||||
document.getElementById('debug').addEventListener('change', ev => {
|
||||
const visible = ev.target.checked;
|
||||
for (let helper of helpers) {
|
||||
helper.visible = visible;
|
||||
}
|
||||
});
|
||||
document.getElementById('daynight').addEventListener('change', ev => {
|
||||
night = !night;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Forge • Hello World</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"three": "/examples/js/vendor/three/build/three.module.js",
|
||||
"@forge-gfx/forge": "/dist/forge.module.js"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script type="module">
|
||||
import * as THREE from "three";
|
||||
import { SplatMesh } from "@forge-gfx/forge";
|
||||
import { getAssetFileURL } from "/examples/js/get-asset-url.js";
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||||
const renderer = new THREE.WebGLRenderer();
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
document.body.appendChild(renderer.domElement)
|
||||
|
||||
const splatURL = await getAssetFileURL("butterfly-wings-closed.spz");
|
||||
const butterfly = new SplatMesh({ url: splatURL });
|
||||
butterfly.quaternion.set(1, 0, 0, 0);
|
||||
butterfly.position.set(0, -0.5, -3);
|
||||
scene.add(butterfly);
|
||||
|
||||
renderer.setAnimationLoop(function animate(time) {
|
||||
renderer.render(scene, camera);
|
||||
butterfly.rotation.y += 0.01;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Forge • Hello World</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"three": "/examples/js/vendor/three/build/three.module.js",
|
||||
"@forge-gfx/forge": "/dist/forge.module.js"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script type="module">
|
||||
import * as THREE from "three";
|
||||
import { SplatMesh } from "@forge-gfx/forge";
|
||||
import { getAssetFileURL } from "/examples/js/get-asset-url.js";
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||||
const renderer = new THREE.WebGLRenderer();
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
document.body.appendChild(renderer.domElement)
|
||||
|
||||
const splatURL = await getAssetFileURL("butterfly-wings-closed.spz");
|
||||
const butterfly = new SplatMesh({ url: splatURL });
|
||||
butterfly.quaternion.set(1, 0, 0, 0);
|
||||
butterfly.position.set(0, -0.5, -3);
|
||||
scene.add(butterfly);
|
||||
|
||||
renderer.setAnimationLoop(function animate(time) {
|
||||
renderer.render(scene, camera);
|
||||
butterfly.rotation.y += 0.01;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user