Skip to content

Commit ec2df1e

Browse files
committed
feat: cloth simulation 3D web app with GitHub actions deploy
1 parent 1bd8d16 commit ec2df1e

9 files changed

Lines changed: 187 additions & 105 deletions

File tree

‎.github/workflows/deploy.yml‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
name: Deploy Cloth Simulation to GitHub Pages
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
- master
8+
workflow_dispatch:
9+
10+
permissions:
11+
contents: read
12+
pages: write
13+
id-token: write
14+
15+
concurrency:
16+
group: pages
17+
cancel-in-progress: true
18+
19+
jobs:
20+
build-and-deploy:
21+
environment:
22+
name: github-pages
23+
url: ${{ steps.deployment.outputs.page_url }}
24+
runs-on: ubuntu-latest
25+
steps:
26+
- name: Checkout
27+
uses: actions/checkout@v4
28+
29+
- name: Setup Node.js
30+
uses: actions/setup-node@v4
31+
with:
32+
node-version: 20
33+
cache: 'npm'
34+
35+
- name: Install dependencies
36+
run: npm ci
37+
38+
- name: Build project
39+
run: npm run build
40+
41+
- name: Setup Pages
42+
uses: actions/configure-pages@v5
43+
44+
- name: Upload artifact
45+
uses: actions/upload-pages-artifact@v3
46+
with:
47+
path: './dist'
48+
49+
- name: Deploy to GitHub Pages
50+
id: deployment
51+
uses: actions/deploy-pages@v4

‎README.md‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Cloth Simulation 3D
2+
3+
## Description
4+
5+
Cloth Simulation 3D is a real-time web-based interactive 3D cloth physics simulation. Built using WebGL and Three.js, it simulates cloth dynamics using Verlet integration and distance constraints. Users can interact with the cloth by dragging an obstacle sphere and manipulating wind forces directly on screen.
6+
7+
## Tech Stack
8+
9+
- Core: HTML5, JavaScript (ES6+), CSS3
10+
- 3D Engine: Three.js
11+
- Physics Engine: Custom Verlet Integration Cloth Simulator
12+
- Build Tool: Vite
13+
- Deployment: GitHub Actions & GitHub Pages
14+
15+
## Features
16+
17+
- Real-time cloth physics simulation powered by CPU Verlet integration.
18+
- Multiple pinning modes: Top 2 Corners, Top Edge, 4 Corners, and Free Hanging.
19+
- Interactive obstacle collision with a movable 3D sphere.
20+
- Mouse-driven wind interaction using right-click drag.
21+
- Customizable physical parameters including Gravity, Air Damping, and Solver Iterations.
22+
- Rendering options for Solid Fabric texture and Wireframe mesh mode.
23+
- Directional lighting and real-time shadow mapping.
24+
25+
## Installation and Local Setup
26+
27+
1. Clone the repository:
28+
git clone https://github.com/shinnei/cloth-simulation.git
29+
30+
2. Install dependencies:
31+
npm install
32+
33+
3. Run the local development server:
34+
npm run dev
35+
36+
4. Open your browser and navigate to http://localhost:5173
37+
38+
## Contact Information
39+
40+
- Author: shinnei
41+
- GitHub: https://shinnei.github.io/

‎index.html‎

Lines changed: 37 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,31 @@
1313
<div id="app">
1414
<canvas id="webgl-canvas"></canvas>
1515

16-
<button id="ui-toggle-btn" class="ui-hamburger-btn hidden" title="Open Settings">
17-
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
18-
<line x1="3" y1="6" x2="21" y2="6"></line>
19-
<line x1="3" y1="12" x2="21" y2="12"></line>
20-
<line x1="3" y1="18" x2="21" y2="18"></line>
21-
</svg>
22-
</button>
16+
<div id="ui-top-actions" class="ui-top-actions hidden">
17+
<a href="https://shinnei.github.io/" target="_blank" rel="noopener noreferrer" class="ui-icon-btn ui-github-btn" title="GitHub Repository">
18+
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
19+
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.53 1.032 1.53 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"/>
20+
</svg>
21+
</a>
22+
<button id="ui-toggle-btn" class="ui-hamburger-btn" title="Open Settings">
23+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
24+
<line x1="3" y1="6" x2="21" y2="6"></line>
25+
<line x1="3" y1="12" x2="21" y2="12"></line>
26+
<line x1="3" y1="18" x2="21" y2="18"></line>
27+
</svg>
28+
</button>
29+
</div>
2330

2431
<div id="ui-panel" class="ui-window">
2532
<div class="ui-header">
2633
<span class="ui-title">Cloth Simulation 3D</span>
2734
<div class="ui-header-right">
2835
<span id="ui-fps" class="ui-fps-badge">60.0 FPS</span>
36+
<a href="https://shinnei.github.io/" target="_blank" rel="noopener noreferrer" class="ui-header-github-btn" title="GitHub Repository">
37+
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
38+
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.53 1.032 1.53 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"/>
39+
</svg>
40+
</a>
2941
<button id="ui-close-btn" class="ui-close-btn" title="Collapse Window">✕</button>
3042
</div>
3143
</div>
@@ -53,68 +65,44 @@
5365
</div>
5466

5567
<div class="ui-group">
56-
<div class="ui-group-title">Physics Parameters</div>
68+
<div class="ui-group-title">Appearance</div>
5769

5870
<div class="ui-row">
59-
<label for="sld-gravity" class="ui-label">Gravity Y: <span id="val-gravity">-9.8</span></label>
60-
<input type="range" id="sld-gravity" min="-25" max="5" step="0.5" value="-9.8" class="ui-slider" />
61-
</div>
62-
63-
<div class="ui-row">
64-
<label for="sld-damping" class="ui-label">Air Damping: <span id="val-damping">0.98</span></label>
65-
<input type="range" id="sld-damping" min="0.90" max="0.999" step="0.005" value="0.98" class="ui-slider" />
71+
<label class="ui-label">Render Mode:</label>
72+
<select id="sel-render-mode" class="ui-select">
73+
<option value="solid" selected>Solid Fabric</option>
74+
<option value="wireframe">Wireframe Mesh</option>
75+
</select>
6676
</div>
6777

68-
<div class="ui-row">
69-
<label for="sld-stiffness" class="ui-label">Solver Iterations: <span id="val-stiffness">12</span></label>
70-
<input type="range" id="sld-stiffness" min="1" max="30" step="1" value="12" class="ui-slider" />
78+
<div class="ui-row ui-checkbox-row">
79+
<input type="checkbox" id="chk-shadows" checked class="ui-checkbox" />
80+
<label for="chk-shadows" class="ui-label">Enable Shadows</label>
7181
</div>
7282
</div>
7383

7484
<div class="ui-group">
75-
<div class="ui-group-title">Wind Simulation</div>
85+
<div class="ui-group-title">Physics Parameters</div>
7686

77-
<div class="ui-row ui-checkbox-row">
78-
<input type="checkbox" id="chk-wind-enable" class="ui-checkbox" />
79-
<label for="chk-wind-enable" class="ui-label">Enable Wind Force</label>
80-
</div>
81-
8287
<div class="ui-row">
83-
<label for="sld-wind-strength" class="ui-label">Wind Strength: <span id="val-wind-strength">12.0</span></label>
84-
<input type="range" id="sld-wind-strength" min="0" max="50" step="1" value="12" class="ui-slider" />
88+
<label for="sld-gravity" class="ui-label">Gravity Y: <span id="val-gravity">-9.8</span></label>
89+
<input type="range" id="sld-gravity" min="-25" max="5" step="0.5" value="-9.8" class="ui-slider" />
8590
</div>
8691

8792
<div class="ui-row">
88-
<label for="sld-wind-angle" class="ui-label">Wind Angle: <span id="val-wind-angle">45°</span></label>
89-
<input type="range" id="sld-wind-angle" min="0" max="360" step="5" value="45" class="ui-slider" />
90-
</div>
91-
92-
<div class="ui-row ui-checkbox-row">
93-
<input type="checkbox" id="chk-wind-turbulence" checked class="ui-checkbox" />
94-
<label for="chk-wind-turbulence" class="ui-label">Turbulence / Gusts</label>
93+
<label for="sld-damping" class="ui-label">Air Damping: <span id="val-damping">0.98</span></label>
94+
<input type="range" id="sld-damping" min="0.90" max="0.999" step="0.005" value="0.98" class="ui-slider" />
9595
</div>
96-
</div>
9796

98-
<div class="ui-group">
99-
<div class="ui-group-title">Appearance</div>
100-
10197
<div class="ui-row">
102-
<label class="ui-label">Render Mode:</label>
103-
<select id="sel-render-mode" class="ui-select">
104-
<option value="solid" selected>Solid Fabric</option>
105-
<option value="wireframe">Wireframe Mesh</option>
106-
</select>
107-
</div>
108-
109-
<div class="ui-row ui-checkbox-row">
110-
<input type="checkbox" id="chk-shadows" checked class="ui-checkbox" />
111-
<label for="chk-shadows" class="ui-label">Enable Shadows</label>
98+
<label for="sld-stiffness" class="ui-label">Solver Iterations: <span id="val-stiffness">12</span></label>
99+
<input type="range" id="sld-stiffness" min="1" max="30" step="1" value="12" class="ui-slider" />
112100
</div>
113101
</div>
114102

115103
<div class="ui-footer-tip">
116-
💡 <strong>Left Mouse:</strong> Rotate camera angle / Drag sphere.<br>
117-
💡 <strong>Right Mouse Drag:</strong> Rotate & adjust wind vector arrow!
104+
💡 <strong>Left Mouse:</strong> Rotate camera / Drag sphere<br>
105+
💡 <strong>Right Mouse Drag:</strong> Add wind
118106
</div>
119107
</div>
120108
</div>

‎node_modules/.vite/deps/_metadata.json‎

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/cloth.js‎

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ export class ClothSimulator {
121121

122122
this.sphereEnabled = true;
123123
this.sphereRadius = 1.8;
124-
this.spherePos = { x: 0, y: -1.2, z: 0.8 };
124+
this.spherePos = { x: 0, y: -1.2, z: 2.0 };
125125

126126
this.floorEnabled = true;
127127
this.floorY = -4.0;
@@ -178,13 +178,12 @@ export class ClothSimulator {
178178
}
179179

180180
if (c < this.segmentsX && r < this.segmentsY) {
181-
const pDiagonal1 = this.particles[getIdx(c + 1, r + 1)];
182-
this.constraints.push(new ClothConstraint(pCurrent, pDiagonal1));
183-
}
184-
if (c > 0 && r < this.segmentsY) {
185-
const pLeft = this.particles[getIdx(c, r)];
186-
const pDiagonal2 = this.particles[getIdx(c - 1, r + 1)];
187-
this.constraints.push(new ClothConstraint(pLeft, pDiagonal2));
181+
const pDiag1 = this.particles[getIdx(c + 1, r + 1)];
182+
const pRight = this.particles[getIdx(c + 1, r)];
183+
const pDown = this.particles[getIdx(c, r + 1)];
184+
185+
this.constraints.push(new ClothConstraint(pCurrent, pDiag1));
186+
this.constraints.push(new ClothConstraint(pRight, pDown));
188187
}
189188

190189
if (c < this.segmentsX - 1) {
@@ -456,7 +455,7 @@ export class ClothSimulator {
456455
if (len > 0.00001) {
457456
normals[i * 3] = nx / len;
458457
normals[i * 3 + 1] = ny / len;
459-
normals[i * 3 + 2] = len > 0 ? nz / len : 1;
458+
normals[i * 3 + 2] = nz / len;
460459
} else {
461460
normals[i * 3 + 2] = 1.0;
462461
}

‎src/main.js‎

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ const sphereMat = new THREE.MeshStandardMaterial({
216216
metalness: 0.7
217217
});
218218
const sphereMesh = new THREE.Mesh(sphereGeo, sphereMat);
219-
sphereMesh.position.set(0, -1.2, 0.8);
219+
sphereMesh.position.set(0, -1.2, 2.0);
220220
sphereMesh.castShadow = true;
221221
sphereMesh.receiveShadow = true;
222222
scene.add(sphereMesh);
@@ -298,20 +298,8 @@ window.addEventListener('pointermove', (e) => {
298298
if (isDraggingSphere) {
299299
raycaster.setFromCamera(mouse, camera);
300300
if (raycaster.ray.intersectPlane(dragPlane, planeIntersection)) {
301-
let targetX = planeIntersection.x;
302-
let targetZ = planeIntersection.z;
303-
304-
// Limit max sphere displacement based on pinning elasticity
305-
if (clothSim.pinMode === 'top-4-corners') {
306-
targetZ = Math.max(-1.2, Math.min(1.4, targetZ));
307-
targetX = Math.max(-2.6, Math.min(2.6, targetX));
308-
} else if (clothSim.pinMode === 'top-edge' || clothSim.pinMode === 'top-2-corners') {
309-
targetZ = Math.max(-3.0, Math.min(3.0, targetZ));
310-
targetX = Math.max(-3.5, Math.min(3.5, targetX));
311-
}
312-
313-
sphereMesh.position.x = targetX;
314-
sphereMesh.position.z = targetZ;
301+
sphereMesh.position.x = planeIntersection.x;
302+
sphereMesh.position.z = planeIntersection.z;
315303

316304
clothSim.spherePos.x = sphereMesh.position.x;
317305
clothSim.spherePos.y = sphereMesh.position.y;

‎src/style.css‎

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,21 @@ html, body {
3030
Clean & Natural GUI Styling
3131
========================================================================== */
3232

33-
.ui-hamburger-btn {
33+
.ui-top-actions {
3434
position: fixed;
3535
top: 16px;
3636
right: 16px;
3737
z-index: 1000;
38+
display: flex;
39+
align-items: center;
40+
gap: 8px;
41+
}
42+
43+
.ui-top-actions.hidden {
44+
display: none !important;
45+
}
46+
47+
.ui-icon-btn, .ui-hamburger-btn {
3848
width: 40px;
3949
height: 40px;
4050
background: #2d323e;
@@ -47,18 +57,15 @@ html, body {
4757
cursor: pointer;
4858
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
4959
transition: all 0.15s ease;
60+
text-decoration: none;
5061
}
5162

52-
.ui-hamburger-btn:hover {
63+
.ui-icon-btn:hover, .ui-hamburger-btn:hover {
5364
background: #3b82f6;
5465
border-color: #3b82f6;
5566
color: #ffffff;
5667
}
5768

58-
.ui-hamburger-btn.hidden {
59-
display: none !important;
60-
}
61-
6269
.ui-window {
6370
position: fixed;
6471
top: 16px;
@@ -104,6 +111,19 @@ html, body {
104111
gap: 8px;
105112
}
106113

114+
.ui-header-github-btn {
115+
color: #94a3b8;
116+
display: flex;
117+
align-items: center;
118+
justify-content: center;
119+
text-decoration: none;
120+
transition: color 0.15s ease;
121+
}
122+
123+
.ui-header-github-btn:hover {
124+
color: #60a5fa;
125+
}
126+
107127
.ui-fps-badge {
108128
background: #181a20;
109129
border: 1px solid #3b82f6;

0 commit comments

Comments
 (0)