import { PlaneShape3D } from "@woosh/meep-engine/src/core/geom/3d/shape/PlaneShape3D.js";
import { BoxShape3D } from "@woosh/meep-engine/src/core/geom/3d/shape/BoxShape3D.js";
import { ClothDynamics } from "@woosh/meep-engine/src/engine/physics/cloth/ecs/ClothDynamics.js";
import { ClothDynamicsFlags } from "@woosh/meep-engine/src/engine/physics/cloth/ecs/ClothDynamicsFlags.js";
import { ClothPin } from "@woosh/meep-engine/src/engine/physics/cloth/ecs/ClothPin.js";
import { make_box_geometry } from "@woosh/meep-engine/src/shade/renderer/geometry/primitives/make_box_geometry.js";
import { createScene } from "./scene.js";
import { createSheet } from "./sheet.js";
const ACROSS = 5, ALONG = 161; // vertices across and along the ribbon
const WIDTH = .1, LENGTH = 4, LIFT = .1;
const TRAY = .5, WALL = .22; // inner width and wall height
const LOWER = 1, RELEASE = .4; // the top end comes down at 1 m/s and is let go 0.4 m up
const TOP = LIFT + LENGTH;
// Self-collision holds vertices 3.8 cm apart, three quarters of an edge either side. The pairs
// are found once per step, so the cloth steps at 120 Hz to let less of a moving layer slip
// through before its pairs are found.
const scene = await createScene({ distance: 1.3, yaw: .5, pitch: .85, focus: [0, .08, 0], stepRate: 120 });
// The ribbon hangs straight down with its lower end just off the tray floor. A slight
// wave out of its plane, and a half twist along its length, decide which way each
// part buckles as it lands.
const position = new Float32Array(ACROSS * ALONG * 3), uv = new Float32Array(ACROSS * ALONG * 2);
const triangle = [];
for (let j = 0; j < ALONG; j++) {
const along = j / (ALONG - 1);
const turn = along * Math.PI;
const wave = .015 * Math.sin(along * 23);
for (let i = 0; i < ACROSS; i++) {
const v = j * ACROSS + i, across = (i / (ACROSS - 1) - .5) * WIDTH;
position.set([
Math.cos(turn) * across - Math.sin(turn) * wave,
LIFT + along * LENGTH,
Math.sin(turn) * across + Math.cos(turn) * wave,
], v * 3);
uv.set([i / (ACROSS - 1), along], v * 2);
if (i < ACROSS - 1 && j < ALONG - 1) {
const a = v, b = v + 1, c = v + ACROSS, d = c + 1;
triangle.push(...((i + j) % 2 ? [a, c, b, b, c, d] : [a, c, d, a, d, b]));
}
}
}
const top = Array.from({ length: ACROSS }, (_, i) => (ALONG - 1) * ACROSS + i);
// Cloth has one radius, `thickness`, for its contacts and for self-collision. The ribbon sets it
// to the 1.9 cm self-collision needs, and the tray takes 1.5 cm of it back with negative
// inflation, so the fabric still lies 4 mm off the floor and the walls.
const SELF_RADIUS = .019, CONTACT = .004;
const tray = { friction: .9, inflation: CONTACT - SELF_RADIUS };
scene.collide(scene.point(), PlaneShape3D.GROUND, tray);
// A low tray keeps the pile together. Each wall is drawn and collided with.
for (const [x, z, w, d] of [[1, 0, .03, TRAY + .06], [-1, 0, .03, TRAY + .06], [0, 1, TRAY, .03], [0, -1, TRAY, .03]]) {
const at = [x * (TRAY + .03) / 2, WALL / 2, z * (TRAY + .03) / 2];
scene.collide(scene.decor(make_box_geometry(w, WALL, d), "#a07a52", ...at), BoxShape3D.from_size(w, WALL, d), tray);
}
const sheet = createSheet(scene.ecd, {
position, uv, triangle,
material: scene.material("#ffffff", { texture: await scene.canvasTexture(ribbonTexture()), roughness: .55 }),
dynamics: ClothDynamics.from({
bend: .49, damping: .0075, slack: 1, thickness: SELF_RADIUS, substeps: 2, iterations: 4,
flags: ClothDynamicsFlags.SelfCollision,
}),
});
// Its top edge is held and lowered, so the ribbon folds onto the pile at walking pace
// rather than arriving from four metres up at nine metres a second.
const holder = scene.point(0, TOP, 0);
let time = 0, held = false;
function hold() {
scene.ecd.addComponentToEntity(holder.entity, ClothPin.from({ cloth: sheet.entity, particles: top }));
held = true;
}
function letGo() {
scene.ecd.removeComponentFromEntity(holder.entity, ClothPin);
held = false;
}
hold();
const self = document.getElementById("self");
self.addEventListener("change", () => {
const dynamics = sheet.cloth.dynamics;
dynamics.flags = self.checked
? dynamics.flags | ClothDynamicsFlags.SelfCollision
: dynamics.flags & ~ClothDynamicsFlags.SelfCollision;
});
document.getElementById("drop").addEventListener("click", () => {
if (held) letGo();
scene.place(holder, 0, TOP, 0);
scene.cloth.reseed(sheet.entity);
hold();
time = 0;
});
scene.onStep(dt => {
time += dt;
if (!held) return;
const y = TOP - LOWER * time;
if (y > RELEASE) scene.move(holder, 0, y, 0);
else letGo();
});
await scene.start();
document.querySelector("fieldset").disabled = false;
// Colour runs from one end to the other, so the order of the layers in the pile shows.
function ribbonTexture() {
const canvas = document.createElement("canvas");
canvas.width = 64;
canvas.height = 1024;
const g = canvas.getContext("2d");
const gradient = g.createLinearGradient(0, 0, 0, 1024);
gradient.addColorStop(0, "#2b59c3");
gradient.addColorStop(.5, "#d6455d");
gradient.addColorStop(1, "#f2b33d");
g.fillStyle = gradient;
g.fillRect(0, 0, 64, 1024);
g.fillStyle = "#f4f1e8";
g.fillRect(0, 0, 6, 1024);
g.fillRect(58, 0, 6, 1024);
for (let y = 0; y < 1024; y += 1024 / 12) g.fillRect(0, y, 64, 4);
return canvas;
}
import { EngineHarness } from "@woosh/meep-engine/src/engine/EngineHarness.js";
import TopDownCameraController from "@woosh/meep-engine/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.js";
import Entity from "@woosh/meep-engine/src/engine/ecs/Entity.js";
import { ParentEntitySystem } from "@woosh/meep-engine/src/engine/ecs/parent/ParentEntitySystem.js";
import { Transform64 } from "@woosh/meep-engine/src/engine/ecs/transform/Transform64.js";
import { t64_announce_change } from "@woosh/meep-engine/src/engine/ecs/transform/t64_announce_change.js";
import { TRANSFORM64_EVENT_PLACED } from "@woosh/meep-engine/src/engine/ecs/transform/TRANSFORM64_EVENT_PLACED.js";
import { TransformAttachmentSystem } from "@woosh/meep-engine/src/engine/ecs/transform-attachment/TransformAttachmentSystem.js";
import { MeshSkinSystem } from "@woosh/meep-engine/src/engine/graphics3/MeshSkinSystem.js";
import { ShadedGeometry } from "@woosh/meep-engine/src/engine/graphics/ecs/mesh-v2/ShadedGeometry.js";
import { ShadedGeometrySystem } from "@woosh/meep-engine/src/engine/graphics3/ShadedGeometrySystem.js";
import { Light } from "@woosh/meep-engine/src/engine/graphics/ecs/light/Light.js";
import { LightType } from "@woosh/meep-engine/src/engine/graphics/ecs/light/LightType.js";
import { Collider } from "@woosh/meep-engine/src/engine/physics/ecs/Collider.js";
import { ClothCollider } from "@woosh/meep-engine/src/engine/physics/cloth/ecs/ClothCollider.js";
import { ClothColliderSystem } from "@woosh/meep-engine/src/engine/physics/cloth/ecs/ClothColliderSystem.js";
import { ClothPin } from "@woosh/meep-engine/src/engine/physics/cloth/ecs/ClothPin.js";
import { ClothRig } from "@woosh/meep-engine/src/engine/physics/cloth/ecs/ClothRig.js";
import { ClothSystem } from "@woosh/meep-engine/src/engine/physics/cloth/ecs/ClothSystem.js";
import { StandardShadeMaterial } from "@woosh/meep-engine/src/shade/renderer/material/StandardShadeMaterial.js";
import { ShadeDrawSide } from "@woosh/meep-engine/src/shade/renderer/material/ShadeDrawSide.js";
import { ColorSpace } from "@woosh/meep-engine/src/shade/renderer/texture/ColorSpace.js";
import { ShadeImage } from "@woosh/meep-engine/src/shade/renderer/texture/source/ShadeImage.js";
import { ShadeTexture } from "@woosh/meep-engine/src/shade/renderer/texture/ShadeTexture.js";
import { meshlet_geometry_build_from_geometry } from "@woosh/meep-engine/src/shade/renderer/geometry/meshlet_geometry_build_from_geometry.js";
import { make_box_geometry } from "@woosh/meep-engine/src/shade/renderer/geometry/primitives/make_box_geometry.js";
import { Color } from "@woosh/meep-engine/src/core/color/Color.js";
import Vector3 from "@woosh/meep-engine/src/core/geom/Vector3.js";
export async function createScene({ distance = 6.5, yaw = .25, pitch = .18, focus = [0, 1.5, 0], stepRate = 60 } = {}) {
// Held until start(): see there.
let tick = () => {}, playing = false;
// Moves the example's holders and colliders, then steps every cloth, on the engine's fixed step.
class ExampleClothSystem extends ClothSystem {
fixedUpdate(dt) {
if (!playing) return;
tick(dt);
super.fixedUpdate(dt);
}
}
const cloth = new ExampleClothSystem();
const colliders = new ClothColliderSystem();
const engine = await EngineHarness.bootstrap({
configuration(config, engine) {
const scene = EngineHarness.shadeScene(engine);
config.addSystem(new ShadedGeometrySystem(engine.graphics, scene));
config.addSystem(new MeshSkinSystem(engine.graphics, scene));
config.addSystem(new ParentEntitySystem());
config.addSystem(new TransformAttachmentSystem());
config.addSystem(colliders);
config.addSystem(cloth);
},
});
await EngineHarness.buildBasics({
engine, enableTerrain: false, enableWater: false, enableLights: true,
enableShadows: true, shadowmapResolution: 2048,
focus: new Vector3(...focus), distance, pitch, yaw,
// Shade spreads the sun's shadow cascades over the camera's whole near..far range, so
// far is three opening distances and zooming out stops at two: the scene stays inside
// far at any zoom, and the cascades are not stretched over empty floor.
cameraFarDistance: distance * 3, showFps: false,
});
engine.entityManager.dataset.getAnyComponent(TopDownCameraController).component.distanceMax = distance * 2;
engine.entityManager.fixedUpdateStepSize = 1 / stepRate;
engine.graphics.renderer.feature_automatic_exposure_enabled = false;
const ecd = engine.entityManager.dataset;
ecd.registerManyComponentTypes([ClothRig, ClothPin]);
ecd.traverseEntities([Light], light => {
if (light.type.getValue() === LightType.DIRECTION) light.intensity.set(6);
});
function material(hex, { texture, roughness = .9 } = {}) {
const m = new StandardShadeMaterial();
m.diffuse_color.copy(Color.from_sRGB_to_linear(Color.parse(hex)));
m.roughness_factor = roughness;
m.draw_side = ShadeDrawSide.Double;
if (texture) m.texture_albedo = texture;
return m;
}
async function canvasTexture(canvas) {
const image = ShadeImage.fromImageBitmap(await createImageBitmap(canvas));
image.color_space = ColorSpace.SRGB;
return ShadeTexture.from(image);
}
// An entity with nothing but a place: a holder, or the ground's collider.
function point(x = 0, y = 0, z = 0) {
const transform = new Transform64();
transform.setTranslation(x, y, z);
transform.updateMatrix();
return { entity: new Entity().add(transform).build(ecd), transform };
}
function decor(geometry, color, x, y, z) {
const transform = new Transform64();
transform.setTranslation(x, y, z);
const entity = new Entity().add(transform)
.add(ShadedGeometry.from(meshlet_geometry_build_from_geometry(geometry), typeof color === "string" ? material(color) : color))
.build(ecd);
return { entity, transform };
}
function move(object, x, y, z, qx = 0, qy = 0, qz = 0, qw = 1) {
object.transform.setTranslation(x, y, z);
object.transform.setRotation(qx, qy, qz, qw);
object.transform.updateMatrix();
t64_announce_change(ecd, object.entity);
}
// A move that is a jump: colliders start again from here rather than sweeping across.
function place(object, ...pose) {
move(object, ...pose);
ecd.sendEvent(object.entity, TRANSFORM64_EVENT_PLACED);
}
// Cloth collides with an entity that carries a Collider and a ClothCollider.
function collide(object, shape, { friction = .5, inflation = 0 } = {}) {
const collider = new Collider();
collider.shape = shape;
collider.friction = friction;
ecd.addComponentToEntity(object.entity, collider);
ecd.addComponentToEntity(object.entity, ClothCollider.from({ inflation }));
return { ...object, collider };
}
decor(make_box_geometry(14, .2, 14), "#1b2433", 0, -.1, 0);
const pause = document.getElementById("pause");
pause.addEventListener("click", () => {
playing = !playing;
pause.textContent = playing ? "Pause" : "Resume";
pause.setAttribute("aria-pressed", String(!playing));
});
// Start the cloth once a frame showing the scene has finished on the GPU. Shade builds the
// scene's pipelines over its first frames, which take seconds; stepping through them would
// bank those seconds as fixed steps the engine then has to catch up on.
async function start() {
await new Promise(resolve => {
const rendered = () => {
engine.graphics.on.postRender.remove(rendered);
resolve();
};
engine.graphics.on.postRender.add(rendered);
});
await engine.graphics.renderer.device.queue.onSubmittedWorkDone();
playing = true;
}
return {
engine, ecd, cloth, colliders, material, canvasTexture, point, decor, move, place, collide, start,
onStep: callback => { tick = callback; },
};
}
// A sheet of cloth for ClothSystem: a triangle mesh skinned with one joint per vertex.
//
// The sheet is built here as a model, the way a loader describes one: the mesh, a root joint,
// and a joint per vertex hanging from the root at the vertex's rest position. Placed like any
// model, it becomes entities, and `cloth_proxy_from_mesh` reads its joints and triangles into a
// cloth: a hard stretch row per edge and a hinge per interior edge. ClothSystem then moves every
// joint, both its position and the turn of the surface around it, and GPU skinning moves and
// lights the mesh.
import Entity from "@woosh/meep-engine/src/engine/ecs/Entity.js";
import { Transform64 } from "@woosh/meep-engine/src/engine/ecs/transform/Transform64.js";
import { prefab_compile } from "@woosh/meep-engine/src/engine/graphics3/prefab/prefab_compile.js";
import { prefab_instantiate } from "@woosh/meep-engine/src/engine/graphics3/prefab/prefab_instantiate.js";
import { cloth_proxy_from_mesh } from "@woosh/meep-engine/src/engine/physics/cloth/build/cloth_proxy_from_mesh.js";
import { Cloth } from "@woosh/meep-engine/src/engine/physics/cloth/ecs/Cloth.js";
import { ClothRig } from "@woosh/meep-engine/src/engine/physics/cloth/ecs/ClothRig.js";
import { Skin } from "@woosh/meep-engine/src/shade/renderer/animation/Skin.js";
import { Attribute } from "@woosh/meep-engine/src/shade/renderer/geometry/Attribute.js";
import { Geometry } from "@woosh/meep-engine/src/shade/renderer/geometry/Geometry.js";
import { StandardAttributes } from "@woosh/meep-engine/src/shade/renderer/geometry/StandardAttributes.js";
import { meshlet_geometry_build_from_geometry } from "@woosh/meep-engine/src/shade/renderer/geometry/meshlet_geometry_build_from_geometry.js";
import { SceneBundle } from "@woosh/meep-engine/src/shade/renderer/loader/SceneBundle.js";
import { SceneNode } from "@woosh/meep-engine/src/shade/renderer/loader/SceneNode.js";
/**
* @param {EntityComponentDataset} ecd
* @param {object} options
* @param {Float32Array} options.position rest positions, 3 per vertex, world space
* @param {Float32Array} options.uv 2 per vertex
* @param {number[]} options.triangle vertex triples; particle `i` of the cloth is vertex `i`
* @param {StandardShadeMaterial} options.material
* @param {ClothDynamics} options.dynamics
* @returns {{entity: number, cloth: Cloth}} the entity carrying the cloth, which a `ClothPin` names
*/
export function createSheet(ecd, { position, uv, triangle, material, dynamics }) {
const count = position.length / 3;
// Vertex i is weighted wholly to joint i; the bind normals are the rest surface's.
const joints = new Uint16Array(count * 4), weights = new Float32Array(count * 4);
for (let i = 0; i < count; i++) {
joints[i * 4] = i;
weights[i * 4] = 1;
}
const geometry = new Geometry();
geometry.addAttribute(Attribute.from(Float32Array.from(position), 3, StandardAttributes.Position));
geometry.addAttribute(Attribute.from(restNormals(position, triangle), 3, StandardAttributes.Normal));
geometry.addAttribute(Attribute.from(Float32Array.from(uv), 2, StandardAttributes.TextureCoordinates0));
geometry.addAttribute(Attribute.from(joints, 4, StandardAttributes.SkinningJoints));
geometry.addAttribute(Attribute.from(weights, 4, StandardAttributes.SkinningWeights));
geometry.index = Attribute.from(Uint32Array.from(triangle), 1, "index");
const bundle = new SceneBundle();
const mesh = bundle.add_node(SceneNode.from({ name: "Sheet", geometry: meshlet_geometry_build_from_geometry(geometry), material }));
const rig = bundle.add_node(SceneNode.from({ name: "Rig" }));
const names = [], vertexJoints = [];
for (let i = 0; i < count; i++) {
names.push(`Vertex_${i}`);
vertexJoints.push(bundle.add_node(SceneNode.from({
name: names[i], parent: rig, translation: [position[i * 3], position[i * 3 + 1], position[i * 3 + 2]],
})));
}
// The root joint comes last, so joint i stays vertex i. It is the frame the cloth is simulated in.
const inverseBind = new Float32Array((count + 1) * 16);
for (let j = 0; j <= count; j++) {
const m = j * 16;
inverseBind[m] = inverseBind[m + 5] = inverseBind[m + 10] = inverseBind[m + 15] = 1;
if (j < count) for (let k = 0; k < 3; k++) inverseBind[m + 12 + k] = -position[j * 3 + k];
}
bundle.skins = [Skin.from({ joints: [...vertexJoints, rig], inverse_bind_matrices: inverseBind, meshes: [mesh] })];
const prefab = prefab_compile(bundle);
const { proxy, error } = cloth_proxy_from_mesh(prefab, prefab.skins[0], names, triangle, { name: "sheet" });
if (proxy === null) throw new Error(error);
const entity = new Entity().add(new Transform64()).build(ecd);
const instance = prefab_instantiate(prefab, ecd, entity);
const cloth = Cloth.from(dynamics);
// The rig names the mesh whose MeshSkin the proxy's joints index.
ecd.addComponentToEntity(entity, ClothRig.from(proxy, instance.mesh_entities[0]));
ecd.addComponentToEntity(entity, cloth);
return { entity, cloth };
}
// Area-weighted vertex normals of the rest surface.
function restNormals(p, triangle) {
const normal = new Float32Array(p.length);
for (let t = 0; t < triangle.length; t += 3) {
const a = triangle[t] * 3, b = triangle[t + 1] * 3, c = triangle[t + 2] * 3;
const ux = p[b] - p[a], uy = p[b + 1] - p[a + 1], uz = p[b + 2] - p[a + 2];
const vx = p[c] - p[a], vy = p[c + 1] - p[a + 1], vz = p[c + 2] - p[a + 2];
const nx = uy * vz - uz * vy, ny = uz * vx - ux * vz, nz = ux * vy - uy * vx;
for (const v of [a, b, c]) {
normal[v] += nx;
normal[v + 1] += ny;
normal[v + 2] += nz;
}
}
for (let i = 0; i < normal.length; i += 3) {
const l = Math.hypot(normal[i], normal[i + 1], normal[i + 2]) || 1;
for (let k = 0; k < 3; k++) normal[i + k] /= l;
}
return normal;
}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex">
<link rel="icon" href="data:,">
<title>Ribbon pile · Meep</title>
<style>
* { box-sizing: border-box; }
html, body { margin: 0; width: 100%; height: 100%; overflow: hidden; background: #080c16; color: #edf3ff; font: 14px system-ui, sans-serif; }
.panel { position: fixed; z-index: 100; left: 20px; bottom: 20px; width: 280px; padding: 16px; background: #080c16de; border: 1px solid #29344b; border-radius: 14px; backdrop-filter: blur(12px); }
fieldset { border: 0; padding: 0; margin: 0; }
fieldset > :first-child { margin-top: 0; }
label { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin: 12px 0 6px; }
label.toggle { justify-content: flex-start; }
input { accent-color: #6bedc2; }
button { margin-top: 12px; margin-right: 6px; background: #23314a; color: inherit; border: 1px solid #465675; padding: 7px 16px; border-radius: 8px; cursor: pointer; }
button:focus-visible, input:focus-visible { outline: 2px solid #6bedc2; outline-offset: 3px; }
:disabled { opacity: .5; }
.hint { position: fixed; z-index: 100; bottom: 20px; right: 20px; margin: 0; color: #a8b6cd; font-size: 12px; pointer-events: none; }
@media (max-width: 650px) { .panel { left: 10px; bottom: 10px; width: 250px; padding: 12px; } .hint { display: none; } }
</style>
</head>
<body>
<section class="panel" aria-label="Ribbon pile controls">
<fieldset disabled>
<label class="toggle"><input id="self" type="checkbox" checked>Self-collision</label>
<button id="drop" type="button">Start again</button><button id="pause" type="button" aria-pressed="false">Pause</button>
</fieldset>
</section>
<p class="hint">Drag to orbit · Scroll to zoom</p>
<script type="module" src="./src/main.js"></script>
</body>
</html>
{
"name": "@meep-examples/cloth-ribbon",
"version": "0.1.0",
"private": true,
"type": "module",
"description": "A long ribbon lowered into a tray folds into a pile, its layers held apart by self-collision.",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@woosh/meep-engine": "3.55.0"
},
"devDependencies": {
"@rollup/plugin-strip": "^3.0.4",
"vite": "^8.0.13"
}
}
# Ribbon pile
A four-metre ribbon is lowered into a tray and let go. It folds over itself again and again, and self-collision keeps every layer resting on the one below; switch it off and the ribbon falls through itself into a flat tangle.
Run `npm ci` and `npm run dev`, then open `/demo.html`. Build with `npm run build`.
The ribbon is 5 × 161 vertices, 10 cm wide, hanging with a slight wave and a half twist that decide where it buckles. Its colour runs from blue at the bottom to yellow at the top, so the order of the layers shows in the pile. Its top edge is held and lowered at 1 m/s and released 0.4 m above the floor; dropped whole, the top would land at about 9 m/s and throw the pile out of the tray.
`ClothSystem` simulates it. `src/sheet.js` builds the ribbon in code as a skinned model with one joint per vertex, and `cloth_proxy_from_mesh` turns the joints and triangles into a cloth proxy: a hard stretch row per edge and a hinge per interior edge. A `Cloth` and a `ClothRig` go on the ribbon's entity. The top edge is held by a `ClothPin` on a holder entity that the example lowers each step. Removing the pin lets go, and the ribbon falls the rest of the way at the speed the holder was carrying it. **Start again** puts the holder back at the top, calls `ClothSystem#reseed` and adds the pin again.
Self-collision keeps vertex pairs that share no stretch or bend row at least 3.8 cm apart. Its pairs are found once per step, so a layer that moves farther than that within a step can pass through another; the cloth steps at 120 Hz to keep that from happening. Counting mesh edges that cross a triangle of the ribbon, sampled every half second through the drop and after it settles, finds none. Self-collision has no friction, so the tray's walls keep the pile stacked.
`ClothDynamics#thickness` is both the contact distance and the self-collision radius, so the ribbon sets it to the 1.9 cm self-collision needs. The floor and walls take 1.5 cm of that back with a `ClothCollider` inflation of −1.5 cm, so the fabric still lies 4 mm off the tray. The rest of the fabric is bend 0.49 and damping 0.0075 on `ClothDynamics`' normalised scales, 2 substeps × 4 iterations a step.
`src/main.js` sets up the scene. `src/scene.js` boots the engine, and holds the cloth until a frame showing the scene has finished on the GPU.
import { defineConfig } from "vite";
import { copyFileSync, existsSync, mkdirSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { resolve, dirname } from "node:path";
import strip from "@rollup/plugin-strip";
const __dirname = dirname(fileURLToPath(import.meta.url));
export default defineConfig({
root: __dirname,
publicDir: false,
// Keep the engine's imported asset URLs intact during development.
optimizeDeps: {
exclude: ["@woosh/meep-engine"],
include: ["dat.gui", "opentype.js"],
},
plugins: [
{
name: "copy-thumbnail",
apply: "build",
closeBundle() {
const thumb = resolve(__dirname, "thumbnail.png");
const dst = resolve(__dirname, "../../public/examples/cloth-ribbon");
if (existsSync(thumb)) {
mkdirSync(dst, { recursive: true });
copyFileSync(thumb, resolve(dst, "thumbnail.png"));
}
},
},
],
base: "./",
build: {
outDir: resolve(__dirname, "../../public/examples/cloth-ribbon"),
emptyOutDir: false,
rollupOptions: {
input: resolve(__dirname, "demo.html"),
plugins: [
{
...strip(),
apply: 'build'
}
],
},
target: "es2022",
},
});
thumbnail.png · 177.5 KB