base commit
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
__pycache__
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
_*/
|
||||
*.code-workspace
|
||||
.vscode
|
||||
config.json
|
||||
ignore.txt
|
||||
.env
|
||||
.venv
|
||||
.DS_Store
|
||||
*.egg-info
|
||||
*.bak
|
||||
checkpoints
|
||||
results
|
||||
backup
|
||||
node_modules
|
||||
*-lock.json
|
||||
*.config.mjs
|
||||
package.json
|
||||
_TODO*.*
|
||||
@@ -0,0 +1,23 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Alexander G. Morano
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
GO NUTS; JUST TRY NOT TO DO IT IN YOUR HEAD.
|
||||
@@ -0,0 +1,78 @@
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/Amorano/Jovimetrix-examples/blob/master/res/logo-jovimetrix.png">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://github.com/Amorano/Jovimetrix-examples/blob/master/res/logo-jovimetrix-light.png">
|
||||
<img alt="ComfyUI Nodes for creating GLSL shaders">
|
||||
</picture>
|
||||
|
||||
<h2><div align="center">
|
||||
<a href="https://github.com/comfyanonymous/ComfyUI">COMFYUI</a> Nodes for creating GLSL shaders
|
||||
</div></h2>
|
||||
|
||||
<h3><div align="center">
|
||||
JOVI_GLSL IS ONLY GUARANTEED TO SUPPORT <a href="https://github.com/comfyanonymous/ComfyUI">COMFYUI 0.3.7+</a> and <a href="https://github.com/Comfy-Org/ComfyUI_frontend">FRONTEND 1.6.2+</a><br>
|
||||
IF YOU NEED AN OLDER VERSION, PLEASE DO NOT UPDATE.
|
||||
</div></h3>
|
||||
|
||||
<h2><div align="center">
|
||||
|
||||

|
||||

|
||||
|
||||
</div></h2>
|
||||
|
||||
<!---------------------------------------------------------------------------->
|
||||
|
||||
# SPONSORSHIP
|
||||
|
||||
Please consider sponsoring me if you enjoy the results of my work, code or documentation or otherwise. A good way to keep code development open and free is through sponsorship.
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://github.com/sponsors/Amorano)
|
||||
|
||||
[](https://www.paypal.com/paypalme/onarom)
|
||||
|
||||
[](https://www.patreon.com/joviex)
|
||||
|
||||
[](https://ko-fi.com/alexandermorano)
|
||||
|
||||
</div>
|
||||
|
||||
## HIGHLIGHTS
|
||||
|
||||
* `GLSL Node` provides raw access to user written Vertex and Fragment shaders to test at runtime
|
||||
* `Dynamic GLSL` dynamically convert existing GLSL script files into ComfyUI nodes at load time
|
||||
* Supports vector types for 2, 3 and 4 sized tuples (integer or float)
|
||||
* Specific RGB/RGBA color vector support with access to the system/browser level color picker
|
||||
* All `Image` inputs support RGB, RGBA or pure MASK input
|
||||
* Over a dozen hand written GLSL nodes to speed up specific tasks better done on the GPU (10x speedup in most cases)
|
||||
|
||||
## UPDATES
|
||||
|
||||
**2024/12/19** @1.0.0:
|
||||
* initial release
|
||||
|
||||
# INSTALLATION
|
||||
|
||||
[Please see the wiki for advanced use of the environment variables that can be used at startup](https://github.com/Amorano/Jovi_GLSL/wiki)
|
||||
|
||||
## COMFYUI MANAGER
|
||||
|
||||
If you have [ComfyUI Manager](https://github.com/ltdrdata/ComfyUI-Manager) installed, simply search for Jovi_GLSL and install from the manager's database.
|
||||
|
||||
## MANUAL INSTALL
|
||||
Clone the repository into your ComfyUI custom_nodes directory. You can clone the repository with the command:
|
||||
```
|
||||
git clone https://github.com/Amorano/Jovi_GLSL.git
|
||||
```
|
||||
You can then install the requirements by using the command:
|
||||
```
|
||||
.\python_embed\python.exe -s -m pip install -r .\ComfyUI\custom_nodes\Jovi_GLSL\requirements.txt
|
||||
```
|
||||
If you are using a <code>virtual environment</code> (<code><i>venv</i></code>), make sure it is activated before installation. Then install the requirements with the command:
|
||||
```
|
||||
pip install -r .\ComfyUI\custom_nodes\Jovi_GLSL\requirements.txt
|
||||
```
|
||||
# WHERE TO FIND ME
|
||||
|
||||
You can find me on [](https://discord.gg/62TJaZ3Z5r).
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
"""
|
||||
██ ██████ ██ ██ ██ ███ ███ ███████ ████████ ██████ ██ ██ ██
|
||||
██ ██ ██ ██ ██ ██ ████ ████ ██ ██ ██ ██ ██ ██ ██
|
||||
██ ██ ██ ██ ██ ██ ██ ████ ██ █████ ██ ██████ ██ ███
|
||||
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
||||
█████ ██████ ████ ██ ██ ██ ███████ ██ ██ ██ ██ ██ ██
|
||||
|
||||
OPENGL Shaders for ComfyUI
|
||||
http://www.github.com/Amorano/Jovi_GLSL
|
||||
|
||||
@title: Jovi_GLSL
|
||||
@author: amorano
|
||||
@category: GLSL
|
||||
@reference: https://github.com/Amorano/Jovi_GLSL
|
||||
@tags: GLSL, HLSL, shaders
|
||||
@description: Integrates GLSL shader support.
|
||||
@node list:
|
||||
GLSLNode
|
||||
@version: 1.0.0
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import inspect
|
||||
import importlib
|
||||
from pathlib import Path
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from server import PromptServer
|
||||
|
||||
# ==============================================================================
|
||||
# === GLOBAL ===
|
||||
# ==============================================================================
|
||||
|
||||
NODE_CLASS_MAPPINGS = {}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {}
|
||||
WEB_DIRECTORY = "./web"
|
||||
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
ROOT_COMFY = ROOT.parent.parent
|
||||
ROOT_DOC = ROOT / 'res/doc'
|
||||
|
||||
JOV_WEB = ROOT / 'web'
|
||||
JOV_INTERNAL = os.getenv("JOV_INTERNAL", 'false').strip().lower() in ('true', '1', 't')
|
||||
JOV_LOG_LEVEL = os.getenv("JOV_LOG_LEVEL", "INFO")
|
||||
logger.configure(handlers=[{"sink": sys.stdout, "level": JOV_LOG_LEVEL}])
|
||||
|
||||
# ==============================================================================
|
||||
# === CORE NODES ===
|
||||
# ==============================================================================
|
||||
|
||||
class JOVBaseNode:
|
||||
NOT_IDEMPOTENT = True
|
||||
RETURN_TYPES = ("IMAGE", "IMAGE", "MASK")
|
||||
RETURN_NAMES = ('RGBA', 'RGB', 'MASK')
|
||||
FUNCTION = "run"
|
||||
# instance map for caching
|
||||
INSTANCE = {}
|
||||
|
||||
@classmethod
|
||||
def VALIDATE_INPUTS(cls, *arg, **kw) -> bool:
|
||||
# logger.debug(f'validate -- {arg} {kw}')
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls, prompt:bool=False, extra_png:bool=False, dynprompt:bool=False) -> dict:
|
||||
data = {
|
||||
"required": {},
|
||||
"outputs": {
|
||||
0: ("IMAGE", {"tooltips":"Full channel [RGBA] image. If there is an alpha, the image will be masked out with it when using this output."}),
|
||||
1: ("IMAGE", {"tooltips":"Three channel [RGB] image. There will be no alpha."}),
|
||||
2: ("MASK", {"tooltips":"Single channel mask output."}),
|
||||
},
|
||||
"hidden": {
|
||||
"ident": "UNIQUE_ID"
|
||||
}
|
||||
}
|
||||
if prompt:
|
||||
data["hidden"]["prompt"] = "PROMPT"
|
||||
if extra_png:
|
||||
data["hidden"]["extra_pnginfo"] = "EXTRA_PNGINFO"
|
||||
|
||||
if dynprompt:
|
||||
data["hidden"]["dynprompt"] = "DYNPROMPT"
|
||||
return data
|
||||
|
||||
class AnyType(str):
|
||||
"""AnyType input wildcard trick taken from pythongossss's:
|
||||
|
||||
https://github.com/pythongosssss/ComfyUI-Custom-Scripts
|
||||
"""
|
||||
def __ne__(self, __value: object) -> bool:
|
||||
return False
|
||||
|
||||
JOV_TYPE_ANY = AnyType("*")
|
||||
|
||||
# want to make explicit entries; comfy only looks for single type
|
||||
JOV_TYPE_COMFY = "BOOLEAN|FLOAT|INT"
|
||||
JOV_TYPE_VECTOR = "VEC2|VEC3|VEC4|VEC2INT|VEC3INT|VEC4INT|COORD2D"
|
||||
JOV_TYPE_NUMBER = f"{JOV_TYPE_COMFY}|{JOV_TYPE_VECTOR}"
|
||||
JOV_TYPE_IMAGE = "IMAGE|MASK"
|
||||
JOV_TYPE_FULL = f"{JOV_TYPE_NUMBER}|{JOV_TYPE_IMAGE}"
|
||||
|
||||
JOV_TYPE_COMFY = JOV_TYPE_ANY
|
||||
JOV_TYPE_VECTOR = JOV_TYPE_ANY
|
||||
JOV_TYPE_NUMBER = JOV_TYPE_ANY
|
||||
JOV_TYPE_IMAGE = JOV_TYPE_ANY
|
||||
JOV_TYPE_FULL = JOV_TYPE_ANY
|
||||
|
||||
GLSL_INTERNAL = '🧙🏽'
|
||||
GLSL_CUSTOM = '🧙🏽♀️'
|
||||
|
||||
# ==============================================================================
|
||||
# === NODE LOADER ===
|
||||
# ==============================================================================
|
||||
|
||||
def comfy_message(ident:str, route:str, data:dict) -> None:
|
||||
data['id'] = ident
|
||||
PromptServer.instance.send_sync(route, data)
|
||||
|
||||
# ==============================================================================
|
||||
# === NODE LOADER ===
|
||||
# ==============================================================================
|
||||
|
||||
def loader():
|
||||
node_count = 0
|
||||
CLASS_MAPPINGS = {}
|
||||
CLASS_MAPPINGS_WIP = {}
|
||||
NODE_LIST_MAP = {}
|
||||
for fname in ROOT.glob('core/**/*.py'):
|
||||
if fname.stem.startswith('_'):
|
||||
continue
|
||||
|
||||
try:
|
||||
route = str(fname).replace("\\", "/").split("Jovi_GLSL/core/")[1]
|
||||
route = route.split('.')[0].replace('/', '.')
|
||||
module = f"Jovi_GLSL.core.{route}"
|
||||
module = importlib.import_module(module)
|
||||
except Exception as e:
|
||||
logger.warning(f"module failed {fname}")
|
||||
logger.warning(str(e))
|
||||
continue
|
||||
|
||||
# check if there is a dynamic register function....
|
||||
try:
|
||||
for class_name, class_def in module.import_dynamic():
|
||||
setattr(module, class_name, class_def)
|
||||
logger.debug(f"shader: {class_name}")
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
classes = inspect.getmembers(module, inspect.isclass)
|
||||
for class_name, class_object in classes:
|
||||
# assume both attrs are good enough....
|
||||
if not class_name.endswith('BaseNode') and hasattr(class_object, 'NAME') and hasattr(class_object, 'CATEGORY'):
|
||||
name = class_object.NAME
|
||||
CLASS_MAPPINGS[name] = class_object
|
||||
if not name.endswith(GLSL_CUSTOM):
|
||||
desc = class_object.DESCRIPTION if hasattr(class_object, 'DESCRIPTION') else name
|
||||
NODE_LIST_MAP[name] = desc.split('.')[0].strip('\n')
|
||||
else:
|
||||
logger.debug(f"customs {name}")
|
||||
node_count += 1
|
||||
|
||||
logger.info(f"✅ {module.__name__}")
|
||||
logger.info(f"{node_count} nodes loaded")
|
||||
|
||||
global NODE_DISPLAY_NAME_MAPPINGS, NODE_CLASS_MAPPINGS
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {k: v.NAME_PRETTY if hasattr(v, 'NAME_PRETTY') else k for k, v in CLASS_MAPPINGS.items()}
|
||||
CLASS_MAPPINGS.update({k: v for k, v in CLASS_MAPPINGS_WIP.items()})
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update({k: k for k in CLASS_MAPPINGS_WIP.keys()})
|
||||
CLASS_MAPPINGS = {x[0] : x[1] for x in sorted(CLASS_MAPPINGS.items(),
|
||||
key=lambda item: getattr(item[1], 'SORT', 0))}
|
||||
|
||||
for k, v in CLASS_MAPPINGS.items():
|
||||
NODE_CLASS_MAPPINGS[k] = v
|
||||
|
||||
# only do the list on local runs...
|
||||
if JOV_INTERNAL:
|
||||
with open(str(ROOT) + "/node_list.json", "w", encoding="utf-8") as f:
|
||||
json.dump(NODE_LIST_MAP, f, sort_keys=True, indent=4 )
|
||||
|
||||
loader()
|
||||
@@ -0,0 +1,529 @@
|
||||
"""
|
||||
http://www.github.com/Amorano/Jovi_GLSL
|
||||
"""
|
||||
|
||||
import json
|
||||
from enum import Enum
|
||||
from typing import Any, List, Tuple, Union, Optional
|
||||
|
||||
import cv2
|
||||
import torch
|
||||
import numpy as np
|
||||
from loguru import logger
|
||||
|
||||
# ==============================================================================
|
||||
# === GLOBAL ===
|
||||
# ==============================================================================
|
||||
|
||||
IMAGE_SIZE_DEFAULT: int = 512
|
||||
IMAGE_SIZE_MIN: int = 64
|
||||
IMAGE_SIZE_MAX: int = 16384
|
||||
|
||||
# ==============================================================================
|
||||
# === TYPE ===
|
||||
# ==============================================================================
|
||||
|
||||
TYPE_fCOORD2D = Tuple[float, float]
|
||||
|
||||
TYPE_iRGB = Tuple[int, int, int]
|
||||
TYPE_iRGBA = Tuple[int, int, int, int]
|
||||
TYPE_fRGB = Tuple[float, float, float]
|
||||
TYPE_fRGBA = Tuple[float, float, float, float]
|
||||
|
||||
TYPE_PIXEL = Union[int, float, TYPE_iRGB, TYPE_iRGBA, TYPE_fRGB, TYPE_fRGBA]
|
||||
TYPE_IMAGE = Union[np.ndarray, torch.Tensor]
|
||||
|
||||
# ==============================================================================
|
||||
# === ENUMERATION ===
|
||||
# ==============================================================================
|
||||
|
||||
class EnumConvertType(Enum):
|
||||
BOOLEAN = 1
|
||||
FLOAT = 10
|
||||
INT = 12
|
||||
VEC2 = 20
|
||||
VEC2INT = 25
|
||||
VEC3 = 30
|
||||
VEC3INT = 35
|
||||
VEC4 = 40
|
||||
VEC4INT = 45
|
||||
COORD2D = 22
|
||||
STRING = 0
|
||||
LIST = 2
|
||||
DICT = 3
|
||||
IMAGE = 4
|
||||
LATENT = 5
|
||||
# ENUM = 6
|
||||
ANY = 9
|
||||
MASK = 7
|
||||
# MIXLAB LAYER
|
||||
LAYER = 8
|
||||
|
||||
class EnumInterpolation(Enum):
|
||||
NEAREST = cv2.INTER_NEAREST
|
||||
LINEAR = cv2.INTER_LINEAR
|
||||
CUBIC = cv2.INTER_CUBIC
|
||||
AREA = cv2.INTER_AREA
|
||||
LANCZOS4 = cv2.INTER_LANCZOS4
|
||||
LINEAR_EXACT = cv2.INTER_LINEAR_EXACT
|
||||
NEAREST_EXACT = cv2.INTER_NEAREST_EXACT
|
||||
# INTER_MAX = cv2.INTER_MAX
|
||||
# WARP_FILL_OUTLIERS = cv2.WARP_FILL_OUTLIERS
|
||||
# WARP_INVERSE_MAP = cv2.WARP_INVERSE_MAP
|
||||
|
||||
class EnumScaleMode(Enum):
|
||||
# NONE = 0
|
||||
MATTE = 0
|
||||
CROP = 20
|
||||
FIT = 10
|
||||
ASPECT = 30
|
||||
ASPECT_SHORT = 35
|
||||
RESIZE_MATTE = 40
|
||||
|
||||
# ==============================================================================
|
||||
# === CORE SUPPORT ===
|
||||
# ==============================================================================
|
||||
|
||||
def load_file(fname: str) -> str | None:
|
||||
try:
|
||||
with open(fname, 'r', encoding='utf-8') as f:
|
||||
return f.read()
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
def parse_value(val:Any, typ:EnumConvertType, default: Any,
|
||||
clip_min: Optional[float]=None, clip_max: Optional[float]=None,
|
||||
zero:int=0) -> List[Any]:
|
||||
"""Convert target value into the new specified type."""
|
||||
|
||||
if typ == EnumConvertType.ANY:
|
||||
return val
|
||||
|
||||
if isinstance(default, torch.Tensor) and typ not in [EnumConvertType.IMAGE,
|
||||
EnumConvertType.MASK,
|
||||
EnumConvertType.LATENT]:
|
||||
h, w = default.shape[:2]
|
||||
cc = default.shape[2] if len(default.shape) > 2 else 1
|
||||
default = (w, h, cc)
|
||||
|
||||
if val is None:
|
||||
if default is None:
|
||||
return None
|
||||
val = default
|
||||
|
||||
if isinstance(val, dict):
|
||||
# old index?
|
||||
if '0' in val or 0 in val:
|
||||
val = [val.get(i, val.get(str(i), 0)) for i in range(min(len(val), 4))]
|
||||
# coord2d?
|
||||
elif 'x' in val:
|
||||
val = [val.get(c, 0) for c in 'xyzw']
|
||||
# wacky color struct?
|
||||
elif 'r' in val:
|
||||
val = [val.get(c, 0) for c in 'rgba']
|
||||
elif isinstance(val, torch.Tensor) and typ not in [EnumConvertType.IMAGE,
|
||||
EnumConvertType.MASK,
|
||||
EnumConvertType.LATENT]:
|
||||
h, w = val.shape[:2]
|
||||
cc = val.shape[2] if len(val.shape) > 2 else 1
|
||||
val = (w, h, cc)
|
||||
|
||||
new_val = val
|
||||
if typ in [EnumConvertType.FLOAT, EnumConvertType.INT,
|
||||
EnumConvertType.VEC2, EnumConvertType.VEC2INT,
|
||||
EnumConvertType.VEC3, EnumConvertType.VEC3INT,
|
||||
EnumConvertType.VEC4, EnumConvertType.VEC4INT,
|
||||
EnumConvertType.COORD2D]:
|
||||
|
||||
if not isinstance(val, (list, tuple, torch.Tensor)):
|
||||
val = [val]
|
||||
|
||||
size = max(1, int(typ.value / 10))
|
||||
new_val = []
|
||||
for idx in range(size):
|
||||
try:
|
||||
d = default[idx] if idx < len(default) else 0
|
||||
except:
|
||||
try:
|
||||
d = default.get(str(idx), 0)
|
||||
except:
|
||||
d = default
|
||||
|
||||
v = d if val is None else val[idx] if idx < len(val) else d
|
||||
if isinstance(v, (str, )):
|
||||
v = v.strip('\n').strip()
|
||||
if v == '':
|
||||
v = 0
|
||||
|
||||
try:
|
||||
if typ in [EnumConvertType.FLOAT, EnumConvertType.VEC2, EnumConvertType.VEC3, EnumConvertType.VEC4]:
|
||||
v = round(float(v or 0), 16)
|
||||
else:
|
||||
v = int(v)
|
||||
if clip_min is not None:
|
||||
v = max(v, clip_min)
|
||||
if clip_max is not None:
|
||||
v = min(v, clip_max)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
logger.error(f"Error converting value: {val} -- {v}")
|
||||
v = 0
|
||||
|
||||
if v == 0:
|
||||
v = zero
|
||||
new_val.append(v)
|
||||
new_val = new_val[0] if size == 1 else tuple(new_val)
|
||||
elif typ == EnumConvertType.DICT:
|
||||
try:
|
||||
if isinstance(new_val, (str,)):
|
||||
try:
|
||||
new_val = json.loads(new_val)
|
||||
except json.decoder.JSONDecodeError:
|
||||
new_val = {}
|
||||
else:
|
||||
if not isinstance(new_val, (list, tuple,)):
|
||||
new_val = [new_val]
|
||||
new_val = {i: v for i, v in enumerate(new_val)}
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
elif typ == EnumConvertType.LIST:
|
||||
new_val = list(new_val)
|
||||
elif typ == EnumConvertType.STRING:
|
||||
if isinstance(new_val, (str, list, int, float,)):
|
||||
new_val = [new_val]
|
||||
new_val = ", ".join(map(str, new_val)) if not isinstance(new_val, str) else new_val
|
||||
elif typ == EnumConvertType.BOOLEAN:
|
||||
if isinstance(new_val, (torch.Tensor,)):
|
||||
new_val = True
|
||||
elif isinstance(new_val, (dict,)):
|
||||
new_val = len(new_val.keys()) > 0
|
||||
elif isinstance(new_val, (list, tuple,)) and len(new_val) > 0 and (nv := new_val[0]) is not None:
|
||||
if isinstance(nv, (bool, str,)):
|
||||
new_val = bool(nv)
|
||||
elif isinstance(nv, (int, float,)):
|
||||
new_val = nv > 0
|
||||
elif typ == EnumConvertType.LATENT:
|
||||
# covert image into latent
|
||||
if isinstance(new_val, (torch.Tensor,)):
|
||||
new_val = {'samples': new_val.unsqueeze(0)}
|
||||
else:
|
||||
# convert whatever into a latent sample...
|
||||
new_val = torch.empty((4, 64, 64), dtype=torch.uint8).unsqueeze(0)
|
||||
new_val = {'samples': new_val}
|
||||
elif typ == EnumConvertType.IMAGE:
|
||||
# covert image into image? just skip if already an image
|
||||
if not isinstance(new_val, (torch.Tensor,)):
|
||||
color = parse_value(new_val, EnumConvertType.VEC4INT, (0,0,0,255), 0, 255)
|
||||
color = torch.tensor(color, dtype=torch.int32).tolist()
|
||||
new_val = torch.empty((IMAGE_SIZE_MIN, IMAGE_SIZE_MIN, 4), dtype=torch.uint8)
|
||||
new_val[0,:,:] = color[0]
|
||||
new_val[1,:,:] = color[1]
|
||||
new_val[2,:,:] = color[2]
|
||||
new_val[3,:,:] = color[3]
|
||||
elif typ == EnumConvertType.MASK:
|
||||
# @TODO: FIX FOR MULTI-CHAN?
|
||||
if not isinstance(new_val, (torch.Tensor,)):
|
||||
color = parse_value(new_val, EnumConvertType.INT, 0, 0, 255)
|
||||
color = torch.tensor(color, dtype=torch.int32).tolist()
|
||||
new_val = torch.empty((IMAGE_SIZE_MIN, IMAGE_SIZE_MIN, 1), dtype=torch.uint8)
|
||||
new_val[0,:,:] = color
|
||||
|
||||
elif issubclass(typ, Enum):
|
||||
new_val = typ[val]
|
||||
|
||||
if typ == EnumConvertType.COORD2D:
|
||||
new_val = {'x': new_val[0], 'y': new_val[1]}
|
||||
return new_val
|
||||
|
||||
def parse_param(data:dict, key:str, typ:EnumConvertType, default: Any,
|
||||
clip_min: Optional[float]=None, clip_max: Optional[float]=None,
|
||||
zero:int=0) -> List[Any]:
|
||||
"""Convenience because of the dictionary parameters.
|
||||
Convert list of values into a list of specified type.
|
||||
"""
|
||||
val = data.get(key, default)
|
||||
if typ == EnumConvertType.ANY:
|
||||
if val is None:
|
||||
val = [default]
|
||||
return val
|
||||
elif isinstance(val, (list,)):
|
||||
val = val[0]
|
||||
|
||||
if isinstance(val, (str,)):
|
||||
try: val = json.loads(val.replace("'", '"'))
|
||||
except json.JSONDecodeError: pass
|
||||
# see if we are a hacked vector blob... {0:x, 1:y, 2:z, 3:w}
|
||||
elif isinstance(val, dict):
|
||||
# mixlab layer?
|
||||
if (image := val.get('image', None)) is not None:
|
||||
ret = image
|
||||
if (mask := val.get('mask', None)) is not None:
|
||||
while len(mask.shape) < len(image.shape):
|
||||
mask = mask.unsqueeze(-1)
|
||||
ret = torch.cat((image, mask), dim=-1)
|
||||
if ret.ndim > 3:
|
||||
val = [t for t in ret]
|
||||
elif ret.ndim == 3:
|
||||
val = [v.unsqueeze(-1) for v in ret]
|
||||
# vector patch....
|
||||
elif 'xyzw' in val:
|
||||
val = tuple(x for x in val["xyzw"])
|
||||
# latents....
|
||||
elif 'samples' in val:
|
||||
val = tuple(x for x in val["samples"])
|
||||
elif ('0' in val) or (0 in val):
|
||||
val = tuple(val.get(i, val.get(str(i), 0)) for i in range(min(len(val), 4)))
|
||||
elif 'x' in val and 'y' in val:
|
||||
val = tuple(val.get(c, 0) for c in 'xyzw')
|
||||
elif 'r' in val and 'g' in val:
|
||||
val = tuple(val.get(c, 0) for c in 'rgba')
|
||||
elif len(val) == 0:
|
||||
val = tuple()
|
||||
elif isinstance(val, (torch.Tensor,)):
|
||||
# a batch of RGB(A)
|
||||
if val.ndim > 3:
|
||||
val = [t for t in val]
|
||||
# a batch of Grayscale
|
||||
else:
|
||||
val = [t.unsqueeze(-1) for t in val]
|
||||
elif isinstance(val, (list, tuple, set)):
|
||||
if isinstance(val, (tuple, set,)):
|
||||
val = list(val)
|
||||
elif issubclass(type(val), (Enum,)):
|
||||
val = [str(val.name)]
|
||||
|
||||
if not isinstance(val, (list,)):
|
||||
val = [val]
|
||||
return [parse_value(v, typ, default, clip_min, clip_max, zero) for v in val]
|
||||
|
||||
# ==============================================================================
|
||||
# === CONVERSION ===
|
||||
# ==============================================================================
|
||||
|
||||
def cv2tensor_full(image: TYPE_IMAGE, matte:TYPE_PIXEL=(0,0,0,255)) \
|
||||
-> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
|
||||
rgba = image_convert(image, 4)
|
||||
rgb = image_matte(rgba, matte)[...,:3]
|
||||
mask = image_mask(image)
|
||||
rgba = torch.from_numpy(rgba.astype(np.float32) / 255.0)
|
||||
rgb = torch.from_numpy(rgb.astype(np.float32) / 255.0)
|
||||
mask = torch.from_numpy(mask.astype(np.float32) / 255.0)
|
||||
return rgba, rgb, mask
|
||||
|
||||
def tensor2cv(tensor: torch.Tensor, invert_mask:bool=True) -> TYPE_IMAGE:
|
||||
"""Convert a torch Tensor to a numpy ndarray."""
|
||||
if tensor.ndim > 3:
|
||||
raise Exception("Tensor is batch of tensors")
|
||||
|
||||
if tensor.ndim < 3:
|
||||
tensor = tensor.unsqueeze(-1)
|
||||
|
||||
if tensor.shape[2] == 1 and invert_mask:
|
||||
tensor = 1. - tensor
|
||||
|
||||
tensor = tensor.cpu().numpy()
|
||||
return np.clip(255.0 * tensor, 0, 255).astype(np.uint8)
|
||||
|
||||
# ==============================================================================
|
||||
# === IMAGE ===
|
||||
# ==============================================================================
|
||||
|
||||
def image_convert(image: TYPE_IMAGE, channels: int, width: int=None, height: int=None,
|
||||
matte: Tuple[int, ...]=(0, 0, 0, 255)) -> TYPE_IMAGE:
|
||||
"""Force image format to a specific number of channels.
|
||||
Args:
|
||||
image (TYPE_IMAGE): Input image.
|
||||
channels (int): Desired number of channels (1, 3, or 4).
|
||||
width (int): Desired width. `None` means leave unchanged.
|
||||
height (int): Desired height. `None` means leave unchanged.
|
||||
matte (tuple): RGBA color to use as background color for transparent areas.
|
||||
Returns:
|
||||
TYPE_IMAGE: Image with the specified number of channels.
|
||||
"""
|
||||
if image.ndim == 2:
|
||||
image = np.expand_dims(image, axis=-1)
|
||||
|
||||
if (cc := image.shape[2]) != channels:
|
||||
if cc == 1 and channels == 3:
|
||||
image = np.repeat(image, 3, axis=2)
|
||||
elif cc == 1 and channels == 4:
|
||||
rgb = np.repeat(image, 3, axis=2)
|
||||
alpha = np.full(image.shape[:2] + (1,), matte[3], dtype=image.dtype)
|
||||
image = np.concatenate([rgb, alpha], axis=2)
|
||||
elif cc == 3 and channels == 1:
|
||||
image = np.mean(image, axis=2, keepdims=True).astype(image.dtype)
|
||||
elif cc == 3 and channels == 4:
|
||||
alpha = np.full(image.shape[:2] + (1,), matte[3], dtype=image.dtype)
|
||||
image = np.concatenate([image, alpha], axis=2)
|
||||
elif cc == 4 and channels == 1:
|
||||
rgb = image[..., :3]
|
||||
alpha = image[..., 3:4] / 255.0
|
||||
image = (np.mean(rgb, axis=2, keepdims=True) * alpha).astype(image.dtype)
|
||||
elif cc == 4 and channels == 3:
|
||||
image = image[..., :3]
|
||||
|
||||
# Resize if width or height is specified
|
||||
h, w = image.shape[:2]
|
||||
new_width = width if width is not None else w
|
||||
new_height = height if height is not None else h
|
||||
if (new_width, new_height) != (w, h):
|
||||
# Create a new image with the matte color
|
||||
new_image = np.full((new_height, new_width, channels), matte[:channels], dtype=image.dtype)
|
||||
paste_x = (new_width - w) // 2
|
||||
paste_y = (new_height - h) // 2
|
||||
new_image[paste_y:paste_y+h, paste_x:paste_x+w] = image[:h, :w]
|
||||
image = new_image
|
||||
|
||||
return image
|
||||
|
||||
def image_crop(image: TYPE_IMAGE, width:int=None, height:int=None, offset:Tuple[float, float]=(0, 0)) -> TYPE_IMAGE:
|
||||
h, w = image.shape[:2]
|
||||
width = width if width is not None else w
|
||||
height = height if height is not None else h
|
||||
x, y = offset
|
||||
x = max(0, min(width, x))
|
||||
y = max(0, min(width, y))
|
||||
x2 = max(0, min(width, x + width))
|
||||
y2 = max(0, min(height, y + height))
|
||||
points = [(x, y), (x2, y), (x2, y2), (x, y2)]
|
||||
return image_crop_polygonal(image, points)
|
||||
|
||||
def image_crop_center(image: TYPE_IMAGE, width:int=None, height:int=None) -> TYPE_IMAGE:
|
||||
"""Helper crop function to find the "center" of the area of interest."""
|
||||
h, w = image.shape[:2]
|
||||
cx = w // 2
|
||||
cy = h // 2
|
||||
width = w if width is None else width
|
||||
height = h if height is None else height
|
||||
x1 = max(0, int(cx - width // 2))
|
||||
y1 = max(0, int(cy - height // 2))
|
||||
x2 = min(w, int(cx + width // 2)) - 1
|
||||
y2 = min(h, int(cy + height // 2)) - 1
|
||||
points = [(x1, y1), (x2, y1), (x2, y2), (x1, y2)]
|
||||
return image_crop_polygonal(image, points)
|
||||
|
||||
def image_crop_polygonal(image: TYPE_IMAGE, points: List[TYPE_fCOORD2D]) -> TYPE_IMAGE:
|
||||
cc = image.shape[2] if image.ndim == 3 else 1
|
||||
height, width = image.shape[:2]
|
||||
point_mask = np.zeros((height, width), dtype=np.uint8)
|
||||
points = np.array(points, np.int32).reshape((-1, 1, 2))
|
||||
point_mask = cv2.fillPoly(point_mask, [points], 255)
|
||||
x, y, w, h = cv2.boundingRect(point_mask)
|
||||
cropped_image = cv2.resize(image[y:y+h, x:x+w], (w, h)).astype(np.uint8)
|
||||
# Apply the mask to the cropped image
|
||||
point_mask_cropped = cv2.resize(point_mask[y:y+h, x:x+w], (w, h))
|
||||
if cc == 4:
|
||||
mask = image_mask(image, 0)
|
||||
alpha_channel = cv2.resize(mask[y:y+h, x:x+w], (w, h))
|
||||
cropped_image = cv2.cvtColor(cropped_image, cv2.COLOR_BGRA2BGR)
|
||||
cropped_image = cv2.bitwise_and(cropped_image, cropped_image, mask=point_mask_cropped)
|
||||
return image_mask_add(cropped_image, alpha_channel)
|
||||
elif cc == 1:
|
||||
cropped_image = cv2.cvtColor(cropped_image, cv2.COLOR_GRAY2BGR)
|
||||
cropped_image = cv2.bitwise_and(cropped_image, cropped_image, mask=point_mask_cropped)
|
||||
return image_convert(cropped_image, cc)
|
||||
return cv2.bitwise_and(cropped_image, cropped_image, mask=point_mask_cropped)
|
||||
|
||||
def image_mask(image: TYPE_IMAGE, color: TYPE_PIXEL = 255) -> TYPE_IMAGE:
|
||||
"""Create a mask from the image, preserving transparency.
|
||||
|
||||
Args:
|
||||
image (TYPE_IMAGE): Input image, assumed to be 2D or 3D (with or without alpha channel).
|
||||
color (TYPE_PIXEL): Value to fill the mask (default is 255).
|
||||
|
||||
Returns:
|
||||
TYPE_IMAGE: Mask of the image, either the alpha channel or a full mask of the given color.
|
||||
"""
|
||||
if image.ndim == 3 and image.shape[2] == 4:
|
||||
return image[..., 3]
|
||||
|
||||
h, w = image.shape[:2]
|
||||
return np.ones((h, w), dtype=np.uint8) * color
|
||||
|
||||
def image_mask_add(image:TYPE_IMAGE, mask:TYPE_IMAGE=None, alpha:float=255) -> TYPE_IMAGE:
|
||||
"""Put custom mask into an image. If there is no mask, alpha is applied.
|
||||
Images are expanded to 4 channels.
|
||||
Existing 4 channel images with no mask input just return themselves.
|
||||
"""
|
||||
image = image_convert(image, 4)
|
||||
mask = image_mask(image, alpha) if mask is None else image_convert(mask, 1)
|
||||
image[..., 3] = mask if mask.ndim == 2 else mask[:, :, 0]
|
||||
return image
|
||||
|
||||
def image_matte(image: TYPE_IMAGE, color: TYPE_iRGBA=(0,0,0,255), width: int=None, height: int=None) -> TYPE_IMAGE:
|
||||
"""
|
||||
Puts an RGBA image atop a colored matte expanding or clipping the image if requested.
|
||||
|
||||
Args:
|
||||
image (TYPE_IMAGE): The input RGBA image.
|
||||
color (TYPE_iRGBA): The color of the matte as a tuple (R, G, B, A).
|
||||
width (int, optional): The width of the matte. Defaults to the image width.
|
||||
height (int, optional): The height of the matte. Defaults to the image height.
|
||||
|
||||
Returns:
|
||||
TYPE_IMAGE: Composited RGBA image on a matte with original alpha channel.
|
||||
"""
|
||||
|
||||
#if image.ndim != 4 or image.shape[2] != 4:
|
||||
# return image
|
||||
|
||||
# Determine the dimensions of the image and the matte
|
||||
image_height, image_width = image.shape[:2]
|
||||
width = width or image_width
|
||||
height = height or image_height
|
||||
|
||||
# Create a solid matte with the specified color
|
||||
matte = np.full((height, width, 4), color, dtype=np.uint8)
|
||||
|
||||
# Extract the alpha channel from the image
|
||||
alpha = None
|
||||
if image.ndim == 3 and image.shape[2] == 4:
|
||||
alpha = image[:, :, 3] / 255.0
|
||||
|
||||
# Calculate the center position for the image on the matte
|
||||
x_offset = (width - image_width) // 2
|
||||
y_offset = (height - image_height) // 2
|
||||
|
||||
if alpha is not None:
|
||||
# Place the image onto the matte using the alpha channel for blending
|
||||
for c in range(0, 3):
|
||||
matte[y_offset:y_offset + image_height, x_offset:x_offset + image_width, c] = \
|
||||
(1 - alpha) * matte[y_offset:y_offset + image_height, x_offset:x_offset + image_width, c] + \
|
||||
alpha * image[:, :, c]
|
||||
|
||||
# Set the alpha channel of the matte to the maximum of the matte's and the image's alpha
|
||||
matte[y_offset:y_offset + image_height, x_offset:x_offset + image_width, 3] = \
|
||||
np.maximum(matte[y_offset:y_offset + image_height, x_offset:x_offset + image_width, 3], image[:, :, 3])
|
||||
else:
|
||||
image = image[y_offset:y_offset + image_height, x_offset:x_offset + image_width, :]
|
||||
return matte
|
||||
|
||||
def image_scalefit(image: TYPE_IMAGE, width: int, height:int,
|
||||
mode:EnumScaleMode=EnumScaleMode.MATTE,
|
||||
sample:EnumInterpolation=EnumInterpolation.LANCZOS4,
|
||||
matte:TYPE_PIXEL=(0,0,0,0)) -> TYPE_IMAGE:
|
||||
|
||||
match mode:
|
||||
case EnumScaleMode.MATTE | EnumScaleMode.RESIZE_MATTE:
|
||||
image = image_matte(image, matte, width, height)
|
||||
|
||||
case EnumScaleMode.ASPECT:
|
||||
h, w = image.shape[:2]
|
||||
ratio = max(width, height) / max(w, h)
|
||||
image = cv2.resize(image, None, fx=ratio, fy=ratio, interpolation=sample.value)
|
||||
|
||||
case EnumScaleMode.ASPECT_SHORT:
|
||||
h, w = image.shape[:2]
|
||||
ratio = min(width, height) / min(w, h)
|
||||
image = cv2.resize(image, None, fx=ratio, fy=ratio, interpolation=sample.value)
|
||||
|
||||
case EnumScaleMode.CROP:
|
||||
image = image_crop_center(image, width, height)
|
||||
|
||||
case EnumScaleMode.FIT:
|
||||
image = cv2.resize(image, (width, height), interpolation=sample.value)
|
||||
|
||||
if image.ndim == 2:
|
||||
image = np.expand_dims(image, -1)
|
||||
return image
|
||||
+803
@@ -0,0 +1,803 @@
|
||||
"""
|
||||
Jovi_GLSL - http://www.github.com/Amorano/Jovi_GLSL
|
||||
Creation
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Tuple
|
||||
from enum import Enum, EnumMeta as EnumType
|
||||
|
||||
import cv2
|
||||
import glfw
|
||||
import torch
|
||||
import numpy as np
|
||||
import OpenGL.GL as gl
|
||||
from loguru import logger
|
||||
|
||||
from comfy.utils import ProgressBar
|
||||
|
||||
from Jovi_GLSL import GLSL_INTERNAL, GLSL_CUSTOM, JOV_TYPE_IMAGE, ROOT, \
|
||||
JOVBaseNode, \
|
||||
comfy_message
|
||||
|
||||
from Jovi_GLSL.core import IMAGE_SIZE_DEFAULT, IMAGE_SIZE_MAX, IMAGE_SIZE_MIN, \
|
||||
EnumConvertType, EnumInterpolation, EnumScaleMode, \
|
||||
cv2tensor_full, image_convert, image_scalefit, load_file, \
|
||||
parse_param, parse_value, tensor2cv
|
||||
|
||||
# ==============================================================================
|
||||
# === SHADER LOADER ===
|
||||
# ==============================================================================
|
||||
|
||||
ROOT_GLSL = ROOT / 'glsl'
|
||||
GLSL_PROGRAMS = {
|
||||
"vertex": { },
|
||||
"fragment": { }
|
||||
}
|
||||
|
||||
GLSL_PROGRAMS['vertex'].update({str(f.relative_to(ROOT_GLSL).as_posix()):
|
||||
str(f) for f in Path(ROOT_GLSL).rglob('*.vert')})
|
||||
|
||||
USER_GLSL = ROOT / '_user'
|
||||
USER_GLSL.mkdir(parents=True, exist_ok=True)
|
||||
if (USER_GLSL := os.getenv("JOV_GLSL", str(USER_GLSL))) is not None:
|
||||
GLSL_PROGRAMS['vertex'].update({str(f.relative_to(USER_GLSL).as_posix()):
|
||||
str(f) for f in Path(USER_GLSL).rglob('*.vert')})
|
||||
|
||||
GLSL_PROGRAMS['fragment'].update({str(f.relative_to(ROOT_GLSL).as_posix()):
|
||||
str(f) for f in Path(ROOT_GLSL).rglob('*.frag')})
|
||||
if USER_GLSL is not None:
|
||||
GLSL_PROGRAMS['fragment'].update({str(f.relative_to(USER_GLSL).as_posix()):
|
||||
str(f) for f in Path(USER_GLSL).rglob('*.frag')})
|
||||
|
||||
try:
|
||||
prog = GLSL_PROGRAMS['vertex'].pop('.lib/_.vert')
|
||||
PROG_VERTEX = load_file(prog)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
raise Exception("failed load default vertex program .lib/_.vert")
|
||||
|
||||
try:
|
||||
prog = GLSL_PROGRAMS['fragment'].pop('.lib/_.frag')
|
||||
PROG_FRAGMENT = load_file(prog)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
raise Exception("failed load default fragment program .lib/_.frag")
|
||||
|
||||
PROG_HEADER = load_file(ROOT_GLSL / '.lib/_.head')
|
||||
PROG_FOOTER = load_file(ROOT_GLSL / '.lib/_.foot')
|
||||
|
||||
logger.info(f" vertex programs: {len(GLSL_PROGRAMS['vertex'])}")
|
||||
logger.info(f"fragment programs: {len(GLSL_PROGRAMS['fragment'])}")
|
||||
|
||||
# ==============================================================================
|
||||
# === CONSTANT ===
|
||||
# ==============================================================================
|
||||
|
||||
RE_INCLUDE = re.compile(r"^\s+?#include\s+?([A-Za-z\_\-\.\\\/]{3,})$", re.MULTILINE)
|
||||
RE_VARIABLE = re.compile(r"uniform\s+(\w+)\s+(\w+);\s*(?:\/\/\s*([^;|]*))?\s*(?:;\s*([^;|]*))?\s*(?:;\s*([^;|]*))?\s*(?:;\s*([^;|]*))?\s*(?:;\s*([^;|]*))?\s*(?:\|\s*(.*))?$", re.MULTILINE)
|
||||
RE_SHADER_META = re.compile(r"^\/\/\s?([A-Za-z_]{3,}):\s?(.+)$", re.MULTILINE)
|
||||
|
||||
# HALFPI: float = math.pi / 2
|
||||
# TAU: float = math.pi * 2
|
||||
|
||||
LAMBDA_UNIFORM = {
|
||||
'bool': gl.glUniform1i,
|
||||
'int': gl.glUniform1i,
|
||||
'ivec2': gl.glUniform2i,
|
||||
'ivec3': gl.glUniform3i,
|
||||
'ivec4': gl.glUniform4i,
|
||||
'float': gl.glUniform1f,
|
||||
'vec2': gl.glUniform2f,
|
||||
'vec3': gl.glUniform3f,
|
||||
'vec4': gl.glUniform4f,
|
||||
}
|
||||
|
||||
PTYPE = {
|
||||
'bool': EnumConvertType.BOOLEAN,
|
||||
'int': EnumConvertType.INT,
|
||||
'ivec2': EnumConvertType.VEC2INT,
|
||||
'ivec3': EnumConvertType.VEC3INT,
|
||||
'ivec4': EnumConvertType.VEC4INT,
|
||||
'float': EnumConvertType.FLOAT,
|
||||
'vec2': EnumConvertType.VEC2,
|
||||
'vec3': EnumConvertType.VEC3,
|
||||
'vec4': EnumConvertType.VEC4,
|
||||
'sampler2D': EnumConvertType.IMAGE
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
# === ENUMERTATION ===
|
||||
# ==============================================================================
|
||||
|
||||
class EnumEdgeWrap(Enum):
|
||||
CLAMP = 10
|
||||
WRAP = 20
|
||||
MIRROR = 30
|
||||
|
||||
# ==============================================================================
|
||||
# === SHADER ENUMERTATION ===
|
||||
# ==============================================================================
|
||||
|
||||
"""
|
||||
These are enumerations that are exposed to the shader scripts.
|
||||
"""
|
||||
class EnumGLSLColorConvert(Enum):
|
||||
RGB2HSV = 0
|
||||
RGB2LAB = 1
|
||||
RGB2XYZ = 2
|
||||
HSV2RGB = 10
|
||||
HSV2LAB = 11
|
||||
HSV2XYZ = 12
|
||||
LAB2RGB = 20
|
||||
LAB2HSV = 21
|
||||
LAB2XYZ = 22
|
||||
XYZ2RGB = 30
|
||||
XYZ2HSV = 31
|
||||
XYZ2LAB = 32
|
||||
|
||||
# ==============================================================================
|
||||
# === SHADER SUPPORT ===
|
||||
# ==============================================================================
|
||||
|
||||
class CompileException(Exception): pass
|
||||
|
||||
class ShaderCache:
|
||||
"""Cache for compiled shaders to avoid recompilation of identical source code"""
|
||||
def __init__(self):
|
||||
self.vertex_cache = {} # (source, shader_type) -> compiled_shader
|
||||
self.fragment_cache = {}
|
||||
self.program_cache = {} # (vertex_source, fragment_source) -> program
|
||||
|
||||
def get_compiled_shader(self, source: str, shader_type: int) -> int:
|
||||
"""Get compiled shader from cache or compile and cache it"""
|
||||
cache = self.vertex_cache if shader_type == gl.GL_VERTEX_SHADER else self.fragment_cache
|
||||
cache_key = hash(source)
|
||||
|
||||
if cache_key in cache:
|
||||
return cache[cache_key]
|
||||
|
||||
shader = gl.glCreateShader(shader_type)
|
||||
gl.glShaderSource(shader, source)
|
||||
gl.glCompileShader(shader)
|
||||
|
||||
if gl.glGetShaderiv(shader, gl.GL_COMPILE_STATUS) != gl.GL_TRUE:
|
||||
log = gl.glGetShaderInfoLog(shader).decode()
|
||||
gl.glDeleteShader(shader)
|
||||
raise CompileException(log)
|
||||
|
||||
cache[cache_key] = shader
|
||||
return shader
|
||||
|
||||
def get_program(self, vertex_source: str, fragment_source: str) -> int:
|
||||
"""Get linked program from cache or create and cache it"""
|
||||
cache_key = (hash(vertex_source), hash(fragment_source))
|
||||
|
||||
if cache_key in self.program_cache:
|
||||
return self.program_cache[cache_key]
|
||||
|
||||
vertex_shader = self.get_compiled_shader(vertex_source, gl.GL_VERTEX_SHADER)
|
||||
fragment_shader = self.get_compiled_shader(fragment_source, gl.GL_FRAGMENT_SHADER)
|
||||
|
||||
program = gl.glCreateProgram()
|
||||
gl.glAttachShader(program, vertex_shader)
|
||||
gl.glAttachShader(program, fragment_shader)
|
||||
gl.glLinkProgram(program)
|
||||
|
||||
if gl.glGetProgramiv(program, gl.GL_LINK_STATUS) != gl.GL_TRUE:
|
||||
log = gl.glGetProgramInfoLog(program).decode()
|
||||
gl.glDeleteProgram(program)
|
||||
raise RuntimeError(log)
|
||||
|
||||
self.program_cache[cache_key] = program
|
||||
return program
|
||||
|
||||
def cleanup(self):
|
||||
"""Delete all cached shaders and programs"""
|
||||
glfw.make_context_current(self.__window)
|
||||
for shader in self.vertex_cache.values():
|
||||
gl.glDeleteShader(shader)
|
||||
for shader in self.fragment_cache.values():
|
||||
gl.glDeleteShader(shader)
|
||||
for program in self.program_cache.values():
|
||||
gl.glDeleteProgram(program)
|
||||
self.vertex_cache.clear()
|
||||
self.fragment_cache.clear()
|
||||
self.program_cache.clear()
|
||||
|
||||
class GLSLShader:
|
||||
|
||||
def __init__(self, vertex:str=None, fragment:str=None, width:int=IMAGE_SIZE_DEFAULT, height:int=IMAGE_SIZE_DEFAULT, fps:int=30) -> None:
|
||||
if not glfw.init():
|
||||
raise RuntimeError("GLFW did not init")
|
||||
self.__size: Tuple[int, int] = (max(width, IMAGE_SIZE_MIN), max(height, IMAGE_SIZE_MIN))
|
||||
self.__empty_image: np.ndarray = np.zeros((self.__size[1], self.__size[0]), np.uint8)
|
||||
self.__program = None
|
||||
self.__source_vertex: str = None
|
||||
self.__source_fragment: str = None
|
||||
self.__source_vertex_raw: str = None
|
||||
self.__source_fragment_raw: str = None
|
||||
self.__runtime: float = 0
|
||||
self.__fps: int = min(120, max(1, fps))
|
||||
self.__mouse: Tuple[int, int] = (0, 0)
|
||||
self.__last_frame = np.zeros((self.__size[1], self.__size[0]), np.uint8)
|
||||
self.__shaderVar = {}
|
||||
self.__userVar = {}
|
||||
self.__fbo = None
|
||||
self.__fbo_texture = None
|
||||
self.__bgcolor = (0, 0, 0, 1.)
|
||||
self.__textures = {}
|
||||
self.__window = None
|
||||
self.__init_window(vertex, fragment)
|
||||
|
||||
def __cleanup(self) -> None:
|
||||
glfw.make_context_current(self.__window)
|
||||
old = [v[3] for v in self.__userVar.values() if v[0] == 'sampler2D']
|
||||
if len(old):
|
||||
gl.glDeleteTextures(old)
|
||||
|
||||
if self.__fbo_texture:
|
||||
gl.glDeleteTextures(1, [self.__fbo_texture])
|
||||
|
||||
if self.__fbo:
|
||||
gl.glDeleteFramebuffers(1, [self.__fbo])
|
||||
|
||||
if self.__program:
|
||||
gl.glDeleteProgram(self.__program)
|
||||
|
||||
if self.__window:
|
||||
glfw.destroy_window(self.__window)
|
||||
logger.debug("cleanup")
|
||||
|
||||
def __init_window(self, vertex:str=None, fragment:str=None, force:bool=False) -> None:
|
||||
glfw.window_hint(glfw.VISIBLE, glfw.FALSE)
|
||||
self.__cleanup()
|
||||
self.__window = glfw.create_window(self.__size[0], self.__size[1], "hidden", None, None)
|
||||
if not self.__window:
|
||||
raise RuntimeError("GLFW did not init window")
|
||||
|
||||
self.__init_framebuffer()
|
||||
self.__init_program(vertex, fragment, force)
|
||||
logger.debug("init window")
|
||||
|
||||
def __compile_shader(self, source:str, shader_type:str) -> None:
|
||||
glfw.make_context_current(self.__window)
|
||||
shader = gl.glCreateShader(shader_type)
|
||||
gl.glShaderSource(shader, source)
|
||||
gl.glCompileShader(shader)
|
||||
if gl.glGetShaderiv(shader, gl.GL_COMPILE_STATUS) != gl.GL_TRUE:
|
||||
log = gl.glGetShaderInfoLog(shader).decode()
|
||||
logger.error(f"Shader compilation error: {log}")
|
||||
raise CompileException(log)
|
||||
# logger.debug(f"{shader_type} compiled")
|
||||
return shader
|
||||
|
||||
def __init_program(self, vertex:str=None, fragment:str=None, force:bool=False) -> None:
|
||||
vertex = self.__source_vertex_raw if vertex is None else vertex
|
||||
if vertex is None:
|
||||
logger.debug("Vertex program is empty. Using Default.")
|
||||
vertex = PROG_VERTEX
|
||||
|
||||
if (fragment := self.__source_fragment_raw if fragment is None else fragment) is None:
|
||||
logger.debug("Fragment program is empty. Using Default.")
|
||||
fragment = PROG_FRAGMENT
|
||||
|
||||
if not force and vertex == self.__source_vertex_raw and fragment == self.__source_fragment_raw:
|
||||
return
|
||||
|
||||
glfw.make_context_current(self.__window)
|
||||
try:
|
||||
gl.glDeleteProgram(self.__program)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
self.__source_vertex = self.__compile_shader(vertex, gl.GL_VERTEX_SHADER)
|
||||
fragment_full = PROG_HEADER + fragment + PROG_FOOTER
|
||||
self.__source_fragment = self.__compile_shader(fragment_full, gl.GL_FRAGMENT_SHADER)
|
||||
|
||||
self.__program = gl.glCreateProgram()
|
||||
gl.glAttachShader(self.__program, self.__source_vertex)
|
||||
gl.glAttachShader(self.__program, self.__source_fragment)
|
||||
gl.glLinkProgram(self.__program)
|
||||
if gl.glGetProgramiv(self.__program, gl.GL_LINK_STATUS) != gl.GL_TRUE:
|
||||
log = gl.glGetProgramInfoLog(self.__program).decode()
|
||||
logger.error(f"Program linking error: {log}")
|
||||
raise RuntimeError(log)
|
||||
|
||||
self.__source_fragment_raw = fragment
|
||||
self.__source_vertex_raw = vertex
|
||||
|
||||
gl.glUseProgram(self.__program)
|
||||
|
||||
self.__shaderVar = {}
|
||||
statics = ['iResolution', 'iTime', 'iFrameRate', 'iFrame']
|
||||
for s in statics:
|
||||
if (val := gl.glGetUniformLocation(self.__program, s)) > -1:
|
||||
self.__shaderVar[s] = val
|
||||
|
||||
if (resolution := self.__shaderVar.get('iResolution', -1)) > -1:
|
||||
gl.glUniform3f(resolution, self.__size[0], self.__size[1], 0)
|
||||
|
||||
self.__userVar = {}
|
||||
# read the fragment and setup the vars....
|
||||
for match in RE_VARIABLE.finditer(self.__source_fragment_raw):
|
||||
typ, name, default, *_ = match.groups()
|
||||
|
||||
texture = None
|
||||
if typ in ['sampler2D']:
|
||||
texture = self.__textures[name] = gl.glGenTextures(1)
|
||||
else:
|
||||
default = default.strip()
|
||||
if default.startswith('EnumGLSL'):
|
||||
typ = 'int'
|
||||
if (target_enum := getattr(sys.modules[__name__], default, None)) is not None:
|
||||
default = target_enum
|
||||
else:
|
||||
default = 0
|
||||
|
||||
self.__userVar[name] = [
|
||||
# type
|
||||
typ,
|
||||
# gl location
|
||||
gl.glGetUniformLocation(self.__program, name),
|
||||
# default value
|
||||
default,
|
||||
# texture id -- if a texture
|
||||
texture
|
||||
]
|
||||
|
||||
logger.debug("init vars")
|
||||
logger.debug("init program")
|
||||
|
||||
def __init_framebuffer(self) -> None:
|
||||
glfw.make_context_current(self.__window)
|
||||
|
||||
self.__fbo = gl.glGenFramebuffers(1)
|
||||
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.__fbo)
|
||||
|
||||
self.__fbo_texture = gl.glGenTextures(1)
|
||||
gl.glBindTexture(gl.GL_TEXTURE_2D, self.__fbo_texture)
|
||||
glfw.set_window_size(self.__window, self.__size[0], self.__size[1])
|
||||
|
||||
gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGBA32F, self.__size[0], self.__size[1], 0, gl.GL_RGBA, gl.GL_FLOAT, None)
|
||||
|
||||
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR)
|
||||
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR)
|
||||
gl.glFramebufferTexture2D(gl.GL_FRAMEBUFFER, gl.GL_COLOR_ATTACHMENT0, gl.GL_TEXTURE_2D, self.__fbo_texture, 0)
|
||||
|
||||
gl.glViewport(0, 0, self.__size[0], self.__size[1])
|
||||
|
||||
self.__empty_image = np.zeros((self.__size[1], self.__size[0]), np.uint8)
|
||||
logger.debug("init framebuffer")
|
||||
|
||||
def __del__(self) -> None:
|
||||
self.__cleanup()
|
||||
#if self.__window is not None:
|
||||
# if glfw is not None:
|
||||
# glfw.destroy_window(self.__window)
|
||||
# self.__window = None
|
||||
# glfw.terminate()
|
||||
|
||||
@property
|
||||
def vertex(self) -> str:
|
||||
return self.__source_vertex_raw
|
||||
|
||||
@vertex.setter
|
||||
def vertex(self, program:str) -> None:
|
||||
self.__init_program(vertex=program)
|
||||
|
||||
@property
|
||||
def fragment(self) -> str:
|
||||
return self.__source_fragment_raw
|
||||
|
||||
@fragment.setter
|
||||
def fragment(self, program:str) -> None:
|
||||
self.__init_program(fragment=program)
|
||||
|
||||
@property
|
||||
def size(self) -> Tuple[int, int]:
|
||||
return self.__size
|
||||
|
||||
@size.setter
|
||||
def size(self, size:Tuple[int, int]) -> None:
|
||||
size = (min(IMAGE_SIZE_MAX, max(IMAGE_SIZE_MIN, size[0])),
|
||||
min(IMAGE_SIZE_MAX, max(IMAGE_SIZE_MIN, size[1])))
|
||||
|
||||
if size[0] != self.__size[0] or size[1] != self.__size[1]:
|
||||
self.__size = size
|
||||
self.__init_window(force=True)
|
||||
|
||||
@property
|
||||
def runtime(self) -> float:
|
||||
return self.__runtime
|
||||
|
||||
@runtime.setter
|
||||
def runtime(self, runtime:float) -> None:
|
||||
runtime = max(0, runtime)
|
||||
self.__runtime = runtime
|
||||
|
||||
@property
|
||||
def fps(self) -> int:
|
||||
return self.__fps
|
||||
|
||||
@fps.setter
|
||||
def fps(self, fps:int) -> None:
|
||||
fps = max(1, min(120, int(fps)))
|
||||
self.__fps = fps
|
||||
if (iFrameRate := self.__shaderVar.get('iFrameRate', -1)) > -1:
|
||||
glfw.make_context_current(self.__window)
|
||||
gl.glUseProgram(self.__program)
|
||||
gl.glUniform1f(self.__shaderVar['iFrameRate'], iFrameRate)
|
||||
|
||||
@property
|
||||
def mouse(self) -> Tuple[int, int]:
|
||||
return self.__mouse
|
||||
|
||||
@mouse.setter
|
||||
def mouse(self, pos:Tuple[int, int]) -> None:
|
||||
self.__mouse = pos
|
||||
|
||||
@property
|
||||
def frame(self) -> float:
|
||||
return int(self.__runtime * self.__fps)
|
||||
|
||||
@property
|
||||
def last_frame(self) -> float:
|
||||
return self.__last_frame
|
||||
|
||||
@property
|
||||
def bgcolor(self) -> Tuple[int, ...]:
|
||||
return self.__bgcolor
|
||||
|
||||
@bgcolor.setter
|
||||
def bgcolor(self, color:Tuple[int, ...]) -> None:
|
||||
self.__bgcolor = tuple(float(x) / 255. for x in color)
|
||||
|
||||
def render(self, time_delta:float=0.,
|
||||
tile_edge:Tuple[EnumEdgeWrap,...]=(EnumEdgeWrap.CLAMP, EnumEdgeWrap.CLAMP),
|
||||
**kw) -> np.ndarray:
|
||||
|
||||
glfw.make_context_current(self.__window)
|
||||
gl.glUseProgram(self.__program)
|
||||
|
||||
self.runtime = time_delta
|
||||
|
||||
# current time in shader lifetime
|
||||
if (val := self.__shaderVar.get('iTime', -1)) > -1:
|
||||
gl.glUniform1f(val, self.__runtime)
|
||||
|
||||
# the desired FPS
|
||||
if (val := self.__shaderVar.get('iFrameRate', -1)) > -1:
|
||||
gl.glUniform1i(val, self.__fps)
|
||||
|
||||
# the current frame based on the life time and "fps"
|
||||
if (val := self.__shaderVar.get('iFrame', -1)) > -1:
|
||||
gl.glUniform1i(val, self.frame)
|
||||
|
||||
texture_index = 0
|
||||
for uk, uv in self.__userVar.items():
|
||||
p_type, p_loc, p_value, _ = uv
|
||||
val = kw.get(uk, p_value)
|
||||
|
||||
if p_type == 'sampler2D':
|
||||
if (texture := self.__textures.get(uk, None)) is None:
|
||||
logger.error(f"texture [{texture_index}] {uk} is None")
|
||||
texture_index += 1
|
||||
continue
|
||||
|
||||
gl.glActiveTexture(gl.GL_TEXTURE0 + texture_index)
|
||||
gl.glBindTexture(gl.GL_TEXTURE_2D, texture)
|
||||
|
||||
# send in black if nothing in input image
|
||||
if not isinstance(val, (np.ndarray,)):
|
||||
val = self.__empty_image
|
||||
|
||||
# @TODO: could cache this ?
|
||||
val = image_convert(val, 4)
|
||||
val = val[::-1,:]
|
||||
val = val.astype(np.float32) / 255.0
|
||||
val = cv2.resize(val, self.__size, interpolation=cv2.INTER_LINEAR)
|
||||
#
|
||||
gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGBA32F, self.__size[0], self.__size[1], 0, gl.GL_RGBA, gl.GL_FLOAT, val)
|
||||
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR)
|
||||
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR)
|
||||
|
||||
for idx, text_wrap in enumerate([gl.GL_TEXTURE_WRAP_S, gl.GL_TEXTURE_WRAP_T]):
|
||||
match tile_edge[idx]:
|
||||
case EnumEdgeWrap.WRAP:
|
||||
gl.glTexParameteri(gl.GL_TEXTURE_2D, text_wrap, gl.GL_REPEAT)
|
||||
case EnumEdgeWrap.MIRROR:
|
||||
gl.glTexParameteri(gl.GL_TEXTURE_2D, text_wrap, gl.GL_MIRRORED_REPEAT)
|
||||
case _:
|
||||
gl.glTexParameteri(gl.GL_TEXTURE_2D, text_wrap, gl.GL_CLAMP_TO_EDGE)
|
||||
|
||||
gl.glUniform1i(p_loc, texture_index)
|
||||
texture_index += 1
|
||||
elif val:
|
||||
funct = LAMBDA_UNIFORM[p_type]
|
||||
if isinstance(p_value, EnumType):
|
||||
val = p_value[val].value
|
||||
elif isinstance(val, str):
|
||||
val = val.split(',')
|
||||
val = parse_value(val, PTYPE[p_type], 0)
|
||||
if not isinstance(val, (list, tuple)):
|
||||
val = [val]
|
||||
funct(p_loc, *val)
|
||||
|
||||
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.__fbo)
|
||||
gl.glClearColor(*self.__bgcolor)
|
||||
gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
|
||||
gl.glDrawArrays(gl.GL_TRIANGLES, 0, 3)
|
||||
|
||||
data = gl.glReadPixels(0, 0, self.__size[0], self.__size[1], gl.GL_RGBA, gl.GL_UNSIGNED_BYTE)
|
||||
image = np.frombuffer(data, dtype=np.uint8).reshape(self.__size[1], self.__size[0], 4)
|
||||
self.__last_frame = image[::-1, :, :]
|
||||
|
||||
glfw.poll_events()
|
||||
|
||||
return self.__last_frame
|
||||
|
||||
def shader_meta(shader: str) -> Dict[str, Any]:
|
||||
ret = {}
|
||||
for match in RE_SHADER_META.finditer(shader):
|
||||
key, value = match.groups()
|
||||
ret[key] = value
|
||||
ret['_'] = [match.groups() for match in RE_VARIABLE.finditer(shader)]
|
||||
return ret
|
||||
|
||||
def load_file_glsl(fname: str) -> str:
|
||||
|
||||
# first file we load, starts the list of included
|
||||
include = set()
|
||||
|
||||
def scan_include(file:str, idx:int=0) -> str:
|
||||
if idx > 4:
|
||||
return "too many recursive includes"
|
||||
|
||||
file_path = ROOT_GLSL / file
|
||||
if file_path in include:
|
||||
return ""
|
||||
|
||||
include.add(file_path)
|
||||
try:
|
||||
result = load_file(file_path)
|
||||
except FileNotFoundError:
|
||||
return f"File not found: {file_path}"
|
||||
|
||||
# replace #include directives with their content
|
||||
def replace_include(match):
|
||||
lib_path = ROOT_GLSL / match.group(1)
|
||||
if lib_path not in include:
|
||||
return scan_include(lib_path, idx+1)
|
||||
return ""
|
||||
|
||||
return RE_INCLUDE.sub(replace_include, result)
|
||||
|
||||
return scan_include(fname)
|
||||
|
||||
# ==============================================================================
|
||||
# === COMFYUI NODES ===
|
||||
# ==============================================================================
|
||||
|
||||
class GLSLNodeBase(JOVBaseNode):
|
||||
CATEGORY = f"JOVI_GLSL 🔺🟩🔵"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls) -> dict:
|
||||
d = super().INPUT_TYPES()
|
||||
d["optional"] = {
|
||||
'MODE': (EnumScaleMode._member_names_, {"default": EnumScaleMode.MATTE.name}),
|
||||
'WH': ("VEC2INT", {"default": (512, 512), "mij":IMAGE_SIZE_MIN, "label": ['W', 'H']}),
|
||||
'SAMPLE': (EnumInterpolation._member_names_, {"default": EnumInterpolation.LANCZOS4.name}),
|
||||
'MATTE': ("VEC4INT", {"default": (0, 0, 0, 255), "rgb": True}),
|
||||
'EDGE_X': (EnumEdgeWrap._member_names_, {"default": EnumEdgeWrap.CLAMP.name}),
|
||||
'EDGE_Y': (EnumEdgeWrap._member_names_, {"default": EnumEdgeWrap.CLAMP.name}),
|
||||
}
|
||||
return d
|
||||
|
||||
def __init__(self, *arg, **kw) -> None:
|
||||
super().__init__(*arg, **kw)
|
||||
self.__glsl = GLSLShader()
|
||||
self.__delta = 0
|
||||
|
||||
def run(self, ident, **kw) -> Tuple[torch.Tensor]:
|
||||
batch = parse_param(kw, 'BATCH', EnumConvertType.INT, 0, 0, 1048576)[0]
|
||||
delta = parse_param(kw, 'TIME', EnumConvertType.FLOAT, 0)[0]
|
||||
|
||||
# everybody wang comp tonight
|
||||
mode = parse_param(kw, 'MODE', EnumScaleMode, EnumScaleMode.MATTE.name)[0]
|
||||
wihi = parse_param(kw, 'WH', EnumConvertType.VEC2INT, [(512, 512)], IMAGE_SIZE_MIN)[0]
|
||||
sample = parse_param(kw, 'SAMPLE', EnumInterpolation, EnumInterpolation.LANCZOS4.name)[0]
|
||||
matte = parse_param(kw, 'MATTE', EnumConvertType.VEC4INT, [(0, 0, 0, 255)], 0, 255)[0]
|
||||
edge_x = parse_param(kw, 'EDGE_X', EnumEdgeWrap, EnumEdgeWrap.CLAMP.name)[0]
|
||||
edge_y = parse_param(kw, 'EDGE_Y', EnumEdgeWrap, EnumEdgeWrap.CLAMP.name)[0]
|
||||
edge = (edge_x, edge_y)
|
||||
|
||||
try:
|
||||
self.__glsl.vertex = getattr(self, 'VERTEX', kw.pop('PROG_VERT', None))
|
||||
self.__glsl.fragment = getattr(self, 'FRAGMENT', kw.pop('PROG_FRAG', None))
|
||||
except CompileException as e:
|
||||
comfy_message(ident, "jovi-glsl-error", {"id": ident, "e": str(e)})
|
||||
logger.error(self.NAME)
|
||||
logger.error(e)
|
||||
return
|
||||
|
||||
self.__glsl.fps = parse_param(kw, 'FPS', EnumConvertType.INT, 24, 1, 120)[0]
|
||||
|
||||
variables = kw.copy()
|
||||
for p in ['MODE', 'WH', 'SAMPLE', 'MATTE', 'BATCH', 'TIME', 'FPS', 'EDGE_X', 'EDGE_Y']:
|
||||
variables.pop(p, None)
|
||||
|
||||
if batch > 0 or self.__delta != delta:
|
||||
self.__delta = delta
|
||||
step = 1. / self.__glsl.fps
|
||||
|
||||
images = []
|
||||
vars = {}
|
||||
batch = max(1, batch)
|
||||
firstImage = None
|
||||
# check if the input(s) have more than a single entry, get the max...
|
||||
if batch == 1:
|
||||
for k, var in variables.items():
|
||||
if isinstance(var, (torch.Tensor)):
|
||||
batch = max(batch, var.shape[0])
|
||||
var = [image_convert(tensor2cv(v), 4) for v in var]
|
||||
if firstImage is None:
|
||||
firstImage = var[0]
|
||||
elif isinstance(var, (list, tuple,)):
|
||||
batch = max(batch, len(var))
|
||||
|
||||
variables[k] = var if isinstance(var, (list, tuple,)) else [var]
|
||||
|
||||
pbar = ProgressBar(batch)
|
||||
for idx in range(batch):
|
||||
for k, val in variables.items():
|
||||
vars[k] = val[idx % len(val)]
|
||||
|
||||
w, h = wihi
|
||||
if firstImage is not None and mode == EnumScaleMode.MATTE:
|
||||
h, w = firstImage.shape[:2]
|
||||
|
||||
self.__glsl.size = (w, h)
|
||||
|
||||
img = self.__glsl.render(self.__delta, edge, **vars)
|
||||
if mode != EnumScaleMode.MATTE:
|
||||
img = image_scalefit(img, w, h, mode, sample)
|
||||
images.append(cv2tensor_full(img, matte))
|
||||
self.__delta += step
|
||||
comfy_message(ident, "jovi-glsl-time", {"id": ident, "t": self.__delta})
|
||||
pbar.update_absolute(idx)
|
||||
return [torch.stack(i) for i in zip(*images)]
|
||||
|
||||
class GLSLNode(GLSLNodeBase):
|
||||
NAME = "GLSL (JOV_GL) 🍩"
|
||||
DESCRIPTION = """
|
||||
Execute custom GLSL (OpenGL Shading Language) fragment shaders to generate images or apply effects. GLSL is a high-level shading language used for graphics programming, particularly in the context of rendering images or animations. This node allows for real-time rendering of shader effects, providing flexibility and creative control over image processing pipelines. It takes advantage of GPU acceleration for efficient computation, enabling the rapid generation of complex visual effects.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls) -> dict:
|
||||
d = super().INPUT_TYPES()
|
||||
opts = d.get('optional', {})
|
||||
opts.update({
|
||||
'BATCH': ("INT", {"default": 0, "mij": 0, "maj": 1048576}),
|
||||
'FPS': ("INT", {"default": 24, "mij": 1, "maj": 120}),
|
||||
'TIME': ("FLOAT", {"default": 0, "step": 0.0001, "mij": 0}),
|
||||
'VERTEX': ("STRING", {"default": PROG_VERTEX, "multiline": True, "dynamicPrompts": False}),
|
||||
'FRAGMENT': ("STRING", {"default": PROG_FRAGMENT, "multiline": True, "dynamicPrompts": False}),
|
||||
})
|
||||
d['optional'] = opts
|
||||
return d
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(cls, **kw) -> float:
|
||||
return float('nan')
|
||||
|
||||
class GLSLNodeDynamic(GLSLNodeBase):
|
||||
|
||||
PARAM = None
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls) -> dict:
|
||||
original_params = super().INPUT_TYPES()
|
||||
opts = original_params.get('optional', {})
|
||||
#opts.update({
|
||||
# 'FRAGMENT': ("JDATABUCKET", {"fragment": cls.FRAGMENT}),
|
||||
#})
|
||||
|
||||
opts.update({
|
||||
'FRAGMENT': ("STRING", {"default": cls.FRAGMENT}),
|
||||
})
|
||||
|
||||
# parameter list first...
|
||||
data = {}
|
||||
if cls.PARAM is not None:
|
||||
# 1., 1., 1.; 0; 1; 0.01; rgb | End of the Range
|
||||
# default, min, max, step, metadata, tooltip
|
||||
for glsl_type, name, default, val_min, val_max, val_step, meta, tooltip in cls.PARAM:
|
||||
typ = PTYPE[glsl_type]
|
||||
params = {"default": None}
|
||||
|
||||
d = None
|
||||
type_name = JOV_TYPE_IMAGE
|
||||
if glsl_type != 'sampler2D':
|
||||
type_name = typ.name
|
||||
if default is not None:
|
||||
if default.startswith('EnumGLSL'):
|
||||
if (target_enum := globals().get(default.strip(), None)) is not None:
|
||||
# this be an ENUM....
|
||||
type_name = target_enum._member_names_
|
||||
params['default'] = type_name[0]
|
||||
else:
|
||||
params['default'] = 0
|
||||
else:
|
||||
d = default.split(',')
|
||||
params['default'] = parse_value(d, typ, 0)
|
||||
|
||||
if val_min is not None:
|
||||
params['mij'] = parse_value(val_min, EnumConvertType.FLOAT, -sys.maxsize)
|
||||
|
||||
if val_max is not None:
|
||||
params['maj'] = parse_value(val_max, EnumConvertType.FLOAT, sys.maxsize)
|
||||
|
||||
if val_step is not None:
|
||||
d = 1 if typ.name.endswith('INT') else 0.01
|
||||
params['step'] = parse_value(val_step, EnumConvertType.FLOAT, d)
|
||||
|
||||
if meta is not None:
|
||||
if "rgb" in meta:
|
||||
if glsl_type.startswith('vec'):
|
||||
params['linear'] = True
|
||||
else:
|
||||
params['rgb'] = True
|
||||
|
||||
if tooltip is not None:
|
||||
params["tooltip"] = tooltip
|
||||
data[name] = (type_name, params,)
|
||||
|
||||
data.update(opts)
|
||||
original_params['optional'] = data
|
||||
return original_params
|
||||
|
||||
def import_dynamic() -> Tuple[str,...]:
|
||||
ret = []
|
||||
sort = 10000
|
||||
root = str(ROOT_GLSL)
|
||||
for name, fname in GLSL_PROGRAMS['fragment'].items():
|
||||
if (shader := load_file_glsl(fname)) is None:
|
||||
logger.error(f"missing shader file {fname}")
|
||||
continue
|
||||
|
||||
meta = shader_meta(shader)
|
||||
if meta.get('hide', False):
|
||||
continue
|
||||
|
||||
name = meta.get('name', name.split('.')[0]).upper()
|
||||
class_name = name.title().replace(' ', '_')
|
||||
class_name = f'GLSLNode_{class_name}'
|
||||
|
||||
emoji = GLSL_CUSTOM
|
||||
sort_order = sort
|
||||
if fname.startswith(root):
|
||||
emoji = GLSL_INTERNAL
|
||||
sort_order -= 10000
|
||||
|
||||
category = GLSLNodeDynamic.CATEGORY
|
||||
if (sub := meta.get('category', None)) is not None:
|
||||
category += f'/{sub}'
|
||||
|
||||
class_def = type(class_name, (GLSLNodeDynamic,), {
|
||||
"NAME": f'GLSL {name} (JOV_GL) {emoji}'.upper(),
|
||||
"DESCRIPTION": meta.get('desc', name),
|
||||
"CATEGORY": category.upper(),
|
||||
"FRAGMENT": shader,
|
||||
"PARAM": meta.get('_', []),
|
||||
"SORT": sort_order,
|
||||
})
|
||||
|
||||
sort += 10
|
||||
ret.append((class_name, class_def,))
|
||||
return ret
|
||||
@@ -0,0 +1,8 @@
|
||||
// Standard Fragment Shader Footer
|
||||
|
||||
layout(location = 0) out vec4 _fragColor;
|
||||
|
||||
void main()
|
||||
{
|
||||
mainImage(_fragColor, gl_FragCoord.xy);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// name: BASIC FRAGMENT SHADER
|
||||
// desc: draws 2 triangles as a quad for a surface to manipulate
|
||||
// hide: true
|
||||
|
||||
uniform sampler2D image;
|
||||
|
||||
void mainImage( out vec4 fragColor, vec2 fragCoord ) {
|
||||
vec2 uv = fragCoord / iResolution.xy;
|
||||
fragColor = texture(image, uv);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Standard Fragment Shader Header
|
||||
|
||||
#version 460
|
||||
precision highp float;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// GLOBAL
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
uniform vec3 iResolution; // Viewport resolution (pixels)
|
||||
uniform float iTime; // Shader playback time (seconds)
|
||||
uniform float iFrameRate; // Shader frame rate
|
||||
uniform int iFrame; // Shader playback frame
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// SURFACE
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
struct J_Material {
|
||||
vec3 diffuse; // Diffuse color
|
||||
vec3 specular; // Specular color
|
||||
float shininess; // Shininess coefficient
|
||||
vec3 F0; // Fresnel reflectance at normal incidence
|
||||
};
|
||||
|
||||
struct J_Light {
|
||||
vec3 position;
|
||||
vec3 color;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// name: BASIC VERTEX SHADER
|
||||
// desc: draws 2 triangles as a quad for a surface to manipulate
|
||||
|
||||
#version 460
|
||||
precision highp float;
|
||||
|
||||
void main()
|
||||
{
|
||||
vec2 verts[3] = vec2[](vec2(-1, -1), vec2(3, -1), vec2(-1, 3));
|
||||
gl_Position = vec4(verts[gl_VertexID], 0, 1);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// CAMERA
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
#import .lib/const.lib
|
||||
|
||||
// =============================================================================
|
||||
// PROTOTYPES
|
||||
// =============================================================================
|
||||
|
||||
vec3 lib_camera_eye(float fov, vec2 size, vec2 pos);
|
||||
mat3 lib_camera_eye(vec3 pos, vec3 target, float roll);
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// TRANSFORM
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Camera direction based on field of view and screen position
|
||||
vec3 lib_camera_eye(float fov, vec2 size, vec2 pos) {
|
||||
vec2 xy = pos - size * 0.5;
|
||||
float cot_half_fov = tan((90.0 - fov * 0.5) * M_DEG2RAD);
|
||||
float z = size.y * 0.5 * cot_half_fov;
|
||||
return normalize(vec3(xy, -z));
|
||||
}
|
||||
|
||||
// Camera view based on eye, target, and roll angle
|
||||
mat3 lib_camera_eye(vec3 pos, vec3 target, float roll)
|
||||
{
|
||||
vec3 cw = normalize(target-pos);
|
||||
vec3 cp = vec3(sin(roll), cos(roll),0.0);
|
||||
vec3 cu = normalize( cross(cw,cp) );
|
||||
vec3 cv = normalize( cross(cu,cw) );
|
||||
return mat3(cu, cv, cw);
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// COLOR
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
#include .lib/const.lib
|
||||
|
||||
const vec3 D65 = vec3(95.047, 100.0, 108.883);
|
||||
|
||||
// =============================================================================
|
||||
// PROTOTYPES
|
||||
// =============================================================================
|
||||
|
||||
vec3 rgb2hsv(vec3 rgb);
|
||||
vec3 rgb2lab(vec3 rgb);
|
||||
vec3 rgb2xyz(vec3 rgb);
|
||||
vec3 hsv2rgb(vec3 hsv);
|
||||
vec3 hsv2lab(vec3 hsv);
|
||||
vec3 hsv2xyz(vec3 hsv);
|
||||
vec3 lab2rgb(vec3 lab);
|
||||
vec3 lab2hsv(vec3 lab);
|
||||
vec3 lab2xyz(vec3 lab);
|
||||
vec3 xyz2rgb(vec3 xyz);
|
||||
vec3 xyz2hsv(vec3 xyz);
|
||||
vec3 xyz2lab(vec3 xyz);
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// RGB
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
vec3 rgb2hsv(vec3 rgb) {
|
||||
vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
|
||||
vec4 p = mix(vec4(rgb.bg, K.wz), vec4(rgb.gb, K.xy), step(rgb.b, rgb.g));
|
||||
vec4 q = mix(vec4(p.xyw, rgb.r), vec4(rgb.r, p.yzx), step(p.x, rgb.r));
|
||||
float d = q.x - min(q.w, q.y);
|
||||
return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + M_EPSILON)), d / (q.x + M_EPSILON), q.x);
|
||||
}
|
||||
|
||||
vec3 rgb2lab(vec3 rgb) {
|
||||
vec3 xyz = rgb2xyz(rgb);
|
||||
return xyz2lab(xyz);
|
||||
}
|
||||
|
||||
vec3 rgb2xyz(vec3 rgb) {
|
||||
vec3 tmp;
|
||||
tmp.x = (rgb.r > 0.04045) ? pow((rgb.r + 0.055) / 1.055, 2.4) : rgb.r / 12.92;
|
||||
tmp.y = (rgb.g > 0.04045) ? pow((rgb.g + 0.055) / 1.055, 2.4) : rgb.g / 12.92;
|
||||
tmp.z = (rgb.b > 0.04045) ? pow((rgb.b + 0.055) / 1.055, 2.4) : rgb.b / 12.92;
|
||||
return 100.0 * tmp * mat3(
|
||||
0.4124, 0.3576, 0.1805,
|
||||
0.2126, 0.7152, 0.0722,
|
||||
0.0193, 0.1192, 0.9505
|
||||
);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// HSV
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
vec3 hsv2rgb(vec3 hsv) {
|
||||
hsv = vec3(hsv.x, clamp(hsv.yz, 0.0, 1.0));
|
||||
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
|
||||
vec3 p = abs(fract(hsv.xxx + K.xyz) * 6.0 - K.www);
|
||||
return hsv.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), hsv.y);
|
||||
}
|
||||
|
||||
vec3 hsv2lab(vec3 hsv) {
|
||||
float H = hsv.x * 360.0;
|
||||
float S = hsv.y;
|
||||
float V = hsv.z;
|
||||
|
||||
// Convert to LAB
|
||||
float L = V * 100.0;
|
||||
float C = S * L;
|
||||
|
||||
float h = H * M_PI / 180.0;
|
||||
float a = C * cos(h);
|
||||
float b = C * sin(h);
|
||||
|
||||
// Normalize LAB
|
||||
return vec3(L / 100.0, (a + 128.0) / 255.0, (b + 128.0) / 255.0);
|
||||
}
|
||||
|
||||
vec3 hsv2xyz(vec3 hsv) {
|
||||
vec3 rgb = hsv2rgb(hsv);
|
||||
return rgb2xyz(rgb);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// LAB
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
vec3 lab2rgb(vec3 lab) {
|
||||
vec3 xyz = lab2xyz(lab);
|
||||
return xyz2rgb(xyz);
|
||||
}
|
||||
|
||||
vec3 lab2hsv(vec3 lab) {
|
||||
vec3 rgb = lab2rgb(lab);
|
||||
return rgb2hsv(rgb);
|
||||
}
|
||||
|
||||
vec3 lab2xyz(vec3 lab) {
|
||||
float fy = (lab.x + 16.0) / 116.0;
|
||||
float fx = lab.y / 500.0 + fy;
|
||||
float fz = fy - lab.z / 200.0;
|
||||
return vec3(
|
||||
95.047 * ((fx > 0.206897) ? fx * fx * fx : (fx - 16.0 / 116.0) / 7.787),
|
||||
100.000 * ((fy > 0.206897) ? fy * fy * fy : (fy - 16.0 / 116.0) / 7.787),
|
||||
108.883 * ((fz > 0.206897) ? fz * fz * fz : (fz - 16.0 / 116.0) / 7.787)
|
||||
);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// XYZ
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
vec3 xyz2rgb(vec3 xyz) {
|
||||
vec3 v = xyz / D65;
|
||||
vec3 r;
|
||||
r.x = ( v.r > 0.0031308 ) ? (( 1.055 * pow( v.r, ( 1.0 / 2.4 ))) - 0.055 ) : 12.92 * v.r;
|
||||
r.y = ( v.g > 0.0031308 ) ? (( 1.055 * pow( v.g, ( 1.0 / 2.4 ))) - 0.055 ) : 12.92 * v.g;
|
||||
r.z = ( v.b > 0.0031308 ) ? (( 1.055 * pow( v.b, ( 1.0 / 2.4 ))) - 0.055 ) : 12.92 * v.b;
|
||||
return r;
|
||||
}
|
||||
|
||||
vec3 xyz2hsv(vec3 xyz) {
|
||||
vec3 rgb = xyz2rgb(xyz);
|
||||
return rgb2hsv(rgb);
|
||||
}
|
||||
|
||||
vec3 xyz2lab(vec3 xyz) {
|
||||
vec3 n = xyz / D65;
|
||||
vec3 v;
|
||||
v.x = ( n.x > 0.008856 ) ? pow( n.x, 1.0 / 3.0 ) : ( 7.787 * n.x ) + ( 16.0 / 116.0 );
|
||||
v.y = ( n.y > 0.008856 ) ? pow( n.y, 1.0 / 3.0 ) : ( 7.787 * n.y ) + ( 16.0 / 116.0 );
|
||||
v.z = ( n.z > 0.008856 ) ? pow( n.z, 1.0 / 3.0 ) : ( 7.787 * n.z ) + ( 16.0 / 116.0 );
|
||||
return vec3(( 116.0 * v.y ) - 16.0, 500.0 * ( v.x - v.y ), 200.0 * ( v.y - v.z ));
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// CONSTANT
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
#define M_EPSILON 1.0e-10 // zero value for float comparisons
|
||||
|
||||
#define M_DEG2RAD 0.017453292519943 // Degree to radian conversion factor
|
||||
#define M_RAD2DEG 57.29577951308232 // Radian to degree conversion factor
|
||||
#define M_TAU 6.283185307179586 // Tau (2 * Pi)
|
||||
#define M_PI 3.141592653589793 // Pi
|
||||
#define M_PI_2 1.570796326794896 // Pi divided by 2 (π/2)
|
||||
#define M_PI_4 0.785398163397448 // Pi divided by 4 (π/4)
|
||||
#define M_3PI_4 2.356194490192345 // 3 * Pi divided by 4 (3π/4)
|
||||
|
||||
#define M_PHI 1.618033988749895 // Golden ratio (φ)
|
||||
#define M_PHI_INV 0.618033988749895 // Inverse of golden ratio (1/φ)
|
||||
#define M_PHI_SQ 2.618033988749895 // Square of the golden ratio (φ^2)
|
||||
|
||||
#define M_E 2.718281828459045 // Euler's number (base of natural logarithm)
|
||||
#define M_LOG2E 1.442695040888963 // Log base 2 of e
|
||||
#define M_LOG10E 0.434294481903252 // Log base 10 of e
|
||||
#define M_LN2 0.693147180559945 // Natural log of 2
|
||||
#define M_LN10 2.302585092994046 // Natural log of 10
|
||||
|
||||
#define M_SQRT2 1.414213562373095 // Square root of 2
|
||||
#define M_SQRT3 1.732050807568877 // Square root of 3
|
||||
#define M_SQRT1_2 0.707106781186547 // 1 divided by square root of 2 (1/sqrt(2))
|
||||
#define M_SQRT1_3 0.577350269189626 // 1 divided by square root of 3 (1/sqrt(3))
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// PHYSICS
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
#define M_C 299792458.0 // Speed of light in meters per second (m/s)
|
||||
#define M_G 9.80665 // Gravitational acceleration on Earth (m/s²)
|
||||
#define M_PLANCK 6.62607015e-34 // Planck's constant (Js)
|
||||
#define M_KB 1.380649e-23 // Boltzmann constant (J/K)
|
||||
#define M_MASS_E 9.10938356e-31 // Mass of electron (kg)
|
||||
#define M_CHARGE_E 1.602176634e-19 // Elementary charge (C)
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// EASE
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
#define EASE_IN_OUT_SINE(t) (-0.5 * (cos(M_PI * (t)) - 1.0))
|
||||
#define EASE_IN_OUT_CIRC(t) (((t) < 1.0) ? (-0.5 * (sqrt(1.0 - (t) * (t)) - 1.0)) : (0.5 * (sqrt(1.0 - ((t)-2.0) * ((t)-2.0)) + 1.0)))
|
||||
#define EASE_IN_OUT_QUAD(t) (((t) < 0.5) ? (2.0 * (t) * (t)) : (-2.0 * (t) * (t) + 4.0 * (t) - 1.0))
|
||||
|
||||
#define DECAY_EXP(t, lambda) exp(-lambda * (t))
|
||||
#define INTERP_SS(a, b, t) mix((a), (b), smoothstep(0.0, 1.0, (t)))
|
||||
#define INTERP_BOUNCE(t) abs(sin(M_TAU * (t) * (1.0 - (t))))
|
||||
#define INTERP_BOUNCE_VEC(a, b, t) mix((a), (b), INTERP_BOUNCE(t))
|
||||
|
||||
#define INTERP_HERMITE_VEC(a, b, tangentA, tangentB, t) \
|
||||
( \
|
||||
float h00 = 2.0 * (t) * (t) * (t) - 3.0 * (t) * (t) + 1.0; \
|
||||
float h10 = (t) * (t) * (t) - 2.0 * (t) * (t) + (t); \
|
||||
float h01 = -2.0 * (t) * (t) * (t) + 3.0 * (t) * (t); \
|
||||
float h11 = (t) * (t) * (t) - (t) * (t); \
|
||||
(h00 * (a) + h10 * (tangentA) + h01 * (b) + h11 * (tangentB)) \
|
||||
)
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// GENERAL
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// useful for triangle interpolation
|
||||
vec3 barycentricCoords(vec2 p, vec2 a, vec2 b, vec2 c) {
|
||||
vec2 v0 = b - a;
|
||||
vec2 v1 = c - a;
|
||||
vec2 v2 = p - a;
|
||||
float d00 = dot(v0, v0);
|
||||
float d01 = dot(v0, v1);
|
||||
float d11 = dot(v1, v1);
|
||||
float d20 = dot(v2, v0);
|
||||
float d21 = dot(v2, v1);
|
||||
float denom = d00 * d11 - d01 * d01;
|
||||
vec3 result;
|
||||
result.y = (d11 * d20 - d01 * d21) / denom;
|
||||
result.z = (d00 * d21 - d01 * d20) / denom;
|
||||
result.x = 1.0 - result.y - result.z;
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// CURVE
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// =============================================================================
|
||||
// PROTOTYPES
|
||||
// =============================================================================
|
||||
|
||||
vec2 lib_curve_bezierCubic(vec2 p0, vec2 p1, vec2 p2, vec2 p3, float t);
|
||||
vec3 lib_curve_bezierCubic(vec3 p0, vec3 p1, vec3 p2, vec3 p3, float t);
|
||||
vec2 lib_curve_bezierQuadratic(vec2 p0, vec2 p1, vec2 p2, float t);
|
||||
vec3 lib_curve_bezierQuadratic(vec3 p0, vec3 p1, vec3 p2, float t);
|
||||
vec2 lib_curve_catmullRom(vec2 p0, vec2 p1, vec2 p2, vec2 p3, float t);
|
||||
vec3 lib_curve_catmullRom(vec3 p0, vec3 p1, vec3 p2, vec3 p3, float t);
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// CUBIC BEZIER CURVE
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Cubic Bezier curve for 2D vectors
|
||||
vec2 lib_curve_bezierCubic(vec2 p0, vec2 p1, vec2 p2, vec2 p3, float t) {
|
||||
float u = 1.0 - t;
|
||||
return u * u * u * p0 + 3.0 * u * u * t * p1 + 3.0 * u * t * t * p2 + t * t * t * p3;
|
||||
}
|
||||
|
||||
// Cubic Bezier curve between four control points
|
||||
vec3 lib_curve_bezierCubic(vec3 p0, vec3 p1, vec3 p2, vec3 p3, float t) {
|
||||
float u = 1.0 - t;
|
||||
return u * u * u * p0 + 3.0 * u * u * t * p1 + 3.0 * u * t * t * p2 + t * t * t * p3;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// QUADRATIC BEZIER CURVE
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Quadratic Bezier curve for 2D vectors
|
||||
vec2 lib_curve_bezierQuadratic(vec2 p0, vec2 p1, vec2 p2, float t) {
|
||||
float u = 1.0 - t;
|
||||
return u * u * p0 + 2.0 * u * t * p1 + t * t * p2;
|
||||
}
|
||||
|
||||
// Quadratic Bezier curve between three control points
|
||||
vec3 lib_curve_bezierQuadratic(vec3 p0, vec3 p1, vec3 p2, float t) {
|
||||
float u = 1.0 - t;
|
||||
return u * u * p0 + 2.0 * u * t * p1 + t * t * p2;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// CATMULL-ROM SPLINE
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Catmull-Rom spline for 2D vectors
|
||||
vec2 lib_curve_catmullRom(vec2 p0, vec2 p1, vec2 p2, vec2 p3, float t) {
|
||||
vec2 a = 2.0 * p1;
|
||||
vec2 b = p2 - p0;
|
||||
vec2 c = 2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3;
|
||||
vec2 d = -p0 + 3.0 * p1 - 3.0 * p2 + p3;
|
||||
return 0.5 * (a + b * t + c * t * t + d * t * t * t);
|
||||
}
|
||||
|
||||
// Catmull-Rom spline between four points
|
||||
vec3 lib_curve_catmullRom(vec3 p0, vec3 p1, vec3 p2, vec3 p3, float t) {
|
||||
vec3 a = 2.0 * p1;
|
||||
vec3 b = p2 - p0;
|
||||
vec3 c = 2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3;
|
||||
vec3 d = -p0 + 3.0 * p1 - 3.0 * p2 + p3;
|
||||
return 0.5 * (a + b * t + c * t * t + d * t * t * t);
|
||||
}
|
||||
@@ -0,0 +1,784 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// NOISE
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// =============================================================================
|
||||
// PROTOTYPES
|
||||
// =============================================================================
|
||||
|
||||
float lib_noise_dithered(vec2 p);
|
||||
|
||||
float lib_noise_rand(vec2 co);
|
||||
float lib_noise_rand(vec3 co);
|
||||
float lib_noise_rand(vec4 co);
|
||||
|
||||
float lib_noise_gradient(float x);
|
||||
float lib_noise_gradient(vec2 p);
|
||||
float lib_noise_gradient(vec3 p);
|
||||
float lib_noise_gradient(vec4 p);
|
||||
|
||||
float lib_noise_fbm(vec2 x, float H, int octaves);
|
||||
float lib_noise_fbm(vec3 x, float H, int octaves);
|
||||
float lib_noise_fbm(vec4 x, float H, int octaves);
|
||||
|
||||
float lib_noise_value(vec2 p);
|
||||
float lib_noise_value(vec3 p);
|
||||
float lib_noise_value(vec3 p);
|
||||
|
||||
float lib_noise_simplex(vec2 v);
|
||||
float lib_noise_simplex(vec3 v);
|
||||
float lib_noise_simplex(vec4 v);
|
||||
|
||||
float lib_noise_perlin(vec2 p);
|
||||
float lib_noise_perlin(vec3 p);
|
||||
float lib_noise_perlin(vec4 p);
|
||||
|
||||
float lib_noise_fractal(vec2 p, float octaves, float persistence);
|
||||
float lib_noise_fractal(vec3 p, float octaves, float persistence);
|
||||
float lib_noise_fractal(vec4 p, float octaves, float persistence);
|
||||
|
||||
vec2 lib_noise_voronoi(vec2 p);
|
||||
vec2 lib_noise_voronoi(vec3 p);
|
||||
vec2 lib_noise_voronoi(vec4 p);
|
||||
|
||||
float lib_noise_worley(vec2 p, int num_cells);
|
||||
float lib_noise_worley(vec3 p, int num_cells);
|
||||
float lib_noise_worley(vec4 p, int num_cells);
|
||||
|
||||
float lib_noise_turbulence(vec2 p, float size);
|
||||
float lib_noise_turbulence(vec3 p, float size);
|
||||
float lib_noise_turbulence(vec3 p, float size);
|
||||
|
||||
vec2 lib_noise_smooth(int hash, vec2 p);
|
||||
vec3 lib_noise_smooth(int hash, vec3 p);
|
||||
vec4 lib_noise_smooth(int hash, vec4 p);
|
||||
|
||||
//
|
||||
//
|
||||
//
|
||||
|
||||
vec2 grad(vec2 g, vec2 p) {
|
||||
return g * (2.0 * p - 1.0);
|
||||
}
|
||||
|
||||
vec3 grad(vec3 g, vec3 p) {
|
||||
return g * (2.0 * p - 1.0);
|
||||
}
|
||||
|
||||
vec4 grad(vec4 g, vec4 p) {
|
||||
return g * (2.0 * p - 1.0);
|
||||
}
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// DITHERED NOISE
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Dithered noise
|
||||
float lib_noise_dithered(vec2 p) {
|
||||
float base = lib_noise_perlin(p);
|
||||
float dither = fract(sin(dot(floor(p), vec2(12.9898, 78.233))) * 43758.5453123);
|
||||
return base + dither * 0.5;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// RANDOM VALUE
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// 1D Random Noise function
|
||||
float lib_noise_rand(float co) {
|
||||
// Compute hash value for the input coordinate
|
||||
return fract(sin(co * 12.9898) * 43758.5453);
|
||||
}
|
||||
|
||||
// Generate a pseudo-random value based on a 2D coordinate
|
||||
float lib_noise_rand(vec2 co) {
|
||||
return fract(sin(dot(co, vec2(12.9898, 78.233))) * 43758.5453123);
|
||||
}
|
||||
|
||||
// Generate a pseudo-random value based on a 3D coordinate
|
||||
float lib_noise_rand(vec3 co) {
|
||||
return fract(sin(dot(co, vec3(12.9898, 78.233, 45.678))) * 43758.5453123);
|
||||
}
|
||||
|
||||
// Generate a pseudo-random value based on a 4D coordinate
|
||||
float lib_noise_rand(vec4 co) {
|
||||
return fract(sin(dot(co, vec4(12.9898, 78.233, 45.678, 94.673))) * 43758.5453123);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// NOISE GRADIENT
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
float lib_noise_gradient(float x) {
|
||||
float i = floor(x);
|
||||
float f = fract(x);
|
||||
return mix(lib_noise_rand(i), lib_noise_rand(i + 1.0), smoothstep(0.,1.,f));
|
||||
}
|
||||
|
||||
// Generate a 2D gradient noise value
|
||||
float lib_noise_gradient(vec2 p) {
|
||||
vec2 i = floor(p);
|
||||
vec2 f = fract(p);
|
||||
|
||||
float a = lib_noise_rand(i);
|
||||
float b = lib_noise_rand(i + vec2(1.0, 0.0));
|
||||
float c = lib_noise_rand(i + vec2(0.0, 1.0));
|
||||
float d = lib_noise_rand(i + vec2(1.0, 1.0));
|
||||
|
||||
vec2 u = smoothstep(0.,1.,f);
|
||||
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
|
||||
}
|
||||
|
||||
// Generate a 3D gradient noise value
|
||||
float lib_noise_gradient(vec3 p) {
|
||||
vec3 i = floor(p);
|
||||
vec3 f = fract(p);
|
||||
|
||||
float a = lib_noise_rand(i);
|
||||
float b = lib_noise_rand(i + vec3(1.0, 0.0, 0.0));
|
||||
float c = lib_noise_rand(i + vec3(0.0, 1.0, 0.0));
|
||||
float d = lib_noise_rand(i + vec3(1.0, 1.0, 0.0));
|
||||
float e = lib_noise_rand(i + vec3(0.0, 0.0, 1.0));
|
||||
float f0 = lib_noise_rand(i + vec3(1.0, 0.0, 1.0));
|
||||
float g0 = lib_noise_rand(i + vec3(0.0, 1.0, 1.0));
|
||||
float h0 = lib_noise_rand(i + vec3(1.0, 1.0, 1.0));
|
||||
|
||||
vec3 u = smoothstep(0.,1.,f);
|
||||
float v0 = mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
|
||||
float v1 = mix(mix(e, f0, u.x), mix(g0, h0, u.x), u.y);
|
||||
return mix(v0, v1, u.z);
|
||||
}
|
||||
|
||||
// Generate a 4D gradient noise value
|
||||
float lib_noise_gradient(vec4 p) {
|
||||
vec4 i = floor(p);
|
||||
vec4 f = fract(p);
|
||||
|
||||
// Compute random values at the corners of the hypercube
|
||||
float a = lib_noise_rand(i);
|
||||
float b = lib_noise_rand(i + vec4(1.0, 0.0, 0.0, 0.0));
|
||||
float c = lib_noise_rand(i + vec4(0.0, 1.0, 0.0, 0.0));
|
||||
float d = lib_noise_rand(i + vec4(1.0, 1.0, 0.0, 0.0));
|
||||
float e = lib_noise_rand(i + vec4(0.0, 0.0, 1.0, 0.0));
|
||||
float f0 = lib_noise_rand(i + vec4(1.0, 0.0, 1.0, 0.0));
|
||||
float g0 = lib_noise_rand(i + vec4(0.0, 1.0, 1.0, 0.0));
|
||||
float h0 = lib_noise_rand(i + vec4(1.0, 1.0, 1.0, 0.0));
|
||||
float i1 = lib_noise_rand(i + vec4(0.0, 0.0, 0.0, 1.0));
|
||||
float j1 = lib_noise_rand(i + vec4(1.0, 0.0, 0.0, 1.0));
|
||||
float k1 = lib_noise_rand(i + vec4(0.0, 1.0, 0.0, 1.0));
|
||||
float l1 = lib_noise_rand(i + vec4(1.0, 1.0, 0.0, 1.0));
|
||||
float m1 = lib_noise_rand(i + vec4(0.0, 0.0, 1.0, 1.0));
|
||||
float n1 = lib_noise_rand(i + vec4(1.0, 0.0, 1.0, 1.0));
|
||||
float o1 = lib_noise_rand(i + vec4(0.0, 1.0, 1.0, 1.0));
|
||||
float p1 = lib_noise_rand(i + vec4(1.0, 1.0, 1.0, 1.0));
|
||||
|
||||
vec4 u = smoothstep(0.,1.,f);
|
||||
float v0 = mix(mix(mix(a, b, u.x), mix(c, d, u.x), u.y),
|
||||
mix(mix(e, f0, u.x), mix(g0, h0, u.x), u.y), u.z);
|
||||
float v1 = mix(mix(mix(i1, j1, u.x), mix(k1, l1, u.x), u.y),
|
||||
mix(mix(m1, n1, u.x), mix(o1, p1, u.x), u.y), u.w);
|
||||
|
||||
return mix(v0, v1, u.w);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// NOISE fBM
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
#define MAX_OCTAVES 12
|
||||
|
||||
float lib_noise_fbm(vec2 x, float H, int octaves)
|
||||
{
|
||||
float G = exp2(-H);
|
||||
float f = 1.0;
|
||||
float a = 1.0;
|
||||
float t = 0.0;
|
||||
octaves = min(octaves, MAX_OCTAVES);
|
||||
for( int i=0; i < octaves; i++ )
|
||||
{
|
||||
t += a * lib_noise_rand(f*x);
|
||||
f *= 2.0;
|
||||
a *= G;
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
float lib_noise_fbm(vec3 x, float H, int octaves)
|
||||
{
|
||||
float G = exp2(-H);
|
||||
float f = 1.0;
|
||||
float a = 1.0;
|
||||
float t = 0.0;
|
||||
octaves = min(octaves, MAX_OCTAVES);
|
||||
for( int i=0; i<octaves; i++ )
|
||||
{
|
||||
t += a * lib_noise_rand(f*x);
|
||||
f *= 2.0;
|
||||
a *= G;
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
float lib_noise_fbm(vec4 x, float H, int octaves)
|
||||
{
|
||||
float G = exp2(-H);
|
||||
float f = 1.0;
|
||||
float a = 1.0;
|
||||
float t = 0.0;
|
||||
octaves = min(octaves, MAX_OCTAVES);
|
||||
for( int i=0; i<octaves; i++ )
|
||||
{
|
||||
t += a * lib_noise_rand(f*x);
|
||||
f *= 2.0;
|
||||
a *= G;
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// NOISE VALUE
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Value noise function for 2D
|
||||
float lib_noise_value(vec2 p) {
|
||||
vec2 i = floor(p);
|
||||
vec2 f = fract(p);
|
||||
|
||||
float a = lib_noise_rand(i);
|
||||
float b = lib_noise_rand(i + vec2(1.0, 0.0));
|
||||
float c = lib_noise_rand(i + vec2(0.0, 1.0));
|
||||
float d = lib_noise_rand(i + vec2(1.0, 1.0));
|
||||
|
||||
vec2 u = smoothstep(0.,1.,f);
|
||||
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
|
||||
}
|
||||
|
||||
// Value noise function for 3D
|
||||
float lib_noise_value(vec3 p) {
|
||||
vec3 i = floor(p);
|
||||
vec3 f = fract(p);
|
||||
|
||||
float a = lib_noise_rand(i);
|
||||
float b = lib_noise_rand(i + vec3(1.0, 0.0, 0.0));
|
||||
float c = lib_noise_rand(i + vec3(0.0, 1.0, 0.0));
|
||||
float d = lib_noise_rand(i + vec3(1.0, 1.0, 0.0));
|
||||
float e = lib_noise_rand(i + vec3(0.0, 0.0, 1.0));
|
||||
float f1 = lib_noise_rand(i + vec3(1.0, 0.0, 1.0));
|
||||
float g = lib_noise_rand(i + vec3(0.0, 1.0, 1.0));
|
||||
float h = lib_noise_rand(i + vec3(1.0, 1.0, 1.0));
|
||||
|
||||
vec3 u = smoothstep(0.,1.,f);
|
||||
return mix(
|
||||
mix(mix(a, b, u.x), mix(c, d, u.x), u.y),
|
||||
mix(mix(e, f1, u.x), mix(g, h, u.x), u.y),
|
||||
u.z
|
||||
);
|
||||
}
|
||||
|
||||
float lib_noise_value(vec4 p) {
|
||||
vec4 i = floor(p);
|
||||
vec4 f = fract(p);
|
||||
|
||||
float a = lib_noise_rand(i);
|
||||
float b = lib_noise_rand(i + vec4(1.0, 0.0, 0.0, 0.0));
|
||||
float c = lib_noise_rand(i + vec4(0.0, 1.0, 0.0, 0.0));
|
||||
float d = lib_noise_rand(i + vec4(1.0, 1.0, 0.0, 0.0));
|
||||
float e = lib_noise_rand(i + vec4(0.0, 0.0, 1.0, 0.0));
|
||||
float f0 = lib_noise_rand(i + vec4(1.0, 0.0, 1.0, 0.0));
|
||||
float g0 = lib_noise_rand(i + vec4(0.0, 1.0, 1.0, 0.0));
|
||||
float h0 = lib_noise_rand(i + vec4(1.0, 1.0, 1.0, 0.0));
|
||||
float i1 = lib_noise_rand(i + vec4(0.0, 0.0, 0.0, 1.0));
|
||||
float j1 = lib_noise_rand(i + vec4(1.0, 0.0, 0.0, 1.0));
|
||||
float k1 = lib_noise_rand(i + vec4(0.0, 1.0, 0.0, 1.0));
|
||||
float l1 = lib_noise_rand(i + vec4(1.0, 1.0, 0.0, 1.0));
|
||||
float m1 = lib_noise_rand(i + vec4(0.0, 0.0, 1.0, 1.0));
|
||||
float n1 = lib_noise_rand(i + vec4(1.0, 0.0, 1.0, 1.0));
|
||||
float o1 = lib_noise_rand(i + vec4(0.0, 1.0, 1.0, 1.0));
|
||||
float p1 = lib_noise_rand(i + vec4(1.0, 1.0, 1.0, 1.0));
|
||||
|
||||
vec4 u = smoothstep(0.,1.,f);
|
||||
float v0 = mix(mix(mix(a, b, u.x), mix(c, d, u.x), u.y), mix(mix(e, f0, u.x), mix(g0, h0, u.x), u.y), u.z);
|
||||
float v1 = mix(mix(mix(i1, j1, u.x), mix(k1, l1, u.x), u.y), mix(mix(m1, n1, u.x), mix(o1, p1, u.x), u.y), u.w);
|
||||
|
||||
return mix(v0, v1, u.w);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// NOISE SIMPLEX
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// 2D Simplex noise function (simplified)
|
||||
float lib_noise_simplex(vec2 v) {
|
||||
const vec2 C = vec2(0.211324865405187, 0.366025403784439); // (3 - sqrt(3)) / 6
|
||||
vec2 i = floor(v + (v.x + v.y) * C);
|
||||
vec2 x0 = v - i + (i.x + i.y) * C;
|
||||
vec2 i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
|
||||
vec2 x1 = x0 - i1 + C;
|
||||
vec2 x2 = x0 - 1.0 + 2.0 * C;
|
||||
vec3 p = vec3(x0, 0.0);
|
||||
vec3 p1 = vec3(x1, 0.0);
|
||||
vec3 p2 = vec3(x2, 0.0);
|
||||
|
||||
// Dot product and fade curve functions
|
||||
float t0 = 0.5 - dot(p, p);
|
||||
float t1 = 0.5 - dot(p1, p1);
|
||||
float t2 = 0.5 - dot(p2, p2);
|
||||
|
||||
// Fade curve function
|
||||
vec3 g0 = lib_noise_smooth(int(i.x + i.y) & 3, p);
|
||||
vec3 g1 = lib_noise_smooth(int(i.x + i.y + 1.) & 3, p1);
|
||||
vec3 g2 = lib_noise_smooth(int(i.x + i.y + 2.) & 3, p2);
|
||||
|
||||
t0 = t0 < 0.0 ? 0.0 : t0 * t0 * t0 * t0 * dot(g0, p);
|
||||
t1 = t1 < 0.0 ? 0.0 : t1 * t1 * t1 * t1 * dot(g1, p1);
|
||||
t2 = t2 < 0.0 ? 0.0 : t2 * t2 * t2 * t2 * dot(g2, p2);
|
||||
return 70.0 * (t0 + t1 + t2);
|
||||
}
|
||||
|
||||
// 3D Simplex Noise function
|
||||
float lib_noise_simplex(vec3 p) {
|
||||
// Simplex noise constants
|
||||
const vec3 C = vec3(1.0 / 6.0, 1.0 / 3.0, 1.0 / 2.0);
|
||||
|
||||
// Compute Simplex coordinates
|
||||
vec3 i = floor(p + dot(p, vec3(1.0 / 3.0)));
|
||||
vec3 f = fract(p - i + dot(i, C.xxy));
|
||||
|
||||
// Compute gradients
|
||||
float u = smoothstep(0.,1.,f.x);
|
||||
float v = smoothstep(0.,1.,f.y);
|
||||
float w = smoothstep(0.,1.,f.z);
|
||||
|
||||
// Compute corner contributions
|
||||
float grad1 = dot(grad(vec3(0.0, 1.0, 1.0), f), f - vec3(0.0, 0.0, 0.0));
|
||||
float grad2 = dot(grad(vec3(1.0, 0.0, 1.0), f - vec3(1.0, 0.0, 0.0)), f - vec3(1.0, 0.0, 0.0));
|
||||
float grad3 = dot(grad(vec3(1.0, 1.0, 0.0), f - vec3(0.0, 1.0, 0.0)), f - vec3(0.0, 1.0, 0.0));
|
||||
float grad4 = dot(grad(vec3(0.0, 1.0, 1.0), f - vec3(0.0, 0.0, 1.0)), f - vec3(0.0, 0.0, 1.0));
|
||||
|
||||
// Interpolation
|
||||
return mix(mix(mix(grad1, grad2, u), mix(grad3, grad4, v), w), grad1, u);
|
||||
}
|
||||
|
||||
// 4D Simplex Noise function
|
||||
float lib_noise_simplex(vec4 p) {
|
||||
// Simplex noise constants
|
||||
const vec4 C = vec4(0.138196601125010, 0.276393202250020, 0.414589803375030, 0.552786404500040);
|
||||
|
||||
// Compute Simplex coordinates
|
||||
vec4 i = floor(p + dot(p, vec4(0.309016994, 0.577350269, 0.707106781, 0.866025404)));
|
||||
vec4 f = fract(p - i + dot(i, C.xyzx));
|
||||
|
||||
// Compute gradients
|
||||
float u = smoothstep(0.,1.,f.x);
|
||||
float v = smoothstep(0.,1.,f.y);
|
||||
float w = smoothstep(0.,1.,f.z);
|
||||
float t = smoothstep(0.,1.,f.w);
|
||||
|
||||
// Compute corner contributions
|
||||
float grad1 = dot(grad(vec4(1.0, 0.0, 0.0, 0.0), f), f - vec4(0.0, 0.0, 0.0, 0.0));
|
||||
float grad2 = dot(grad(vec4(0.0, 1.0, 0.0, 0.0), f - vec4(1.0, 0.0, 0.0, 0.0)), f - vec4(1.0, 0.0, 0.0, 0.0));
|
||||
float grad3 = dot(grad(vec4(0.0, 0.0, 1.0, 0.0), f - vec4(0.0, 1.0, 0.0, 0.0)), f - vec4(0.0, 1.0, 0.0, 0.0));
|
||||
float grad4 = dot(grad(vec4(0.0, 0.0, 0.0, 1.0), f - vec4(0.0, 0.0, 1.0, 0.0)), f - vec4(0.0, 0.0, 1.0, 0.0));
|
||||
|
||||
// Interpolation
|
||||
return mix(mix(mix(mix(grad1, grad2, u), mix(grad3, grad4, v), w), grad1, u), grad2, t);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// NOISE PERLIN
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Generate 2D Perlin noise
|
||||
// Generate 2D Perlin noise
|
||||
float lib_noise_perlin(vec2 p) {
|
||||
vec2 i = floor(p);
|
||||
vec2 f = fract(p);
|
||||
vec2 u = smoothstep(0.,1.,f);
|
||||
|
||||
float a = lib_noise_rand(i);
|
||||
float b = lib_noise_rand(i + vec2(1.0, 0.0));
|
||||
float c = lib_noise_rand(i + vec2(0.0, 1.0));
|
||||
float d = lib_noise_rand(i + vec2(1.0, 1.0));
|
||||
|
||||
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
|
||||
}
|
||||
|
||||
// 3D Perlin Noise function
|
||||
// 3D Perlin Noise function
|
||||
float lib_noise_perlin(vec3 p) {
|
||||
vec3 i = floor(p);
|
||||
vec3 f = fract(p);
|
||||
vec3 u = smoothstep(0.,1.,f);
|
||||
|
||||
float a = lib_noise_rand(i);
|
||||
float b = lib_noise_rand(i + vec3(1.0, 0.0, 0.0));
|
||||
float c = lib_noise_rand(i + vec3(0.0, 1.0, 0.0));
|
||||
float d = lib_noise_rand(i + vec3(1.0, 1.0, 0.0));
|
||||
float e = lib_noise_rand(i + vec3(0.0, 0.0, 1.0));
|
||||
float f0 = lib_noise_rand(i + vec3(1.0, 0.0, 1.0));
|
||||
float g0 = lib_noise_rand(i + vec3(0.0, 1.0, 1.0));
|
||||
float h0 = lib_noise_rand(i + vec3(1.0, 1.0, 1.0));
|
||||
|
||||
return mix(mix(mix(a, b, u.x), mix(c, d, u.x), u.y), mix(mix(e, f0, u.x), mix(g0, h0, u.x), u.y), u.z);
|
||||
}
|
||||
|
||||
// 4D Perlin Noise function
|
||||
// 4D Perlin Noise function
|
||||
float lib_noise_perlin(vec4 p) {
|
||||
vec4 i = floor(p);
|
||||
vec4 f = fract(p);
|
||||
vec4 u = smoothstep(0.,1.,f);
|
||||
|
||||
float a = lib_noise_rand(i);
|
||||
float b = lib_noise_rand(i + vec4(1.0, 0.0, 0.0, 0.0));
|
||||
float c = lib_noise_rand(i + vec4(0.0, 1.0, 0.0, 0.0));
|
||||
float d = lib_noise_rand(i + vec4(1.0, 1.0, 0.0, 0.0));
|
||||
float e = lib_noise_rand(i + vec4(0.0, 0.0, 1.0, 0.0));
|
||||
float f0 = lib_noise_rand(i + vec4(1.0, 0.0, 1.0, 0.0));
|
||||
float g0 = lib_noise_rand(i + vec4(0.0, 1.0, 1.0, 0.0));
|
||||
float h0 = lib_noise_rand(i + vec4(1.0, 1.0, 1.0, 0.0));
|
||||
float i1 = lib_noise_rand(i + vec4(0.0, 0.0, 0.0, 1.0));
|
||||
float j1 = lib_noise_rand(i + vec4(1.0, 0.0, 0.0, 1.0));
|
||||
float k1 = lib_noise_rand(i + vec4(0.0, 1.0, 0.0, 1.0));
|
||||
float l1 = lib_noise_rand(i + vec4(1.0, 1.0, 0.0, 1.0));
|
||||
float m1 = lib_noise_rand(i + vec4(0.0, 0.0, 1.0, 1.0));
|
||||
float n1 = lib_noise_rand(i + vec4(1.0, 0.0, 1.0, 1.0));
|
||||
float o1 = lib_noise_rand(i + vec4(0.0, 1.0, 1.0, 1.0));
|
||||
float p1 = lib_noise_rand(i + vec4(1.0, 1.0, 1.0, 1.0));
|
||||
|
||||
return mix(mix(mix(a, b, u.x), mix(c, d, u.x), u.y), mix(mix(e, f0, u.x), mix(g0, h0, u.x), u.y), u.z);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// NOISE FRACTAL
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// 2D Fractal noise function with Perlin noise
|
||||
float lib_noise_fractal(vec2 p, float octaves, float persistence) {
|
||||
float total = 0.0;
|
||||
float frequency = 1.0;
|
||||
float amplitude = 1.0;
|
||||
float max_value = 0.0;
|
||||
|
||||
for (float i = 0.0; i < octaves; i++) {
|
||||
total += lib_noise_gradient(p * frequency) * amplitude;
|
||||
max_value += amplitude;
|
||||
amplitude *= persistence;
|
||||
frequency *= 2.0;
|
||||
}
|
||||
|
||||
return total / max_value;
|
||||
}
|
||||
|
||||
// 3D Fractal noise function with Perlin noise
|
||||
float lib_noise_fractal(vec3 p, float octaves, float persistence) {
|
||||
float total = 0.0;
|
||||
float frequency = 1.0;
|
||||
float amplitude = 1.0;
|
||||
float max_value = 0.0;
|
||||
|
||||
for (float i = 0.0; i < octaves; i++) {
|
||||
total += lib_noise_gradient(p * frequency) * amplitude;
|
||||
max_value += amplitude;
|
||||
amplitude *= persistence;
|
||||
frequency *= 2.0;
|
||||
}
|
||||
|
||||
return total / max_value;
|
||||
}
|
||||
|
||||
// 4D Fractal noise function with Perlin noise
|
||||
float lib_noise_fractal(vec4 p, float octaves, float persistence) {
|
||||
float total = 0.0;
|
||||
float frequency = 1.0;
|
||||
float amplitude = 1.0;
|
||||
float max_value = 0.0;
|
||||
|
||||
for (float i = 0.0; i < octaves; i++) {
|
||||
total += lib_noise_gradient(p * frequency) * amplitude;
|
||||
max_value += amplitude;
|
||||
amplitude *= persistence;
|
||||
frequency *= 2.0;
|
||||
}
|
||||
return total / max_value;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// NOISE VORONOI NOISE
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Voronoi noise function for 2D
|
||||
vec2 lib_noise_voronoi(vec2 p) {
|
||||
vec2 n = floor(p);
|
||||
vec2 f = fract(p);
|
||||
|
||||
vec2 m = vec2(8.0);
|
||||
for (int j = -1; j <= 1; j++) {
|
||||
for (int i = -1; i <= 1; i++) {
|
||||
vec2 g = vec2(float(i), float(j));
|
||||
vec2 o = vec2(lib_noise_rand(n + g));
|
||||
vec2 r = g + o - f;
|
||||
float d = dot(r, r);
|
||||
if (d < m.x) {
|
||||
m.y = m.x;
|
||||
m.x = d;
|
||||
} else if (d < m.y) {
|
||||
m.y = d;
|
||||
}
|
||||
}
|
||||
}
|
||||
return sqrt(m);
|
||||
}
|
||||
|
||||
// Voronoi noise function for 3D
|
||||
vec2 lib_noise_voronoi(vec3 p) {
|
||||
vec3 i = floor(p);
|
||||
vec3 f = fract(p);
|
||||
|
||||
float min_dist = 1.0;
|
||||
vec3 nearest_point = vec3(0.0);
|
||||
|
||||
for (float x = -1.0; x <= 1.0; x++) {
|
||||
for (float y = -1.0; y <= 1.0; y++) {
|
||||
for (float z = -1.0; z <= 1.0; z++) {
|
||||
vec3 neighbor = i + vec3(x, y, z);
|
||||
vec3 diff = neighbor - f;
|
||||
float dist = length(diff);
|
||||
if (dist < min_dist) {
|
||||
min_dist = dist;
|
||||
nearest_point = neighbor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return vec2(min_dist, length(nearest_point - f));
|
||||
}
|
||||
|
||||
// Voronoi noise function for 4D
|
||||
vec2 lib_noise_voronoi(vec4 p) {
|
||||
vec4 i = floor(p);
|
||||
vec4 f = fract(p);
|
||||
|
||||
float min_dist = 1.0;
|
||||
vec4 nearest_point = vec4(0.0);
|
||||
|
||||
for (float x = -1.0; x <= 1.0; x++) {
|
||||
for (float y = -1.0; y <= 1.0; y++) {
|
||||
for (float z = -1.0; z <= 1.0; z++) {
|
||||
for (float w = -1.0; w <= 1.0; w++) {
|
||||
vec4 neighbor = i + vec4(x, y, z, w);
|
||||
vec4 diff = neighbor - f;
|
||||
float dist = length(diff);
|
||||
if (dist < min_dist) {
|
||||
min_dist = dist;
|
||||
nearest_point = neighbor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return vec2(min_dist, length(nearest_point - f));
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// NOISE WORLEY
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// 2D Worley noise function (Cellular noise)
|
||||
float lib_noise_worley(vec2 p, int num_cells) {
|
||||
vec2 i = floor(p);
|
||||
vec2 f = fract(p);
|
||||
float d = 1.0; // Initial distance (for min distance to feature points)
|
||||
|
||||
for (int x = -num_cells; x <= num_cells; ++x) {
|
||||
for (int y = -num_cells; y <= num_cells; ++y) {
|
||||
vec2 cell = vec2(float(x), float(y));
|
||||
vec2 point = cell + vec2(lib_noise_rand(i + cell), lib_noise_rand(i + cell + vec2(42.0, 17.0)));
|
||||
vec2 offset = point - f;
|
||||
float len = length(offset);
|
||||
d = min(d, len);
|
||||
}
|
||||
}
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
// 3D Worley noise function (Cellular noise)
|
||||
float lib_noise_worley(vec3 p, int num_cells) {
|
||||
vec3 i = floor(p);
|
||||
vec3 f = fract(p);
|
||||
float d = 1.0; // Initial distance (for min distance to feature points)
|
||||
|
||||
for (int x = -num_cells; x <= num_cells; ++x) {
|
||||
for (int y = -num_cells; y <= num_cells; ++y) {
|
||||
for (int z = -num_cells; z <= num_cells; ++z) {
|
||||
vec3 cell = vec3(float(x), float(y), float(z));
|
||||
vec3 point = cell + vec3(lib_noise_rand(i + cell),
|
||||
lib_noise_rand(i + cell + vec3(42.0, 17.0, 23.0)),
|
||||
lib_noise_rand(i + cell + vec3(23.0, 31.0, 51.0)));
|
||||
vec3 offset = point - f;
|
||||
float len = length(offset);
|
||||
d = min(d, len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
// 4D Worley Noise function
|
||||
float lib_noise_worley(vec4 p, int num_cells) {
|
||||
// Grid cell dimensions
|
||||
float cell_size = 1.0 / float(num_cells);
|
||||
|
||||
// Compute cell coordinates
|
||||
vec4 cell_coords = floor(p / cell_size);
|
||||
|
||||
// Compute the local position within the cell
|
||||
vec4 local_pos = fract(p / cell_size);
|
||||
|
||||
float min_dist = 1.0;
|
||||
|
||||
// Loop over the neighboring cells
|
||||
for (int x = -1; x <= 1; ++x) {
|
||||
for (int y = -1; y <= 1; ++y) {
|
||||
for (int z = -1; z <= 1; ++z) {
|
||||
for (int w = -1; w <= 1; ++w) {
|
||||
vec4 neighbor_cell = vec4(x, y, z, w);
|
||||
vec4 neighbor_coords = cell_coords + neighbor_cell;
|
||||
|
||||
// Randomize the position within the neighboring cell
|
||||
vec4 random_offset = vec4(fract(sin(dot(neighbor_coords, vec4(12.9898, 78.233, 37.719, 4.581))) * 43758.5453));
|
||||
|
||||
// Compute the distance to the random point in the neighboring cell
|
||||
vec4 offset_pos = neighbor_cell * cell_size + random_offset;
|
||||
vec4 diff = p - offset_pos;
|
||||
float dist = length(diff);
|
||||
|
||||
// Update the minimum distance
|
||||
min_dist = min(min_dist, dist);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return min_dist;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// NOISE TURBULENCE
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Turbulence function using 2D Perlin noise
|
||||
float lib_noise_turbulence(vec2 p, float size) {
|
||||
float value = 0.0;
|
||||
float initial_size = size;
|
||||
|
||||
while (size >= 1.0) {
|
||||
value += lib_noise_perlin(p / size) * size;
|
||||
size /= 2.0;
|
||||
}
|
||||
|
||||
return 0.5 * value / initial_size;
|
||||
}
|
||||
|
||||
// Turbulence function using 3D Perlin noise
|
||||
float lib_noise_turbulence(vec3 p, float size) {
|
||||
float value = 0.0;
|
||||
float initial_size = size;
|
||||
|
||||
while (size >= 1.0) {
|
||||
value += lib_noise_gradient(p / size) * size;
|
||||
size /= 2.0;
|
||||
}
|
||||
|
||||
return 0.5 * value / initial_size;
|
||||
}
|
||||
|
||||
float lib_noise_turbulence(vec4 p, float size) {
|
||||
float total = 0.0;
|
||||
float scale = 1.0;
|
||||
|
||||
while (size > 1.0) {
|
||||
total += abs(lib_noise_gradient(p * scale)) / scale;
|
||||
scale *= 2.0;
|
||||
size /= 2.0;
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// GRADIENT SMOOTHING
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Gradient function for 2D
|
||||
vec2 lib_noise_smooth(int hash, vec2 p) {
|
||||
const vec2 grad[4] = vec2[](
|
||||
vec2( 1.0, 1.0),
|
||||
vec2(-1.0, 1.0),
|
||||
vec2( 1.0, -1.0),
|
||||
vec2(-1.0, -1.0)
|
||||
);
|
||||
return grad[hash & 3];
|
||||
}
|
||||
|
||||
// Gradient function for 3D
|
||||
vec3 lib_noise_smooth(int hash, vec3 p) {
|
||||
const vec3 grad[12] = vec3[](
|
||||
vec3( 1.0, 1.0, 0.0),
|
||||
vec3(-1.0, 1.0, 0.0),
|
||||
vec3( 1.0, -1.0, 0.0),
|
||||
vec3(-1.0, -1.0, 0.0),
|
||||
vec3( 1.0, 0.0, 1.0),
|
||||
vec3(-1.0, 0.0, 1.0),
|
||||
vec3( 1.0, 0.0, -1.0),
|
||||
vec3(-1.0, 0.0, -1.0),
|
||||
vec3( 0.0, 1.0, 1.0),
|
||||
vec3( 0.0, -1.0, 1.0),
|
||||
vec3( 0.0, 1.0, -1.0),
|
||||
vec3( 0.0, -1.0, -1.0)
|
||||
);
|
||||
return grad[hash % 12];
|
||||
}
|
||||
|
||||
vec4 lib_noise_smooth(int hash, vec4 p) {
|
||||
const vec4 grad[32] = vec4[](
|
||||
vec4( 1.0, 1.0, 1.0, 0.0),
|
||||
vec4(-1.0, 1.0, 1.0, 0.0),
|
||||
vec4( 1.0, -1.0, 1.0, 0.0),
|
||||
vec4(-1.0, -1.0, 1.0, 0.0),
|
||||
vec4( 1.0, 1.0, -1.0, 0.0),
|
||||
vec4(-1.0, 1.0, -1.0, 0.0),
|
||||
vec4( 1.0, -1.0, -1.0, 0.0),
|
||||
vec4(-1.0, -1.0, -1.0, 0.0),
|
||||
vec4( 1.0, 1.0, 0.0, 1.0),
|
||||
vec4(-1.0, 1.0, 0.0, 1.0),
|
||||
vec4( 1.0, -1.0, 0.0, 1.0),
|
||||
vec4(-1.0, -1.0, 0.0, 1.0),
|
||||
vec4( 1.0, 0.0, 1.0, 1.0),
|
||||
vec4(-1.0, 0.0, 1.0, 1.0),
|
||||
vec4( 1.0, 0.0, -1.0, 1.0),
|
||||
vec4(-1.0, 0.0, -1.0, 1.0),
|
||||
vec4( 0.0, 1.0, 1.0, 1.0),
|
||||
vec4( 0.0, -1.0, 1.0, 1.0),
|
||||
vec4( 0.0, 1.0, -1.0, 1.0),
|
||||
vec4( 0.0, -1.0, -1.0, 1.0),
|
||||
vec4( 1.0, 1.0, 0.0, -1.0),
|
||||
vec4(-1.0, 1.0, 0.0, -1.0),
|
||||
vec4( 1.0, -1.0, 0.0, -1.0),
|
||||
vec4(-1.0, -1.0, 0.0, -1.0),
|
||||
vec4( 1.0, 0.0, 1.0, -1.0),
|
||||
vec4(-1.0, 0.0, 1.0, -1.0),
|
||||
vec4( 1.0, 0.0, -1.0, -1.0),
|
||||
vec4(-1.0, 0.0, -1.0, -1.0),
|
||||
vec4( 0.0, 1.0, 1.0, -1.0),
|
||||
vec4( 0.0, -1.0, 1.0, -1.0),
|
||||
vec4( 0.0, 1.0, -1.0, -1.0),
|
||||
vec4( 0.0, -1.0, -1.0, -1.0)
|
||||
);
|
||||
return grad[hash & 31];
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// SIGNED DISTANCE
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
float lib_sdf_polygon2D(vec2 p, vec2 center, float radius, int sides, float starFactor);
|
||||
float lib_sdf_star2D(vec2 p, float r, float t);
|
||||
float lib_sdf_heart2D(vec2 p);
|
||||
float lib_sdf_ellipse2D(vec2 p, vec2 radii);
|
||||
float lib_sdf_circle2D(vec2 p, float r);
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// SUPPORT FUNCTIONS
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
float lib_sdf_union(float d1, float d2);
|
||||
float lib_sdf_subtract(float d1, float d2);
|
||||
float lib_sdf_round(vec2 s, float r);
|
||||
float lib_sdf_hollow(vec2 s, float thickness);
|
||||
vec3 lib_sdf_hollow(vec3 shape, float r);
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// SHAPE
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// CUBE
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Signed distance box of size radius
|
||||
float lib_sdf_box(vec2 p, vec2 radius)
|
||||
{
|
||||
vec2 d = abs(p)-radius;
|
||||
return length(max(d,0.0)) + min(maxcomp(d),0.0);
|
||||
}
|
||||
|
||||
float lib_sdf_box(vec3 p, vec3 radius)
|
||||
{
|
||||
vec2 d = abs(p)-radius;
|
||||
return length(max(d,0.0)) + min(maxcomp(d),0.0);
|
||||
}
|
||||
|
||||
float lib_sdf_box(vec4 p, vec4 radius)
|
||||
{
|
||||
vec2 d = abs(p)-radius;
|
||||
return length(max(d,0.0)) + min(maxcomp(d),0.0);
|
||||
}
|
||||
|
||||
// Computes the signed distance from a point to a sphere
|
||||
float lib_sdf_sphere(vec3 p, float r) {
|
||||
return length(p) - r;
|
||||
}
|
||||
|
||||
float lib_sdf_cylinder(vec3 p, float r, float h) {
|
||||
vec2 d = vec2(length(p.xy) - r, abs(p.z) - h * 0.5);
|
||||
return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
float lib_sdf_polygon(vec2 p, vec2 center, float r, int sides, float starFactor) {
|
||||
float angle = M_TAU / float(sides);
|
||||
float dist = 1e30; // Initialize to a large value
|
||||
|
||||
for (int i = 0; i < sides; ++i) {
|
||||
// Calculate the vertices of the polygon
|
||||
float a = angle * float(i);
|
||||
vec2 v0 = center + vec2(cos(a), sin(a)) * r;
|
||||
vec2 v1 = center + vec2(cos(a + angle), sin(a + angle)) * r;
|
||||
|
||||
// Calculate the distance from the point to the edge of the polygon
|
||||
vec2 edge = v1 - v0;
|
||||
vec2 toPoint = p - v0;
|
||||
float t = clamp(dot(toPoint, edge) / dot(edge, edge), 0.0, 1.0);
|
||||
vec2 closest = v0 + t * edge;
|
||||
float edgeDistance = length(p - closest);
|
||||
|
||||
// Modulate the radius for star effect
|
||||
float factor = 1.0 + starFactor * (mod(float(i), 2.0) * 2.0 - 1.0);
|
||||
float distance = edgeDistance - r * factor;
|
||||
|
||||
// Update the minimum distance
|
||||
dist = min(dist, distance);
|
||||
}
|
||||
return dist;
|
||||
}
|
||||
|
||||
float lib_sdf_ellipse(vec2 p, vec2 radii) {
|
||||
vec2 q = abs(p) - radii;
|
||||
return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0);
|
||||
}
|
||||
|
||||
float lib_sdf_circle(vec2 p, float r) {
|
||||
return length(p) - r;
|
||||
}
|
||||
|
||||
float lib_sdf_star(vec2 p, float r, float t) {
|
||||
float theta = atan(p.y, p.x);
|
||||
float radius = length(p);
|
||||
float angle = mod(theta, M_TAU / max(1., t));
|
||||
float innerRadius = r * (1.0 - 0.5);
|
||||
float outerRadius = r * (1.0 + 0.5);
|
||||
float d = length(vec2(radius - innerRadius, angle - M_PI / t)) - (outerRadius - innerRadius);
|
||||
return d;
|
||||
}
|
||||
|
||||
float lib_sdf_heart(vec2 p) {
|
||||
float x = p.x;
|
||||
float y = p.y;
|
||||
float a = 1.0 - x * x - (5.0 * y / 4.0 - sqrt(abs(x))) * (5.0 * y / 4.0 - sqrt(abs(x)));
|
||||
return length(vec2(x, y - sqrt(abs(x))) - vec2(0.0, 0.5)) * (abs(a) - 0.5);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// TRANSFORMATION
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Repeats the space with periodic boundary conditions
|
||||
vec3 lib_sdf_repeat(vec3 p, vec3 c) {
|
||||
return mod(p, c) - 0.5 * c;
|
||||
}
|
||||
|
||||
// Union of two distances
|
||||
float lib_sdf_union(float d1, float d2) {
|
||||
return min(d1, d2);
|
||||
}
|
||||
|
||||
// Smooth union of two distances
|
||||
float lib_sdf_unionSmooth(float a, float b, float k) {
|
||||
float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
|
||||
return mix(b, a, h) - k * h * (1.0 - h);
|
||||
}
|
||||
|
||||
// Intersection of two distances
|
||||
float lib_sdf_intersection(float d1, float d2) {
|
||||
return max(d1, d2);
|
||||
}
|
||||
|
||||
// Subtraction of two distances
|
||||
float lib_sdf_difference(float d1, float d2) {
|
||||
return max(d1, -d2);
|
||||
}
|
||||
|
||||
float lib_sdf_hollow2(vec2 s, float thickness) {
|
||||
return abs(s) - thickness;
|
||||
}
|
||||
|
||||
vec3 lib_sdf_hollow3(vec3 shape, float r) {
|
||||
return vec3(abs(shape.x) - r, sign(shape.x) * shape.yz);
|
||||
}
|
||||
|
||||
// Displacement effect using sine functions
|
||||
float lib_sdf_displacement(vec3 p) {
|
||||
return sin(p.x) * sin(p.y) * sin(p.z);
|
||||
}
|
||||
|
||||
// Applies a twisting transformation to a point
|
||||
vec3 lib_sdf_twist(vec3 p, float a) {
|
||||
float c = cos(a * p.y);
|
||||
float s = sin(a * p.y);
|
||||
mat2 m = mat2(c, -s, s, c);
|
||||
return vec3(m * p.xz, p.y);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// RAY INTERSECTION
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Computes ray-box intersection and returns true if intersection occurs
|
||||
bool lib_intersection_aabb(vec3 o, vec3 dir, vec3 bmin, vec3 bmax, inout vec2 e) {
|
||||
vec3 a = (bmin - o) / dir;
|
||||
vec3 b = (bmax - o) / dir;
|
||||
vec3 s = min(a, b);
|
||||
vec3 t = max(a, b);
|
||||
e.x = max(max(s.x, s.y), max(s.z, e.x));
|
||||
e.y = max(min(t.x, t.y), max(t.z, e.y));
|
||||
return e.x < e.y;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// DISTANCE FIELD
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Computes the distance field for a given point
|
||||
// Optionally applies transformations and combines multiple distance functions
|
||||
float dist_field(vec3 p) {
|
||||
// Uncomment transformations to apply
|
||||
// p = sdRep(p, vec3(4.0));
|
||||
// p = sdTwist(p, 3.0);
|
||||
|
||||
// Compute distances to box and sphere
|
||||
float d0 = lib_sdf_box(p, vec3(0.5));
|
||||
float d1 = lib_sdf_sphere(p, 0.6);
|
||||
|
||||
// Combine distances using intersection
|
||||
return lib_sdf_intersection(d1, d0);
|
||||
|
||||
// Optionally include displacement and smooth union
|
||||
// return d + sfDisp(p * 2.5);
|
||||
// return sdUnion_s(d + sfDisp(p * 2.5 * sin(iTime * 1.01)), d1, 0.1);
|
||||
}
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// GRADIENT FUNCTION
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Computes the gradient of the distance field at a point
|
||||
vec3 gradient(vec3 pos) {
|
||||
const vec3 dx = vec3(grad_step, 0.0, 0.0);
|
||||
const vec3 dy = vec3(0.0, grad_step, 0.0);
|
||||
const vec3 dz = vec3(0.0, 0.0, grad_step);
|
||||
return normalize(
|
||||
vec3(
|
||||
dist_field(pos + dx) - dist_field(pos - dx),
|
||||
dist_field(pos + dy) - dist_field(pos - dy),
|
||||
dist_field(pos + dz) - dist_field(pos - dz)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// RAY MARCHING
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Performs ray marching to find the intersection with a distance field
|
||||
bool ray_marching(vec3 o, vec3 dir, inout float depth, inout vec3 n) {
|
||||
float t = 0.0;
|
||||
float d = 10000.0;
|
||||
float dt = 0.0;
|
||||
for (int i = 0; i < 128; i++) {
|
||||
vec3 v = o + dir * t;
|
||||
d = dist_field(v);
|
||||
if (d < 0.001) {
|
||||
break;
|
||||
}
|
||||
dt = min(abs(d), 0.1);
|
||||
t += dt;
|
||||
if (t > depth) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (d >= 0.001) {
|
||||
return false;
|
||||
}
|
||||
|
||||
t -= dt;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
dt *= 0.5;
|
||||
vec3 v = o + dir * (t + dt);
|
||||
if (dist_field(v) >= 0.001) {
|
||||
t += dt;
|
||||
}
|
||||
}
|
||||
|
||||
depth = t;
|
||||
n = normalize(gradient(o + dir * t));
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// SHADING
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
#import .lib/const.lib
|
||||
|
||||
// =============================================================================
|
||||
// PROTOTYPES
|
||||
// =============================================================================
|
||||
|
||||
vec3 lib_shading_fresnel(vec3 F0, vec3 h, vec3 l); // Computes the Fresnel reflection factor
|
||||
vec3 lib_shading_phong_light(vec3 pos, vec3 color); //
|
||||
vec3 lib_shading_phong(vec3 v, vec3 n, vec3 dir, vec3 eye); // Computes Phong shading for a given point
|
||||
|
||||
// =============================================================================
|
||||
// PHONG
|
||||
// =============================================================================
|
||||
|
||||
// Computes the Fresnel reflection factor
|
||||
vec3 lib_shading_fresnel(vec3 F0, vec3 h, vec3 l) {
|
||||
return F0 + (1.0 - F0) * pow(clamp(1.0 - dot(h, l), 0.0, 1.0), 5.0);
|
||||
}
|
||||
|
||||
vec3 lib_shading_phong_light(vec3 pos, vec3 color) {
|
||||
vec3 vl = normalize(pos - v);
|
||||
vec3 diffuse = Kd * vec3(max(0.0, dot(vl, n)));
|
||||
vec3 specular = vec3(max(0.0, dot(vl, ref)));
|
||||
vec3 F = lib_shading_fresnel(Ks, normalize(vl - dir), vl);
|
||||
specular = pow(specular, vec3(shininess));
|
||||
return color * mix(diffuse, specular, F);
|
||||
}
|
||||
|
||||
// Computes Phong shading for a given point
|
||||
vec3 lib_shading_phong(vec3 v, vec3 n, vec3 dir, vec3 eye) {
|
||||
vec3 final = vec3(0.0);
|
||||
|
||||
// Define material properties
|
||||
float shininess = 16.0;
|
||||
vec3 ref = reflect(dir, n);
|
||||
vec3 Ks = vec3(0.5);
|
||||
vec3 Kd = vec3(1.0);
|
||||
|
||||
// Light 0
|
||||
{
|
||||
vec3 light_pos = vec3(20.0, 20.0, 20.0);
|
||||
vec3 light_color = vec3(1.0, 0.7, 0.7);
|
||||
final += lib_shading_phong_light(light_pos, light_color);
|
||||
}
|
||||
|
||||
// Light 1
|
||||
{
|
||||
vec3 light_pos = vec3(-20.0, -20.0, -30.0);
|
||||
vec3 light_color = vec3(0.5, 0.7, 1.0);
|
||||
final += lib_shading_phong_light(light_pos, light_color);
|
||||
}
|
||||
return final;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// VECTOR
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// =============================================================================
|
||||
// DEFINE
|
||||
// =============================================================================
|
||||
|
||||
#define VEC_PROJECT(a, b) ((dot(a, b) / dot(b, b)) * b)
|
||||
#define VEC_ORTHOGONAL2(v) vec2(-(v).y, (v).x)
|
||||
#define VEC_ORTHOGONAL3(v) \
|
||||
(abs((v).x) > abs((v).y) ? normalize(vec3(-(v).z, 0.0, (v).x)) : \
|
||||
normalize(vec3(0.0, (v).z, -(v).y)))
|
||||
#define VEC_ANGLEBETWEEN(a, b) acos(clamp(dot(normalize(a), normalize(b)), -1.0, 1.0))
|
||||
|
||||
// =============================================================================
|
||||
// PROTOTYPE
|
||||
// =============================================================================
|
||||
|
||||
float lib_vec_ndot(in vec2 a, in vec2 b);
|
||||
mat3 lib_vec_rotationXY(vec2 angle);
|
||||
vec2 lib_vec_rotate2(vec2 v, float angle);
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// ANGLE
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Compute the "negative dot product" of two 2D vectors
|
||||
float lib_vec_ndot(in vec2 a, in vec2 b) {
|
||||
return a.x*b.x - a.y*b.y;
|
||||
}
|
||||
|
||||
// Creates a rotation matrix for pitch, yaw
|
||||
mat3 lib_vec_rotationXY( vec2 angle ) {
|
||||
vec2 c = cos( angle );
|
||||
vec2 s = sin( angle );
|
||||
|
||||
return mat3(
|
||||
c.y , 0.0, -s.y,
|
||||
s.y * s.x, c.x, c.y * s.x,
|
||||
s.y * c.x, -s.x, c.y * c.x
|
||||
);
|
||||
}
|
||||
|
||||
// Rotates a 2D vector by angle in radians
|
||||
vec2 lib_vec_rotate2(vec2 v, float angle) {
|
||||
float cosA = cos(angle);
|
||||
float sinA = sin(angle);
|
||||
return vec2(
|
||||
v.x * cosA - v.y * sinA,
|
||||
v.x * sinA + v.y * cosA
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// name: INVERT
|
||||
// desc: Invert the channels of an image along a scalar [0..1] range.
|
||||
// category: ADJUST
|
||||
|
||||
uniform sampler2D image; // | 4-channel data
|
||||
uniform vec4 invert; // 0,0,0,0;0;1 | amount to invert each channel
|
||||
|
||||
void mainImage( out vec4 fragColor, in vec2 fragCoord )
|
||||
{
|
||||
vec2 uv = fragCoord.xy / iResolution.xy;
|
||||
vec4 col = texture(image, uv);
|
||||
fragColor = mix(col, 1.0 - col, invert);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// name: COLOR CONVERSION
|
||||
// desc: Convert an image from one color space (RGB, HSV, LAB, XYZ) to another.
|
||||
// category: COLOR
|
||||
|
||||
#include .lib/color.lib
|
||||
|
||||
uniform sampler2D image; // | Image to convert
|
||||
uniform int operator; // EnumGLSLColorConvert | conversion operation to perform.
|
||||
|
||||
// =============================================================================
|
||||
// SELECTOR
|
||||
// =============================================================================
|
||||
|
||||
vec3 convertColor(vec3 color, int operator) {
|
||||
// RGB
|
||||
if (operator == 0) {
|
||||
return rgb2hsv(color);
|
||||
} else if (operator == 1) {
|
||||
return rgb2lab(color);
|
||||
} else if (operator == 2) {
|
||||
return rgb2xyz(color);
|
||||
// HSV
|
||||
} else if (operator == 10) {
|
||||
return hsv2rgb(color);
|
||||
} else if (operator == 11) {
|
||||
return hsv2lab(color);
|
||||
} else if (operator == 12) {
|
||||
return hsv2xyz(color);
|
||||
// LAB
|
||||
} else if (operator == 20) {
|
||||
return lab2rgb(color);
|
||||
} else if (operator == 21) {
|
||||
return lab2hsv(color);
|
||||
} else if (operator == 22) {
|
||||
return lab2xyz(color);
|
||||
// XYZ
|
||||
} else if (operator == 30) {
|
||||
return xyz2rgb(color);
|
||||
} else if (operator == 31) {
|
||||
return xyz2hsv(color);
|
||||
} else if (operator == 32) {
|
||||
return xyz2lab(color);
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
void mainImage(out vec4 fragColor, vec2 fragCoord) {
|
||||
vec2 uv = fragCoord / iResolution.xy;
|
||||
vec4 color = texture(image, uv);
|
||||
vec3 rgb = convertColor(color.rgb, operator);
|
||||
fragColor = vec4(rgb, color.a);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// name: GRAYSCALE
|
||||
// desc: Convert input to grayscale
|
||||
// category: COLOR
|
||||
|
||||
// default grayscale using NTSC conversion weights
|
||||
uniform sampler2D image; // | MASK, RGB or RGBA
|
||||
uniform vec3 convert; // 0.299, 0.587, 0.114; 0; 1; 0.01 | Scalar for each channel
|
||||
|
||||
void mainImage( out vec4 fragColor, vec2 fragCoord ) {
|
||||
vec2 uv = fragCoord / iResolution.xy;
|
||||
vec4 color = texture(image, uv);
|
||||
vec3 gray = vec3(dot(color.rgb, convert));
|
||||
fragColor = vec4(gray, color.a);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// name: HSV ADJUST
|
||||
// desc: Hue, Saturation and Value adjustment control. Maintains alpha/mask.
|
||||
// category: COLOR
|
||||
|
||||
#include .lib/color.lib
|
||||
|
||||
uniform sampler2D image; // | RGB(A) image
|
||||
uniform vec3 HSV; // 0.,1.,1.;-1;2;0.01 | Adjust the Hue, Saturation or Value
|
||||
|
||||
void mainImage(out vec4 fragColor, vec2 fragCoord) {
|
||||
vec2 uv = fragCoord.xy / iResolution.xy;
|
||||
vec4 color = texture(image, uv);
|
||||
vec3 hsv = rgb2hsv(color.rgb);
|
||||
|
||||
hsv.x = mod(hsv.x + HSV.x, 1.0);
|
||||
hsv.y = clamp(hsv.y * HSV.y, 0.0, 1.0);
|
||||
hsv.z = clamp(hsv.z * HSV.z, 0.0, 1.0);
|
||||
fragColor = vec4(hsv2rgb(hsv), color.a);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// name: NORMAL BLEND
|
||||
// desc: Blend two Normal maps
|
||||
// category: COMPOSE
|
||||
|
||||
uniform sampler2D imageA; // | Input image A to blend with image B
|
||||
uniform sampler2D imageB; // | Input image B to blend with image A
|
||||
uniform float blend; // 0.5; 0; 1; 0.01 | Intensity of blend
|
||||
|
||||
void mainImage( out vec4 fragColor, in vec2 fragCoord )
|
||||
{
|
||||
vec2 uv = fragCoord / iResolution.xy;
|
||||
|
||||
vec3 normalA = texture(imageA, uv).rgb * 2.0 - 1.0;
|
||||
normalA = normalize(normalA);
|
||||
|
||||
vec3 normalB = texture(imageB, uv).rgb * 2.0 - 1.0;
|
||||
normalB = normalize(normalB);
|
||||
|
||||
vec3 blendedNormal = normalize(mix(normalA, normalB, blend));
|
||||
fragColor = vec4((blendedNormal * 0.5) + 0.5, 1.0);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// name: BLEND LINEAR
|
||||
// desc: Simple linear blend between two images. Will stretch/shrink images to fit.
|
||||
// category: COMPOSE
|
||||
|
||||
uniform sampler2D imageA; // | MASK, RGB or RGBA
|
||||
uniform sampler2D imageB; // | MASK, RGB or RGBA
|
||||
uniform float blend_amt; // 0.5; 0; 1; 0.01 | Scalar blend amount
|
||||
|
||||
void mainImage( out vec4 fragColor, vec2 fragCoord ) {
|
||||
vec2 uv = fragCoord / iResolution.xy;
|
||||
vec4 col_a = texture(imageA, uv);
|
||||
vec4 col_b = texture(imageB, uv);
|
||||
fragColor = mix(col_b, col_a, blend_amt);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// name: CONICAL GRADIENT
|
||||
// desc: Generate a conical gradient from black to white
|
||||
// category: CREATE
|
||||
|
||||
#include .lib/const.lib
|
||||
|
||||
uniform vec2 origin; // 0.5,0.5;0;1;0.01 | Intensity of base normal
|
||||
uniform vec2 range; // 0.0,1.0;0;1;0.01 | start of range. 0=start. size of range. 1=full range.
|
||||
uniform float angleOffset; // 0.0; 0; 1; 0.01 | offset of the gradient starting angle
|
||||
|
||||
void mainImage( out vec4 fragColor, in vec2 fragCoord )
|
||||
{
|
||||
vec2 uv = fragCoord / iResolution.xy;
|
||||
uv -= origin;
|
||||
float angle = atan(uv.y, uv.x) + angleOffset * -M_TAU;
|
||||
float norm = mod(angle, M_TAU);
|
||||
norm = (norm / M_TAU + range.x) * range.y;
|
||||
fragColor = vec4(vec3(norm), 1.0);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// name: NORMAL
|
||||
// desc: Convert input into a Normal map
|
||||
// category: CREATE
|
||||
|
||||
uniform sampler2D image; // | Input image to convert into a normal map
|
||||
uniform float scalar; // 1.00;0 | Intensity of base normal
|
||||
uniform float detail; // 1.00;0 | Intensity of detail normal
|
||||
uniform bool flip; // | Reverse the Normal direction
|
||||
|
||||
const mat3 scharr_x = mat3(
|
||||
1.0, 10.0/3.0, 1.0,
|
||||
0.0, 0.0, 0.0,
|
||||
-1.0, -10.0/3.0, -1.0
|
||||
);
|
||||
|
||||
const mat3 scharr_y = mat3(
|
||||
1.0, 0.0, -1.0,
|
||||
10.0/3.0, 0.0, -10.0/3.0,
|
||||
1.0, 0.0, -1.0
|
||||
);
|
||||
|
||||
vec3 scharr(vec2 uv) {
|
||||
vec3 result = vec3(0.0);
|
||||
vec2 texelSize = 1.0 / iResolution.xy;
|
||||
|
||||
for (int i = -1; i <= 1; i++) {
|
||||
for (int j = -1; j <= 1; j++) {
|
||||
vec2 offset = vec2(float(i), float(j)) * texelSize;
|
||||
vec3 color = texture(image, uv + offset).rgb;
|
||||
float luminance = dot(color, vec3(0.299, 0.587, 0.114));
|
||||
result.x += luminance * scharr_x[i+1][j+1] * detail;
|
||||
result.y += luminance * scharr_y[i+1][j+1] * detail;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void mainImage( out vec4 fragColor, in vec2 fragCoord )
|
||||
{
|
||||
vec2 uv = fragCoord / iResolution.xy;
|
||||
|
||||
// detailed normal
|
||||
vec3 normal = vec3(0.0, 0.0, 1.0);
|
||||
normal.xy = scharr(uv).xy * scalar;
|
||||
if (flip) {
|
||||
normal.xy = normal.yx;
|
||||
}
|
||||
normal.x *= -scalar;
|
||||
normal = normalize(normal) * 0.5 + 0.5;
|
||||
fragColor = vec4(normal, 1.0);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// The MIT License
|
||||
// https://www.youtube.com/c/InigoQuilez
|
||||
// https://iquilezles.org/
|
||||
// Copyright © 2015 Inigo Quilez
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
// See https://iquilezles.org/articles/palettes for more information
|
||||
//
|
||||
// name: COLOR PALETTE
|
||||
// desc: Color palette creation using the formula: color(t) = a + b * cos[tau(c*t+d)]. See https://iquilezles.org/articles/palettes for more information.
|
||||
// category: CREATE
|
||||
|
||||
#include .lib/const.lib
|
||||
|
||||
uniform vec3 bias; // 0.5,0.5,0.5;0 | scale and bias (dc offset)
|
||||
uniform vec3 amp; // 0.5,0.5,0.5 | contrast and brightness (amplitude)
|
||||
uniform vec3 freq; // 1,1,1 | color cycle (R, G and B) (frequency)
|
||||
uniform vec3 phase; // 0,0,0 | starting offset for the cycle
|
||||
|
||||
void mainImage( out vec4 fragColor, in vec2 fragCoord )
|
||||
{
|
||||
vec2 uv = fragCoord.xy / iResolution.xy;
|
||||
vec3 col = bias + amp * cos(M_PI * (freq * uv.x + phase));
|
||||
fragColor = vec4(col, 1.0);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// name: POSTERIZE
|
||||
// desc: Reduce the pixel color data range
|
||||
// category: FILTER
|
||||
|
||||
uniform sampler2D image; // | RGB(A) image
|
||||
uniform int steps; // 63;1;255;1 | Pixel data range allowed
|
||||
|
||||
void mainImage( out vec4 fragColor, in vec2 fragCoord )
|
||||
{
|
||||
vec2 uv = fragCoord / iResolution.xy;
|
||||
vec4 orig = texture(image, uv.xy);
|
||||
float step = max(1., min(255., float(steps) - 0.5));
|
||||
vec3 color = floor(orig.xyz * step) / step;
|
||||
fragColor = vec4(color, orig.a);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// name: FILTER RANGE
|
||||
// desc: Select pixels from start color through end color. Maintains alpha/mask.
|
||||
// category: FILTER
|
||||
|
||||
uniform sampler2D image; // | RGB(A) image
|
||||
uniform vec3 start; // 0,0,0;0;1;0.01;rgb | Start of the Range
|
||||
uniform vec3 end; // 1,1,1;0;1;0.01;rgb | End of the Range
|
||||
|
||||
void mainImage( out vec4 fragColor, in vec2 fragCoord ) {
|
||||
vec2 uv = fragCoord / iResolution.xy;
|
||||
vec4 color = texture(image, uv);
|
||||
bool isInside = all(greaterThanEqual(color.rgb, start)) && all(lessThanEqual(color.rgb, end));
|
||||
fragColor = vec4(vec3(isInside ? 1.0 : 0.0), color.a);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// name: DIRECTIONAL WARP
|
||||
// desc: Domain warp an image with a direction and distortion map
|
||||
// category: MODIFY
|
||||
|
||||
#include .lib/const.lib
|
||||
|
||||
uniform sampler2D image; // | RGB(A) image
|
||||
uniform sampler2D distortion; // | RGB(A) image used as a LUMA mask for distortion
|
||||
uniform sampler2D direction; // | RGB(A) image used as a LUMA mask for direction
|
||||
uniform float strength; // 64;0;;1 | Pixel data range allowed
|
||||
|
||||
vec2 warp(vec2 uv)
|
||||
{
|
||||
vec4 uv_distortion = texture(distortion, uv);
|
||||
float distortion_val = dot(uv_distortion.rgb, vec3(0.299, 0.587, 0.114));
|
||||
|
||||
vec4 uv_direction = texture(direction, uv);
|
||||
float angle = dot(uv_direction.rgb, vec3(0.299, 0.587, 0.114)) * M_TAU;
|
||||
vec2 direction_val = vec2(cos(angle), sin(angle));
|
||||
uv += direction_val * distortion_val * strength / iResolution.xy;
|
||||
return uv;
|
||||
}
|
||||
|
||||
void mainImage( out vec4 fragColor, in vec2 fragCoord )
|
||||
{
|
||||
vec2 uv = warp(fragCoord / iResolution.xy);
|
||||
fragColor = texture(image, uv);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// name: TRANSFORM
|
||||
// desc: Abuse UV space to create repetitions. Maximums are row: width/4; column: height/4
|
||||
// category: MODIFY
|
||||
|
||||
#include .lib/const.lib
|
||||
|
||||
uniform sampler2D image; // | RGB(A) input to repeat
|
||||
uniform vec2 offset; // 0.0,0.0;-0.5;0.5;0.001 | positional offset (-0.5..0.5)
|
||||
uniform float rotate; // 0;0;1;0.001 | rotation from 0..2pi
|
||||
uniform vec2 tile; // 1.0,1.0;1;2048;1 | repetitions on X and Y
|
||||
|
||||
void mainImage( out vec4 fragColor, in vec2 fragCoord )
|
||||
{
|
||||
// normalize + offset
|
||||
vec2 uv = (fragCoord - offset * iResolution.xy) / iResolution.xy;
|
||||
|
||||
// rotation matrix
|
||||
float cosAngle = cos(rotate * M_TAU);
|
||||
float sinAngle = sin(rotate * M_TAU);
|
||||
mat2 rotationMatrix = mat2(cosAngle, -sinAngle, sinAngle, cosAngle);
|
||||
|
||||
// center rotate, scale
|
||||
uv = rotationMatrix * (uv - 0.5) + 0.5;
|
||||
vec2 repeat = vec2(min(iResolution.x / 4., tile.x), min(iResolution.y / 4., tile.y));
|
||||
uv *= repeat;
|
||||
fragColor = texture(image, uv);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"GLSL (JOV_GL) \ud83c\udf69": "Execute custom GLSL (OpenGL Shading Language) fragment shaders to generate images or apply effects",
|
||||
"GLSL BLEND LINEAR (JOV_GL) \ud83e\uddd9\ud83c\udffd": "Simple linear blend between two images",
|
||||
"GLSL COLOR CONVERSION (JOV_GL) \ud83e\uddd9\ud83c\udffd": "Convert an image from one color space (RGB, HSV, LAB, XYZ) to another",
|
||||
"GLSL COLOR PALETTE (JOV_GL) \ud83e\uddd9\ud83c\udffd": "Color palette creation using the formula: color(t) = a + b * cos[tau(c*t+d)]",
|
||||
"GLSL CONICAL GRADIENT (JOV_GL) \ud83e\uddd9\ud83c\udffd": "Generate a conical gradient from black to white",
|
||||
"GLSL DIRECTIONAL WARP (JOV_GL) \ud83e\uddd9\ud83c\udffd": "Domain warp an image with a direction and distortion map",
|
||||
"GLSL FILTER RANGE (JOV_GL) \ud83e\uddd9\ud83c\udffd": "Select pixels from start color through end color",
|
||||
"GLSL GRAYSCALE (JOV_GL) \ud83e\uddd9\ud83c\udffd": "Convert input to grayscale",
|
||||
"GLSL HSV ADJUST (JOV_GL) \ud83e\uddd9\ud83c\udffd": "Hue, Saturation and Value adjustment control",
|
||||
"GLSL INVERT (JOV_GL) \ud83e\uddd9\ud83c\udffd": "Invert the channels of an image along a scalar [0",
|
||||
"GLSL NORMAL (JOV_GL) \ud83e\uddd9\ud83c\udffd": "Convert input into a Normal map",
|
||||
"GLSL NORMAL BLEND (JOV_GL) \ud83e\uddd9\ud83c\udffd": "Blend two Normal maps",
|
||||
"GLSL POSTERIZE (JOV_GL) \ud83e\uddd9\ud83c\udffd": "Reduce the pixel color data range",
|
||||
"GLSL TRANSFORM (JOV_GL) \ud83e\uddd9\ud83c\udffd": "Abuse UV space to create repetitions"
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
[project]
|
||||
name = "jovi_glsl"
|
||||
description = "Integrates GLSL shader support."
|
||||
version = "1.0.0"
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = [
|
||||
"aenum>=3.1.15,<4",
|
||||
"glfw>=2.8.0",
|
||||
"loguru>=0.7.2",
|
||||
"numpy>=1.26.4",
|
||||
"opencv-contrib-python>=4.10.0.84",
|
||||
"PyOpenGL>=3.1.7"
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://github.com/Amorano/JOVI_GLSL"
|
||||
|
||||
[tool.comfy]
|
||||
PublisherId = "amorano"
|
||||
DisplayName = "JOVI_GLSL"
|
||||
Icon = ""
|
||||
@@ -0,0 +1,6 @@
|
||||
aenum>=3.1.15,<4
|
||||
glfw>=2.8.0
|
||||
loguru>=0.7.2
|
||||
numpy>=1.26.4,<2.0.0
|
||||
opencv-contrib-python>=4.10.0.84
|
||||
PyOpenGL>=3.1.7
|
||||
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* File: node_glsl.js
|
||||
* Project: Jovi_GLSL
|
||||
*
|
||||
*/
|
||||
|
||||
import { api } from "../../scripts/api.js";
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { flashBackgroundColor, widgetHide, widgetSizeModeHook } from './util_jov.js';
|
||||
|
||||
const _id = "GLSL (JOV_GL) 🍩";
|
||||
const EVENT_JOVI_GLSL_ERROR = "jovi-glsl-error";
|
||||
const EVENT_JOVI_GLSL_TIME = "jovi-glsl-time";
|
||||
const RE_VARIABLE = /uniform\s+(\w+)\s+(\w+);(?:\s*\/\/\s*([0-9.,\s]*))?\s*(?:;\s*([0-9.-]+))?\s*(?:;\s*([0-9.-]+))?\s*(?:;\s*([0-9.-]+))?\s*(?:\|\s*(.*))?$/gm
|
||||
|
||||
app.registerExtension({
|
||||
name: 'jovi_glsl.node.' + _id,
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name !== _id) {
|
||||
return;
|
||||
}
|
||||
|
||||
widgetSizeModeHook(nodeType, true);
|
||||
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = async function () {
|
||||
const me = onNodeCreated?.apply(this);
|
||||
const self = this;
|
||||
const widget_time = this.widgets.find(w => w.name == 'TIME');
|
||||
const widget_vertex = this.widgets.find(w => w.name == 'VERTEX');
|
||||
const widget_fragment = this.widgets.find(w => w.name == 'FRAGMENT');
|
||||
widget_vertex.options.menu = false;
|
||||
widget_fragment.options.menu = false;
|
||||
|
||||
// parse this for vars... check existing vars and "types" and keep
|
||||
// or ignore as is the case -- I should stick to a common set of
|
||||
// names/types so mine don't disconnect/rename on a script change.
|
||||
// Parse the GLSL code for uniform variables and initialize corresponding widgets
|
||||
function shader_changed() {
|
||||
let widgets = [];
|
||||
const matches = [...widget_fragment.value.matchAll(RE_VARIABLE)];
|
||||
matches.forEach(match => {
|
||||
const [full_match, varType, varName, varValue] = match;
|
||||
let exist = self.inputs?.find(w => w.name == varName);
|
||||
let type;
|
||||
if (varType == 'int') {
|
||||
type = "INT";
|
||||
} else if (varType == 'float') {
|
||||
type = "FLOAT";
|
||||
} else if (varType == 'bool') {
|
||||
type = "BOOLEAN";
|
||||
} else if (varType.startsWith('ivec') || varType.startsWith('vec')) {
|
||||
const idx = varType[varType.length - 1];
|
||||
type = `VEC${idx}`;
|
||||
if (varType.startsWith('ivec')) {
|
||||
type += 'INT';
|
||||
}
|
||||
} else if (varType == "sampler2D") {
|
||||
type = "IMAGE";
|
||||
}
|
||||
|
||||
if (exist === undefined) {
|
||||
if (["INT", "FLOAT", "BOOLEAN", "IMAGE"].includes(type)) {
|
||||
exist = self.addInput(varName, type);
|
||||
} else if (varType.startsWith('ivec') || varType.startsWith('vec')) {
|
||||
const idx = varType[varType.length - 1];
|
||||
let type = `VEC${idx}`;
|
||||
if (varType.startsWith('ivec')) {
|
||||
type += 'INT';
|
||||
}
|
||||
exist = self.addInput(varName, type);
|
||||
}
|
||||
} else {
|
||||
exist.type = type;
|
||||
}
|
||||
exist.value = varValue;
|
||||
widgets.push(varName);
|
||||
});
|
||||
|
||||
while (self.inputs?.length > widgets.length) {
|
||||
let idx = 0;
|
||||
self.inputs.forEach(i => {
|
||||
if (!widgets.includes(i.name)) {
|
||||
self.removeInput(idx);
|
||||
} else {
|
||||
idx += 1;
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
widget_vertex.inputEl.addEventListener('input', function () {
|
||||
shader_changed();
|
||||
});
|
||||
|
||||
widget_fragment.inputEl.addEventListener('input', function () {
|
||||
shader_changed();
|
||||
});
|
||||
|
||||
function python_glsl_error(event) {
|
||||
if (event.detail.id != self.id) {
|
||||
return;
|
||||
}
|
||||
console.error(event.detail.e);
|
||||
flashBackgroundColor(widget_fragment.inputEl, 250, 3, "#FF2222AA");
|
||||
}
|
||||
|
||||
function python_glsl_time(event) {
|
||||
if (event.detail.id != self.id) {
|
||||
return;
|
||||
}
|
||||
if (!widget_time.hidden) {
|
||||
widget_time.value = event.detail.t;
|
||||
app.canvas.setDirty(true);
|
||||
}
|
||||
}
|
||||
|
||||
api.addEventListener(EVENT_JOVI_GLSL_ERROR, python_glsl_error);
|
||||
api.addEventListener(EVENT_JOVI_GLSL_TIME, python_glsl_time);
|
||||
|
||||
this.onDestroy = () => {
|
||||
api.removeEventListener(EVENT_JOVI_GLSL_ERROR, python_glsl_error);
|
||||
api.removeEventListener(EVENT_JOVI_GLSL_TIME, python_glsl_time);
|
||||
};
|
||||
|
||||
setTimeout(() => { shader_changed(); }, 10);
|
||||
return me;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* File: node_glsl_dynamic.js
|
||||
* Project: Jovi_GLSL
|
||||
*
|
||||
*/
|
||||
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { widgetSizeModeHook, widgetHide } from './util_jov.js'
|
||||
|
||||
const _id = "GLSL DYNAMIC (JOV_GL) 🧙🏽♀️";
|
||||
|
||||
app.registerExtension({
|
||||
name: 'jovi_glsl.node.' + _id,
|
||||
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||||
if (!nodeData.name.endsWith("(JOV_GL) 🧙🏽♀️")) {
|
||||
return;
|
||||
}
|
||||
|
||||
widgetSizeModeHook(nodeType);
|
||||
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = async function () {
|
||||
const me = onNodeCreated?.apply(this);
|
||||
const widget_fragment = this.widgets.find(w => w.name == 'FRAGMENT');
|
||||
widget_fragment.options.menu = false;
|
||||
widgetHide(this, widget_fragment);
|
||||
return me;
|
||||
}
|
||||
}
|
||||
});
|
||||
+620
@@ -0,0 +1,620 @@
|
||||
/**
|
||||
* File: util_jov.js
|
||||
* Project: Jovi_GLSL
|
||||
*
|
||||
*/
|
||||
|
||||
import { app } from "../../scripts/app.js"
|
||||
|
||||
const _REGEX = /\d/;
|
||||
|
||||
const _MAP = {
|
||||
STRING: "📝",
|
||||
BOOLEAN: "🇴",
|
||||
INT: "🔟",
|
||||
FLOAT: "🛟",
|
||||
VEC2: "🇽🇾",
|
||||
COORD2D: "🇽🇾",
|
||||
VEC2INT: "🇽🇾",
|
||||
VEC3: "🇽🇾\u200c🇿",
|
||||
VEC3INT: "🇽🇾\u200c🇿",
|
||||
VEC4: "🇽🇾\u200c🇿\u200c🇼",
|
||||
VEC4INT: "🇽🇾\u200c🇿\u200c🇼",
|
||||
LIST: "🧾",
|
||||
DICT: "📖",
|
||||
IMAGE: "🖼️",
|
||||
MASK: "😷"
|
||||
}
|
||||
|
||||
export const CONVERTED_TYPE = "converted-widget"
|
||||
|
||||
// return the internal mapping type name
|
||||
export function widget_type_name(type) { return _MAP[type];}
|
||||
|
||||
export function widget_get_type(config) {
|
||||
// Special handling for COMBO so we restrict links based on the entries
|
||||
let type = config?.[0]
|
||||
let linkType = type
|
||||
if (type instanceof Array) {
|
||||
type = 'COMBO'
|
||||
linkType = linkType.join(',')
|
||||
}
|
||||
return { type, linkType }
|
||||
}
|
||||
|
||||
export const widgetFind = (widgets, name) => widgets.find(w => w.name == name);
|
||||
|
||||
export const widgetFindOutput = (widgets, name) => {
|
||||
for (let i = 0; i < widgets.length; i++) {
|
||||
if (widgets[i].name == name) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function widgetRemove(node, widgetOrSlot) {
|
||||
let index = 0;
|
||||
if (typeof widgetOrSlot == 'number') {
|
||||
index = widgetOrSlot;
|
||||
}
|
||||
else if (widgetOrSlot) {
|
||||
index = node.widgets.indexOf(widgetOrSlot);
|
||||
}
|
||||
if (index > -1) {
|
||||
const w = node.widgets[index];
|
||||
if (w.canvas) {
|
||||
w.canvas.remove()
|
||||
}
|
||||
if (w.inputEl) {
|
||||
w.inputEl.remove()
|
||||
}
|
||||
w.onRemoved?.()
|
||||
node.widgets.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
export function widgetRemoveAll(node) {
|
||||
if (node.widgets) {
|
||||
for (const w of node.widgets) {
|
||||
widgetRemove(node, w);
|
||||
}
|
||||
node.widgets.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
export function widgetHide(node, widget, suffix = '') {
|
||||
if ((widget?.hidden || false) || widget.type?.startsWith(CONVERTED_TYPE + suffix)) {
|
||||
return;
|
||||
}
|
||||
widget.origType = widget.type;
|
||||
widget.type = CONVERTED_TYPE + suffix;
|
||||
widget.hidden = true;
|
||||
|
||||
widget.origComputeSize = widget.computeSize;
|
||||
widget.computeSize = () => [0, -4];
|
||||
|
||||
widget.origSerializeValue = widget.serializeValue;
|
||||
widget.serializeValue = async () => {
|
||||
// Prevent serializing the widget if we have no input linked
|
||||
if (!node.inputs) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let node_input = node.inputs.find((i) => i.widget?.name == widget.name);
|
||||
if (!node_input || !node_input.link) {
|
||||
return undefined;
|
||||
}
|
||||
return widget.origSerializeValue ? widget.origSerializeValue() : widget.value;
|
||||
}
|
||||
|
||||
// Hide any linked widgets, e.g. seed+seedControl
|
||||
if (widget.linkedWidgets) {
|
||||
for (const w of widget.linkedWidgets) {
|
||||
widgetHide(node, w, ':' + widget.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function widgetShow(widget) {
|
||||
if (widget?.origType) {
|
||||
widget.type = widget.origType;
|
||||
delete widget.origType;
|
||||
}
|
||||
|
||||
widget.computeSize = widget.origComputeSize;
|
||||
delete widget.origComputeSize;
|
||||
|
||||
if (widget.origSerializeValue) {
|
||||
widget.serializeValue = widget.origSerializeValue;
|
||||
delete widget.origSerializeValue;
|
||||
}
|
||||
|
||||
widget.hidden = false;
|
||||
if (widget?.linkedWidgets) {
|
||||
for (const w of widget.linkedWidgets) {
|
||||
widgetShow(w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function widgetShowVector(widget, values={}, type) {
|
||||
widgetShow(widget);
|
||||
if (["FLOAT"].includes(type)) {
|
||||
type = "VEC1";
|
||||
} else if (["INT"].includes(type)) {
|
||||
type = "VEC1INT";
|
||||
} else if (type == "BOOLEAN") {
|
||||
type = "toggle";
|
||||
}
|
||||
|
||||
if (type !== undefined) {
|
||||
widget.type = type;
|
||||
}
|
||||
|
||||
if (widget.value === undefined) {
|
||||
widget.value = widget.options?.default || {};
|
||||
}
|
||||
|
||||
// convert widget.value to pure dict/object
|
||||
if (Array.isArray(widget.value)) {
|
||||
let new_val = {};
|
||||
for (let i = 0; i < widget.value.length; i++) {
|
||||
new_val[i] = widget.value[i];
|
||||
}
|
||||
widget.value = new_val;
|
||||
}
|
||||
|
||||
widget.options.step = 1;
|
||||
widget.options.round = 1;
|
||||
widget.options.precision = 6;
|
||||
if (widget.type != 'toggle') {
|
||||
let size = 1;
|
||||
const match = _REGEX.exec(widget.type);
|
||||
if (match) {
|
||||
size = match[0];
|
||||
}
|
||||
if (!widget.type.endsWith('INT') && widget.type != 'BOOLEAN') {
|
||||
widget.options.step = 0.01;
|
||||
widget.options.round = 0.001;
|
||||
}
|
||||
|
||||
widget.value = {};
|
||||
for (let i = 0; i < size; i++) {
|
||||
widget.value[i] = widget.type.endsWith('INT') ? Math.round(values[i]) : Number(values[i]);
|
||||
}
|
||||
} else {
|
||||
widget.value = values[0] ? true : false;
|
||||
}
|
||||
}
|
||||
|
||||
export function widgetProcessAny(widget, subtype="FLOAT") {
|
||||
widgetShow(widget);
|
||||
//input.type = subtype;
|
||||
if (subtype == "BOOLEAN") {
|
||||
widget.type = "toggle";
|
||||
} else if (subtype == "FLOAT" || subtype == "INT") {
|
||||
widget.type = "number";
|
||||
if (widget?.options) {
|
||||
if (subtype=="FLOAT") {
|
||||
widget.options.precision = 3;
|
||||
widget.options.step = 1;
|
||||
widget.options.round = 0.1;
|
||||
} else {
|
||||
widget.options.precision = 0;
|
||||
widget.options.step = 10;
|
||||
widget.options.round = 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
widget.type = subtype;
|
||||
}
|
||||
}
|
||||
|
||||
export function widgetToWidget(node, widget) {
|
||||
widgetShow(widget);
|
||||
//const sz = node.size;
|
||||
node.removeInput(node.inputs.findIndex((i) => i.widget?.name == widget.name));
|
||||
|
||||
for (const widget of node.widgets) {
|
||||
widget.last_y -= LiteGraph.NODE_SLOT_HEIGHT;
|
||||
}
|
||||
nodeFitHeight(node);
|
||||
|
||||
// Restore original size but grow if needed
|
||||
//node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])]);
|
||||
//node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])]);
|
||||
}
|
||||
|
||||
export function widgetToInput(node, widget, config) {
|
||||
widgetHide(node, widget, '-jov');
|
||||
|
||||
const { linkType } = widget_get_type(config);
|
||||
|
||||
// Add input and store widget config for creating on primitive node
|
||||
//const sz = node.size
|
||||
node.addInput(widget.name, linkType, {
|
||||
widget: { name: widget.name, config },
|
||||
})
|
||||
|
||||
for (const widget of node.widgets) {
|
||||
widget.last_y += LiteGraph.NODE_SLOT_HEIGHT;
|
||||
}
|
||||
nodeFitHeight(node);
|
||||
|
||||
// Restore original size but grow if needed
|
||||
//node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])])
|
||||
}
|
||||
|
||||
export function widgetGetHovered() {
|
||||
if (typeof app == 'undefined') return;
|
||||
|
||||
const node = app.canvas.node_over;
|
||||
if (!node || !node.widgets) return;
|
||||
|
||||
const graphPos = app.canvas.graph_mouse;
|
||||
const x = graphPos[0] - node.pos[0];
|
||||
const y = graphPos[1] - node.pos[1];
|
||||
|
||||
let pos_y;
|
||||
for (const w of node.widgets) {
|
||||
let widgetWidth, widgetHeight;
|
||||
if (w.computeSize) {
|
||||
const sz = w.computeSize();
|
||||
widgetWidth = sz[0] || 0;
|
||||
widgetHeight = sz[1] || 0;
|
||||
} else {
|
||||
widgetWidth = w.width || node.size[0] || 0;
|
||||
widgetHeight = LiteGraph.NODE_WIDGET_HEIGHT;
|
||||
}
|
||||
|
||||
if (pos_y === undefined) {
|
||||
pos_y = w.last_y || 0;
|
||||
}
|
||||
|
||||
if (widgetHeight > 0 && widgetWidth > 0 && w.last_y !== undefined && x >= 6 && x <= widgetWidth - 12 && y >= w.last_y && y <= w.last_y + widgetHeight) {
|
||||
return {
|
||||
widget: w,
|
||||
x1: 6 + node.pos[0],
|
||||
y1: node.pos[1] + w.last_y,
|
||||
x2: node.pos[0] + widgetWidth - 12,
|
||||
y2: node.pos[1] + w.last_y + widgetHeight
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function nodeFitHeight(node) {
|
||||
const size_old = node.size;
|
||||
node.computeSize();
|
||||
node.setSize([Math.max(size_old[0], node.size[0]), Math.min(size_old[1], node.size[1])]);
|
||||
node.setDirtyCanvas(!0, !1);
|
||||
app.graph.setDirtyCanvas(!0, !1);
|
||||
}
|
||||
|
||||
// flash status for each element
|
||||
const flashStatusMap = new Map();
|
||||
|
||||
export async function flashBackgroundColor(element, duration, flashCount, color="red") {
|
||||
if (flashStatusMap.get(element)) {
|
||||
return;
|
||||
}
|
||||
|
||||
flashStatusMap.set(element, true);
|
||||
const originalColor = element.style.backgroundColor;
|
||||
|
||||
for (let i = 0; i < flashCount; i++) {
|
||||
element.style.backgroundColor = color;
|
||||
await new Promise(resolve => setTimeout(resolve, duration / 2));
|
||||
element.style.backgroundColor = originalColor;
|
||||
await new Promise(resolve => setTimeout(resolve, duration / 2));
|
||||
}
|
||||
flashStatusMap.set(element, false);
|
||||
}
|
||||
|
||||
export function widgetSizeModeHook(nodeType, always_wh=false) {
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
const me = onNodeCreated?.apply(this);
|
||||
const wh = this.widgets.find(w => w.name == 'WH');
|
||||
const samp = this.widgets.find(w => w.name == 'SAMPLE');
|
||||
const mode = this.widgets.find(w => w.name == 'MODE');
|
||||
mode.callback = () => {
|
||||
widgetHide(this, wh);
|
||||
widgetHide(this, samp);
|
||||
|
||||
if (always_wh || !['MATTE'].includes(mode.value)) {
|
||||
widgetShow(wh);
|
||||
}
|
||||
if (!['CROP', 'MATTE'].includes(mode.value)) {
|
||||
widgetShow(samp);
|
||||
}
|
||||
nodeFitHeight(this);
|
||||
}
|
||||
setTimeout(() => { mode.callback(); }, 20);
|
||||
return me;
|
||||
}
|
||||
}
|
||||
|
||||
export function widgetOutputHookType(node, control_key, match_output=0) {
|
||||
const combo = node.widgets.find(w => w.name == control_key);
|
||||
const output = node.outputs[match_output];
|
||||
|
||||
if (!output || !combo) {
|
||||
throw new Error("Required widgets not found");
|
||||
}
|
||||
|
||||
const oldCallback = combo.callback;
|
||||
combo.callback = () => {
|
||||
const me = oldCallback?.apply(this, arguments);
|
||||
node.outputs[match_output].name = widget_type_name(combo.value);
|
||||
node.outputs[match_output].type = combo.value;
|
||||
return me;
|
||||
}
|
||||
setTimeout(() => { combo.callback(); }, 10);
|
||||
}
|
||||
|
||||
/*
|
||||
* matchFloatSize forces the target to be float[n] based on its type size
|
||||
*/
|
||||
export function widgetHookAB(node, control_key, output_type_match=true) {
|
||||
|
||||
const AA = node.widgets.find(w => w.name == '🅰️🅰️');
|
||||
const BB = node.widgets.find(w => w.name == '🅱️🅱️');
|
||||
const combo = node.widgets.find(w => w.name == control_key);
|
||||
|
||||
if (combo === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
widgetHookControl(node, control_key, AA);
|
||||
widgetHookControl(node, control_key, BB);
|
||||
if (output_type_match) {
|
||||
widgetOutputHookType(node, control_key);
|
||||
}
|
||||
setTimeout(() => { combo.callback(); }, 5);
|
||||
|
||||
return combo;
|
||||
};
|
||||
|
||||
/*
|
||||
* matchFloatSize forces the target to be float[n] based on its type size
|
||||
*/
|
||||
export function widgetHookControl(node, control_key, target, matchFloatSize=false) {
|
||||
const initializeTrack = (widget) => {
|
||||
const track = {};
|
||||
for (let i = 0; i < 4; i++) {
|
||||
track[i] = widget.options?.default[i];
|
||||
}
|
||||
Object.assign(track, widget.value);
|
||||
return track;
|
||||
};
|
||||
|
||||
const { widgets } = node;
|
||||
const combo = widgets.find(w => w.name == control_key);
|
||||
|
||||
if (!target || !combo) {
|
||||
throw new Error("Required widgets not found");
|
||||
}
|
||||
|
||||
const data = {
|
||||
//track_xyzw: target.options?.default, //initializeTrack(target),
|
||||
track_xyzw: initializeTrack(target),
|
||||
target,
|
||||
combo
|
||||
};
|
||||
|
||||
const oldCallback = combo.callback;
|
||||
combo.callback = () => {
|
||||
const me = oldCallback?.apply(this, arguments);
|
||||
widgetHide(node, target, "-jov");
|
||||
if (["VEC2", "VEC2INT", "COORD2D", "VEC3", "VEC3INT", "VEC4", "VEC4INT", "BOOLEAN", "INT", "FLOAT"].includes(combo.value)) {
|
||||
let type = combo.value;
|
||||
if (matchFloatSize) {
|
||||
type = "FLOAT";
|
||||
if (["VEC2", "VEC2INT", "COORD2D"].includes(combo.value)) {
|
||||
type = "VEC2";
|
||||
} else if (["VEC3", "VEC3INT"].includes(combo.value)) {
|
||||
type = "VEC3";
|
||||
} else if (["VEC4", "VEC4INT"].includes(combo.value)) {
|
||||
type = "VEC4";
|
||||
}
|
||||
}
|
||||
widgetShowVector(target, data.track_xyzw, type);
|
||||
}
|
||||
nodeFitHeight(node);
|
||||
return me;
|
||||
}
|
||||
|
||||
target.options.menu = false;
|
||||
target.callback = () => {
|
||||
if (target.type == "toggle") {
|
||||
data.track_xyzw[0] = target.value ? 1 : 0;
|
||||
} else {
|
||||
Object.keys(target.value).forEach((key) => {
|
||||
data.track_xyzw[key] = target.value[key];
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
function arrayToObject(values, length, parseFn) {
|
||||
const result = {};
|
||||
for (let i = 0; i < length; i++) {
|
||||
result[i] = parseFn(values[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function domRenderTemplate(template, data) {
|
||||
for (const key in data) {
|
||||
if (Object.prototype.hasOwnProperty.call(data, key)) {
|
||||
const regex = new RegExp(`{{\\s*${key}\\s*}}`, 'g')
|
||||
template = template.replace(regex, data[key])
|
||||
}
|
||||
}
|
||||
return template
|
||||
}
|
||||
|
||||
export function domFoldableToggle(elementId, symbolId) {
|
||||
const content = document.getElementById(elementId)
|
||||
const symbol = document.getElementById(symbolId)
|
||||
if (content.style.display == 'none' || content.style.display == '') {
|
||||
content.style.display = 'flex'
|
||||
symbol.innerHTML = '▽' // Down arrow
|
||||
} else {
|
||||
content.style.display = 'none'
|
||||
symbol.innerHTML = '▷' // Right arrow
|
||||
}
|
||||
}
|
||||
|
||||
export function domInnerValueChange(node, pos, widget, value, event=undefined) {
|
||||
const type = widget.type.includes("INT") ? Number : parseFloat
|
||||
widget.value = arrayToObject(value, Object.keys(value).length, type);
|
||||
if (
|
||||
widget.options &&
|
||||
widget.options.property &&
|
||||
node.properties[widget.options.property] !== undefined
|
||||
) {
|
||||
node.setProperty(widget.options.property, widget.value)
|
||||
}
|
||||
if (widget.callback) {
|
||||
|
||||
widget.callback(widget.value, app.canvas, node, pos, event)
|
||||
}
|
||||
}
|
||||
|
||||
export function domWidgetOffset(
|
||||
widget,
|
||||
ctx,
|
||||
node,
|
||||
widgetWidth,
|
||||
widgetY,
|
||||
height
|
||||
) {
|
||||
const margin = 10
|
||||
const elRect = ctx.canvas.getBoundingClientRect()
|
||||
const transform = new DOMMatrix()
|
||||
.scaleSelf(
|
||||
elRect.width / ctx.canvas.width,
|
||||
elRect.height / ctx.canvas.height
|
||||
)
|
||||
.multiplySelf(ctx.getTransform())
|
||||
.translateSelf(0, widgetY + margin)
|
||||
|
||||
const scale = new DOMMatrix().scaleSelf(transform.a, transform.d)
|
||||
Object.assign(widget.inputEl.style, {
|
||||
transformOrigin: '0 0',
|
||||
transform: scale,
|
||||
left: `${transform.e}px`,
|
||||
top: `${transform.d + transform.f}px`,
|
||||
width: `${widgetWidth}px`,
|
||||
height: `${(height || widget.parent?.inputHeight || 32) - margin}px`,
|
||||
position: 'absolute',
|
||||
background: !node.color ? '' : node.color,
|
||||
color: !node.color ? '' : 'white',
|
||||
zIndex: 5, //app.graph._nodes.indexOf(node),
|
||||
})
|
||||
}
|
||||
|
||||
export function domEscapeHtml(unsafe) {
|
||||
return unsafe
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
export function domShowModal(innerHTML, eventCallback, timeout=null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const modal = document.createElement("div");
|
||||
modal.className = "modal";
|
||||
modal.innerHTML = innerHTML;
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// center
|
||||
const modalContent = modal.querySelector(".jov-modal-content");
|
||||
modalContent.style.position = "absolute";
|
||||
modalContent.style.left = "50%";
|
||||
modalContent.style.top = "50%";
|
||||
modalContent.style.transform = "translate(-50%, -50%)";
|
||||
|
||||
let timeoutId;
|
||||
|
||||
const handleEvent = (event) => {
|
||||
const targetId = event.target.id;
|
||||
const result = eventCallback(targetId);
|
||||
|
||||
if (result != null) {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = null;
|
||||
}
|
||||
modal.remove();
|
||||
resolve(result);
|
||||
}
|
||||
};
|
||||
modalContent.addEventListener("click", handleEvent);
|
||||
modalContent.addEventListener("dblclick", handleEvent);
|
||||
|
||||
if (timeout) {
|
||||
timeout *= 1000;
|
||||
timeoutId = setTimeout(() => {
|
||||
modal.remove();
|
||||
reject(new Error("TIMEOUT"));
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
//setTimeout(() => {
|
||||
// modal.dispatchEvent(new Event('tick'));
|
||||
//}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
export function colorHex2RGB(hex) {
|
||||
hex = hex.replace(/^#/, '');
|
||||
const bigint = parseInt(hex, 16);
|
||||
const r = (bigint >> 16) & 255;
|
||||
const g = (bigint >> 8) & 255;
|
||||
const b = bigint & 255;
|
||||
return [r, g, b];
|
||||
}
|
||||
|
||||
/*
|
||||
* Parse a string "255,255,255,255" or a List[255,255,255,255] into hex
|
||||
*/
|
||||
export function colorRGB2Hex(input) {
|
||||
const rgbArray = typeof input == 'string' ? input.match(/\d+/g) : input;
|
||||
if (rgbArray.length < 3) {
|
||||
throw new Error('input not 3 or 4 values');
|
||||
}
|
||||
const hexValues = rgbArray.map((value, index) => {
|
||||
if (index == 3 && !value) return 'ff';
|
||||
const hex = parseInt(value).toString(16);
|
||||
return hex.length == 1 ? '0' + hex : hex;
|
||||
});
|
||||
return '#' + hexValues.slice(0, 3).join('') + (hexValues[3] || '');
|
||||
}
|
||||
|
||||
export function colorLerpHex(colorStart, colorEnd, lerp) {
|
||||
// Parse color strings into RGB arrays
|
||||
const startRGB = colorHex2RGB(colorStart);
|
||||
const endRGB = colorHex2RGB(colorEnd);
|
||||
|
||||
// Linearly interpolate each RGB component
|
||||
const lerpedRGB = startRGB.map((component, index) => {
|
||||
return Math.round(component + (endRGB[index] - component) * lerp);
|
||||
});
|
||||
|
||||
// Convert the interpolated RGB values back to a hex color string
|
||||
return colorRGB2Hex(lerpedRGB);
|
||||
}
|
||||
|
||||
export function colorContrast(hexColor) {
|
||||
const rgb = colorHex2RGB(hexColor);
|
||||
const L = 0.2126 * rgb[0] / 255. + 0.7152 * rgb[1] / 255. + 0.0722 * rgb[2] / 255.;
|
||||
return L > 0.790 ? "#000" : "#CCC";
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
/**
|
||||
* File: widget_vector.js
|
||||
* Project: Jovi_GLSL
|
||||
*/
|
||||
|
||||
import { app } from "../../scripts/app.js"
|
||||
import { widgetToInput, widgetToWidget, domInnerValueChange, colorHex2RGB, colorRGB2Hex } from './util_jov.js'
|
||||
import { $el } from "../../scripts/ui.js"
|
||||
/** @import { IWidget, LGraphCanvas } from '../../types/litegraph/litegraph.d.ts' */
|
||||
|
||||
const VectorWidget = (app, inputName, options, initial, desc='') => {
|
||||
const values = options[1]?.default || initial;
|
||||
/** @type {IWidget} */
|
||||
const widget = {
|
||||
name: inputName,
|
||||
type: options[0],
|
||||
y: 0,
|
||||
value: values,
|
||||
options: options[1]
|
||||
}
|
||||
|
||||
if (widget.options?.rgb || false) {
|
||||
widget.options.maj = 255;
|
||||
widget.options.mij = 0;
|
||||
widget.options.label = ['🟥', '🟩', '🟦', 'ALPHA'];
|
||||
}
|
||||
|
||||
if (options[0].endsWith('INT')) {
|
||||
widget.options.step = 1;
|
||||
widget.options.round = 1;
|
||||
widget.options.precision = 0;
|
||||
widget.options.step = 1;
|
||||
} else {
|
||||
if (widget.options?.rgb || false) {
|
||||
widget.options.maj = 1;
|
||||
}
|
||||
widget.options.precision = widget.options?.precision || 6;
|
||||
widget.options.step = widget.options?.step || 0.0075;
|
||||
widget.options.round = widget.options?.round || 1 / 10 ** widget.options.step;
|
||||
}
|
||||
|
||||
const offset_y = 4;
|
||||
const widget_padding_left = 13;
|
||||
const widget_padding = 30;
|
||||
const label_full = 72;
|
||||
const label_center = label_full/2;
|
||||
/** @type {HTMLInputElement} */
|
||||
let picker;
|
||||
|
||||
widget.draw = function(ctx, node, width, Y, height) {
|
||||
if ((app.canvas.ds.scale < 0.50) || (!this.type.startsWith("VEC") && this.type != "COORD2D")) return;
|
||||
ctx.save()
|
||||
ctx.beginPath()
|
||||
ctx.lineWidth = 1
|
||||
ctx.fillStyle = LiteGraph.WIDGET_OUTLINE_COLOR
|
||||
ctx.roundRect(widget_padding_left+2, Y, width - widget_padding, height, 15)
|
||||
ctx.stroke()
|
||||
ctx.lineWidth = 1
|
||||
ctx.fillStyle = LiteGraph.WIDGET_BGCOLOR
|
||||
ctx.roundRect(widget_padding_left+2, Y, width - widget_padding, height, 15)
|
||||
ctx.fill()
|
||||
|
||||
// label
|
||||
ctx.fillStyle = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR
|
||||
ctx.fillText(inputName, label_center - (inputName.length * 1.5), Y + height / 2 + offset_y)
|
||||
let x = label_full + 1
|
||||
|
||||
const fields = Object.keys(this?.value || []);
|
||||
let count = fields.length;
|
||||
if (widget.options?.rgb) {
|
||||
count += 0.23;
|
||||
}
|
||||
const element_width = (width - label_full - widget_padding) / count;
|
||||
const element_width2 = element_width / 2;
|
||||
|
||||
let converted = [];
|
||||
for (const idx of fields) {
|
||||
ctx.save()
|
||||
ctx.beginPath()
|
||||
ctx.fillStyle = LiteGraph.WIDGET_OUTLINE_COLOR
|
||||
// separation bar
|
||||
if (idx != fields.length || (idx == fields.length && !this.options?.rgb)) {
|
||||
ctx.moveTo(x, Y)
|
||||
ctx.lineTo(x, Y+height)
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// value
|
||||
ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR
|
||||
const it = this.value[idx.toString()];
|
||||
const precision = widget.options?.precision || 0;
|
||||
let value = Number(it);
|
||||
if (precision > 0) {
|
||||
value = value.toFixed(Math.min(2, precision));
|
||||
}
|
||||
converted.push(value);
|
||||
const text = value.toString();
|
||||
ctx.fillText(text, x + element_width2 - text.length * 3.3, Y + height/2 + offset_y);
|
||||
ctx.restore();
|
||||
x += element_width;
|
||||
}
|
||||
|
||||
if (this.options?.rgb && converted.length > 2) {
|
||||
try {
|
||||
ctx.fillStyle = colorRGB2Hex(converted);
|
||||
} catch (e) {
|
||||
console.error(converted, e);
|
||||
ctx.fillStyle = "#FFF";
|
||||
}
|
||||
ctx.roundRect(width - 1.17 * widget_padding, Y+1, 19, height-2, 16);
|
||||
ctx.fill()
|
||||
}
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
function clamp(widget, v, idx) {
|
||||
v = Math.min(v, widget.options?.maj !== undefined ? widget.options.maj : v);
|
||||
v = Math.max(v, widget.options?.mij !== undefined ? widget.options.mij : v);
|
||||
const precision = widget.options?.precision || 0;
|
||||
widget.value[idx] = (precision == 0) ? Number(v) : parseFloat(v).toFixed(precision);
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo ▶️, 🖱️, 😀
|
||||
* @this IWidget
|
||||
*/
|
||||
widget.onPointerDown = function (pointer, node, canvas) {
|
||||
const e = pointer.eDown
|
||||
const x = e.canvasX - node.pos[0] - label_full;
|
||||
const size = Object.keys(this.value).length;
|
||||
const element_width = (node.size[0] - label_full - widget_padding * 1.25) / size;
|
||||
const index = Math.floor(x / element_width);
|
||||
|
||||
pointer.onClick = (eUp) => {
|
||||
if (index >= 0 && index < size) {
|
||||
const pos = [eUp.canvasX - node.pos[0], eUp.canvasY - node.pos[1]]
|
||||
const old_value = { ...this.value };
|
||||
const label = this.options?.label ? this.name + '➖' + this.options.label?.[index] : this.name;
|
||||
|
||||
LGraphCanvas.active_canvas.prompt(label, this.value[index], function(v) {
|
||||
if (/^[0-9+\-*/()\s]+|\d+\.\d+$/.test(v)) {
|
||||
try {
|
||||
v = eval(v);
|
||||
} catch {
|
||||
// Suppressed exception
|
||||
}
|
||||
}
|
||||
if (this.value[index] != v) {
|
||||
setTimeout(
|
||||
function () {
|
||||
clamp(this, v, index);
|
||||
domInnerValueChange(node, pos, this, this.value, eUp);
|
||||
}.bind(this), 20)
|
||||
}
|
||||
}.bind(this), eUp);
|
||||
|
||||
if (old_value != this.value) {
|
||||
setTimeout(
|
||||
function () {
|
||||
domInnerValueChange(node, pos, this, this.value, eUp);
|
||||
}.bind(this), 20);
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
if (!this.options?.rgb) return;
|
||||
|
||||
const rgba = Object.values(this?.value || []);
|
||||
const color = colorRGB2Hex(rgba.slice(0, 3));
|
||||
|
||||
if (index != size && (x < 0 && rgba.length > 2)) {
|
||||
const target = Object.values(rgba.map((item) => 255 - item)).slice(0, 3);
|
||||
this.value = Object.values(this.value);
|
||||
this.value.splice(0, 3, ...target);
|
||||
return
|
||||
}
|
||||
|
||||
if (!picker) {
|
||||
// firefox?
|
||||
//position: "absolute", // Use absolute positioning for consistency
|
||||
//left: `${eUp.pageX}px`, // Use pageX for more consistent placement
|
||||
//top: `${eUp.pageY}px`,
|
||||
picker = $el("input", {
|
||||
type: "color",
|
||||
parent: document.body,
|
||||
style: {
|
||||
position: "fixed",
|
||||
left: `${eUp.clientX}px`,
|
||||
top: `${eUp.clientY}px`,
|
||||
height: "0px",
|
||||
width: "0px",
|
||||
padding: "0px",
|
||||
opacity: 0,
|
||||
},
|
||||
});
|
||||
picker.addEventListener('blur', () => picker.style.display = 'none')
|
||||
picker.addEventListener('input', () => {
|
||||
if (!picker.value) return;
|
||||
|
||||
widget.value = colorHex2RGB(picker.value);
|
||||
if (rgba.length > 3) {
|
||||
widget.value.push(rgba[3]);
|
||||
}
|
||||
canvas.setDirty(true)
|
||||
})
|
||||
} else {
|
||||
picker.style.display = 'revert'
|
||||
picker.style.left = `${eUp.clientX}px`
|
||||
picker.style.top = `${eUp.clientY}px`
|
||||
}
|
||||
picker.value = color;
|
||||
requestAnimationFrame(() => {
|
||||
picker.showPicker()
|
||||
picker.focus()
|
||||
})
|
||||
}
|
||||
|
||||
pointer.onDrag = (eMove) => {
|
||||
if (!eMove.deltaX || !(index > -1)) return;
|
||||
|
||||
let v = parseFloat(this.value[index]);
|
||||
v += this.options.step * Math.sign(eMove.deltaX);
|
||||
clamp(this, v, index);
|
||||
}
|
||||
}
|
||||
|
||||
widget.serializeValue = async () => {
|
||||
const value = widget.value;
|
||||
if (value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.reduce((acc, tuple, index) => ({ ...acc, [index]: tuple }), {});
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
widget.desc = desc;
|
||||
return widget;
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "jovi_glsl.widget.spinner",
|
||||
async getCustomWidgets(app) {
|
||||
return {
|
||||
VEC2: (node, inputName, inputData, app) => ({
|
||||
widget: node.addCustomWidget(VectorWidget(app, inputName, inputData, [0, 0])),
|
||||
}),
|
||||
VEC3: (node, inputName, inputData, app) => ({
|
||||
widget: node.addCustomWidget(VectorWidget(app, inputName, inputData, [0, 0, 0])),
|
||||
}),
|
||||
VEC4: (node, inputName, inputData, app) => ({
|
||||
widget: node.addCustomWidget(VectorWidget(app, inputName, inputData, [0, 0, 0, 0])),
|
||||
}),
|
||||
VEC2INT: (node, inputName, inputData, app) => ({
|
||||
widget: node.addCustomWidget(VectorWidget(app, inputName, inputData, [0, 0])),
|
||||
}),
|
||||
VEC3INT: (node, inputName, inputData, app) => ({
|
||||
widget: node.addCustomWidget(VectorWidget(app, inputName, inputData, [0, 0, 0])),
|
||||
}),
|
||||
VEC4INT: (node, inputName, inputData, app) => ({
|
||||
widget: node.addCustomWidget(VectorWidget(app, inputName, inputData, [0, 0, 0, 0])),
|
||||
})
|
||||
}
|
||||
},
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
const myTypes = ['RGB', 'VEC2', 'VEC3', 'VEC4', 'VEC2INT', 'VEC3INT', 'VEC4INT']
|
||||
const inputTypes = nodeData.input;
|
||||
if (inputTypes) {
|
||||
const matchingTypes = ['required', 'optional']
|
||||
.flatMap(type => Object.entries(inputTypes[type] || [])
|
||||
.filter(([_, value]) => myTypes.includes(value[0]))
|
||||
);
|
||||
|
||||
// CLEANUP ON REMOVE
|
||||
if (matchingTypes.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// MENU CONVERSIONS
|
||||
const getExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
|
||||
nodeType.prototype.getExtraMenuOptions = function (_, options) {
|
||||
const me = getExtraMenuOptions?.apply(this, arguments);
|
||||
const widgetToInputArray = [];
|
||||
for (const [widgetName, additionalInfo] of matchingTypes) {
|
||||
const widget = Object.values(this.widgets).find(m => m.name == widgetName);
|
||||
if (myTypes.includes(widget.type) || widget.type.endsWith('-jov')) {
|
||||
if (!widget.hidden) {
|
||||
const widgetToInputObject = {
|
||||
content: `Convert ${widgetName} to input`,
|
||||
callback: () => widgetToInput(this, widget, additionalInfo)
|
||||
};
|
||||
widgetToInputArray.push(widgetToInputObject);
|
||||
} else {
|
||||
const widgetToInputObject = {
|
||||
content: `Convert ${widgetName} to widget`,
|
||||
callback: () => widgetToWidget(this, widget, additionalInfo)
|
||||
};
|
||||
widgetToInputArray.push(widgetToInputObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (widgetToInputArray.length) {
|
||||
options.push(...widgetToInputArray, null);
|
||||
}
|
||||
return me;
|
||||
};
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user