Initial commit

This commit is contained in:
Diego Marcos
2025-05-23 08:52:14 -07:00
committed by Diego Marcos Segura
commit 3332f609a9
129 changed files with 64575 additions and 0 deletions
+105
View File
@@ -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>
+90
View File
@@ -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"
}
}
+120
View File
@@ -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>
+121
View File
@@ -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>
+46
View File
@@ -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>
+8
View File
@@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="refresh" content="0;url=/" />
</head>
<body>
</body>
</html>
+75
View File
@@ -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/";
+215
View File
@@ -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

+91
View File
@@ -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;
}
+15
View File
@@ -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;
}
}
+100
View File
@@ -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>
+94
View File
@@ -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>
+141
View File
@@ -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>
+106
View File
@@ -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>
+222
View File
@@ -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>
+46
View File
@@ -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>