gopeak 2.3.6 → 2.3.8
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/LICENSE +21 -21
- package/README.md +279 -392
- package/build/addon/auto_reload/auto_reload.gd +126 -126
- package/build/addon/auto_reload/plugin.cfg +7 -7
- package/build/addon/godot_mcp_editor/mcp_client.gd +178 -178
- package/build/addon/godot_mcp_editor/plugin.cfg +6 -6
- package/build/addon/godot_mcp_editor/plugin.gd +84 -84
- package/build/addon/godot_mcp_editor/tool_executor.gd +114 -114
- package/build/addon/godot_mcp_editor/tools/animation_tools.gd +502 -502
- package/build/addon/godot_mcp_editor/tools/resource_tools.gd +425 -425
- package/build/addon/godot_mcp_editor/tools/scene_tools.gd +761 -761
- package/build/addon/godot_mcp_runtime/godot_mcp_runtime.gd +33 -33
- package/build/addon/godot_mcp_runtime/mcp_runtime_autoload.gd +671 -619
- package/build/addon/godot_mcp_runtime/plugin.cfg +7 -7
- package/build/cli/notify.js +4 -3
- package/build/cli.js +18 -18
- package/build/godot-bridge.js +11 -11
- package/build/index.js +261 -125
- package/build/scripts/godot_operations.gd +6823 -6823
- package/build/visualizer/events.js +19 -19
- package/build/visualizer/panel.js +34 -34
- package/build/visualizer/usages.js +14 -14
- package/build/visualizer-server.js +11 -11
- package/build/visualizer.html +2596 -2596
- package/package.json +106 -105
- package/scripts/postinstall.mjs +29 -29
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
[plugin]
|
|
2
|
-
|
|
3
|
-
name="Godot MCP Runtime"
|
|
4
|
-
description="Runtime communication addon for Godot MCP server. Enables real-time scene inspection, property modification, and method calling from AI assistants."
|
|
5
|
-
author="godot-mcp"
|
|
6
|
-
version="1.0.0"
|
|
7
|
-
script="godot_mcp_runtime.gd"
|
|
1
|
+
[plugin]
|
|
2
|
+
|
|
3
|
+
name="Godot MCP Runtime"
|
|
4
|
+
description="Runtime communication addon for Godot MCP server. Enables real-time scene inspection, property modification, and method calling from AI assistants."
|
|
5
|
+
author="godot-mcp"
|
|
6
|
+
version="1.0.0"
|
|
7
|
+
script="godot_mcp_runtime.gd"
|
package/build/cli/notify.js
CHANGED
|
@@ -42,9 +42,10 @@ export async function showNotification() {
|
|
|
42
42
|
}
|
|
43
43
|
// --- Star prompt (one-time) ---
|
|
44
44
|
if (!hasStarPrompted) {
|
|
45
|
-
await askYesNo(' \u2b50 Star GoPeak on GitHub? (y/n): ');
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
const wantsStar = await askYesNo(' \u2b50 Star GoPeak on GitHub? (y/n): ');
|
|
46
|
+
if (wantsStar) {
|
|
47
|
+
await handleStar();
|
|
48
|
+
}
|
|
48
49
|
writeFileSync(STAR_PROMPTED_FILE, new Date().toISOString());
|
|
49
50
|
console.log('');
|
|
50
51
|
}
|
package/build/cli.js
CHANGED
|
@@ -65,24 +65,24 @@ async function main() {
|
|
|
65
65
|
}
|
|
66
66
|
function printHelp() {
|
|
67
67
|
const version = getLocalVersion();
|
|
68
|
-
console.log(`
|
|
69
|
-
GoPeak v${version} — AI-Powered Godot Development via MCP
|
|
70
|
-
|
|
71
|
-
Usage:
|
|
72
|
-
gopeak Start MCP server (default)
|
|
73
|
-
gopeak setup Install shell hooks for update notifications
|
|
74
|
-
gopeak check Check for GoPeak updates
|
|
75
|
-
gopeak check --bg Background check (used by shell hooks)
|
|
76
|
-
gopeak check --quiet Print only if update available
|
|
77
|
-
gopeak star Star GoPeak on GitHub
|
|
78
|
-
gopeak uninstall Remove shell hooks
|
|
79
|
-
gopeak version Show current version
|
|
80
|
-
gopeak help Show this help
|
|
81
|
-
|
|
82
|
-
Shell hooks wrap these commands with update notifications:
|
|
83
|
-
claude, codex, gemini, opencode, omc, omx
|
|
84
|
-
|
|
85
|
-
More info: https://github.com/HaD0Yun/Gopeak-godot-mcp
|
|
68
|
+
console.log(`
|
|
69
|
+
GoPeak v${version} — AI-Powered Godot Development via MCP
|
|
70
|
+
|
|
71
|
+
Usage:
|
|
72
|
+
gopeak Start MCP server (default)
|
|
73
|
+
gopeak setup Install shell hooks for update notifications
|
|
74
|
+
gopeak check Check for GoPeak updates
|
|
75
|
+
gopeak check --bg Background check (used by shell hooks)
|
|
76
|
+
gopeak check --quiet Print only if update available
|
|
77
|
+
gopeak star Star GoPeak on GitHub
|
|
78
|
+
gopeak uninstall Remove shell hooks
|
|
79
|
+
gopeak version Show current version
|
|
80
|
+
gopeak help Show this help
|
|
81
|
+
|
|
82
|
+
Shell hooks wrap these commands with update notifications:
|
|
83
|
+
claude, codex, gemini, opencode, omc, omx
|
|
84
|
+
|
|
85
|
+
More info: https://github.com/HaD0Yun/Gopeak-godot-mcp
|
|
86
86
|
`.trim());
|
|
87
87
|
}
|
|
88
88
|
main().catch((err) => {
|
package/build/godot-bridge.js
CHANGED
|
@@ -482,17 +482,17 @@ export class GodotBridge extends EventEmitter {
|
|
|
482
482
|
this.emit(eventName, payload);
|
|
483
483
|
}
|
|
484
484
|
getDefaultVisualizerHtml() {
|
|
485
|
-
return `<!doctype html>
|
|
486
|
-
<html lang="en">
|
|
487
|
-
<head>
|
|
488
|
-
<meta charset="utf-8" />
|
|
489
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
490
|
-
<title>Godot MCP Visualizer</title>
|
|
491
|
-
</head>
|
|
492
|
-
<body>
|
|
493
|
-
<h1>Godot MCP Visualizer</h1>
|
|
494
|
-
<p>Run the map_project tool to load visualization data.</p>
|
|
495
|
-
</body>
|
|
485
|
+
return `<!doctype html>
|
|
486
|
+
<html lang="en">
|
|
487
|
+
<head>
|
|
488
|
+
<meta charset="utf-8" />
|
|
489
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
490
|
+
<title>Godot MCP Visualizer</title>
|
|
491
|
+
</head>
|
|
492
|
+
<body>
|
|
493
|
+
<h1>Godot MCP Visualizer</h1>
|
|
494
|
+
<p>Run the map_project tool to load visualization data.</p>
|
|
495
|
+
</body>
|
|
496
496
|
</html>`;
|
|
497
497
|
}
|
|
498
498
|
rejectAllPending(error) {
|
package/build/index.js
CHANGED
|
@@ -475,10 +475,20 @@ class GodotServer {
|
|
|
475
475
|
const params = (args && typeof args === 'object') ? args : {};
|
|
476
476
|
const RUNTIME_PORT = 7777;
|
|
477
477
|
const RUNTIME_HOST = '127.0.0.1';
|
|
478
|
-
const
|
|
478
|
+
const timeoutOverride = Number.parseInt(process.env.GOPEAK_RUNTIME_TIMEOUT_MS || '', 10);
|
|
479
|
+
const TIMEOUT_MS = Number.isInteger(timeoutOverride) && timeoutOverride > 0 ? timeoutOverride : 10000;
|
|
480
|
+
const expectsScreenshot = command === 'capture_screenshot' || command === 'capture_viewport';
|
|
481
|
+
const screenshotDir = expectsScreenshot ? mkdtempSync(join(tmpdir(), 'gopeak-runtime-screenshot-')) : null;
|
|
482
|
+
const screenshotPath = screenshotDir ? join(screenshotDir, 'capture.png') : null;
|
|
483
|
+
const runtimeParams = screenshotPath ? { ...params, output_path: screenshotPath } : params;
|
|
484
|
+
const cleanupScreenshotDir = () => {
|
|
485
|
+
if (screenshotDir) {
|
|
486
|
+
rmSync(screenshotDir, { recursive: true, force: true });
|
|
487
|
+
}
|
|
488
|
+
};
|
|
479
489
|
return new Promise((resolve) => {
|
|
480
490
|
const socket = createTcpConnection({ port: RUNTIME_PORT, host: RUNTIME_HOST }, () => {
|
|
481
|
-
const payload = JSON.stringify({ command, params, id: Date.now() });
|
|
491
|
+
const payload = JSON.stringify({ command, params: runtimeParams, id: Date.now() });
|
|
482
492
|
socket.write(payload + '\n');
|
|
483
493
|
});
|
|
484
494
|
let responseBuffer = Buffer.alloc(0);
|
|
@@ -489,6 +499,7 @@ class GodotServer {
|
|
|
489
499
|
}
|
|
490
500
|
resolved = true;
|
|
491
501
|
socket.destroy();
|
|
502
|
+
cleanupScreenshotDir();
|
|
492
503
|
resolve({
|
|
493
504
|
content: [{ type: 'text', text: `Runtime command '${command}' timed out after ${TIMEOUT_MS}ms. Ensure the Godot game is running with the MCP runtime addon enabled.` }],
|
|
494
505
|
});
|
|
@@ -500,7 +511,36 @@ class GodotServer {
|
|
|
500
511
|
resolved = true;
|
|
501
512
|
clearTimeout(timer);
|
|
502
513
|
socket.destroy();
|
|
514
|
+
if (parsed.type === 'screenshot_file' && parsed.path) {
|
|
515
|
+
const returnedPath = String(parsed.path);
|
|
516
|
+
if (!screenshotPath || normalize(returnedPath) !== normalize(screenshotPath)) {
|
|
517
|
+
cleanupScreenshotDir();
|
|
518
|
+
resolve({
|
|
519
|
+
content: [{ type: 'text', text: `Rejected screenshot file path outside the GoPeak-managed capture path: '${returnedPath}'` }],
|
|
520
|
+
});
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
try {
|
|
524
|
+
const imageData = readFileSync(screenshotPath).toString('base64');
|
|
525
|
+
cleanupScreenshotDir();
|
|
526
|
+
resolve({
|
|
527
|
+
content: [
|
|
528
|
+
{ type: 'text', text: `Screenshot captured: ${parsed.width}x${parsed.height} ${parsed.format}` },
|
|
529
|
+
{ type: 'image', data: imageData, mimeType: 'image/png' },
|
|
530
|
+
],
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
catch (error) {
|
|
534
|
+
cleanupScreenshotDir();
|
|
535
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
536
|
+
resolve({
|
|
537
|
+
content: [{ type: 'text', text: `Failed to read screenshot file '${screenshotPath}': ${message}` }],
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
503
542
|
if (parsed.type === 'screenshot' && parsed.data) {
|
|
543
|
+
cleanupScreenshotDir();
|
|
504
544
|
resolve({
|
|
505
545
|
content: [
|
|
506
546
|
{ type: 'text', text: `Screenshot captured: ${parsed.width}x${parsed.height} ${parsed.format}` },
|
|
@@ -509,6 +549,7 @@ class GodotServer {
|
|
|
509
549
|
});
|
|
510
550
|
return;
|
|
511
551
|
}
|
|
552
|
+
cleanupScreenshotDir();
|
|
512
553
|
resolve({
|
|
513
554
|
content: [{ type: 'text', text: JSON.stringify(parsed, null, 2) }],
|
|
514
555
|
});
|
|
@@ -551,7 +592,8 @@ class GodotServer {
|
|
|
551
592
|
newlineIndex = responseBuffer.indexOf(0x0a);
|
|
552
593
|
}
|
|
553
594
|
if (parsedMessages.length > 0) {
|
|
554
|
-
const candidate = parsedMessages.find((message) => message?.type === '
|
|
595
|
+
const candidate = parsedMessages.find((message) => message?.type === 'screenshot_file' && message?.path)
|
|
596
|
+
?? parsedMessages.find((message) => message?.type === 'screenshot' && message?.data)
|
|
555
597
|
?? parsedMessages.find((message) => message?.type === 'pong')
|
|
556
598
|
?? parsedMessages.find((message) => message?.type && message.type !== 'welcome')
|
|
557
599
|
?? null;
|
|
@@ -567,6 +609,7 @@ class GodotServer {
|
|
|
567
609
|
clearTimeout(timer);
|
|
568
610
|
const responseData = responseBuffer.toString('utf8').trim();
|
|
569
611
|
resolved = true;
|
|
612
|
+
cleanupScreenshotDir();
|
|
570
613
|
try {
|
|
571
614
|
const parsed = JSON.parse(responseData);
|
|
572
615
|
resolve({
|
|
@@ -585,6 +628,7 @@ class GodotServer {
|
|
|
585
628
|
}
|
|
586
629
|
resolved = true;
|
|
587
630
|
clearTimeout(timer);
|
|
631
|
+
cleanupScreenshotDir();
|
|
588
632
|
resolve({
|
|
589
633
|
content: [{ type: 'text', text: `Failed to connect to Godot runtime addon at ${RUNTIME_HOST}:${RUNTIME_PORT}: ${error.message}. Ensure the game is running with the MCP runtime autoload enabled.` }],
|
|
590
634
|
});
|
|
@@ -1077,7 +1121,7 @@ class GodotServer {
|
|
|
1077
1121
|
this.logDebug(`Command: ${cmd}`);
|
|
1078
1122
|
try {
|
|
1079
1123
|
const { stdout, stderr } = await execAsync(cmd);
|
|
1080
|
-
return { stdout, stderr };
|
|
1124
|
+
return { stdout, stderr: this.sanitizeGodotStderr(stderr) };
|
|
1081
1125
|
}
|
|
1082
1126
|
finally {
|
|
1083
1127
|
rmSync(paramsDir, { recursive: true, force: true });
|
|
@@ -1089,7 +1133,7 @@ class GodotServer {
|
|
|
1089
1133
|
const execError = error;
|
|
1090
1134
|
return {
|
|
1091
1135
|
stdout: execError.stdout,
|
|
1092
|
-
stderr: execError.stderr,
|
|
1136
|
+
stderr: this.sanitizeGodotStderr(execError.stderr),
|
|
1093
1137
|
};
|
|
1094
1138
|
}
|
|
1095
1139
|
throw error;
|
|
@@ -1589,7 +1633,7 @@ class GodotServer {
|
|
|
1589
1633
|
this.logDebug('Killing existing Godot process before starting a new one');
|
|
1590
1634
|
this.activeProcess.process.kill();
|
|
1591
1635
|
}
|
|
1592
|
-
const cmdArgs = ['-d', '--path', args.projectPath];
|
|
1636
|
+
const cmdArgs = ['--headless', '-d', '--path', args.projectPath];
|
|
1593
1637
|
if (args.scene && this.validatePath(args.scene)) {
|
|
1594
1638
|
this.logDebug(`Adding scene parameter: ${args.scene}`);
|
|
1595
1639
|
cmdArgs.push(args.scene);
|
|
@@ -2287,6 +2331,27 @@ class GodotServer {
|
|
|
2287
2331
|
}
|
|
2288
2332
|
return null;
|
|
2289
2333
|
}
|
|
2334
|
+
sanitizeGodotStderr(stderr) {
|
|
2335
|
+
if (!stderr) {
|
|
2336
|
+
return stderr;
|
|
2337
|
+
}
|
|
2338
|
+
const ignoredPatterns = [
|
|
2339
|
+
/WARNING: ObjectDB instances leaked at exit/i,
|
|
2340
|
+
/at:\s+cleanup\s+\(core\/object\/object\.cpp:/i,
|
|
2341
|
+
/ERROR:\s+\d+\s+resources still in use at exit/i,
|
|
2342
|
+
/at:\s+clear\s+\(core\/io\/resource\.cpp:/i,
|
|
2343
|
+
];
|
|
2344
|
+
const filteredLines = stderr
|
|
2345
|
+
.split(/\r?\n/)
|
|
2346
|
+
.filter((line) => {
|
|
2347
|
+
const trimmed = line.trim();
|
|
2348
|
+
if (!trimmed) {
|
|
2349
|
+
return false;
|
|
2350
|
+
}
|
|
2351
|
+
return !ignoredPatterns.some((pattern) => pattern.test(trimmed));
|
|
2352
|
+
});
|
|
2353
|
+
return filteredLines.join('\n').trim();
|
|
2354
|
+
}
|
|
2290
2355
|
/**
|
|
2291
2356
|
* Capture/update current intent snapshot
|
|
2292
2357
|
*/
|
|
@@ -4646,6 +4711,80 @@ class GodotServer {
|
|
|
4646
4711
|
// ============================================
|
|
4647
4712
|
// Project Search Handlers
|
|
4648
4713
|
// ============================================
|
|
4714
|
+
searchProjectNatively(projectPath, query, fileTypes, useRegex, caseSensitive, maxResults) {
|
|
4715
|
+
const normalizedExtensions = new Set(fileTypes.map((ext) => ext.replace(/^\./, '').toLowerCase()).filter(Boolean));
|
|
4716
|
+
const result = {
|
|
4717
|
+
query,
|
|
4718
|
+
results: [],
|
|
4719
|
+
summary: {
|
|
4720
|
+
files_searched: 0,
|
|
4721
|
+
files_with_matches: 0,
|
|
4722
|
+
total_matches: 0,
|
|
4723
|
+
truncated: false,
|
|
4724
|
+
},
|
|
4725
|
+
};
|
|
4726
|
+
const regex = useRegex ? new RegExp(query, caseSensitive ? '' : 'i') : null;
|
|
4727
|
+
const queryToCheck = caseSensitive ? query : query.toLowerCase();
|
|
4728
|
+
const visit = (dirPath) => {
|
|
4729
|
+
if (result.summary.total_matches >= maxResults) {
|
|
4730
|
+
result.summary.truncated = true;
|
|
4731
|
+
return;
|
|
4732
|
+
}
|
|
4733
|
+
for (const entry of readdirSync(dirPath, { withFileTypes: true })) {
|
|
4734
|
+
if (result.summary.total_matches >= maxResults) {
|
|
4735
|
+
result.summary.truncated = true;
|
|
4736
|
+
return;
|
|
4737
|
+
}
|
|
4738
|
+
if (entry.name === '.git' || entry.name === 'node_modules' || entry.name === '.godot') {
|
|
4739
|
+
continue;
|
|
4740
|
+
}
|
|
4741
|
+
const entryPath = join(dirPath, entry.name);
|
|
4742
|
+
if (entry.isDirectory()) {
|
|
4743
|
+
visit(entryPath);
|
|
4744
|
+
continue;
|
|
4745
|
+
}
|
|
4746
|
+
if (!entry.isFile()) {
|
|
4747
|
+
continue;
|
|
4748
|
+
}
|
|
4749
|
+
const extension = entry.name.includes('.') ? entry.name.split('.').pop()?.toLowerCase() || '' : '';
|
|
4750
|
+
if (!normalizedExtensions.has(extension)) {
|
|
4751
|
+
continue;
|
|
4752
|
+
}
|
|
4753
|
+
result.summary.files_searched += 1;
|
|
4754
|
+
const content = readFileSync(entryPath, 'utf8');
|
|
4755
|
+
const lines = content.split('\n');
|
|
4756
|
+
const matches = [];
|
|
4757
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
4758
|
+
if (result.summary.total_matches >= maxResults) {
|
|
4759
|
+
result.summary.truncated = true;
|
|
4760
|
+
break;
|
|
4761
|
+
}
|
|
4762
|
+
const line = lines[index];
|
|
4763
|
+
const match = regex
|
|
4764
|
+
? regex.exec(line)?.[0]
|
|
4765
|
+
: ((caseSensitive ? line : line.toLowerCase()).includes(queryToCheck) ? query : '');
|
|
4766
|
+
if (match) {
|
|
4767
|
+
matches.push({
|
|
4768
|
+
line: index + 1,
|
|
4769
|
+
content: line.trim(),
|
|
4770
|
+
match,
|
|
4771
|
+
});
|
|
4772
|
+
result.summary.total_matches += 1;
|
|
4773
|
+
}
|
|
4774
|
+
}
|
|
4775
|
+
if (matches.length > 0) {
|
|
4776
|
+
const relativePath = entryPath.slice(projectPath.length + 1).replace(/\\/g, '/');
|
|
4777
|
+
result.results.push({
|
|
4778
|
+
file: `res://${relativePath}`,
|
|
4779
|
+
matches,
|
|
4780
|
+
});
|
|
4781
|
+
result.summary.files_with_matches += 1;
|
|
4782
|
+
}
|
|
4783
|
+
}
|
|
4784
|
+
};
|
|
4785
|
+
visit(projectPath);
|
|
4786
|
+
return result;
|
|
4787
|
+
}
|
|
4649
4788
|
/**
|
|
4650
4789
|
* Handle the search_project tool
|
|
4651
4790
|
*/
|
|
@@ -4669,12 +4808,9 @@ class GodotServer {
|
|
|
4669
4808
|
caseSensitive: args.caseSensitive || false,
|
|
4670
4809
|
maxResults: args.maxResults || 100,
|
|
4671
4810
|
};
|
|
4672
|
-
const
|
|
4673
|
-
if (stderr && stderr.includes('ERROR')) {
|
|
4674
|
-
return this.createErrorResponse(`Failed to search project: ${stderr}`, ['Check if the query/regex pattern is valid']);
|
|
4675
|
-
}
|
|
4811
|
+
const result = this.searchProjectNatively(args.projectPath, params.query, params.fileTypes, params.regex, params.caseSensitive, params.maxResults);
|
|
4676
4812
|
return {
|
|
4677
|
-
content: [{ type: 'text', text:
|
|
4813
|
+
content: [{ type: 'text', text: JSON.stringify(result) }],
|
|
4678
4814
|
};
|
|
4679
4815
|
}
|
|
4680
4816
|
catch (error) {
|
|
@@ -5102,146 +5238,146 @@ class GodotServer {
|
|
|
5102
5238
|
const shaderTemplates = {
|
|
5103
5239
|
medieval: {
|
|
5104
5240
|
description: 'Warm stone/aged material look',
|
|
5105
|
-
code: `shader_type spatial;
|
|
5106
|
-
render_mode blend_mix, depth_draw_opaque, cull_back, diffuse_burley, specular_schlick_ggx;
|
|
5107
|
-
|
|
5108
|
-
uniform sampler2D albedo_texture : source_color, filter_linear_mipmap;
|
|
5109
|
-
uniform float roughness : hint_range(0.0, 1.0) = 0.85;
|
|
5110
|
-
uniform float age_factor : hint_range(0.0, 1.0) = 0.3;
|
|
5111
|
-
uniform vec3 tint_color : source_color = vec3(0.9, 0.85, 0.75);
|
|
5112
|
-
|
|
5113
|
-
void fragment() {
|
|
5114
|
-
vec4 albedo = texture(albedo_texture, UV);
|
|
5115
|
-
vec3 aged = mix(albedo.rgb, albedo.rgb * tint_color, age_factor);
|
|
5116
|
-
ALBEDO = aged;
|
|
5117
|
-
ROUGHNESS = roughness;
|
|
5118
|
-
SPECULAR = 0.2;
|
|
5119
|
-
METALLIC = 0.0;
|
|
5241
|
+
code: `shader_type spatial;
|
|
5242
|
+
render_mode blend_mix, depth_draw_opaque, cull_back, diffuse_burley, specular_schlick_ggx;
|
|
5243
|
+
|
|
5244
|
+
uniform sampler2D albedo_texture : source_color, filter_linear_mipmap;
|
|
5245
|
+
uniform float roughness : hint_range(0.0, 1.0) = 0.85;
|
|
5246
|
+
uniform float age_factor : hint_range(0.0, 1.0) = 0.3;
|
|
5247
|
+
uniform vec3 tint_color : source_color = vec3(0.9, 0.85, 0.75);
|
|
5248
|
+
|
|
5249
|
+
void fragment() {
|
|
5250
|
+
vec4 albedo = texture(albedo_texture, UV);
|
|
5251
|
+
vec3 aged = mix(albedo.rgb, albedo.rgb * tint_color, age_factor);
|
|
5252
|
+
ALBEDO = aged;
|
|
5253
|
+
ROUGHNESS = roughness;
|
|
5254
|
+
SPECULAR = 0.2;
|
|
5255
|
+
METALLIC = 0.0;
|
|
5120
5256
|
}`,
|
|
5121
5257
|
},
|
|
5122
5258
|
cyberpunk: {
|
|
5123
5259
|
description: 'Neon glow with pulsing effect',
|
|
5124
|
-
code: `shader_type spatial;
|
|
5125
|
-
render_mode blend_mix, depth_draw_opaque, cull_back;
|
|
5126
|
-
|
|
5127
|
-
uniform vec3 neon_color : source_color = vec3(1.0, 0.0, 0.8);
|
|
5128
|
-
uniform float pulse_speed : hint_range(0.0, 10.0) = 2.0;
|
|
5129
|
-
uniform float intensity : hint_range(0.0, 5.0) = 2.5;
|
|
5130
|
-
uniform float base_brightness : hint_range(0.0, 1.0) = 0.3;
|
|
5131
|
-
|
|
5132
|
-
void fragment() {
|
|
5133
|
-
float pulse = sin(TIME * pulse_speed) * 0.5 + 0.5;
|
|
5134
|
-
vec3 emissive = neon_color * intensity * (0.5 + pulse * 0.5);
|
|
5135
|
-
|
|
5136
|
-
ALBEDO = neon_color * base_brightness;
|
|
5137
|
-
EMISSION = emissive;
|
|
5138
|
-
METALLIC = 0.8;
|
|
5139
|
-
ROUGHNESS = 0.2;
|
|
5260
|
+
code: `shader_type spatial;
|
|
5261
|
+
render_mode blend_mix, depth_draw_opaque, cull_back;
|
|
5262
|
+
|
|
5263
|
+
uniform vec3 neon_color : source_color = vec3(1.0, 0.0, 0.8);
|
|
5264
|
+
uniform float pulse_speed : hint_range(0.0, 10.0) = 2.0;
|
|
5265
|
+
uniform float intensity : hint_range(0.0, 5.0) = 2.5;
|
|
5266
|
+
uniform float base_brightness : hint_range(0.0, 1.0) = 0.3;
|
|
5267
|
+
|
|
5268
|
+
void fragment() {
|
|
5269
|
+
float pulse = sin(TIME * pulse_speed) * 0.5 + 0.5;
|
|
5270
|
+
vec3 emissive = neon_color * intensity * (0.5 + pulse * 0.5);
|
|
5271
|
+
|
|
5272
|
+
ALBEDO = neon_color * base_brightness;
|
|
5273
|
+
EMISSION = emissive;
|
|
5274
|
+
METALLIC = 0.8;
|
|
5275
|
+
ROUGHNESS = 0.2;
|
|
5140
5276
|
}`,
|
|
5141
5277
|
},
|
|
5142
5278
|
nature: {
|
|
5143
5279
|
description: 'Wind sway for foliage',
|
|
5144
|
-
code: `shader_type spatial;
|
|
5145
|
-
render_mode blend_mix, depth_draw_opaque, cull_disabled;
|
|
5146
|
-
|
|
5147
|
-
uniform sampler2D albedo_texture : source_color, filter_linear_mipmap;
|
|
5148
|
-
uniform float wind_strength : hint_range(0.0, 2.0) = 0.3;
|
|
5149
|
-
uniform float wind_speed : hint_range(0.0, 5.0) = 1.5;
|
|
5150
|
-
uniform float wind_scale : hint_range(0.1, 10.0) = 1.0;
|
|
5151
|
-
|
|
5152
|
-
void vertex() {
|
|
5153
|
-
float wind = sin(TIME * wind_speed + VERTEX.x * wind_scale + VERTEX.z * wind_scale * 0.7);
|
|
5154
|
-
float height_factor = UV.y;
|
|
5155
|
-
VERTEX.x += wind * wind_strength * height_factor;
|
|
5156
|
-
VERTEX.z += wind * wind_strength * height_factor * 0.5;
|
|
5157
|
-
}
|
|
5158
|
-
|
|
5159
|
-
void fragment() {
|
|
5160
|
-
vec4 albedo = texture(albedo_texture, UV);
|
|
5161
|
-
ALBEDO = albedo.rgb;
|
|
5162
|
-
ALPHA = albedo.a;
|
|
5163
|
-
ALPHA_SCISSOR_THRESHOLD = 0.5;
|
|
5164
|
-
ROUGHNESS = 0.8;
|
|
5280
|
+
code: `shader_type spatial;
|
|
5281
|
+
render_mode blend_mix, depth_draw_opaque, cull_disabled;
|
|
5282
|
+
|
|
5283
|
+
uniform sampler2D albedo_texture : source_color, filter_linear_mipmap;
|
|
5284
|
+
uniform float wind_strength : hint_range(0.0, 2.0) = 0.3;
|
|
5285
|
+
uniform float wind_speed : hint_range(0.0, 5.0) = 1.5;
|
|
5286
|
+
uniform float wind_scale : hint_range(0.1, 10.0) = 1.0;
|
|
5287
|
+
|
|
5288
|
+
void vertex() {
|
|
5289
|
+
float wind = sin(TIME * wind_speed + VERTEX.x * wind_scale + VERTEX.z * wind_scale * 0.7);
|
|
5290
|
+
float height_factor = UV.y;
|
|
5291
|
+
VERTEX.x += wind * wind_strength * height_factor;
|
|
5292
|
+
VERTEX.z += wind * wind_strength * height_factor * 0.5;
|
|
5293
|
+
}
|
|
5294
|
+
|
|
5295
|
+
void fragment() {
|
|
5296
|
+
vec4 albedo = texture(albedo_texture, UV);
|
|
5297
|
+
ALBEDO = albedo.rgb;
|
|
5298
|
+
ALPHA = albedo.a;
|
|
5299
|
+
ALPHA_SCISSOR_THRESHOLD = 0.5;
|
|
5300
|
+
ROUGHNESS = 0.8;
|
|
5165
5301
|
}`,
|
|
5166
5302
|
},
|
|
5167
5303
|
scifi: {
|
|
5168
5304
|
description: 'Clean metallic with LED accents',
|
|
5169
|
-
code: `shader_type spatial;
|
|
5170
|
-
render_mode blend_mix, depth_draw_opaque, cull_back;
|
|
5171
|
-
|
|
5172
|
-
uniform sampler2D albedo_texture : source_color, filter_linear_mipmap;
|
|
5173
|
-
uniform vec3 led_color : source_color = vec3(0.0, 0.8, 1.0);
|
|
5174
|
-
uniform float led_intensity : hint_range(0.0, 3.0) = 1.5;
|
|
5175
|
-
uniform float metallic_value : hint_range(0.0, 1.0) = 0.9;
|
|
5176
|
-
|
|
5177
|
-
void fragment() {
|
|
5178
|
-
vec4 albedo = texture(albedo_texture, UV);
|
|
5179
|
-
float led_mask = step(0.9, albedo.r) * step(0.9, albedo.g) * step(0.9, albedo.b);
|
|
5180
|
-
|
|
5181
|
-
ALBEDO = mix(albedo.rgb, albedo.rgb * 0.3, led_mask);
|
|
5182
|
-
EMISSION = led_color * led_intensity * led_mask;
|
|
5183
|
-
METALLIC = metallic_value;
|
|
5184
|
-
ROUGHNESS = mix(0.3, 0.1, led_mask);
|
|
5305
|
+
code: `shader_type spatial;
|
|
5306
|
+
render_mode blend_mix, depth_draw_opaque, cull_back;
|
|
5307
|
+
|
|
5308
|
+
uniform sampler2D albedo_texture : source_color, filter_linear_mipmap;
|
|
5309
|
+
uniform vec3 led_color : source_color = vec3(0.0, 0.8, 1.0);
|
|
5310
|
+
uniform float led_intensity : hint_range(0.0, 3.0) = 1.5;
|
|
5311
|
+
uniform float metallic_value : hint_range(0.0, 1.0) = 0.9;
|
|
5312
|
+
|
|
5313
|
+
void fragment() {
|
|
5314
|
+
vec4 albedo = texture(albedo_texture, UV);
|
|
5315
|
+
float led_mask = step(0.9, albedo.r) * step(0.9, albedo.g) * step(0.9, albedo.b);
|
|
5316
|
+
|
|
5317
|
+
ALBEDO = mix(albedo.rgb, albedo.rgb * 0.3, led_mask);
|
|
5318
|
+
EMISSION = led_color * led_intensity * led_mask;
|
|
5319
|
+
METALLIC = metallic_value;
|
|
5320
|
+
ROUGHNESS = mix(0.3, 0.1, led_mask);
|
|
5185
5321
|
}`,
|
|
5186
5322
|
},
|
|
5187
5323
|
horror: {
|
|
5188
5324
|
description: 'Dark with subtle pulsing shadows',
|
|
5189
|
-
code: `shader_type spatial;
|
|
5190
|
-
render_mode blend_mix, depth_draw_opaque, cull_back;
|
|
5191
|
-
|
|
5192
|
-
uniform sampler2D albedo_texture : source_color, filter_linear_mipmap;
|
|
5193
|
-
uniform float darkness : hint_range(0.0, 1.0) = 0.6;
|
|
5194
|
-
uniform float pulse_speed : hint_range(0.0, 5.0) = 0.5;
|
|
5195
|
-
uniform vec3 shadow_tint : source_color = vec3(0.1, 0.0, 0.15);
|
|
5196
|
-
|
|
5197
|
-
void fragment() {
|
|
5198
|
-
float pulse = sin(TIME * pulse_speed) * 0.1 + 0.9;
|
|
5199
|
-
vec4 albedo = texture(albedo_texture, UV);
|
|
5200
|
-
vec3 darkened = mix(albedo.rgb, shadow_tint, darkness);
|
|
5201
|
-
|
|
5202
|
-
ALBEDO = darkened * pulse;
|
|
5203
|
-
ROUGHNESS = 0.9;
|
|
5204
|
-
METALLIC = 0.0;
|
|
5325
|
+
code: `shader_type spatial;
|
|
5326
|
+
render_mode blend_mix, depth_draw_opaque, cull_back;
|
|
5327
|
+
|
|
5328
|
+
uniform sampler2D albedo_texture : source_color, filter_linear_mipmap;
|
|
5329
|
+
uniform float darkness : hint_range(0.0, 1.0) = 0.6;
|
|
5330
|
+
uniform float pulse_speed : hint_range(0.0, 5.0) = 0.5;
|
|
5331
|
+
uniform vec3 shadow_tint : source_color = vec3(0.1, 0.0, 0.15);
|
|
5332
|
+
|
|
5333
|
+
void fragment() {
|
|
5334
|
+
float pulse = sin(TIME * pulse_speed) * 0.1 + 0.9;
|
|
5335
|
+
vec4 albedo = texture(albedo_texture, UV);
|
|
5336
|
+
vec3 darkened = mix(albedo.rgb, shadow_tint, darkness);
|
|
5337
|
+
|
|
5338
|
+
ALBEDO = darkened * pulse;
|
|
5339
|
+
ROUGHNESS = 0.9;
|
|
5340
|
+
METALLIC = 0.0;
|
|
5205
5341
|
}`,
|
|
5206
5342
|
},
|
|
5207
5343
|
cartoon: {
|
|
5208
5344
|
description: 'Cel-shaded toon look',
|
|
5209
|
-
code: `shader_type spatial;
|
|
5210
|
-
render_mode blend_mix, depth_draw_opaque, cull_back;
|
|
5211
|
-
|
|
5212
|
-
uniform sampler2D albedo_texture : source_color, filter_linear_mipmap;
|
|
5213
|
-
uniform vec3 outline_color : source_color = vec3(0.0, 0.0, 0.0);
|
|
5214
|
-
uniform float shade_levels : hint_range(2.0, 8.0) = 3.0;
|
|
5215
|
-
|
|
5216
|
-
void fragment() {
|
|
5217
|
-
vec4 albedo = texture(albedo_texture, UV);
|
|
5218
|
-
ALBEDO = albedo.rgb;
|
|
5219
|
-
ROUGHNESS = 1.0;
|
|
5220
|
-
SPECULAR = 0.0;
|
|
5221
|
-
}
|
|
5222
|
-
|
|
5223
|
-
void light() {
|
|
5224
|
-
float NdotL = dot(NORMAL, LIGHT);
|
|
5225
|
-
float stepped = floor(NdotL * shade_levels) / shade_levels;
|
|
5226
|
-
DIFFUSE_LIGHT += stepped * ATTENUATION * LIGHT_COLOR;
|
|
5345
|
+
code: `shader_type spatial;
|
|
5346
|
+
render_mode blend_mix, depth_draw_opaque, cull_back;
|
|
5347
|
+
|
|
5348
|
+
uniform sampler2D albedo_texture : source_color, filter_linear_mipmap;
|
|
5349
|
+
uniform vec3 outline_color : source_color = vec3(0.0, 0.0, 0.0);
|
|
5350
|
+
uniform float shade_levels : hint_range(2.0, 8.0) = 3.0;
|
|
5351
|
+
|
|
5352
|
+
void fragment() {
|
|
5353
|
+
vec4 albedo = texture(albedo_texture, UV);
|
|
5354
|
+
ALBEDO = albedo.rgb;
|
|
5355
|
+
ROUGHNESS = 1.0;
|
|
5356
|
+
SPECULAR = 0.0;
|
|
5357
|
+
}
|
|
5358
|
+
|
|
5359
|
+
void light() {
|
|
5360
|
+
float NdotL = dot(NORMAL, LIGHT);
|
|
5361
|
+
float stepped = floor(NdotL * shade_levels) / shade_levels;
|
|
5362
|
+
DIFFUSE_LIGHT += stepped * ATTENUATION * LIGHT_COLOR;
|
|
5227
5363
|
}`,
|
|
5228
5364
|
},
|
|
5229
5365
|
};
|
|
5230
5366
|
const effectTemplates = {
|
|
5231
|
-
glow: `
|
|
5232
|
-
uniform float glow_power : hint_range(0.0, 5.0) = 1.5;
|
|
5367
|
+
glow: `
|
|
5368
|
+
uniform float glow_power : hint_range(0.0, 5.0) = 1.5;
|
|
5233
5369
|
// Add to fragment(): EMISSION += ALBEDO * glow_power;`,
|
|
5234
|
-
hologram: `
|
|
5235
|
-
uniform float scan_speed : hint_range(0.0, 10.0) = 2.0;
|
|
5236
|
-
uniform float scan_lines : hint_range(10.0, 100.0) = 50.0;
|
|
5237
|
-
// Add to fragment():
|
|
5238
|
-
// float scan = sin(UV.y * scan_lines + TIME * scan_speed) * 0.5 + 0.5;
|
|
5370
|
+
hologram: `
|
|
5371
|
+
uniform float scan_speed : hint_range(0.0, 10.0) = 2.0;
|
|
5372
|
+
uniform float scan_lines : hint_range(10.0, 100.0) = 50.0;
|
|
5373
|
+
// Add to fragment():
|
|
5374
|
+
// float scan = sin(UV.y * scan_lines + TIME * scan_speed) * 0.5 + 0.5;
|
|
5239
5375
|
// ALPHA = 0.7 * scan;`,
|
|
5240
|
-
dissolve: `
|
|
5241
|
-
uniform sampler2D noise_texture : filter_linear;
|
|
5242
|
-
uniform float dissolve_amount : hint_range(0.0, 1.0) = 0.0;
|
|
5243
|
-
// Add to fragment():
|
|
5244
|
-
// float noise = texture(noise_texture, UV).r;
|
|
5376
|
+
dissolve: `
|
|
5377
|
+
uniform sampler2D noise_texture : filter_linear;
|
|
5378
|
+
uniform float dissolve_amount : hint_range(0.0, 1.0) = 0.0;
|
|
5379
|
+
// Add to fragment():
|
|
5380
|
+
// float noise = texture(noise_texture, UV).r;
|
|
5245
5381
|
// if (noise < dissolve_amount) discard;`,
|
|
5246
5382
|
};
|
|
5247
5383
|
const theme = args.theme;
|