942 lines
36 KiB
Python
942 lines
36 KiB
Python
'''
|
|
Copyright (C) 2023 CG Cookie
|
|
http://cgcookie.com
|
|
hello@cgcookie.com
|
|
|
|
Created by Jonathan Denning, Jonathan Williamson
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
'''
|
|
|
|
|
|
#######################################################################
|
|
# THE FOLLOWING FUNCTIONS ARE ONLY FOR THE TRANSITION FROM BGL TO GPU #
|
|
# THIS FILE **SHOULD** GO AWAY ONCE WE DROP SUPPORT FOR BLENDER 2.83 #
|
|
# AROUND JUNE 2023 AS BLENDER 2.93 HAS GPU MODULE #
|
|
#######################################################################
|
|
|
|
import os
|
|
import re
|
|
import traceback
|
|
from inspect import isroutine
|
|
from itertools import chain
|
|
from contextlib import contextmanager
|
|
|
|
import bpy
|
|
import gpu
|
|
|
|
from mathutils import Matrix, Vector
|
|
|
|
from .blender import get_path_from_addon_common
|
|
from .globals import Globals
|
|
from .decorators import only_in_blender_version, warn_once, add_cache
|
|
from .maths import mid
|
|
from .utils import Dict
|
|
from ..terminal import term_printer
|
|
|
|
|
|
# note: not all supported by user system, but we don't need full functionality
|
|
# https://en.wikipedia.org/wiki/OpenGL_Shading_Language#Versions
|
|
# OpenGL GLSL OpenGL GLSL
|
|
# 2.0 110 4.0 400
|
|
# 2.1 120 4.1 410
|
|
# 3.0 130 4.2 420
|
|
# 3.1 140 4.3 430
|
|
# 3.2 150 4.4 440
|
|
# 3.3 330 4.5 450
|
|
# 4.6 460
|
|
|
|
|
|
if bpy.app.version < (3,4,0):
|
|
use_bgl_default = True
|
|
use_gpu_default = False
|
|
use_gpu_scissor = False
|
|
elif bpy.app.version < (3,5,1):
|
|
use_bgl_default = False # gpu.platform.backend_type_get() in {'OPENGL',}
|
|
use_gpu_default = True # not use_bgl_default
|
|
use_gpu_scissor = False
|
|
else:
|
|
use_bgl_default = False # gpu.platform.backend_type_get() in {'OPENGL',}
|
|
use_gpu_default = True # not use_bgl_default
|
|
use_gpu_scissor = True
|
|
|
|
print(f'Addon Common: {use_bgl_default=} {use_gpu_default=} {use_gpu_scissor=}')
|
|
|
|
def get_blend(): return gpu.state.blend_get()
|
|
def blend(mode, *, use_gpu=use_gpu_default, use_bgl=use_bgl_default, only=None):
|
|
assert use_gpu or use_bgl
|
|
if use_bgl:
|
|
import bgl
|
|
if only != 'function':
|
|
if mode == 'NONE':
|
|
bgl.glDisable(bgl.GL_BLEND)
|
|
else:
|
|
bgl.glEnable(bgl.GL_BLEND)
|
|
if only != 'enable':
|
|
map_mode_bgl = {
|
|
'ALPHA': (bgl.GL_SRC_ALPHA, bgl.GL_ONE_MINUS_SRC_ALPHA),
|
|
'ALPHA_PREMULT': (bgl.GL_ONE, bgl.GL_ONE_MINUS_SRC_ALPHA),
|
|
'ADDITIVE': (bgl.GL_SRC_ALPHA, bgl.GL_ONE),
|
|
'ADDITIVE_PREMULT': (bgl.GL_ONE, bgl.GL_ONE),
|
|
'MULTIPLY': (bgl.GL_DST_COLOR, bgl.GL_ZERO),
|
|
'SUBTRACT': (bgl.GL_ONE, bgl.GL_ONE),
|
|
'INVERT': (bgl.GL_ONE_MINUS_DST_COLOR, bgl.GL_ZERO),
|
|
}
|
|
bgl.glBlendFunc(*map_mode_bgl[mode])
|
|
if use_gpu:
|
|
if not only:
|
|
gpu.state.blend_set(mode)
|
|
elif only == 'enable':
|
|
if (mode == 'NONE') != (gpu.state.blend_get() == 'NONE'):
|
|
# enabled-ness is different (one is enabled and other disabled)
|
|
gpu.state.blend_set(mode)
|
|
elif only == 'function':
|
|
if gpu.state.blend_get() != 'NONE':
|
|
# only set when blending is already enabled
|
|
gpu.state.blend_set(mode)
|
|
|
|
|
|
def depth_test(mode, *, use_gpu=use_gpu_default, use_bgl=use_bgl_default):
|
|
assert use_gpu or use_bgl
|
|
if use_bgl:
|
|
import bgl
|
|
if mode == 'NONE':
|
|
bgl.glDisable(bgl.GL_DEPTH_TEST)
|
|
else:
|
|
bgl.glEnable(bgl.GL_DEPTH_TEST)
|
|
map_mode_bgl = {
|
|
'NEVER': bgl.GL_NEVER,
|
|
'LESS': bgl.GL_LESS,
|
|
'EQUAL': bgl.GL_EQUAL,
|
|
'LESS_EQUAL': bgl.GL_LEQUAL,
|
|
'GREATER': bgl.GL_GREATER,
|
|
'GREATER_EQUAL': bgl.GL_GEQUAL,
|
|
'ALWAYS': bgl.GL_ALWAYS,
|
|
# NOTE: no equivalent for `bgl.GL_NOTEQUAL` in `gpu` module as of Blender 3.5.1
|
|
}
|
|
bgl.glDepthFunc(map_mode_bgl[mode])
|
|
if use_gpu:
|
|
gpu.state.depth_test_set(mode)
|
|
def get_depth_test(*, use_gpu=use_gpu_default, use_bgl=use_bgl_default):
|
|
assert use_gpu or use_bgl
|
|
if use_bgl:
|
|
return bgl_get_integerv('GL_DEPTH_FUNC')
|
|
if use_gpu:
|
|
return gpu.state.depth_test_get()
|
|
|
|
def depth_mask(enable, *, use_gpu=use_gpu_default, use_bgl=use_bgl_default):
|
|
assert use_gpu or use_bgl
|
|
if use_bgl:
|
|
import bgl
|
|
bgl.glDepthMask(bgl.GL_TRUE if enable else bgl.GL_FALSE)
|
|
if use_gpu:
|
|
gpu.state.depth_mask_set(enable)
|
|
def get_depth_mask(*, use_gpu=use_gpu_default, use_bgl=use_bgl_default):
|
|
assert use_gpu or use_bgl
|
|
if use_bgl:
|
|
return bgl_get_integerv('GL_DEPTH_WRITEMASK')
|
|
if use_gpu:
|
|
return gpu.state.depth_mask_get()
|
|
|
|
def line_width(width): gpu.state.line_width_set(width)
|
|
def get_line_width(): return gpu.state.line_width_get()
|
|
|
|
def point_size(size): gpu.state.point_size_set(size)
|
|
def get_point_size(): return gpu.state.point_size_get()
|
|
|
|
def scissor(left, bottom, width, height, *, use_gpu=use_gpu_default, use_bgl=use_bgl_default):
|
|
assert use_gpu or use_bgl
|
|
if use_bgl or (not use_gpu_scissor):
|
|
import bgl
|
|
bgl.glScissor(left, bottom, width, height)
|
|
if use_gpu and use_gpu_scissor:
|
|
gpu.state.scissor_set(left, bottom, width, height)
|
|
def get_scissor(*, use_gpu=use_gpu_default, use_bgl=use_bgl_default):
|
|
assert use_gpu or use_bgl
|
|
if use_bgl or (not use_gpu_scissor):
|
|
return bgl_get_integerv_tuple('GL_SCISSOR_BOX', 4)
|
|
if use_gpu and use_gpu_scissor:
|
|
return gpu.state.scissor_get()
|
|
|
|
def scissor_test(enable, *, use_gpu=use_gpu_default, use_bgl=use_bgl_default):
|
|
assert use_gpu or use_bgl
|
|
if use_bgl or (not use_gpu_scissor):
|
|
bgl_enable('GL_SCISSOR_TEST', enable)
|
|
if use_gpu and use_gpu_scissor:
|
|
gpu.state.scissor_test_set(enable)
|
|
def get_scissor_test(*, use_gpu=use_gpu_default, use_bgl=use_bgl_default):
|
|
assert use_gpu or use_bgl
|
|
if use_bgl or (not use_gpu_scissor):
|
|
return bgl_is_enabled('GL_SCISSOR_TEST')
|
|
if use_gpu and use_gpu_scissor:
|
|
# NOTE: no equivalent in `gpu` module as of Blender 3.5.1
|
|
# return gpu.state.scissor_test_get()
|
|
return False
|
|
|
|
def culling(mode, *, use_gpu=use_gpu_default, use_bgl=use_bgl_default):
|
|
assert use_gpu or use_bgl
|
|
if use_bgl:
|
|
import bgl
|
|
if mode == 'NONE':
|
|
bgl.glDisable(bgl.GL_CULL_FACE)
|
|
else:
|
|
bgl.glEnable(bgl.GL_CULL_FACE)
|
|
map_mode_bgl = {
|
|
'FRONT': bgl.GL_FRONT,
|
|
'BACK': bgl.GL_BACK,
|
|
}
|
|
bgl.glCullFace(map_mode_bgl[mode])
|
|
if use_gpu:
|
|
gpu.state.face_culling_set(mode)
|
|
|
|
|
|
#########################
|
|
# opengl errors
|
|
|
|
@add_cache('_error_check', True)
|
|
@add_cache('_error_count', 0)
|
|
@add_cache('_error_limit', 10)
|
|
def get_glerror(title, *, use_bgl=use_bgl_default):
|
|
if not use_bgl:
|
|
# NOTE: no equivalent in `gpu` module as of Blender 3.5.1
|
|
return False
|
|
if not get_glerror._error_check: return
|
|
import bgl
|
|
err = bgl.glGetError()
|
|
if err == bgl.GL_NO_ERROR:
|
|
return False
|
|
get_glerror._error_count += 1
|
|
if get_glerror._error_count >= get_glerror._error_limit:
|
|
return True
|
|
error_map = {
|
|
getattr(bgl, k): s
|
|
for (k,s) in [
|
|
# https://www.khronos.org/opengl/wiki/OpenGL_Error#Meaning_of_errors
|
|
('GL_INVALID_ENUM', 'invalid enum'),
|
|
('GL_INVALID_VALUE', 'invalid value'),
|
|
('GL_INVALID_OPERATION', 'invalid operation'),
|
|
('GL_STACK_OVERFLOW', 'stack overflow'), # does not exist in b3d 2.8x for OSX??
|
|
('GL_STACK_UNDERFLOW', 'stack underflow'), # does not exist in b3d 2.8x for OSX??
|
|
('GL_OUT_OF_MEMORY', 'out of memory'),
|
|
('GL_INVALID_FRAMEBUFFER_OPERATION', 'invalid framebuffer operation'),
|
|
('GL_CONTEXT_LOST', 'context lost'),
|
|
('GL_TABLE_TOO_LARGE', 'table too large'), # deprecated in OpenGL 3.0, removed in 3.1 core and above
|
|
]
|
|
if hasattr(bgl, k)
|
|
}
|
|
print(f'ERROR {get_glerror._error_count}/{get_glerror._error_limit} ({title}): {error_map.get(err, f"code {err}")}')
|
|
traceback.print_stack()
|
|
return True
|
|
|
|
|
|
|
|
#######################################
|
|
# shader
|
|
|
|
# https://developer.blender.org/rB21c658b718b9
|
|
# https://developer.blender.org/T74139
|
|
def get_srgb_shim(force=False):
|
|
if not force: return ''
|
|
return 'vec4 blender_srgb_to_framebuffer_space(vec4 c) { return pow(c, vec4(1.0/2.2, 1.0/2.2, 1.0/2.2, 1.0)); }'
|
|
|
|
def shader_parse_string(string, *, includeVersion=True, constant_overrides=None, define_overrides=None, force_shim=False):
|
|
# NOTE: GEOMETRY SHADER NOT FULLY SUPPORTED, YET
|
|
# need to find a way to handle in/out
|
|
constant_overrides = constant_overrides or {}
|
|
define_overrides = define_overrides or {}
|
|
uniforms, varyings, attributes, consts = [],[],[],[]
|
|
vertSource, geoSource, fragSource, commonSource = [],[],[],[]
|
|
vertVersion, geoVersion, fragVersion = '','',''
|
|
mode = 'common'
|
|
lines = string.splitlines()
|
|
for i_line,line in enumerate(lines):
|
|
sline = line.lstrip()
|
|
if re.match(r'uniform ', sline):
|
|
uniforms.append(line)
|
|
elif re.match(r'attribute ', sline):
|
|
attributes.append(line)
|
|
elif re.match(r'varying ', sline):
|
|
varyings.append(line)
|
|
elif re.match(r'const ', sline):
|
|
m = re.match(r'const +(?P<type>bool|int|float|vec\d) +(?P<var>[a-zA-Z0-9_]+) *= *(?P<val>[^;]+);', sline)
|
|
if m is None:
|
|
print(f'Shader could not match const line ({i_line}): {line}')
|
|
elif m.group('var') in constant_overrides:
|
|
line = 'const %s %s = %s' % (m.group('type'), m.group('var'), constant_overrides[m.group('var')])
|
|
consts.append(line)
|
|
elif re.match(r'#define ', sline):
|
|
m0 = re.match(r'#define +(?P<var>[a-zA-Z0-9_]+)$', sline)
|
|
m1 = re.match(r'#define +(?P<var>[a-zA-Z0-9_]+) +(?P<val>.+)$', sline)
|
|
if m0 and m0.group('var') in define_overrides:
|
|
if not define_overrides[m0.group('var')]:
|
|
line = ''
|
|
if m1 and m1.group('var') in define_overrides:
|
|
line = '#define %s %s' % (m1.group('var'), define_overrides[m1.group('var')])
|
|
if not m0 and not m1:
|
|
print(f'Shader could not match #define line ({i_line}): {line}')
|
|
consts.append(line)
|
|
elif re.match(r'#version ', sline):
|
|
match mode:
|
|
case 'common': vertVersion = geoVersion = fragVersion = line, line, line
|
|
case 'vert': vertVersion = line
|
|
case 'geo': geoVersion = line
|
|
case 'frag': fragVersion = line
|
|
case _: assert False, f'Addon Common: Unhandled mode {mode}'
|
|
elif mode == 'common' and re.match(r'precision ', sline):
|
|
commonSource.append(line)
|
|
elif m := re.match(r'//+ +(?P<mode>common|vert(ex)?|geo(m(etry)?)?|frag(ment)?) shader', sline.lower()):
|
|
match m['mode'][0]:
|
|
case 'c': mode = 'common'
|
|
case 'v': mode = 'vert'
|
|
case 'g': mode = 'geo'
|
|
case 'f': mode = 'frag'
|
|
else:
|
|
if not line.strip(): continue
|
|
match mode:
|
|
case 'common': commonSource.append(line)
|
|
case 'vert': vertSource.append(line)
|
|
case 'geo': geoSource.append(line)
|
|
case 'frag': fragSource.append(line)
|
|
case _: assert False, f'Addon Common: Unhandled mode {mode}'
|
|
assert vertSource, f'could not detect vertex shader'
|
|
assert fragSource, f'could not detect fragment shader'
|
|
v_attributes = [a.replace('attribute ', 'in ') for a in attributes]
|
|
v_varyings = [v.replace('varying ', 'out ') for v in varyings]
|
|
f_varyings = [v.replace('varying ', 'in ') for v in varyings]
|
|
srcVertex = '\n'.join(chain(
|
|
([vertVersion] if includeVersion else []),
|
|
uniforms,
|
|
v_attributes,
|
|
v_varyings,
|
|
consts,
|
|
commonSource,
|
|
vertSource,
|
|
))
|
|
srcFragment = '\n'.join(chain(
|
|
([fragVersion] if includeVersion else []),
|
|
uniforms,
|
|
f_varyings,
|
|
consts,
|
|
[get_srgb_shim(force=force_shim)],
|
|
['/////////////////////'],
|
|
commonSource,
|
|
fragSource,
|
|
))
|
|
return (srcVertex, srcFragment)
|
|
|
|
def shader_read_file(filename):
|
|
filename_guess = get_path_from_addon_common('common', 'shaders', filename)
|
|
if os.path.exists(filename): pass
|
|
elif os.path.exists(filename_guess): filename = filename_guess
|
|
else: assert False, f"Shader file could not be found: {filename} ({filename_guess})"
|
|
|
|
contents = open(filename, 'rt').read()
|
|
while m_include := re.search(r'\n *#include +"(?P<filename>[^"]+)" *\n', contents):
|
|
include_contents = shader_read_file(m_include['filename'])
|
|
contents = contents[:m_include.start()] + f'\n{include_contents}\n' + contents[m_include.end():]
|
|
return contents
|
|
|
|
def shader_parse_file(filename, **kwargs):
|
|
return shader_parse_string(shader_read_file(filename), **kwargs)
|
|
|
|
|
|
def clean_shader_source(source):
|
|
source = source + '\n' # add newline at end
|
|
source = re.sub(r'/[*](\n|.)*?[*]/', '', source) # remove multi-line comments
|
|
source = re.sub(r'//.*?\n', '\n', source) # remove single line comments
|
|
source = re.sub(r'\n+', '\n', source) # remove multiple newlines
|
|
source = re.sub(r'[ \t]+\n', '\n', source) # trim end of lines
|
|
return source
|
|
|
|
re_shader_var = re.compile(
|
|
r'((layout\((?P<layout>[^)]*)\))\s+)?'
|
|
r'((?P<qualifier>noperspective|flat|smooth)\s+)?'
|
|
r'(?P<uio>uniform|in|out)\s+'
|
|
r'(?P<type>[a-zA-Z0-9_]+)\s+'
|
|
r'(?P<var>[a-zA-Z0-9_]+)'
|
|
r'(\s*=\s*(?P<defval>[^;]+))?\s*;'
|
|
)
|
|
re_shader_var_parts = ['qualifier', 'uio', 'type', 'var', 'defval', 'layout']
|
|
def split_shader_vars(source):
|
|
shader_vars = {
|
|
m['var']: { part: m[part] for part in re_shader_var_parts }
|
|
for m in re_shader_var.finditer(source)
|
|
}
|
|
source = re_shader_var.sub('', source)
|
|
source = '\n'.join(l for l in source.splitlines() if l.strip())
|
|
return (shader_vars, source)
|
|
|
|
re_shader_struct = re.compile(r'struct\s+(?P<name>[a-zA-Z0-9_]+)\s+[{](?P<attribs>[^}]+)[}]\s*;')
|
|
re_shader_struct_attrib = re.compile(r'(?P<type>[a-zA-Z0-9_]+)\s+(?P<name>[a-zA-Z0-9_]+)\n*;')
|
|
def split_shader_structs(source):
|
|
structs = {
|
|
m['name']: {
|
|
'name': m['name'],
|
|
'full': m.group(0),
|
|
'attribs': [ (ma['type'], ma['name']) for ma in re_shader_struct_attrib.finditer(m['attribs']) ],
|
|
'type': { ma['name']: ma['type'] for ma in re_shader_struct_attrib.finditer(m['attribs']) },
|
|
}
|
|
for m in re_shader_struct.finditer(source)
|
|
}
|
|
source = re_shader_struct.sub('', source)
|
|
source = '\n'.join(l for l in source.splitlines() if l.strip())
|
|
return (structs, source)
|
|
|
|
def shader_var_to_ctype(shader_type, shader_varname):
|
|
return (shader_varname, shader_type_to_ctype(shader_type))
|
|
|
|
def shader_type_to_ctype(shader_type):
|
|
import ctypes
|
|
match shader_type:
|
|
case 'mat4': return (ctypes.c_float * 4) * 4
|
|
case 'vec4': return ctypes.c_float * 4
|
|
case 'ivec4': return ctypes.c_int * 4
|
|
case _: assert False, f'Unhandled shader type {shader_type}'
|
|
|
|
def shader_struct_to_UBO(shadername, struct, varname):
|
|
import ctypes
|
|
# copied+modified from scripts/addons/mesh_snap_utitilies_line/drawing_utilities.py
|
|
class GPU_UBO(ctypes.Structure):
|
|
_pack_ = 16
|
|
_fields_ = [ shader_var_to_ctype(t, n) for (t, n) in struct['attribs'] ]
|
|
ubo_data = GPU_UBO()
|
|
ubo_data_size = ctypes.sizeof(ubo_data)
|
|
ubo_data_slots = ubo_data_size // ctypes.sizeof(ctypes.c_float)
|
|
if False:
|
|
term_printer.boxed(
|
|
f'Struct: "{struct["name"]} {varname}" ({ubo_data_size}bytes, {ubo_data_slots}slots)',
|
|
f'Attribs: ' + '; '.join(f'{k} {v}' for (k,v) in struct['attribs']),
|
|
title=f'GPU Shader Struct: {shadername}',
|
|
)
|
|
ubo_buffer = gpu.types.Buffer('UBYTE', ubo_data_size, ubo_data)
|
|
ubo = gpu.types.GPUUniformBuf(ubo_buffer)
|
|
def setter(name, value):
|
|
# print(f'UBO_Wrapper.set {name} = {value} ({type(value)})')
|
|
shader_type = struct['type'][name]
|
|
match shader_type:
|
|
case 'mat4':
|
|
a = getattr(ubo_data, name)
|
|
CType = shader_type_to_ctype('vec4')
|
|
if len(value) == 3: value = value.to_4x4()
|
|
assert len(value) == 4 and len(value[0]) == 4
|
|
a[0] = CType(value[0][0], value[1][0], value[2][0], value[3][0])
|
|
a[1] = CType(value[0][1], value[1][1], value[2][1], value[3][1])
|
|
a[2] = CType(value[0][2], value[1][2], value[2][2], value[3][2])
|
|
a[3] = CType(value[0][3], value[1][3], value[2][3], value[3][3])
|
|
case 'vec4'|'ivec4':
|
|
CType = shader_type_to_ctype(shader_type)
|
|
if len(value) == 2: value = (*value, 0.0, 0.0)
|
|
elif len(value) == 3: value = (*value, 0.0)
|
|
assert len(value) == 4
|
|
setattr(ubo_data, name, CType(*value))
|
|
class UBO_Wrapper:
|
|
def __init__(self):
|
|
pass
|
|
def set_shader(self, shader):
|
|
self.__dict__['_shader'] = shader
|
|
def __setattr__(self, name, value):
|
|
self.assign(name, value)
|
|
def slots_used(self):
|
|
return ubo_data_slots
|
|
def assign(self, name, value):
|
|
try:
|
|
setter(name, value)
|
|
except Exception as e:
|
|
print(f'Caught Exception while trying to set {name} = {value}')
|
|
print(f' Shader: {shadername}')
|
|
print(f' Exception: {e}')
|
|
def update_shader(self, *, debug_print=False):
|
|
try:
|
|
if debug_print:
|
|
print(f'UPDATING SHADER: {shadername} {varname}')
|
|
shader = self.__dict__['_shader']
|
|
buf = gpu.types.Buffer('UBYTE', ubo_data_size, ubo_data)
|
|
if debug_print:
|
|
print(buf)
|
|
ubo.update(buf)
|
|
shader.uniform_block(varname, ubo)
|
|
del buf
|
|
except Exception as e:
|
|
print(f'Caught Exception while trying to update shader')
|
|
print(f' Shader: {shadername}')
|
|
print(f' Struct: {struct["name"]}')
|
|
print(f' Variable: {varname}')
|
|
print(f' Exception: {e}')
|
|
return UBO_Wrapper()
|
|
|
|
gpu_type_size = {
|
|
'bool',
|
|
'uint', 'uvec2', 'uvec3', 'uvec4',
|
|
'int', 'ivec2', 'ivec3', 'ivec4',
|
|
'float', 'vec2', 'vec3', 'vec4',
|
|
'mat3', 'mat4',
|
|
}
|
|
def glsl_to_gpu_type(t):
|
|
if t in gpu_type_size:
|
|
return t.upper()
|
|
return t
|
|
|
|
re_shader_location = re.compile(r'location *= *(?P<location>\d+)')
|
|
def gpu_shader(name, vert_source, frag_source, *, defines=None):
|
|
vert_source, frag_source = map(clean_shader_source, (vert_source, frag_source))
|
|
vert_shader_structs, vert_source = split_shader_structs(vert_source)
|
|
frag_shader_structs, frag_source = split_shader_structs(frag_source)
|
|
shader_structs = vert_shader_structs | frag_shader_structs
|
|
vert_shader_vars, vert_source = split_shader_vars(vert_source)
|
|
frag_shader_vars, frag_source = split_shader_vars(frag_source)
|
|
shader_vars = vert_shader_vars | frag_shader_vars
|
|
uniform_vars = { k:v for (k,v) in shader_vars.items() if v['uio'] == 'uniform' }
|
|
in_vars = { k:v for (k,v) in vert_shader_vars.items() if v['uio'] == 'in' }
|
|
inout_vars = { k:v for (k,v) in vert_shader_vars.items() if v['uio'] == 'out' }
|
|
out_vars = { k:v for (k,v) in frag_shader_vars.items() if v['uio'] == 'out'}
|
|
|
|
if False:
|
|
def nonetoempty(s): return s if s else ''
|
|
def divider(s): return f'\n{"═"*5}╡ {s} ╞{"═"*(120-(len(s) + 4 + 5))}\n\n'
|
|
term_printer.boxed(
|
|
*(ss['full'] for ss in vert_shader_structs.values()),
|
|
divider('Uniforms, Inputs, InOuts, Outputs'),
|
|
f'{"Layout":12s} {"Qualifier":13s} {"UIO":7s} {"Type":10s} {"Var Name":20s} {"Def Val"}',
|
|
f'{"-"*12 } {"-"*13 } {"-"*7 } {"-"*10 } {"-"*20 } {"-"*(120-(12+1+13+1+7+1+10+1+20+1))}',
|
|
*(
|
|
f'{nonetoempty(sv["layout"]):12s} '
|
|
f'{nonetoempty(sv["qualifier"]):13s} ' # noperspective
|
|
f'{nonetoempty(sv["uio"]):7s} ' # uniform
|
|
f'{nonetoempty(sv["type"]):10s} '
|
|
f'{nonetoempty(sv["var"]):20s} '
|
|
f'{nonetoempty(sv["defval"])}'
|
|
for sv in chain(uniform_vars.values(), in_vars.values(), inout_vars.values(), out_vars.values())
|
|
),
|
|
divider('Vertex Shader'),
|
|
vert_source,
|
|
divider('Fragment Shader'),
|
|
frag_source,
|
|
title=f'GPUSader {name}'
|
|
)
|
|
|
|
shader_info = gpu.types.GPUShaderCreateInfo()
|
|
|
|
# STRUCTS
|
|
# Note: as of 2023.06.04, multiple structs caused compiler errors that were difficult to debug.
|
|
# I believe it is due to how Blender constructs the platform-specific shader from the GPU shader.
|
|
assert len(shader_structs) <= 1, f'Cannot support shaders with more than one struct, found {len(shader_structs)} in {name}'
|
|
for struct in shader_structs.values():
|
|
# print(f'typedef_source("{struct["full"]}")')
|
|
shader_info.typedef_source(struct['full'])
|
|
UBOs = Dict()
|
|
def update_shader(*, debug_print=False):
|
|
for n in UBOs:
|
|
if n in ['update_shader', 'set_shader']: continue
|
|
UBOs[n].update_shader(debug_print=debug_print)
|
|
UBOs.update_shader = update_shader
|
|
def set_shader(shader):
|
|
for n in UBOs:
|
|
if n in ['update_shader', 'set_shader']: continue
|
|
UBOs[n].set_shader(shader)
|
|
UBOs.set_shader = set_shader
|
|
|
|
slot_samplers = 0
|
|
slot_structs = 0
|
|
slot_input = 0
|
|
slot_output = 0
|
|
|
|
# UNIFORMS
|
|
for uniform_var in uniform_vars.values():
|
|
slot = None
|
|
if uniform_var['layout'] and (m_location := re_shader_location.search(uniform_var['layout'])):
|
|
slot = int(m_location['location'])
|
|
|
|
match uniform_var['type']:
|
|
case 'sampler2D':
|
|
if slot is None: slot = slot_samplers
|
|
shader_info.sampler(slot, 'FLOAT_2D', uniform_var['var'])
|
|
slot_samplers = max(slot + 1, slot_samplers)
|
|
case t if t in gpu_type_size:
|
|
shader_info.push_constant(glsl_to_gpu_type(uniform_var['type']), uniform_var['var'])
|
|
case _:
|
|
if slot is None: slot = slot_structs
|
|
shader_info.uniform_buf(slot, uniform_var['type'], uniform_var['var'])
|
|
ubo_wrapper = shader_struct_to_UBO(name, shader_structs[uniform_var['type']], uniform_var['var'])
|
|
UBOs[uniform_var['var']] = ubo_wrapper
|
|
# print(f'uniform struct {uniform_var["type"]} {uniform_var["var"]} {slot=}')
|
|
slot_structs = max(slot + ubo_wrapper.slots_used(), slot_structs)
|
|
if False:
|
|
term_printer.boxed(
|
|
str(UBOs),
|
|
title=f'Uniforms'
|
|
)
|
|
|
|
# PREPROCESSING DEFINE DIRECTIVES
|
|
if defines:
|
|
for k,v in defines.items():
|
|
shader_info.define(str(k), str(v))
|
|
|
|
# INPUTS
|
|
for in_var in in_vars.values():
|
|
shader_info.vertex_in(slot_input, glsl_to_gpu_type(in_var['type']), in_var['var'])
|
|
slot_input += 1
|
|
|
|
# INTERFACE
|
|
safe_name = re.sub(r'[^a-zA-Z0-9_]', '_', name)
|
|
safe_name = re.sub(r'__+', '_', safe_name)
|
|
shader_interface = gpu.types.GPUStageInterfaceInfo(f'interface_{safe_name}') # NOTE: DO NOT CALL IT `interface`
|
|
qualified_fns = {
|
|
'noperspective': shader_interface.no_perspective,
|
|
'flat': shader_interface.flat,
|
|
'smooth': shader_interface.smooth,
|
|
None: shader_interface.smooth,
|
|
}
|
|
needs_interface = False
|
|
for inout_var in inout_vars.values():
|
|
needs_interface = True
|
|
qualified_fn = qualified_fns[inout_var['qualifier']]
|
|
qualified_fn(glsl_to_gpu_type(inout_var['type']), inout_var['var'])
|
|
if needs_interface:
|
|
shader_info.vertex_out(shader_interface)
|
|
|
|
# OUTPUTS
|
|
for out_var in out_vars.values():
|
|
# https://wiki.blender.org/wiki/Style_Guide/GLSL#Shared_Shader_Files:~:text=If%20fragment%20shader%20is%20writing%20to%20gl_FragDepth%2C%20usage%20must%20be%20correctly%20defined%20in%20the%20shader%27s%20create%20info%20using%20.depth_write(DepthWrite).
|
|
if out_var['var'] == 'gl_FragDepth':
|
|
if hasattr(shader_info, 'depth_write'):
|
|
# SHOULD BE INCLUDED IN 4.0, AND HOPEFULLY IN 3.6
|
|
shader_info.depth_write('ANY')
|
|
if bpy.app.version < (3, 4, 0) or gpu.platform.backend_type_get() == 'OPENGL':
|
|
continue
|
|
shader_info.fragment_out(slot_output, glsl_to_gpu_type(out_var['type']), out_var['var'])
|
|
slot_output += 1
|
|
|
|
if False:
|
|
print(shader_vars)
|
|
print(vert_source)
|
|
print(frag_source)
|
|
|
|
shader_info.vertex_source(vert_source)
|
|
shader_info.fragment_source(frag_source)
|
|
|
|
shader = gpu.shader.create_from_info(shader_info)
|
|
UBOs.set_shader(shader)
|
|
del shader_interface
|
|
del shader_info
|
|
return shader, UBOs
|
|
|
|
# return gpu.types.GPUShader(vert_source, frag_source)
|
|
|
|
|
|
######################################################################################################
|
|
|
|
|
|
class FrameBuffer:
|
|
def __init__(self, width, height):
|
|
self._width, self._height = None, None
|
|
self._is_bound = False
|
|
self.resize(width, height)
|
|
|
|
def resize(self, width, height, clear_color=True, clear_depth=True):
|
|
assert not self._is_bound, 'Cannot resize a bounded FrameBuffer'
|
|
|
|
width, height = max(1, int(width)), max(1, int(height))
|
|
if self._width == width and self._height == height: return
|
|
self._width, self._height = width, height
|
|
|
|
vx, vy, vw, vh = -1, -1, 2 / self._width, 2 / self._height
|
|
self._matrix = Matrix([
|
|
[vw, 0, 0, vx],
|
|
[ 0, vh, 0, vy],
|
|
[ 0, 0, 1, 0],
|
|
[ 0, 0, 0, 1],
|
|
])
|
|
|
|
self._tex_color = gpu.types.GPUTexture((self._width, self._height), format='RGBA8')
|
|
self._tex_depth = gpu.types.GPUTexture((self._width, self._height), format='DEPTH_COMPONENT32F')
|
|
|
|
self._framebuffer = gpu.types.GPUFrameBuffer(
|
|
color_slots={ 'texture': self._tex_color },
|
|
depth_slot=self._tex_depth,
|
|
)
|
|
|
|
@property
|
|
def color_texture(self): return self._tex_color
|
|
@property
|
|
def width(self): return self._width
|
|
@property
|
|
def height(self): return self._height
|
|
|
|
def _set_viewport(self):
|
|
o = self._framebuffer if False else gpu.state
|
|
o.viewport_set(0, 0, self._width, self._height)
|
|
def _reset_viewport(self):
|
|
o = self._cur_fbo if False else gpu.state
|
|
o.viewport_set(*self._cur_viewport)
|
|
|
|
def _set_projection(self):
|
|
gpu.matrix.load_projection_matrix(self._matrix)
|
|
def _reset_projection(self):
|
|
gpu.matrix.load_projection_matrix(self._cur_projection)
|
|
|
|
def _set_scissor(self):
|
|
ScissorStack.push(0, self._height - 1, self._width, self._height, clamp=False)
|
|
def _reset_scissor(self):
|
|
ScissorStack.pop()
|
|
|
|
def _clear(self):
|
|
self._framebuffer.clear(color=(0.0, 0.0, 0.0, 0.0), depth=1.0)
|
|
|
|
@contextmanager
|
|
def bind(self):
|
|
assert not self._is_bound, 'Cannot bind a bounded FrameBuffer'
|
|
try:
|
|
self._is_bound = True
|
|
self._cur_fbo = gpu.state.active_framebuffer_get()
|
|
self._cur_viewport = gpu.state.viewport_get()
|
|
self._cur_projection = gpu.matrix.get_projection_matrix()
|
|
with self._framebuffer.bind():
|
|
self._set_viewport()
|
|
self._set_projection()
|
|
self._set_scissor()
|
|
self._clear()
|
|
yield None
|
|
except Exception as e:
|
|
print(f'Caught exception while FrameBuffer was bound:')
|
|
print(f' {e}')
|
|
Globals.debugger.print_exception()
|
|
raise e
|
|
finally:
|
|
self._reset_scissor()
|
|
self._reset_projection()
|
|
self._reset_viewport()
|
|
self._cur_fbo = None
|
|
self._cur_viewport = None
|
|
self._cur_projection = None
|
|
self._is_bound = False
|
|
|
|
|
|
|
|
|
|
######################################################################################################
|
|
|
|
|
|
class ScissorStack:
|
|
is_started = False
|
|
scissor_test_was_enabled = False
|
|
stack = None # stack of (l,t,w,h) in region-coordinates, because viewport is set to region
|
|
msg_stack = None
|
|
|
|
@staticmethod
|
|
def start(context):
|
|
assert not ScissorStack.is_started, 'Attempting to start a started ScissorStack'
|
|
|
|
# region pos and size are window-coordinates
|
|
rgn = context.region
|
|
rl,rb,rw,rh = rgn.x, rgn.y, rgn.width, rgn.height
|
|
rt = rb + rh - 1
|
|
|
|
# remember the current scissor box settings so we can return to them when done
|
|
ScissorStack.scissor_test_was_enabled = get_scissor_test()
|
|
get_glerror('get_scissor_test')
|
|
if ScissorStack.scissor_test_was_enabled:
|
|
pl, pb, pw, ph = get_scissor() #ScissorStack.buf
|
|
get_glerror('get_scissor')
|
|
pt = pb + ph - 1
|
|
ScissorStack.stack = [(pl, pt, pw, ph)]
|
|
ScissorStack.msg_stack = ['init']
|
|
# don't need to enable, because we are already scissoring!
|
|
# TODO: this is not tested!
|
|
else:
|
|
ScissorStack.stack = [(0, rh - 1, rw, rh)]
|
|
ScissorStack.msg_stack = ['init']
|
|
scissor_test(True)
|
|
|
|
# we're ready to go!
|
|
ScissorStack.is_started = True
|
|
ScissorStack._set_scissor()
|
|
|
|
@staticmethod
|
|
def end(force=False):
|
|
if not force:
|
|
assert ScissorStack.is_started, 'Attempting to end a non-started ScissorStack'
|
|
assert len(ScissorStack.stack) == 1, 'Attempting to end a non-empty ScissorStack (size: %d)' % (len(ScissorStack.stack)-1)
|
|
scissor_test(ScissorStack.scissor_test_was_enabled)
|
|
ScissorStack.is_started = False
|
|
ScissorStack.stack = None
|
|
|
|
@staticmethod
|
|
def _set_scissor():
|
|
assert ScissorStack.is_started, 'Attempting to set scissor settings with non-started ScissorStack'
|
|
# print(f'ScissorStack: {ScissorStack.stack}')
|
|
l,t,w,h = ScissorStack.stack[-1]
|
|
b = t - (h - 1)
|
|
scissor(l, b, w, h)
|
|
get_glerror('scissor')
|
|
|
|
@staticmethod
|
|
def push(nl, nt, nw, nh, msg='', clamp=True):
|
|
# note: pos and size are already in region-coordinates, but it is specified from top-left corner
|
|
|
|
assert ScissorStack.is_started, 'Attempting to push to a non-started ScissorStack!'
|
|
|
|
if clamp:
|
|
# get previous scissor box
|
|
pl, pt, pw, ph = ScissorStack.stack[-1]
|
|
pr = pl + (pw - 1)
|
|
pb = pt - (ph - 1)
|
|
# compute right and bottom of new scissor box
|
|
nr = nl + (nw - 1)
|
|
nb = nt - (nh - 1) - 1 # sub 1 (not certain why this needs to be)
|
|
# compute clamped l,r,t,b,w,h
|
|
cl, cr, ct, cb = mid(nl,pl,pr), mid(nr,pl,pr), mid(nt,pt,pb), mid(nb,pt,pb)
|
|
cw, ch = max(0, cr - cl + 1), max(0, ct - cb + 1)
|
|
ScissorStack.stack.append((int(cl), int(ct), int(cw), int(ch)))
|
|
else:
|
|
ScissorStack.stack.append((int(nl), int(nt), int(nw), int(nh)))
|
|
ScissorStack.msg_stack.append(msg)
|
|
|
|
ScissorStack._set_scissor()
|
|
|
|
@staticmethod
|
|
def pop():
|
|
assert len(ScissorStack.stack) > 1, 'Attempting to pop from empty ScissorStack!'
|
|
ScissorStack.stack.pop()
|
|
ScissorStack.msg_stack.pop()
|
|
ScissorStack._set_scissor()
|
|
|
|
@staticmethod
|
|
@contextmanager
|
|
def wrap(*args, disabled=False, **kwargs):
|
|
if disabled:
|
|
yield None
|
|
return
|
|
try:
|
|
ScissorStack.push(*args, **kwargs)
|
|
yield None
|
|
ScissorStack.pop()
|
|
except Exception as e:
|
|
ScissorStack.pop()
|
|
print(f'Caught exception while scissoring')
|
|
print(f'{args=} {kwargs=}')
|
|
print(f'Exception: {e}')
|
|
Globals.debugger.print_exception()
|
|
raise e
|
|
|
|
@staticmethod
|
|
def get_current_view():
|
|
assert ScissorStack.is_started
|
|
assert ScissorStack.stack
|
|
l, t, w, h = ScissorStack.stack[-1]
|
|
#r, b = l + (w - 1), t - (h - 1)
|
|
return (l, t, w, h)
|
|
|
|
@staticmethod
|
|
def print_view_stack():
|
|
for i,st in enumerate(ScissorStack.stack):
|
|
l, t, w, h = st
|
|
#r, b = l + (w - 1), t - (h - 1)
|
|
print((' '*i) + str((l,t,w,h)) + ' ' + ScissorStack.msg_stack[i])
|
|
|
|
@staticmethod
|
|
def is_visible():
|
|
vl,vt,vw,vh = ScissorStack.get_current_view()
|
|
return vw > 0 and vh > 0
|
|
|
|
@staticmethod
|
|
def is_box_visible(l, t, w, h):
|
|
if w <= 0 or h <= 0: return False
|
|
vl, vt, vw, vh = ScissorStack.get_current_view()
|
|
if vw <= 0 or vh <= 0: return False
|
|
vr, vb = vl + (vw - 1), vt - (vh - 1)
|
|
r, b = l + (w - 1), t - (h - 1)
|
|
return not (l > vr or r < vl or t < vb or b > vt)
|
|
|
|
|
|
|
|
|
|
|
|
#######################################
|
|
# gather gpu information
|
|
|
|
# https://www.khronos.org/registry/OpenGL-Refpages/gl2.1/xhtml/glGetString.xml
|
|
@only_in_blender_version('< 3.0')
|
|
def gpu_info():
|
|
import bgl
|
|
return {
|
|
'vendor': bgl.glGetString(bgl.GL_VENDOR),
|
|
'renderer': bgl.glGetString(bgl.GL_RENDERER),
|
|
'version': bgl.glGetString(bgl.GL_VERSION),
|
|
'shading': bgl.glGetString(bgl.GL_SHADING_LANGUAGE_VERSION),
|
|
}
|
|
|
|
@only_in_blender_version('>= 3.0', '< 3.4')
|
|
def gpu_info():
|
|
return {
|
|
'vendor': gpu.platform.vendor_get(),
|
|
'renderer': gpu.platform.renderer_get(),
|
|
'version': gpu.platform.version_get(),
|
|
}
|
|
|
|
@only_in_blender_version('>= 3.4')
|
|
def gpu_info():
|
|
platform = {
|
|
'backend': gpu.platform.backend_type_get(),
|
|
'device': gpu.platform.device_type_get(),
|
|
'vendor': gpu.platform.vendor_get(),
|
|
'renderer': gpu.platform.renderer_get(),
|
|
'version': gpu.platform.version_get(),
|
|
}
|
|
cap = [(a, getattr(gpu.capabilities, a)) for a in dir(gpu.capabilities) if 'extensions' not in a]
|
|
cap = [(a, fn) for (a, fn) in cap if isroutine(fn)]
|
|
capabilities = {}
|
|
for (a, fn) in cap:
|
|
try: capabilities[a] = fn()
|
|
except: pass
|
|
return platform | capabilities
|
|
|
|
if not bpy.app.background:
|
|
print(f'Addon Common: {gpu_info()}')
|
|
|
|
|
|
####################################
|
|
# helper functions
|
|
|
|
@contextmanager
|
|
@add_cache('_buffers', dict())
|
|
def bgl_get_temp_buffer(type_str, size):
|
|
import bgl
|
|
bufs, key = bgl_get_temp_buffer._buffers, (type_str, size)
|
|
if key not in bufs:
|
|
bufs[key] = bgl.Buffer(getattr(bgl, type_str), size)
|
|
yield bufs[key]
|
|
|
|
def bgl_get_integerv(pname_str, *, type_str='GL_INT'):
|
|
import bgl
|
|
with bgl_get_temp_buffer(type_str, 1) as buf:
|
|
bgl.glGetIntegerv(getattr(bgl, pname_str), buf)
|
|
return buf[0]
|
|
|
|
def bgl_get_integerv_tuple(pname_str, size, *, type_str='GL_INT'):
|
|
import bgl
|
|
with bgl_get_temp_buffer(type_str, size) as buf:
|
|
bgl.glGetIntegerv(getattr(bgl, pname_str), buf)
|
|
return tuple(buf)
|
|
|
|
def bgl_is_enabled(pname_str):
|
|
import bgl
|
|
return (bgl.glIsEnabled(getattr(bgl, pname_str)) == bgl.GL_TRUE)
|
|
|
|
def bgl_enable(pname_str, enabled):
|
|
import bgl
|
|
pname = getattr(bgl, pname_str)
|
|
if enabled: bgl.glEnable(pname)
|
|
else: bgl.glDisable(pname)
|
|
|