base commit

This commit is contained in:
Alexander G. Morano
2024-12-19 21:31:19 -05:00
commit dd8592af9d
39 changed files with 4626 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto
+21
View File
@@ -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*.*
+23
View File
@@ -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.
+78
View File
@@ -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">
![KNIVES!](https://badgen.net/github/open-issues/Amorano/JOVI_GLSL)
![FORKS!](https://badgen.net/github/forks/Amorano/JOVI_GLSL)
</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">
[![BE A GITHUB SPONSOR ❤️](https://img.shields.io/badge/sponsor-30363D?style=for-the-badge&logo=GitHub-Sponsors&logoColor=#EA4AAA)](https://github.com/sponsors/Amorano)
[![DIRECTLY SUPPORT ME VIA PAYPAL](https://img.shields.io/badge/PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white)](https://www.paypal.com/paypalme/onarom)
[![PATREON SUPPORTER](https://img.shields.io/badge/Patreon-F96854?style=for-the-badge&logo=patreon&logoColor=white)](https://www.patreon.com/joviex)
[![SUPPORT ME ON KO-FI!](https://ko-fi.com/img/githubbutton_sm.svg)](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 [![DISCORD](https://dcbadge.vercel.app/api/server/62TJaZ3Z5r?style=flat-square)](https://discord.gg/62TJaZ3Z5r).
+188
View File
@@ -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()
+529
View File
@@ -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
View File
@@ -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
+8
View File
@@ -0,0 +1,8 @@
// Standard Fragment Shader Footer
layout(location = 0) out vec4 _fragColor;
void main()
{
mainImage(_fragColor, gl_FragCoord.xy);
}
+10
View File
@@ -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);
}
+30
View File
@@ -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;
};
+11
View File
@@ -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);
}
+34
View File
@@ -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);
}
+138
View File
@@ -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 ));
}
+83
View File
@@ -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;
}
+68
View File
@@ -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);
}
+784
View File
@@ -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];
}
+261
View File
@@ -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;
}
+57
View File
@@ -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;
}
+53
View File
@@ -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
);
}
+13
View File
@@ -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);
}
+52
View File
@@ -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);
}
+14
View File
@@ -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);
}
+19
View File
@@ -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);
}
+21
View File
@@ -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);
}
+14
View File
@@ -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);
}
+19
View File
@@ -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);
}
+51
View File
@@ -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);
}
+24
View File
@@ -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);
}
+15
View File
@@ -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);
}
+14
View File
@@ -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);
}
+28
View File
@@ -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);
}
+27
View File
@@ -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);
}
+16
View File
@@ -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"
}
+21
View File
@@ -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 = ""
+6
View File
@@ -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
+130
View File
@@ -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;
}
}
});
+30
View File
@@ -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
View File
@@ -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 = '&#9661' // Down arrow
} else {
content.style.display = 'none'
symbol.innerHTML = '&#9655' // 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
}
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";
}
+311
View File
@@ -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;
};
}
}
})