mirror of
https://github.com/antos-rde/antosdk-apps.git
synced 2024-11-08 06:28:29 +01:00
update ShaderPlayground
This commit is contained in:
parent
916fd7b418
commit
52e24e9b2b
@ -4,4 +4,7 @@ Playground for working with Open GL shader language, the sharder is rendered
|
||||
with the Three.js library
|
||||
|
||||
## Change logs
|
||||
- v0.0.2-a:
|
||||
- Remove GLSLX, use the default WEBGL API for shader compiling
|
||||
- Allow save/open shader source code to/from file (JSON)
|
||||
- v0.0.1-a: Initial version
|
@ -47,8 +47,7 @@
|
||||
"scheme.html",
|
||||
"package.json",
|
||||
"README.md",
|
||||
"main.css",
|
||||
"glslx.js"
|
||||
"main.css"
|
||||
],
|
||||
"dest":"build/debug"
|
||||
}
|
||||
@ -79,6 +78,20 @@
|
||||
"build and run": {
|
||||
"depend": ["clean", "build", "copy", "run"],
|
||||
"jobs": []
|
||||
},
|
||||
"locale": {
|
||||
"require": ["locale"],
|
||||
"jobs": [
|
||||
{
|
||||
"name":"locale-gen",
|
||||
"data": {
|
||||
"src": "",
|
||||
"exclude": ["build/"],
|
||||
"locale": "en_GB",
|
||||
"dest": "package.json"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
@ -4,4 +4,7 @@ Playground for working with Open GL shader language, the sharder is rendered
|
||||
with the Three.js library
|
||||
|
||||
## Change logs
|
||||
- v0.0.2-a:
|
||||
- Remove GLSLX, use the default WEBGL API for shader compiling
|
||||
- Allow save/open shader source code to/from file (JSON)
|
||||
- v0.0.1-a: Initial version
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,16 +1,42 @@
|
||||
{
|
||||
"pkgname": "ShaderPlayground",
|
||||
"app":"ShaderPlayground",
|
||||
"name":"OpenGL Shader Playground",
|
||||
"description":"OpenGL Shader Playground",
|
||||
"info":{
|
||||
"app": "ShaderPlayground",
|
||||
"name": "OpenGL Shader Playground",
|
||||
"description": "OpenGL Shader Playground",
|
||||
"info": {
|
||||
"author": "Xuan Sang LE",
|
||||
"email": "mrsang@iohub.dev"
|
||||
},
|
||||
"version":"0.0.1-a",
|
||||
"category":"Development",
|
||||
"iconclass":"bi bi-lightbulb-fill",
|
||||
"mimes":["none"],
|
||||
"dependencies":["libthreejs@0.0.129-r"],
|
||||
"locale": {}
|
||||
"version": "0.0.2-a",
|
||||
"category": "Development",
|
||||
"iconclass": "bi bi-lightbulb-fill",
|
||||
"mimes": [
|
||||
"application/json"
|
||||
],
|
||||
"dependencies": [
|
||||
"libthreejs@0.0.129-r"
|
||||
],
|
||||
"locale": {},
|
||||
"locales": {
|
||||
"en_GB": {
|
||||
"Name": "Name",
|
||||
"Path/URL": "Path/URL",
|
||||
"Ok": "Ok",
|
||||
"Add texture": "Add texture",
|
||||
"File": "File",
|
||||
"New": "New",
|
||||
"Open": "Open",
|
||||
"Save": "Save",
|
||||
"All fields should be filled": "All fields should be filled",
|
||||
"Select image file": "Select image file",
|
||||
"Unknown save path": "Unknown save path",
|
||||
"Fragment": "Fragment",
|
||||
"Vertex": "Vertex",
|
||||
"Textures": "Textures",
|
||||
"Unsaved shader": "Unsaved shader",
|
||||
"Ignore unsaved file?": "Ignore unsaved file?",
|
||||
"Open file": "Open file",
|
||||
"Save as": "Save as"
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
File diff suppressed because one or more lines are too long
@ -39,7 +39,6 @@ namespace OS {
|
||||
export namespace application {
|
||||
declare var ace: any;
|
||||
declare var THREE: any;
|
||||
declare var GLSLX:any;
|
||||
|
||||
class AddTextureDialog extends GUI.BasicDialog
|
||||
{
|
||||
@ -106,20 +105,32 @@ namespace OS {
|
||||
|
||||
class ShaderEditor
|
||||
{
|
||||
static frg_template: string;
|
||||
private ums: any[];
|
||||
private glsl_values: string[];
|
||||
private cursors: any[];
|
||||
private editor: any;
|
||||
private current_idx: number;
|
||||
private editormux:boolean;
|
||||
private gl_compiling_ctx: WebGLRenderingContext;
|
||||
renderer: ShaderRenderer;
|
||||
private tmp_canvas: HTMLCanvasElement;
|
||||
private _filehandle: API.VFS.BaseFileHandle;
|
||||
private _onfilechange: (t: string) => void;
|
||||
private _ontextureadded: (data: GenericObject<any>) => void;
|
||||
constructor(domel: HTMLElement, renderer: ShaderRenderer)
|
||||
{
|
||||
const empty_main = "void main(){}";
|
||||
this.glsl_values = [ShaderEditor.frg_template, ""];
|
||||
this.renderer = renderer;
|
||||
this.ums = [new ace.UndoManager(), new ace.UndoManager()];
|
||||
this.current_idx = -1;
|
||||
this.glsl_values = [empty_main, ""];
|
||||
this.tmp_canvas = $("<canvas />")[0] as HTMLCanvasElement;
|
||||
this.gl_compiling_ctx = this.tmp_canvas.getContext("webgl");
|
||||
this._filehandle = undefined;
|
||||
this.editormux = false;
|
||||
ace.require("ace/ext/language_tools");
|
||||
this._onfilechange = (v) =>{};
|
||||
this._ontextureadded = (t) => {};
|
||||
this.editor = ace.edit(domel);
|
||||
this.editor.setOptions({
|
||||
enableBasicAutocompletion: true,
|
||||
@ -136,19 +147,23 @@ namespace OS {
|
||||
this.editor.getCursorPosition(),
|
||||
this.editor.getCursorPosition()
|
||||
];
|
||||
|
||||
this.editor.on("input", (e) => {
|
||||
const value = this.editor.getValue();
|
||||
|
||||
const result = GLSLX.compile(value, {
|
||||
format: "json"
|
||||
});
|
||||
if (result.output) {
|
||||
this.editor.getSession().setAnnotations([]);
|
||||
this.glsl_values[this.current_idx] = value;
|
||||
this.renderer.apply_mat(this.glsl_values[0],this.glsl_values[1]);
|
||||
} else {
|
||||
const reg_str = "<stdin>:([0-9]+):([0-9]+):\\s*error:\\s*(.*)\\n";
|
||||
const matches = (result.log as string).match(new RegExp(reg_str, "g"));
|
||||
const stype = this.current_idx == 0 ? this.gl_compiling_ctx.FRAGMENT_SHADER: this.gl_compiling_ctx.VERTEX_SHADER;
|
||||
const errors = this.compile(value, stype);
|
||||
if(this.filehandle.dirty === false && !this.editormux)
|
||||
{
|
||||
this.filehandle.dirty = true;
|
||||
this._onfilechange(`${this.filehandle.path}*`);
|
||||
}
|
||||
if(this.editormux)
|
||||
{
|
||||
this.editormux = false;
|
||||
}
|
||||
if(errors) {
|
||||
const reg_str = "ERROR:\\s*([0-9]+):([0-9]+):\\s*(.*)\\n";
|
||||
const matches = errors.match(new RegExp(reg_str, "g"));
|
||||
if(matches)
|
||||
{
|
||||
this.editor.getSession().setAnnotations(
|
||||
@ -158,8 +173,8 @@ namespace OS {
|
||||
if(err_data)
|
||||
{
|
||||
ret = {
|
||||
row: parseInt(err_data[1]) - 1,
|
||||
column: parseInt(err_data[2]),
|
||||
row: parseInt(err_data[2]) - 1,
|
||||
column: 0,
|
||||
text: err_data[3],
|
||||
type: "error"
|
||||
};
|
||||
@ -169,9 +184,131 @@ namespace OS {
|
||||
);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this.editor.getSession().setAnnotations([]);
|
||||
this.glsl_values[this.current_idx] = value;
|
||||
this.renderer.apply_mat(this.glsl_values[0], this.glsl_values[1]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public set onfilechange(fn:(v: string) => void)
|
||||
{
|
||||
this._onfilechange = fn;
|
||||
}
|
||||
|
||||
public set ontextureadded(fn:(v:GenericObject<any>)=> void)
|
||||
{
|
||||
this._ontextureadded = fn;
|
||||
}
|
||||
|
||||
public set filehandle(v: API.VFS.BaseFileHandle)
|
||||
{
|
||||
this._filehandle = v;
|
||||
this.read();
|
||||
}
|
||||
|
||||
public get filehandle(): API.VFS.BaseFileHandle
|
||||
{
|
||||
return this._filehandle;
|
||||
}
|
||||
|
||||
read() {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
if (this._filehandle === undefined) {
|
||||
this.renderer.textures.length = 0;
|
||||
this._filehandle = "Untitled".asFileHandle();
|
||||
this._onfilechange(this._filehandle.path);
|
||||
this.glsl_values = [ShaderEditor.frg_template, ""];
|
||||
if(this.current_idx != 2 && this.current_idx != -1)
|
||||
{
|
||||
this.editormux = true;
|
||||
this.editor.setValue(this.glsl_values[this.current_idx]);
|
||||
}
|
||||
this._ontextureadded(undefined);
|
||||
return resolve(undefined);
|
||||
}
|
||||
try
|
||||
{
|
||||
const data = await this._filehandle.read("json");
|
||||
this.glsl_values[0] = data.source[0];
|
||||
this.glsl_values[1] = data.source[1];
|
||||
if(this.current_idx != 2 && this.current_idx != -1)
|
||||
{
|
||||
this.editormux = true;
|
||||
this.editor.setValue(this.glsl_values[this.current_idx]);
|
||||
}
|
||||
this._ontextureadded(undefined);
|
||||
for(const v of data.textures)
|
||||
{
|
||||
this._ontextureadded(v);
|
||||
}
|
||||
this._onfilechange(this._filehandle.path);
|
||||
resolve(undefined);
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
write(p: string) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let path = p;
|
||||
const error = __("Unknown save path");
|
||||
if(!path)
|
||||
{
|
||||
if(this._filehandle === undefined)
|
||||
return reject(error);
|
||||
path = this._filehandle.path;
|
||||
}
|
||||
if(path === "Untitled")
|
||||
{
|
||||
return reject(error);
|
||||
}
|
||||
try{
|
||||
this._filehandle.setPath(path);
|
||||
const data = {} as GenericObject<any>;
|
||||
if(this.current_idx != 2)
|
||||
{
|
||||
this.glsl_values[this.current_idx] = this.editor.getValue();
|
||||
}
|
||||
data.source = this.glsl_values;
|
||||
data.textures = this.renderer.textures.map((v) => {
|
||||
return {
|
||||
name: v.name,
|
||||
path: v.path
|
||||
};
|
||||
});
|
||||
this.filehandle.cache = data;
|
||||
const ret = await this.filehandle.write("object");
|
||||
this._filehandle.dirty = false;
|
||||
this._onfilechange(`${this.filehandle.path}`);
|
||||
resolve(undefined);
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private compile(code: string, type: number)
|
||||
{
|
||||
// Compiles either a shader of type gl.VERTEX_SHADER or gl.FRAGMENT_SHADER
|
||||
let shader = this.gl_compiling_ctx.createShader( type );
|
||||
this.gl_compiling_ctx.shaderSource( shader, code );
|
||||
this.gl_compiling_ctx.compileShader( shader );
|
||||
let errors: string = undefined;
|
||||
if ( !this.gl_compiling_ctx.getShaderParameter(shader, this.gl_compiling_ctx.COMPILE_STATUS) ) {
|
||||
errors = this.gl_compiling_ctx.getShaderInfoLog( shader );
|
||||
}
|
||||
this.gl_compiling_ctx.deleteShader(shader);
|
||||
return errors;
|
||||
}
|
||||
|
||||
edit(index:number): void
|
||||
{
|
||||
if(index < 0)
|
||||
@ -185,7 +322,7 @@ namespace OS {
|
||||
this.glsl_values[1] = this.editor.getValue();
|
||||
this.cursors[1] = this.editor.getCursorPosition();
|
||||
}
|
||||
else
|
||||
else if(index === 1)
|
||||
{
|
||||
this.glsl_values[0] = this.editor.getValue();
|
||||
this.cursors[0] = this.editor.getCursorPosition();
|
||||
@ -199,6 +336,7 @@ namespace OS {
|
||||
return;
|
||||
}
|
||||
this.current_idx = index;
|
||||
this.editormux = true;
|
||||
this.editor.getSession().setUndoManager(new ace.UndoManager());
|
||||
this.editor.setValue(this.glsl_values[index]);
|
||||
this.editor.getSession().setUndoManager(this.ums[index]);
|
||||
@ -220,6 +358,7 @@ namespace OS {
|
||||
cleanup(): void
|
||||
{
|
||||
this.renderer.cleanup();
|
||||
$(this.tmp_canvas).remove();
|
||||
}
|
||||
resize(): void
|
||||
{
|
||||
@ -344,6 +483,19 @@ namespace OS {
|
||||
this.mesh.material = mat;
|
||||
}
|
||||
}
|
||||
|
||||
ShaderEditor.frg_template = `\
|
||||
#ifdef GL_ES
|
||||
precision mediump float;
|
||||
#endif
|
||||
// uniform vec2 u_resolution;
|
||||
// uniform vec2 u_mouse;
|
||||
uniform float u_time;
|
||||
|
||||
void main() {
|
||||
gl_FragColor = vec4(abs(sin(u_time)),0.0,0.0,1.0);
|
||||
}\
|
||||
`;
|
||||
/**
|
||||
*
|
||||
* @class ShaderPlayground
|
||||
@ -355,13 +507,21 @@ namespace OS {
|
||||
*/
|
||||
private tabbar: GUI.tag.TabBarTag;
|
||||
private editor: ShaderEditor;
|
||||
|
||||
constructor(args: AppArgumentsType[]) {
|
||||
super("ShaderPlayground", args);
|
||||
}
|
||||
main(): void {
|
||||
this.init_editor();
|
||||
this.init_textures_list();
|
||||
this.bindKey("ALT-N", () => {
|
||||
return this.newFile();
|
||||
});
|
||||
this.bindKey("ALT-O", () => {
|
||||
return this.openFile();
|
||||
});
|
||||
this.bindKey("CTRL-S", () => {
|
||||
return this.saveFile();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -394,9 +554,38 @@ namespace OS {
|
||||
this.on("resize", (e) =>{
|
||||
this.editor.resize();
|
||||
});
|
||||
this.editor.onfilechange = (v: string) => {
|
||||
(this.scheme as GUI.tag.WindowTag).apptitle = v;
|
||||
};
|
||||
this.editor.ontextureadded = (v: GenericObject<any>) =>
|
||||
{
|
||||
this.add_texture(v);
|
||||
}
|
||||
this.editor.filehandle = undefined;
|
||||
this.tabbar.selected = 0;
|
||||
}
|
||||
|
||||
private add_texture(data: GenericObject<any>): void
|
||||
{
|
||||
{
|
||||
const listview = this.find("texture-list") as GUI.tag.ListViewTag;
|
||||
if(!data)
|
||||
{
|
||||
this.editor.renderer.textures = [];
|
||||
listview.data = this.editor.renderer.textures;
|
||||
return;
|
||||
}
|
||||
const loader = new THREE.TextureLoader();
|
||||
const texture = loader.load(data.path.asFileHandle().getlink());
|
||||
texture.minFilter = THREE.NearestFilter;
|
||||
texture.magFilter = THREE.NearestFilter;
|
||||
texture.wrapS = THREE.RepeatWrapping;
|
||||
texture.wrapT = THREE.RepeatWrapping;
|
||||
data.texture = texture;
|
||||
listview.push(data);
|
||||
this.editor.renderer.needupdateTexture = true;
|
||||
}
|
||||
}
|
||||
private init_textures_list(): void
|
||||
{
|
||||
const listview = this.find("texture-list") as GUI.tag.ListViewTag;
|
||||
@ -404,24 +593,10 @@ namespace OS {
|
||||
{
|
||||
text: "__(Add texture)",
|
||||
iconclass: "bi bi-plus",
|
||||
onbtclick: (e) => {
|
||||
onbtclick: (_e: any) => {
|
||||
this
|
||||
.openDialog(new AddTextureDialog())
|
||||
.then((data) => {
|
||||
if(!data)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const loader = new THREE.TextureLoader();
|
||||
const texture = loader.load(data.path.asFileHandle().getlink());
|
||||
texture.minFilter = THREE.NearestFilter;
|
||||
texture.magFilter = THREE.NearestFilter;
|
||||
texture.wrapS = THREE.RepeatWrapping;
|
||||
texture.wrapT = THREE.RepeatWrapping;
|
||||
data.texture = texture;
|
||||
listview.push(data);
|
||||
this.editor.renderer.needupdateTexture = true;
|
||||
});
|
||||
.then((data) => this.add_texture(data));
|
||||
}
|
||||
}
|
||||
];
|
||||
@ -430,7 +605,7 @@ namespace OS {
|
||||
this.editor.renderer.needupdateTexture = true;
|
||||
return true;
|
||||
};
|
||||
listview.data = this.editor.renderer.textures;
|
||||
this.add_texture(undefined);
|
||||
}
|
||||
|
||||
private selectTab(): void
|
||||
@ -449,12 +624,124 @@ namespace OS {
|
||||
this.editor.edit(index);
|
||||
}
|
||||
|
||||
protected cleanup(_e: any): void
|
||||
menu() {
|
||||
return [
|
||||
{
|
||||
text: "__(File)",
|
||||
nodes: [
|
||||
{
|
||||
text: "__(New)",
|
||||
dataid: "new",
|
||||
shortcut: 'A-N'
|
||||
},
|
||||
{
|
||||
text: "__(Open)",
|
||||
dataid: "open",
|
||||
shortcut: 'A-O'
|
||||
},
|
||||
{
|
||||
text: "__(Save)",
|
||||
dataid: "save",
|
||||
shortcut: 'C-S'
|
||||
}
|
||||
],
|
||||
onchildselect: (e) => {
|
||||
switch (e.data.item.data.dataid) {
|
||||
case "new":
|
||||
return this.newFile();
|
||||
case "open":
|
||||
return this.openFile();
|
||||
case "save":
|
||||
return this.saveFile();
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
private ignore_unsaved(): Promise<boolean>
|
||||
{
|
||||
return new Promise( async (resolve, reject) =>{
|
||||
if(this.editor.filehandle.dirty === true)
|
||||
{
|
||||
const r = await this.ask({
|
||||
title: __("Unsaved shader"),
|
||||
text: __("Ignore unsaved file?")
|
||||
});
|
||||
if(!r)
|
||||
{
|
||||
return resolve(false);
|
||||
}
|
||||
return resolve(true);
|
||||
}
|
||||
return resolve(true);
|
||||
});
|
||||
}
|
||||
private async newFile()
|
||||
{
|
||||
const ignore = await this.ignore_unsaved();
|
||||
if(!ignore)
|
||||
return;
|
||||
this.editor.filehandle = undefined;
|
||||
}
|
||||
private async openFile() {
|
||||
try{
|
||||
const ignore = await this.ignore_unsaved();
|
||||
if(!ignore)
|
||||
return;
|
||||
const d = await this.openDialog("FileDialog",{
|
||||
title: __("Open file"),
|
||||
mimes: this.meta().mimes
|
||||
});
|
||||
|
||||
this.editor.filehandle.setPath(d.file.path);
|
||||
await this.editor.read();
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
this.error(__(e.toString()), e);
|
||||
}
|
||||
}
|
||||
|
||||
private async saveFile() {
|
||||
if (this.editor.filehandle.path !== "Untitled") {
|
||||
return this.editor.write(undefined);
|
||||
}
|
||||
const f = await this.openDialog("FileDialog",{
|
||||
title: __("Save as"),
|
||||
file: this.editor.filehandle
|
||||
});
|
||||
let handle = f.file.path.asFileHandle();
|
||||
if(f.file.type === "file") {
|
||||
handle = handle.parent();
|
||||
}
|
||||
try{
|
||||
await this.editor.write(`${handle.path}/${f.name}`);
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
this.error(__(e.toString()), e);
|
||||
}
|
||||
}
|
||||
|
||||
protected cleanup(e: any): void
|
||||
{
|
||||
if(this.editor.filehandle.dirty)
|
||||
{
|
||||
this.ignore_unsaved()
|
||||
.then((d) =>{
|
||||
if(d)
|
||||
{
|
||||
this.editor.filehandle.dirty = false;
|
||||
this.quit(true);
|
||||
}
|
||||
});
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
this.editor.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Application dependenicies preload
|
||||
*/
|
||||
|
@ -1,16 +1,42 @@
|
||||
{
|
||||
"pkgname": "ShaderPlayground",
|
||||
"app":"ShaderPlayground",
|
||||
"name":"OpenGL Shader Playground",
|
||||
"description":"OpenGL Shader Playground",
|
||||
"info":{
|
||||
"app": "ShaderPlayground",
|
||||
"name": "OpenGL Shader Playground",
|
||||
"description": "OpenGL Shader Playground",
|
||||
"info": {
|
||||
"author": "Xuan Sang LE",
|
||||
"email": "mrsang@iohub.dev"
|
||||
},
|
||||
"version":"0.0.1-a",
|
||||
"category":"Development",
|
||||
"iconclass":"bi bi-lightbulb-fill",
|
||||
"mimes":["none"],
|
||||
"dependencies":["libthreejs@0.0.129-r"],
|
||||
"locale": {}
|
||||
"version": "0.0.2-a",
|
||||
"category": "Development",
|
||||
"iconclass": "bi bi-lightbulb-fill",
|
||||
"mimes": [
|
||||
"application/json"
|
||||
],
|
||||
"dependencies": [
|
||||
"libthreejs@0.0.129-r"
|
||||
],
|
||||
"locale": {},
|
||||
"locales": {
|
||||
"en_GB": {
|
||||
"Name": "Name",
|
||||
"Path/URL": "Path/URL",
|
||||
"Ok": "Ok",
|
||||
"Add texture": "Add texture",
|
||||
"File": "File",
|
||||
"New": "New",
|
||||
"Open": "Open",
|
||||
"Save": "Save",
|
||||
"All fields should be filled": "All fields should be filled",
|
||||
"Select image file": "Select image file",
|
||||
"Unknown save path": "Unknown save path",
|
||||
"Fragment": "Fragment",
|
||||
"Vertex": "Vertex",
|
||||
"Textures": "Textures",
|
||||
"Unsaved shader": "Unsaved shader",
|
||||
"Ignore unsaved file?": "Ignore unsaved file?",
|
||||
"Open file": "Open file",
|
||||
"Save as": "Save as"
|
||||
}
|
||||
}
|
||||
}
|
@ -335,7 +335,7 @@
|
||||
"description": "https://raw.githubusercontent.com/lxsang/antosdk-apps/master/ShaderPlayground/README.md",
|
||||
"category": "Development",
|
||||
"author": "Xuan Sang LE",
|
||||
"version": "0.0.1-a",
|
||||
"version": "0.0.2-a",
|
||||
"dependencies": ["libthreejs@0.0.129-r"],
|
||||
"download": "https://raw.githubusercontent.com/lxsang/antosdk-apps/master/ShaderPlayground/build/release/ShaderPlayground.zip"
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user