@rune-kit/rune 2.2.0 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -34
- package/compiler/__tests__/pack-split.test.js +145 -0
- package/compiler/bin/rune.js +0 -5
- package/compiler/doctor.js +42 -0
- package/compiler/emitter.js +27 -4
- package/compiler/parser.js +41 -3
- package/compiler/transformer.js +10 -6
- package/compiler/transforms/compliance.js +40 -0
- package/extensions/ai-ml/PACK.md +38 -474
- package/extensions/ai-ml/skills/ai-agents.md +172 -0
- package/extensions/ai-ml/skills/code-sandbox.md +187 -0
- package/extensions/ai-ml/skills/deep-research.md +146 -0
- package/extensions/ai-ml/skills/embedding-search.md +66 -0
- package/extensions/ai-ml/skills/fine-tuning-guide.md +74 -0
- package/extensions/ai-ml/skills/llm-architect.md +125 -0
- package/extensions/ai-ml/skills/llm-integration.md +64 -0
- package/extensions/ai-ml/skills/prompt-patterns.md +72 -0
- package/extensions/ai-ml/skills/rag-patterns.md +66 -0
- package/extensions/ai-ml/skills/web-extraction.md +114 -0
- package/extensions/analytics/PACK.md +19 -484
- package/extensions/analytics/skills/ab-testing.md +72 -0
- package/extensions/analytics/skills/dashboard-patterns.md +83 -0
- package/extensions/analytics/skills/data-validation.md +68 -0
- package/extensions/analytics/skills/funnel-analysis.md +81 -0
- package/extensions/analytics/skills/sql-patterns.md +57 -0
- package/extensions/analytics/skills/statistical-analysis.md +79 -0
- package/extensions/analytics/skills/tracking-setup.md +71 -0
- package/extensions/backend/PACK.md +44 -618
- package/extensions/backend/skills/api-patterns.md +84 -0
- package/extensions/backend/skills/async-pipeline.md +193 -0
- package/extensions/backend/skills/auth-patterns.md +97 -0
- package/extensions/backend/skills/background-jobs.md +133 -0
- package/extensions/backend/skills/caching-patterns.md +108 -0
- package/extensions/backend/skills/cli-generation.md +133 -0
- package/extensions/backend/skills/database-patterns.md +87 -0
- package/extensions/backend/skills/middleware-patterns.md +104 -0
- package/extensions/chrome-ext/PACK.md +19 -921
- package/extensions/chrome-ext/skills/cws-preflight.md +143 -0
- package/extensions/chrome-ext/skills/cws-publish.md +104 -0
- package/extensions/chrome-ext/skills/ext-ai-integration.md +251 -0
- package/extensions/chrome-ext/skills/ext-messaging.md +139 -0
- package/extensions/chrome-ext/skills/ext-storage.md +133 -0
- package/extensions/chrome-ext/skills/mv3-scaffold.md +164 -0
- package/extensions/content/PACK.md +43 -335
- package/extensions/content/skills/blog-patterns.md +88 -0
- package/extensions/content/skills/cms-integration.md +131 -0
- package/extensions/content/skills/content-scoring.md +107 -0
- package/extensions/content/skills/i18n.md +83 -0
- package/extensions/content/skills/mdx-authoring.md +137 -0
- package/extensions/content/skills/reference.md +1014 -0
- package/extensions/content/skills/seo-patterns.md +67 -0
- package/extensions/content/skills/video-repurpose.md +153 -0
- package/extensions/devops/PACK.md +38 -457
- package/extensions/devops/skills/chaos-testing.md +67 -0
- package/extensions/devops/skills/ci-cd.md +75 -0
- package/extensions/devops/skills/docker.md +58 -0
- package/extensions/devops/skills/edge-serverless.md +163 -0
- package/extensions/devops/skills/infra-as-code.md +158 -0
- package/extensions/devops/skills/kubernetes.md +110 -0
- package/extensions/devops/skills/monitoring.md +57 -0
- package/extensions/devops/skills/server-setup.md +64 -0
- package/extensions/devops/skills/ssl-domain.md +42 -0
- package/extensions/ecommerce/PACK.md +62 -226
- package/extensions/ecommerce/skills/cart-system.md +79 -0
- package/extensions/ecommerce/skills/inventory-mgmt.md +102 -0
- package/extensions/ecommerce/skills/order-management.md +126 -0
- package/extensions/ecommerce/skills/payment-integration.md +472 -0
- package/extensions/ecommerce/skills/shopify-dev.md +69 -0
- package/extensions/ecommerce/skills/subscription-billing.md +93 -0
- package/extensions/ecommerce/skills/tax-compliance.md +117 -0
- package/extensions/gamedev/PACK.md +66 -317
- package/extensions/gamedev/skills/asset-pipeline.md +74 -0
- package/extensions/gamedev/skills/audio-system.md +129 -0
- package/extensions/gamedev/skills/camera-system.md +87 -0
- package/extensions/gamedev/skills/ecs.md +98 -0
- package/extensions/gamedev/skills/game-loops.md +72 -0
- package/extensions/gamedev/skills/input-system.md +199 -0
- package/extensions/gamedev/skills/multiplayer.md +180 -0
- package/extensions/gamedev/skills/particles.md +105 -0
- package/extensions/gamedev/skills/physics-engine.md +89 -0
- package/extensions/gamedev/skills/scene-management.md +146 -0
- package/extensions/gamedev/skills/threejs-patterns.md +90 -0
- package/extensions/gamedev/skills/webgl.md +71 -0
- package/extensions/mobile/PACK.md +56 -223
- package/extensions/mobile/skills/app-store-connect.md +152 -0
- package/extensions/mobile/skills/app-store-prep.md +66 -0
- package/extensions/mobile/skills/deep-linking.md +109 -0
- package/extensions/mobile/skills/flutter.md +60 -0
- package/extensions/mobile/skills/ios-build-pipeline.md +142 -0
- package/extensions/mobile/skills/native-bridge.md +66 -0
- package/extensions/mobile/skills/ota-updates.md +97 -0
- package/extensions/mobile/skills/push-notifications.md +111 -0
- package/extensions/mobile/skills/react-native.md +82 -0
- package/extensions/saas/PACK.md +26 -720
- package/extensions/saas/skills/billing-integration.md +121 -0
- package/extensions/saas/skills/feature-flags.md +130 -0
- package/extensions/saas/skills/multi-tenant.md +103 -0
- package/extensions/saas/skills/onboarding-flow.md +139 -0
- package/extensions/saas/skills/subscription-flow.md +95 -0
- package/extensions/saas/skills/team-management.md +144 -0
- package/extensions/security/PACK.md +10 -448
- package/extensions/security/skills/api-security.md +140 -0
- package/extensions/security/skills/compliance.md +68 -0
- package/extensions/security/skills/owasp-audit.md +64 -0
- package/extensions/security/skills/pentest-patterns.md +77 -0
- package/extensions/security/skills/secret-mgmt.md +65 -0
- package/extensions/security/skills/supply-chain.md +65 -0
- package/extensions/trading/PACK.md +18 -535
- package/extensions/trading/skills/chart-components.md +55 -0
- package/extensions/trading/skills/experiment-loop.md +125 -0
- package/extensions/trading/skills/fintech-patterns.md +47 -0
- package/extensions/trading/skills/indicator-library.md +58 -0
- package/extensions/trading/skills/quant-analysis.md +111 -0
- package/extensions/trading/skills/realtime-data.md +58 -0
- package/extensions/trading/skills/trade-logic.md +104 -0
- package/extensions/ui/PACK.md +34 -853
- package/extensions/ui/skills/a11y-audit.md +91 -0
- package/extensions/ui/skills/animation-patterns.md +106 -0
- package/extensions/ui/skills/component-patterns.md +75 -0
- package/extensions/ui/skills/design-decision.md +98 -0
- package/extensions/ui/skills/design-system.md +68 -0
- package/extensions/ui/skills/landing-patterns.md +155 -0
- package/extensions/ui/skills/palette-picker.md +162 -0
- package/extensions/ui/skills/react-health.md +90 -0
- package/extensions/ui/skills/type-system.md +125 -0
- package/extensions/ui/skills/web-vitals.md +153 -0
- package/extensions/zalo/PACK.md +117 -0
- package/extensions/zalo/skills/zalo-oa-mcp.md +317 -0
- package/extensions/zalo/skills/zalo-oa-messaging.md +429 -0
- package/extensions/zalo/skills/zalo-oa-setup.md +236 -0
- package/extensions/zalo/skills/zalo-oa-webhook.md +189 -0
- package/extensions/zalo/skills/zalo-personal-messaging.md +194 -0
- package/extensions/zalo/skills/zalo-personal-setup.md +153 -0
- package/extensions/zalo/skills/zalo-rate-guard.md +219 -0
- package/package.json +1 -1
- package/skills/brainstorm/SKILL.md +63 -1
- package/skills/cook/SKILL.md +84 -5
- package/skills/mcp-builder/SKILL.md +48 -1
- package/skills/review/SKILL.md +42 -5
- package/skills/review-intake/SKILL.md +17 -1
- package/skills/skill-router/SKILL.md +89 -7
- package/skills/team/SKILL.md +24 -1
- package/skills/test/SKILL.md +18 -0
- package/skills/verification/SKILL.md +40 -1
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "multiplayer"
|
|
3
|
+
pack: "@rune/gamedev"
|
|
4
|
+
description: "WebSocket game server and client prediction — authoritative server model, client-side prediction, reconciliation, entity interpolation, and lag compensation."
|
|
5
|
+
model: sonnet
|
|
6
|
+
tools: [Read, Edit, Write, Grep, Glob, Bash]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# multiplayer
|
|
10
|
+
|
|
11
|
+
WebSocket game server and client prediction — authoritative server model, client-side prediction, reconciliation, entity interpolation, and lag compensation.
|
|
12
|
+
|
|
13
|
+
Authoritative server model: server owns game state, clients send inputs only. Never trust client position.
|
|
14
|
+
|
|
15
|
+
#### Workflow
|
|
16
|
+
|
|
17
|
+
**Step 1 — Assess multiplayer architecture**
|
|
18
|
+
Use Grep to find existing WebSocket code: `WebSocket`, `socket.io`, `ws`, `onmessage`, `postMessage`. Determine: is there a server or is this client-only? What is the tick rate?
|
|
19
|
+
|
|
20
|
+
**Step 2 — Implement authoritative server**
|
|
21
|
+
Emit: Node.js WebSocket server with fixed-tick update (20Hz sufficient), input queue per player, server-side physics/movement, state broadcast.
|
|
22
|
+
|
|
23
|
+
**Step 3 — Implement client prediction**
|
|
24
|
+
Emit: local input application before server confirmation, pending input buffer, reconciliation on server update, smooth interpolation for remote entities.
|
|
25
|
+
|
|
26
|
+
**Step 4 — Add lag compensation**
|
|
27
|
+
Emit: snapshot buffer for entity interpolation (render ~100ms behind server), client prediction with rollback on desync.
|
|
28
|
+
|
|
29
|
+
#### WebSocket Game Server Pattern
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
// Server (Node.js + ws) — authoritative game server
|
|
33
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
34
|
+
|
|
35
|
+
interface PlayerInput { seq: number; keys: { up: boolean; down: boolean; left: boolean; right: boolean }; }
|
|
36
|
+
interface PlayerState { id: string; x: number; y: number; vx: number; vy: number; }
|
|
37
|
+
|
|
38
|
+
const wss = new WebSocketServer({ port: 3001 });
|
|
39
|
+
const players = new Map<string, PlayerState>();
|
|
40
|
+
const inputQueues = new Map<string, PlayerInput[]>();
|
|
41
|
+
|
|
42
|
+
wss.on('connection', (ws: WebSocket, req) => {
|
|
43
|
+
const id = crypto.randomUUID();
|
|
44
|
+
players.set(id, { id, x: 0, y: 0, vx: 0, vy: 0 });
|
|
45
|
+
inputQueues.set(id, []);
|
|
46
|
+
|
|
47
|
+
ws.send(JSON.stringify({ type: 'init', id, state: Object.fromEntries(players) }));
|
|
48
|
+
broadcast({ type: 'player_joined', id });
|
|
49
|
+
|
|
50
|
+
ws.on('message', (raw) => {
|
|
51
|
+
const msg = JSON.parse(raw.toString());
|
|
52
|
+
if (msg.type === 'input') {
|
|
53
|
+
inputQueues.get(id)?.push(msg.input);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
ws.on('close', () => {
|
|
58
|
+
players.delete(id);
|
|
59
|
+
inputQueues.delete(id);
|
|
60
|
+
broadcast({ type: 'player_left', id });
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Fixed-tick server update (20Hz is sufficient for authoritative server)
|
|
65
|
+
const TICK_MS = 50;
|
|
66
|
+
setInterval(() => {
|
|
67
|
+
inputQueues.forEach((queue, id) => {
|
|
68
|
+
const player = players.get(id)!;
|
|
69
|
+
const input = queue.shift(); // process one input per tick
|
|
70
|
+
if (input) applyInput(player, input);
|
|
71
|
+
integratePhysics(player);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
broadcast({ type: 'state_update', tick: Date.now(), players: Object.fromEntries(players) });
|
|
75
|
+
}, TICK_MS);
|
|
76
|
+
|
|
77
|
+
function broadcast(msg: object) {
|
|
78
|
+
const data = JSON.stringify(msg);
|
|
79
|
+
wss.clients.forEach(c => c.readyState === WebSocket.OPEN && c.send(data));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function applyInput(p: PlayerState, input: PlayerInput) {
|
|
83
|
+
const speed = 200;
|
|
84
|
+
p.vx = (input.keys.right ? 1 : 0) - (input.keys.left ? 1 : 0);
|
|
85
|
+
p.vy = (input.keys.down ? 1 : 0) - (input.keys.up ? 1 : 0);
|
|
86
|
+
const len = Math.hypot(p.vx, p.vy);
|
|
87
|
+
if (len > 0) { p.vx = (p.vx / len) * speed; p.vy = (p.vy / len) * speed; }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function integratePhysics(p: PlayerState) {
|
|
91
|
+
p.x += p.vx * (TICK_MS / 1000);
|
|
92
|
+
p.y += p.vy * (TICK_MS / 1000);
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
#### Client Prediction & Reconciliation
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
// Client — predict locally, reconcile on server update
|
|
100
|
+
class NetworkedPlayer {
|
|
101
|
+
private pendingInputs: { seq: number; input: PlayerInput; }[] = [];
|
|
102
|
+
private seq = 0;
|
|
103
|
+
localState: PlayerState;
|
|
104
|
+
serverState: PlayerState;
|
|
105
|
+
|
|
106
|
+
constructor(initial: PlayerState) {
|
|
107
|
+
this.localState = { ...initial };
|
|
108
|
+
this.serverState = { ...initial };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
sendInput(keys: PlayerInput['keys'], ws: WebSocket) {
|
|
112
|
+
const input: PlayerInput = { seq: ++this.seq, keys };
|
|
113
|
+
this.pendingInputs.push({ seq: this.seq, input });
|
|
114
|
+
ws.send(JSON.stringify({ type: 'input', input }));
|
|
115
|
+
|
|
116
|
+
// Apply immediately (client prediction)
|
|
117
|
+
applyInputToState(this.localState, keys);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
reconcile(serverUpdate: PlayerState & { lastProcessedSeq: number }) {
|
|
121
|
+
this.serverState = serverUpdate;
|
|
122
|
+
|
|
123
|
+
// Remove acknowledged inputs
|
|
124
|
+
this.pendingInputs = this.pendingInputs.filter(p => p.seq > serverUpdate.lastProcessedSeq);
|
|
125
|
+
|
|
126
|
+
// Reapply unacknowledged inputs on top of server state
|
|
127
|
+
this.localState = { ...serverUpdate };
|
|
128
|
+
for (const { input } of this.pendingInputs) {
|
|
129
|
+
applyInputToState(this.localState, input.keys);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function applyInputToState(state: PlayerState, keys: PlayerInput['keys']) {
|
|
135
|
+
const dt = 1 / 60;
|
|
136
|
+
const speed = 200;
|
|
137
|
+
state.x += ((keys.right ? 1 : 0) - (keys.left ? 1 : 0)) * speed * dt;
|
|
138
|
+
state.y += ((keys.down ? 1 : 0) - (keys.up ? 1 : 0)) * speed * dt;
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
#### Lag Compensation & Entity Interpolation
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
// Interpolate remote entities between server snapshots (smooth movement, ~100ms behind)
|
|
146
|
+
interface Snapshot { tick: number; timestamp: number; entities: Map<string, PlayerState>; }
|
|
147
|
+
|
|
148
|
+
class EntityInterpolator {
|
|
149
|
+
private buffer: Snapshot[] = [];
|
|
150
|
+
private readonly delay = 100; // ms behind server
|
|
151
|
+
|
|
152
|
+
addSnapshot(snapshot: Snapshot) {
|
|
153
|
+
this.buffer.push(snapshot);
|
|
154
|
+
// Keep only last 1 second of snapshots
|
|
155
|
+
const cutoff = Date.now() - 1000;
|
|
156
|
+
this.buffer = this.buffer.filter(s => s.timestamp > cutoff);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
getInterpolatedState(entityId: string): PlayerState | null {
|
|
160
|
+
const renderTime = Date.now() - this.delay;
|
|
161
|
+
|
|
162
|
+
// Find the two snapshots bracketing renderTime
|
|
163
|
+
const newer = this.buffer.find(s => s.timestamp >= renderTime);
|
|
164
|
+
const older = this.buffer.slice().reverse().find(s => s.timestamp < renderTime);
|
|
165
|
+
|
|
166
|
+
if (!older || !newer) return newer?.entities.get(entityId) ?? null;
|
|
167
|
+
|
|
168
|
+
const t = (renderTime - older.timestamp) / (newer.timestamp - older.timestamp);
|
|
169
|
+
const a = older.entities.get(entityId);
|
|
170
|
+
const b = newer.entities.get(entityId);
|
|
171
|
+
if (!a || !b) return null;
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
...a,
|
|
175
|
+
x: a.x + (b.x - a.x) * t,
|
|
176
|
+
y: a.y + (b.y - a.y) * t,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
```
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "particles"
|
|
3
|
+
pack: "@rune/gamedev"
|
|
4
|
+
description: "GPU particle system with WebGL instancing — object-pooled particles, emission presets, and performance-safe rendering for 10k+ particles."
|
|
5
|
+
model: sonnet
|
|
6
|
+
tools: [Read, Edit, Write, Grep, Glob, Bash]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# particles
|
|
10
|
+
|
|
11
|
+
GPU particle system with WebGL instancing — object-pooled particles, emission presets, and performance-safe rendering for 10k+ particles.
|
|
12
|
+
|
|
13
|
+
#### Workflow
|
|
14
|
+
|
|
15
|
+
**Step 1 — Detect particle usage**
|
|
16
|
+
Use Grep to find particle code: `particles`, `emitter`, `ParticleSystem`, `createBufferSource`. Understand current implementation: CPU vs GPU, pooling strategy, and draw call count.
|
|
17
|
+
|
|
18
|
+
**Step 2 — Audit particle performance**
|
|
19
|
+
Check for: new particle objects created on emit (GC pressure), no object pool, drawing each particle as a separate draw call, no life/alpha fade.
|
|
20
|
+
|
|
21
|
+
**Step 3 — Emit particle system**
|
|
22
|
+
Emit: ParticleSystem with pre-allocated pool, update/render separation, emitter presets for common effects (explosion, sparks, smoke).
|
|
23
|
+
|
|
24
|
+
#### GPU Particles with WebGL Instancing
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
// GPU particle system — update on CPU, render 10k+ particles via instancing
|
|
28
|
+
interface Particle {
|
|
29
|
+
x: number; y: number;
|
|
30
|
+
vx: number; vy: number;
|
|
31
|
+
life: number; maxLife: number;
|
|
32
|
+
size: number;
|
|
33
|
+
r: number; g: number; b: number; a: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class ParticleSystem {
|
|
37
|
+
private particles: Particle[] = [];
|
|
38
|
+
private pool: Particle[] = [];
|
|
39
|
+
private readonly maxParticles: number;
|
|
40
|
+
|
|
41
|
+
constructor(maxParticles = 5000) {
|
|
42
|
+
this.maxParticles = maxParticles;
|
|
43
|
+
// Pre-allocate pool
|
|
44
|
+
for (let i = 0; i < maxParticles; i++) {
|
|
45
|
+
this.pool.push({ x:0,y:0,vx:0,vy:0,life:0,maxLife:1,size:4,r:1,g:1,b:1,a:1 });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
emit(x: number, y: number, count: number, config: Partial<Particle> = {}) {
|
|
50
|
+
for (let i = 0; i < count && this.particles.length < this.maxParticles; i++) {
|
|
51
|
+
const p = this.pool.pop() ?? { x:0,y:0,vx:0,vy:0,life:0,maxLife:1,size:4,r:1,g:1,b:1,a:1 };
|
|
52
|
+
const angle = Math.random() * Math.PI * 2;
|
|
53
|
+
const speed = 50 + Math.random() * 150;
|
|
54
|
+
Object.assign(p, {
|
|
55
|
+
x, y,
|
|
56
|
+
vx: Math.cos(angle) * speed,
|
|
57
|
+
vy: Math.sin(angle) * speed,
|
|
58
|
+
life: 1, maxLife: 0.5 + Math.random() * 1.5,
|
|
59
|
+
size: 3 + Math.random() * 5,
|
|
60
|
+
r: 1, g: 0.5, b: 0.1, a: 1,
|
|
61
|
+
...config,
|
|
62
|
+
});
|
|
63
|
+
this.particles.push(p);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
update(dt: number) {
|
|
68
|
+
const gravity = 200;
|
|
69
|
+
for (let i = this.particles.length - 1; i >= 0; i--) {
|
|
70
|
+
const p = this.particles[i];
|
|
71
|
+
p.x += p.vx * dt;
|
|
72
|
+
p.y += p.vy * dt;
|
|
73
|
+
p.vy += gravity * dt;
|
|
74
|
+
p.life -= dt / p.maxLife;
|
|
75
|
+
p.a = Math.max(0, p.life);
|
|
76
|
+
if (p.life <= 0) {
|
|
77
|
+
this.pool.push(p);
|
|
78
|
+
this.particles.splice(i, 1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Render with Canvas 2D (swap with WebGL instancing for >10k particles)
|
|
84
|
+
render(ctx: CanvasRenderingContext2D) {
|
|
85
|
+
for (const p of this.particles) {
|
|
86
|
+
ctx.globalAlpha = p.a;
|
|
87
|
+
ctx.fillStyle = `rgb(${p.r * 255 | 0},${p.g * 255 | 0},${p.b * 255 | 0})`;
|
|
88
|
+
ctx.beginPath();
|
|
89
|
+
ctx.arc(p.x, p.y, p.size * p.life, 0, Math.PI * 2);
|
|
90
|
+
ctx.fill();
|
|
91
|
+
}
|
|
92
|
+
ctx.globalAlpha = 1;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Emitter presets
|
|
97
|
+
const EMITTER_PRESETS = {
|
|
98
|
+
explosion: (x: number, y: number, sys: ParticleSystem) =>
|
|
99
|
+
sys.emit(x, y, 80, { r: 1, g: 0.4, b: 0 }),
|
|
100
|
+
sparks: (x: number, y: number, sys: ParticleSystem) =>
|
|
101
|
+
sys.emit(x, y, 20, { r: 1, g: 0.9, b: 0.2, size: 2, maxLife: 0.8 }),
|
|
102
|
+
smoke: (x: number, y: number, sys: ParticleSystem) =>
|
|
103
|
+
sys.emit(x, y, 10, { r: 0.5, g: 0.5, b: 0.5, size: 12, maxLife: 3, vx: 0, vy: -30 }),
|
|
104
|
+
};
|
|
105
|
+
```
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "physics-engine"
|
|
3
|
+
pack: "@rune/gamedev"
|
|
4
|
+
description: "Physics integration — Rapier.js, rigid bodies, constraints, raycasting, collision callbacks, deterministic simulation."
|
|
5
|
+
model: sonnet
|
|
6
|
+
tools: [Read, Edit, Write, Grep, Glob, Bash]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# physics-engine
|
|
10
|
+
|
|
11
|
+
Physics integration — Rapier.js, rigid bodies, constraints, raycasting, collision callbacks, deterministic simulation.
|
|
12
|
+
|
|
13
|
+
#### Workflow
|
|
14
|
+
|
|
15
|
+
**Step 1 — Detect physics setup**
|
|
16
|
+
Use Grep to find physics libraries: `rapier`, `cannon`, `ammo`, `@dimforge/rapier3d`, `RigidBody`, `Collider`. Read physics initialization and body creation to understand: engine choice, world configuration, and collision handling.
|
|
17
|
+
|
|
18
|
+
**Step 2 — Audit physics configuration**
|
|
19
|
+
Check for: physics step tied to render frame (non-deterministic), missing collision groups (everything collides with everything), no sleep threshold (wasted CPU on static objects), raycasts without max distance (expensive), and missing body cleanup on entity destroy.
|
|
20
|
+
|
|
21
|
+
**Step 3 — Emit optimized physics**
|
|
22
|
+
Emit: Rapier.js (WASM, deterministic) setup with proper collision groups, sleep thresholds, event-driven collision callbacks, and raycasting utility.
|
|
23
|
+
|
|
24
|
+
#### Example
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
// Rapier.js (WASM) — setup with collision groups and raycasting
|
|
28
|
+
import RAPIER from '@dimforge/rapier3d-compat';
|
|
29
|
+
|
|
30
|
+
await RAPIER.init();
|
|
31
|
+
const world = new RAPIER.World({ x: 0, y: -9.81, z: 0 });
|
|
32
|
+
|
|
33
|
+
// Collision groups: player=0x0001, enemy=0x0002, ground=0x0004, projectile=0x0008
|
|
34
|
+
const GROUPS = { PLAYER: 0x0001, ENEMY: 0x0002, GROUND: 0x0004, PROJECTILE: 0x0008 };
|
|
35
|
+
|
|
36
|
+
// Ground — static, collides with everything
|
|
37
|
+
const groundBody = world.createRigidBody(RAPIER.RigidBodyDesc.fixed().setTranslation(0, 0, 0));
|
|
38
|
+
world.createCollider(
|
|
39
|
+
RAPIER.ColliderDesc.cuboid(50, 0.1, 50)
|
|
40
|
+
.setCollisionGroups((GROUPS.GROUND << 16) | 0xFFFF),
|
|
41
|
+
groundBody,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// Player — dynamic, collides with ground + enemy (not own projectiles)
|
|
45
|
+
const playerBody = world.createRigidBody(RAPIER.RigidBodyDesc.dynamic().setTranslation(0, 5, 0));
|
|
46
|
+
world.createCollider(
|
|
47
|
+
RAPIER.ColliderDesc.capsule(0.5, 0.3)
|
|
48
|
+
.setCollisionGroups((GROUPS.PLAYER << 16) | (GROUPS.GROUND | GROUPS.ENEMY))
|
|
49
|
+
.setActiveEvents(RAPIER.ActiveEvents.COLLISION_EVENTS),
|
|
50
|
+
playerBody,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// Raycast utility
|
|
54
|
+
function raycast(origin: RAPIER.Vector3, direction: RAPIER.Vector3, maxDist = 100) {
|
|
55
|
+
const ray = new RAPIER.Ray(origin, direction);
|
|
56
|
+
const hit = world.castRay(ray, maxDist, true);
|
|
57
|
+
if (hit) {
|
|
58
|
+
const point = ray.pointAt(hit.timeOfImpact);
|
|
59
|
+
return { point, normal: hit.normal, collider: hit.collider };
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
#### Collision Event Handling
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
// Event-driven collision callbacks — no polling
|
|
69
|
+
const eventQueue = new RAPIER.EventQueue(true);
|
|
70
|
+
|
|
71
|
+
// In fixed update step:
|
|
72
|
+
world.step(eventQueue);
|
|
73
|
+
|
|
74
|
+
eventQueue.drainCollisionEvents((handle1, handle2, started) => {
|
|
75
|
+
const body1 = world.getRigidBody(world.getCollider(handle1).parent()!);
|
|
76
|
+
const body2 = world.getRigidBody(world.getCollider(handle2).parent()!);
|
|
77
|
+
|
|
78
|
+
const entity1 = entityMap.get(body1.handle);
|
|
79
|
+
const entity2 = entityMap.get(body2.handle);
|
|
80
|
+
|
|
81
|
+
if (started) {
|
|
82
|
+
entity1?.onCollisionEnter(entity2);
|
|
83
|
+
entity2?.onCollisionEnter(entity1);
|
|
84
|
+
} else {
|
|
85
|
+
entity1?.onCollisionExit(entity2);
|
|
86
|
+
entity2?.onCollisionExit(entity1);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
```
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "scene-management"
|
|
3
|
+
pack: "@rune/gamedev"
|
|
4
|
+
description: "Scene transitions, preloading, serialization — stack-based SceneManager with fade transitions, asset preloading before enter, and level JSON serialization."
|
|
5
|
+
model: sonnet
|
|
6
|
+
tools: [Read, Edit, Write, Grep, Glob, Bash]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# scene-management
|
|
10
|
+
|
|
11
|
+
Scene transitions, preloading, serialization — stack-based SceneManager with fade transitions, asset preloading before enter, and level JSON serialization.
|
|
12
|
+
|
|
13
|
+
#### Workflow
|
|
14
|
+
|
|
15
|
+
**Step 1 — Detect scene structure**
|
|
16
|
+
Use Grep to find scene code: `Scene`, `GameState`, `StateMachine`, `push`, `pop`, `replace`. Read to understand: how scenes transition, whether assets are preloaded, and how scene data is persisted.
|
|
17
|
+
|
|
18
|
+
**Step 2 — Audit scene management**
|
|
19
|
+
Check for: abrupt scene switches with no transition (jarring UX), assets loaded mid-scene causing stutters, no scene stack (can't return to previous state), missing cleanup on exit.
|
|
20
|
+
|
|
21
|
+
**Step 3 — Emit SceneManager**
|
|
22
|
+
Emit: stack-based SceneManager with fade in/out, asset preloading tied to scene.assets[], push/pop/replace operations, and level serialization/deserialization.
|
|
23
|
+
|
|
24
|
+
#### Scene Stack with Transitions and Preloading
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
// Scene manager — stack-based, asset preloading, fade transitions
|
|
28
|
+
interface Scene {
|
|
29
|
+
name: string;
|
|
30
|
+
assets: string[]; // asset keys to preload before entering
|
|
31
|
+
onEnter(data?: unknown): void;
|
|
32
|
+
onExit(): void;
|
|
33
|
+
update(dt: number): void;
|
|
34
|
+
render(ctx: CanvasRenderingContext2D): void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
class SceneManager {
|
|
38
|
+
private stack: Scene[] = [];
|
|
39
|
+
private loader: AssetLoader;
|
|
40
|
+
private transitioning = false;
|
|
41
|
+
private fadeAlpha = 0;
|
|
42
|
+
private fadeDir: 1 | -1 = 1;
|
|
43
|
+
|
|
44
|
+
constructor(loader: AssetLoader) { this.loader = loader; }
|
|
45
|
+
|
|
46
|
+
get current(): Scene | undefined { return this.stack[this.stack.length - 1]; }
|
|
47
|
+
|
|
48
|
+
async push(scene: Scene, data?: unknown) {
|
|
49
|
+
if (this.transitioning) return;
|
|
50
|
+
this.transitioning = true;
|
|
51
|
+
|
|
52
|
+
await this.fadeOut();
|
|
53
|
+
await this.preloadScene(scene);
|
|
54
|
+
this.stack.push(scene);
|
|
55
|
+
scene.onEnter(data);
|
|
56
|
+
await this.fadeIn();
|
|
57
|
+
|
|
58
|
+
this.transitioning = false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async pop() {
|
|
62
|
+
if (this.transitioning || this.stack.length <= 1) return;
|
|
63
|
+
this.transitioning = true;
|
|
64
|
+
|
|
65
|
+
await this.fadeOut();
|
|
66
|
+
this.current?.onExit();
|
|
67
|
+
this.stack.pop();
|
|
68
|
+
await this.fadeIn();
|
|
69
|
+
|
|
70
|
+
this.transitioning = false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async replace(scene: Scene, data?: unknown) {
|
|
74
|
+
if (this.transitioning) return;
|
|
75
|
+
this.transitioning = true;
|
|
76
|
+
|
|
77
|
+
await this.fadeOut();
|
|
78
|
+
this.current?.onExit();
|
|
79
|
+
this.stack.pop();
|
|
80
|
+
await this.preloadScene(scene);
|
|
81
|
+
this.stack.push(scene);
|
|
82
|
+
scene.onEnter(data);
|
|
83
|
+
await this.fadeIn();
|
|
84
|
+
|
|
85
|
+
this.transitioning = false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private async preloadScene(scene: Scene) {
|
|
89
|
+
// Preload only assets not already cached
|
|
90
|
+
await Promise.all(scene.assets.map(key => this.loader.load(key, `/assets/${key}`)));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
renderTransition(ctx: CanvasRenderingContext2D, w: number, h: number) {
|
|
94
|
+
if (this.fadeAlpha > 0) {
|
|
95
|
+
ctx.save();
|
|
96
|
+
ctx.globalAlpha = this.fadeAlpha;
|
|
97
|
+
ctx.fillStyle = '#000';
|
|
98
|
+
ctx.fillRect(0, 0, w, h);
|
|
99
|
+
ctx.restore();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private fadeOut(): Promise<void> {
|
|
104
|
+
return new Promise(resolve => {
|
|
105
|
+
this.fadeDir = 1;
|
|
106
|
+
const interval = setInterval(() => {
|
|
107
|
+
this.fadeAlpha = Math.min(1, this.fadeAlpha + 0.05);
|
|
108
|
+
if (this.fadeAlpha >= 1) { clearInterval(interval); resolve(); }
|
|
109
|
+
}, 16);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private fadeIn(): Promise<void> {
|
|
114
|
+
return new Promise(resolve => {
|
|
115
|
+
this.fadeDir = -1;
|
|
116
|
+
const interval = setInterval(() => {
|
|
117
|
+
this.fadeAlpha = Math.max(0, this.fadeAlpha - 0.05);
|
|
118
|
+
if (this.fadeAlpha <= 0) { clearInterval(interval); resolve(); }
|
|
119
|
+
}, 16);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Level serialization — save/load level state as JSON
|
|
125
|
+
interface LevelData {
|
|
126
|
+
name: string;
|
|
127
|
+
entities: Array<{ type: string; x: number; y: number; props: Record<string, unknown> }>;
|
|
128
|
+
tilemap: number[][];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function serializeLevel(world: World): LevelData {
|
|
132
|
+
const entities: LevelData['entities'] = [];
|
|
133
|
+
for (const id of world.query(Position)) {
|
|
134
|
+
const pos = world.getComponent(id, Position)!;
|
|
135
|
+
entities.push({ type: 'generic', x: pos.x, y: pos.y, props: {} });
|
|
136
|
+
}
|
|
137
|
+
return { name: 'level1', entities, tilemap: [] };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function deserializeLevel(data: LevelData, world: World) {
|
|
141
|
+
for (const e of data.entities) {
|
|
142
|
+
const id = world.createEntity();
|
|
143
|
+
world.addComponent(id, new Position(e.x, e.y));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "threejs-patterns"
|
|
3
|
+
pack: "@rune/gamedev"
|
|
4
|
+
description: "Three.js patterns — scene setup, React Three Fiber integration, PBR materials, post-processing, performance optimization."
|
|
5
|
+
model: sonnet
|
|
6
|
+
tools: [Read, Edit, Write, Grep, Glob, Bash]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# threejs-patterns
|
|
10
|
+
|
|
11
|
+
Three.js patterns — scene setup, React Three Fiber integration, PBR materials, post-processing, performance optimization.
|
|
12
|
+
|
|
13
|
+
#### Workflow
|
|
14
|
+
|
|
15
|
+
**Step 1 — Detect Three.js setup**
|
|
16
|
+
Use Grep to find Three.js usage: `THREE.`, `useThree`, `useFrame`, `Canvas`, `@react-three/fiber`, `@react-three/drei`. Read the main scene file to understand: renderer setup, scene graph structure, camera type, and lighting model.
|
|
17
|
+
|
|
18
|
+
**Step 2 — Audit performance**
|
|
19
|
+
Check for: objects created inside `useFrame` (GC pressure), missing `dispose()` on unmount (memory leak), no frustum culling on large scenes, textures without power-of-two dimensions, unoptimized geometry (too many draw calls), and missing LOD for distant objects.
|
|
20
|
+
|
|
21
|
+
**Step 3 — Emit optimized scene**
|
|
22
|
+
Emit: properly structured R3F scene with declarative lights, memoized geometries, disposal on unmount, instanced meshes for repeated objects, and post-processing pipeline.
|
|
23
|
+
|
|
24
|
+
#### Example
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
// React Three Fiber — optimized scene with instancing and post-processing
|
|
28
|
+
import { Canvas, useFrame } from '@react-three/fiber';
|
|
29
|
+
import { OrbitControls, Environment, useGLTF, Instances, Instance } from '@react-three/drei';
|
|
30
|
+
import { EffectComposer, Bloom, Vignette } from '@react-three/postprocessing';
|
|
31
|
+
import { useRef, useMemo } from 'react';
|
|
32
|
+
|
|
33
|
+
function InstancedTrees({ count = 500 }) {
|
|
34
|
+
const positions = useMemo(() =>
|
|
35
|
+
Array.from({ length: count }, () => [
|
|
36
|
+
(Math.random() - 0.5) * 100,
|
|
37
|
+
0,
|
|
38
|
+
(Math.random() - 0.5) * 100,
|
|
39
|
+
] as [number, number, number]),
|
|
40
|
+
[count]);
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<Instances limit={count}>
|
|
44
|
+
<cylinderGeometry args={[0.2, 0.4, 3]} />
|
|
45
|
+
<meshStandardMaterial color="#4a7c59" />
|
|
46
|
+
{positions.map((pos, i) => <Instance key={i} position={pos} />)}
|
|
47
|
+
</Instances>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function GameScene() {
|
|
52
|
+
return (
|
|
53
|
+
<Canvas camera={{ position: [0, 10, 20], fov: 60 }} gl={{ antialias: true }}>
|
|
54
|
+
<ambientLight intensity={0.3} />
|
|
55
|
+
<directionalLight position={[10, 20, 10]} intensity={1} castShadow />
|
|
56
|
+
<Environment preset="sunset" />
|
|
57
|
+
<InstancedTrees count={500} />
|
|
58
|
+
<OrbitControls maxPolarAngle={Math.PI / 2.2} />
|
|
59
|
+
<EffectComposer>
|
|
60
|
+
<Bloom intensity={0.3} luminanceThreshold={0.8} />
|
|
61
|
+
<Vignette offset={0.3} darkness={0.6} />
|
|
62
|
+
</EffectComposer>
|
|
63
|
+
</Canvas>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
#### LOD (Level of Detail) Pattern
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import * as THREE from 'three';
|
|
72
|
+
|
|
73
|
+
// Swap geometry based on camera distance
|
|
74
|
+
function buildLODMesh(
|
|
75
|
+
highGeo: THREE.BufferGeometry,
|
|
76
|
+
midGeo: THREE.BufferGeometry,
|
|
77
|
+
lowGeo: THREE.BufferGeometry,
|
|
78
|
+
material: THREE.Material,
|
|
79
|
+
): THREE.LOD {
|
|
80
|
+
const lod = new THREE.LOD();
|
|
81
|
+
lod.addLevel(new THREE.Mesh(highGeo, material), 0); // < 20 units
|
|
82
|
+
lod.addLevel(new THREE.Mesh(midGeo, material), 20); // 20–80 units
|
|
83
|
+
lod.addLevel(new THREE.Mesh(lowGeo, material), 80); // > 80 units
|
|
84
|
+
return lod;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// In render loop — LOD auto-updates based on camera distance
|
|
88
|
+
// scene.add(buildLODMesh(highGeo, midGeo, lowGeo, mat));
|
|
89
|
+
// lod.update(camera); // call once per frame
|
|
90
|
+
```
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "webgl"
|
|
3
|
+
pack: "@rune/gamedev"
|
|
4
|
+
description: "WebGL patterns — shader programming, GLSL, buffer management, texture handling, instanced rendering."
|
|
5
|
+
model: sonnet
|
|
6
|
+
tools: [Read, Edit, Write, Grep, Glob, Bash]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# webgl
|
|
10
|
+
|
|
11
|
+
WebGL patterns — shader programming, GLSL, buffer management, texture handling, instanced rendering.
|
|
12
|
+
|
|
13
|
+
#### Workflow
|
|
14
|
+
|
|
15
|
+
**Step 1 — Detect WebGL usage**
|
|
16
|
+
Use Grep to find WebGL code: `getContext('webgl`, `gl.createShader`, `gl.createProgram`, `*.glsl`, `*.vert`, `*.frag`. Read shader files and GL initialization to understand: WebGL version, shader complexity, and buffer strategy.
|
|
17
|
+
|
|
18
|
+
**Step 2 — Audit shader and buffer efficiency**
|
|
19
|
+
Check for: uniforms set every frame that don't change (use UBO), separate draw calls for identical geometry (use instancing), textures not using mipmaps, missing `gl.deleteBuffer`/`gl.deleteTexture` cleanup, and shaders with expensive per-fragment branching.
|
|
20
|
+
|
|
21
|
+
**Step 3 — Emit optimized WebGL code**
|
|
22
|
+
Emit: WebGL2 setup with proper context attributes, VAO-based buffer management, instanced rendering for repeated geometry, and GLSL shaders with documented inputs/outputs.
|
|
23
|
+
|
|
24
|
+
#### Example
|
|
25
|
+
|
|
26
|
+
```glsl
|
|
27
|
+
// Vertex shader — instanced rendering with per-instance transform
|
|
28
|
+
#version 300 es
|
|
29
|
+
layout(location = 0) in vec3 aPosition;
|
|
30
|
+
layout(location = 1) in vec3 aNormal;
|
|
31
|
+
layout(location = 2) in mat4 aInstanceMatrix; // per-instance (locations 2-5)
|
|
32
|
+
|
|
33
|
+
uniform mat4 uViewProjection;
|
|
34
|
+
|
|
35
|
+
out vec3 vNormal;
|
|
36
|
+
out vec3 vWorldPos;
|
|
37
|
+
|
|
38
|
+
void main() {
|
|
39
|
+
vec4 worldPos = aInstanceMatrix * vec4(aPosition, 1.0);
|
|
40
|
+
vWorldPos = worldPos.xyz;
|
|
41
|
+
vNormal = mat3(transpose(inverse(aInstanceMatrix))) * aNormal;
|
|
42
|
+
gl_Position = uViewProjection * worldPos;
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```glsl
|
|
47
|
+
// Fragment shader — PBR-lite with single directional light
|
|
48
|
+
#version 300 es
|
|
49
|
+
precision highp float;
|
|
50
|
+
|
|
51
|
+
in vec3 vNormal;
|
|
52
|
+
in vec3 vWorldPos;
|
|
53
|
+
out vec4 fragColor;
|
|
54
|
+
|
|
55
|
+
uniform vec3 uLightDir;
|
|
56
|
+
uniform vec3 uCameraPos;
|
|
57
|
+
uniform vec3 uBaseColor;
|
|
58
|
+
|
|
59
|
+
void main() {
|
|
60
|
+
vec3 N = normalize(vNormal);
|
|
61
|
+
vec3 L = normalize(uLightDir);
|
|
62
|
+
vec3 V = normalize(uCameraPos - vWorldPos);
|
|
63
|
+
vec3 H = normalize(L + V);
|
|
64
|
+
|
|
65
|
+
float diffuse = max(dot(N, L), 0.0);
|
|
66
|
+
float specular = pow(max(dot(N, H), 0.0), 32.0);
|
|
67
|
+
vec3 ambient = uBaseColor * 0.15;
|
|
68
|
+
|
|
69
|
+
fragColor = vec4(ambient + uBaseColor * diffuse + vec3(specular * 0.5), 1.0);
|
|
70
|
+
}
|
|
71
|
+
```
|