├── .gitignore ├── shader.blend ├── shader.blend1 ├── media ├── city.png └── cavern.png ├── Image.py ├── shader ├── mesh.frag ├── fullscreen.vert ├── instances.frag ├── image.frag ├── ribbon.frag ├── mesh.vert ├── instances.vert ├── ribbon.vert ├── city.frag └── shader.frag ├── Metadata.py ├── __init__.py ├── .gitattributes ├── Render.py ├── Mesh.py ├── blender_manifest.toml ├── Engine.py ├── Panel.py ├── README.md ├── Material.py └── auto_load.py /.gitignore: -------------------------------------------------------------------------------- 1 | /__pycache__ 2 | /.venv 3 | /build 4 | /dist 5 | /renders 6 | /videos -------------------------------------------------------------------------------- /shader.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leon196/blender-glsl-addon/HEAD/shader.blend -------------------------------------------------------------------------------- /shader.blend1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leon196/blender-glsl-addon/HEAD/shader.blend1 -------------------------------------------------------------------------------- /media/city.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leon196/blender-glsl-addon/HEAD/media/city.png -------------------------------------------------------------------------------- /media/cavern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leon196/blender-glsl-addon/HEAD/media/cavern.png -------------------------------------------------------------------------------- /Image.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import gpu 3 | 4 | images: dict = {} 5 | 6 | # images = { 7 | # "image": gpu.texture.from_image(bpy.context.scene.settings["image"]) 8 | # } -------------------------------------------------------------------------------- /shader/mesh.frag: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | varying vec3 pos; 4 | 5 | vec4 FragColor; 6 | 7 | void main() 8 | { 9 | FragColor = vec4(1,0,0,1); 10 | } -------------------------------------------------------------------------------- /shader/fullscreen.vert: -------------------------------------------------------------------------------- 1 | 2 | attribute vec3 position; 3 | varying vec2 uv; 4 | 5 | void main() 6 | { 7 | uv = position.xy * 0.5 + 0.5; 8 | gl_Position = vec4(position, 1.); 9 | } -------------------------------------------------------------------------------- /shader/instances.frag: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | varying vec2 uv; 4 | 5 | vec4 FragColor; 6 | 7 | void main() 8 | { 9 | FragColor = vec4(uv*.5+.5,0,1); 10 | } -------------------------------------------------------------------------------- /shader/image.frag: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | uniform sampler2D image; 4 | uniform float fade; 5 | varying vec2 uv; 6 | vec4 FragColor; 7 | 8 | void main() 9 | { 10 | FragColor = texture2D(image, uv) * fade; 11 | } -------------------------------------------------------------------------------- /shader/ribbon.frag: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | varying vec2 uv; 4 | 5 | vec4 FragColor; 6 | 7 | void main() 8 | { 9 | float shade = uv.y; 10 | shade = step(.5, fract(uv.x*2.)); 11 | FragColor = vec4(vec3(shade),1); 12 | } -------------------------------------------------------------------------------- /shader/mesh.vert: -------------------------------------------------------------------------------- 1 | 2 | attribute vec3 position; 3 | 4 | uniform mat4 worldMatrix; 5 | uniform mat4 viewMatrix; 6 | uniform mat4 windowMatrix; 7 | 8 | varying vec2 uv; 9 | 10 | void main() 11 | { 12 | uv = position.xy; 13 | gl_Position = windowMatrix * viewMatrix * worldMatrix * vec4(position, 1.); 14 | } -------------------------------------------------------------------------------- /Metadata.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | class Setting(bpy.types.PropertyGroup): 4 | 5 | name: bpy.props.StringProperty() 6 | value: bpy.props.FloatProperty() 7 | 8 | class Metadata(bpy.types.PropertyGroup): 9 | 10 | vertex: bpy.props.StringProperty( 11 | name="Vertex", 12 | default="//shader/mesh.vert", 13 | subtype='FILE_PATH') 14 | 15 | fragment: bpy.props.StringProperty( 16 | name="Fragment", 17 | default="//shader/mesh.frag", 18 | subtype='FILE_PATH') 19 | 20 | settings: bpy.props.CollectionProperty( 21 | name="Settings", 22 | type=Setting) 23 | 24 | instances: bpy.props.IntProperty( 25 | name="Instances", 26 | default=1, 27 | ) 28 | 29 | class FrameData(bpy.types.PropertyGroup): 30 | 31 | frames: bpy.props.IntProperty( 32 | name="Frames", 33 | default=1, 34 | ) -------------------------------------------------------------------------------- /shader/instances.vert: -------------------------------------------------------------------------------- 1 | #extension GL_EXT_draw_instanced: enable 2 | 3 | attribute vec3 position; 4 | // attribute vec3 normal; 5 | 6 | uniform mat4 worldMatrix; 7 | uniform mat4 viewMatrix; 8 | uniform mat4 windowMatrix; 9 | 10 | varying vec2 uv; 11 | // varying vec3 normal; 12 | 13 | // Dave Hoskins 14 | // https://www.shadertoy.com/view/4djSRW 15 | vec4 hash44(vec4 p4) 16 | { 17 | p4 = fract(p4 * vec4(.1031, .1030, .0973, .1099)); 18 | p4 += dot(p4, p4.wzxy+33.33); 19 | return fract((p4.xxyz+p4.yzzw)*p4.zywx); 20 | } 21 | 22 | void main() 23 | { 24 | uv = position.xy; 25 | vec3 pos = position; 26 | // pos += hash44(vec4(gl_InstanceID+357.))-.5) 27 | float id = float(gl_InstanceID); 28 | float grid = 10.; 29 | float x = mod(id, grid); 30 | float y = floor(mod(id, (grid*grid))/grid); 31 | float z = floor(id/(grid*grid)); 32 | pos += vec3(x, y, z); 33 | gl_Position = windowMatrix * viewMatrix * worldMatrix * vec4(pos, 1.); 34 | } -------------------------------------------------------------------------------- /shader/ribbon.vert: -------------------------------------------------------------------------------- 1 | #extension GL_EXT_draw_instanced: enable 2 | 3 | attribute vec3 position; 4 | 5 | uniform mat4 worldMatrix; 6 | uniform mat4 viewMatrix; 7 | uniform mat4 windowMatrix; 8 | 9 | uniform float stretch; 10 | uniform float range; 11 | uniform float time; 12 | 13 | varying vec2 uv; 14 | 15 | mat2 rot(float a) { float c=cos(a), s=sin(a); return mat2(c,s,-s,c); } 16 | 17 | // Dave Hoskins 18 | // https://www.shadertoy.com/view/4djSRW 19 | vec4 hash44(vec4 p4) 20 | { 21 | p4 = fract(p4 * vec4(.1031, .1030, .0973, .1099)); 22 | p4 += dot(p4, p4.wzxy+33.33); 23 | return fract((p4.xxyz+p4.yzzw)*p4.zywx); 24 | } 25 | 26 | vec3 curve (float t, float id) 27 | { 28 | vec3 p = vec3(1,0,0); 29 | 30 | vec4 rng = hash44(vec4(id+7583.)); 31 | vec4 rng2 = hash44(vec4(id+7583.)); 32 | 33 | p = (rng.xyz-.5)*range; 34 | 35 | p.xz *= rot(t*2.+rng2.y); 36 | p.yx *= rot(t*3.+rng2.z); 37 | 38 | return p; 39 | } 40 | 41 | void main() 42 | { 43 | float id = float(gl_InstanceID); 44 | vec4 rng = hash44(vec4(id+775.)); 45 | vec4 rng2 = hash44(vec4(id+9576.)); 46 | 47 | vec3 pos = position; 48 | float x = position.x; 49 | float y = position.y; 50 | 51 | pos = curve(y * stretch * (1.+rng2.x) + time, id); 52 | pos.z += x * .05; 53 | // pos += (rng.xyz-.5);//*(1.+rng.w); 54 | 55 | gl_Position = windowMatrix * viewMatrix * worldMatrix * vec4(pos, 1.); 56 | uv = position.xy*.5+.5; 57 | } -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or modify 2 | # it under the terms of the GNU General Public License as published by 3 | # the Free Software Foundation; either version 3 of the License, or 4 | # (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, but 7 | # WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTIBILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 9 | # General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program. If not, see . 13 | 14 | import bpy 15 | 16 | from . import Metadata 17 | from . import Panel 18 | from . import auto_load 19 | 20 | auto_load.init() 21 | 22 | def register(): 23 | 24 | auto_load.register() 25 | 26 | bpy.types.Object.metadata = bpy.props.PointerProperty(type=Metadata.Metadata) 27 | bpy.types.Scene.framedata = bpy.props.PointerProperty(type=Metadata.FrameData) 28 | 29 | for panel in Panel.get_panels(): 30 | panel.COMPAT_ENGINES.add('SHADER') 31 | 32 | def unregister(): 33 | 34 | auto_load.unregister() 35 | 36 | del bpy.types.Object.metadata 37 | del bpy.types.Scene.framedata 38 | 39 | for panel in Panel.get_panels(): 40 | if 'SHADER' in panel.COMPAT_ENGINES: 41 | panel.COMPAT_ENGINES.remove('SHADER') 42 | 43 | if __name__ == "__main__": 44 | register() -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Normalize EOL for all files that Git considers text files. 2 | * text=auto eol=lf 3 | # https://gitattributes.io/api/web 4 | 5 | # Graphics 6 | *.ai binary 7 | *.bmp binary 8 | *.eps binary 9 | *.gif binary 10 | *.gifv binary 11 | *.ico binary 12 | *.jng binary 13 | *.jp2 binary 14 | *.jpg binary 15 | *.jpeg binary 16 | *.jpx binary 17 | *.jxr binary 18 | *.pdf binary 19 | *.png binary 20 | *.psb binary 21 | *.psd binary 22 | *.svg text 23 | *.svgz binary 24 | *.tif binary 25 | *.tiff binary 26 | *.wbmp binary 27 | *.webp binary 28 | 29 | 30 | # Audio 31 | *.kar binary 32 | *.m4a binary 33 | *.mid binary 34 | *.midi binary 35 | *.mp3 binary 36 | *.ogg binary 37 | *.ra binary 38 | 39 | # Video 40 | *.3gpp binary 41 | *.3gp binary 42 | *.as binary 43 | *.asf binary 44 | *.asx binary 45 | *.avi binary 46 | *.fla binary 47 | *.flv binary 48 | *.m4v binary 49 | *.mng binary 50 | *.mov binary 51 | *.mp4 binary 52 | *.mpeg binary 53 | *.mpg binary 54 | *.ogv binary 55 | *.swc binary 56 | *.swf binary 57 | *.webm binary 58 | 59 | # Archives 60 | *.7z binary 61 | *.gz binary 62 | *.jar binary 63 | *.rar binary 64 | *.tar binary 65 | *.zip binary 66 | 67 | # Fonts 68 | *.ttf binary 69 | *.eot binary 70 | *.otf binary 71 | *.woff binary 72 | *.woff2 binary 73 | -------------------------------------------------------------------------------- /Render.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import gpu 3 | import mathutils 4 | 5 | from . import Mesh 6 | from . import Image 7 | from . import Material 8 | from mathutils import ( Matrix ) 9 | 10 | shared: dict = {} 11 | 12 | shared["tick"] = 0 13 | shared["time"] = 0 14 | shared["viewMatrix"] = Matrix.Identity(4) 15 | shared["windowMatrix"] = Matrix.Identity(4) 16 | shared["viewMatrixInvert"] = Matrix.Identity(4) 17 | shared["windowMatrixInvert"] = Matrix.Identity(4) 18 | 19 | def draw(object): 20 | 21 | gpu.state.depth_test_set('LESS_EQUAL') 22 | gpu.state.depth_mask_set(True) 23 | 24 | update_uniforms(object) 25 | id = object.name 26 | mesh = Mesh.data[id] 27 | material = Material.data[id] 28 | batch = mesh.batch 29 | instances = object.metadata.instances 30 | 31 | if instances > 1: 32 | batch.draw_instanced(material.shader, instance_count=instances) 33 | else: 34 | batch.draw(material.shader) 35 | 36 | gpu.state.depth_mask_set(False) 37 | 38 | def update_matrix(windowMatrix, viewMatrix): 39 | 40 | shared["viewMatrix"] = viewMatrix 41 | shared["windowMatrix"] = windowMatrix 42 | shared["viewMatrixInvert"] = viewMatrix.inverted() 43 | shared["windowMatrixInvert"] = windowMatrix.inverted() 44 | 45 | def update_uniforms(object): 46 | 47 | id = object.name 48 | material = Material.data[id] 49 | shader = material.shader 50 | uniforms = material.uniforms 51 | for uniform in uniforms: 52 | 53 | name = uniform.name 54 | 55 | if name == "worldMatrix": 56 | try: shader.uniform_float(name, object.matrix_world) 57 | except: pass 58 | 59 | elif name in Image.images: 60 | try: shader.uniform_sampler(name, Image.images[name]) 61 | except: pass 62 | 63 | elif name in shared: 64 | try: shader.uniform_float(name, shared[name]) 65 | except: pass 66 | 67 | else: 68 | for setting in object.metadata.settings: 69 | if name == setting.name: 70 | try: shader.uniform_float(name, setting.value) 71 | except: pass -------------------------------------------------------------------------------- /shader/city.frag: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | varying vec2 uv; 4 | uniform float angle; 5 | uniform float count; 6 | uniform float size; 7 | uniform float range; 8 | uniform float falloff; 9 | uniform float tick; 10 | uniform mat4 viewMatrix; 11 | uniform mat4 viewMatrixInvert; 12 | uniform mat4 windowMatrixInvert; 13 | vec4 FragColor; 14 | 15 | mat2 rot(float a) { float c=cos(a), s=sin(a); return mat2(c,s,-s,c); } 16 | 17 | // Inigo Quilez 18 | // https://iquilezles.org/articles/distfunctions/ 19 | float sdBox( vec3 p, vec3 b ) 20 | { 21 | vec3 q = abs(p) - b; 22 | return length(max(q,0.0)) + min(max(q.x,max(q.y,q.z)),0.0); 23 | } 24 | 25 | // Dave Hoskins 26 | // https://www.shadertoy.com/view/4djSRW 27 | vec4 hash44(vec4 p4) 28 | { 29 | p4 = fract(p4 * vec4(.1031, .1030, .0973, .1099)); 30 | p4 += dot(p4, p4.wzxy+33.33); 31 | return fract((p4.xxyz+p4.yzzw)*p4.zywx); 32 | } 33 | 34 | float map (vec3 p) 35 | { 36 | float d = 100.; 37 | float a = 1.; 38 | float t = angle; 39 | for (float i = 0.; i < count; ++i) 40 | { 41 | p.xy *= rot(t+a); 42 | p = abs(p)-range*a; 43 | d = min(d, sdBox(p, vec3(size*a))); 44 | a /= falloff; 45 | } 46 | return abs(d) - .01; 47 | } 48 | 49 | void main () 50 | { 51 | vec3 color = vec3(0); 52 | float alpha = 0.; 53 | vec2 p = uv - 0.5; 54 | vec3 pos = viewMatrixInvert[3].xyz; 55 | vec3 ray = normalize(mat3(viewMatrixInvert) * (windowMatrixInvert * vec4(p*2.,-1,1)).xyz); 56 | vec4 rng = hash44(vec4(gl_FragCoord.xy, 196., tick)); 57 | float shade = 0.; 58 | float total = 0.; 59 | float dist = 0.; 60 | float max_dist = 100.; 61 | for (shade = 1.; shade > 0.; shade -= 1./200.) 62 | { 63 | dist = map(pos); 64 | if (dist < 0.001 * total || total > max_dist) break; 65 | dist *= 0.9 + 0.1 * rng.z; 66 | total += dist; 67 | pos += ray * dist; 68 | } 69 | if (total < max_dist) 70 | { 71 | #define M(u) map(pos-u) 72 | #define AO(u) clamp(abs((dist - M(ray*u))/u),0.,1.) 73 | #define N(x,y,z) normalize(vec3(x,y,z)) 74 | color = vec3(1); 75 | color *= AO(.01); 76 | color *= AO(.1); 77 | color *= AO(1.); 78 | color *= shade; 79 | } 80 | 81 | FragColor = vec4(color, 1.); 82 | } -------------------------------------------------------------------------------- /Mesh.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import numpy as np 3 | 4 | from bpy.types import ( Mesh, Collection ) 5 | from gpu.types import ( GPUBatch, GPUVertBuf, GPUVertFormat, GPUIndexBuf ) 6 | 7 | data = {} 8 | 9 | class Mesh(): 10 | 11 | batch: GPUBatch 12 | vertex_count: int = 0 13 | 14 | def __init__(self, batch, vertex_count): 15 | 16 | self.batch = batch 17 | self.vertex_count = vertex_count 18 | 19 | def make_plane() -> GPUBatch: 20 | 21 | positions = [(-1, 1, 0), (-1, -1, 0), (1, -1, 0), (1, 1, 0)] 22 | indices = [(0,1,2), (2,3,0)] 23 | vertex_count = 4 24 | 25 | attributes = GPUVertFormat() 26 | attributes.attr_add(id="position", comp_type="F32", len=3, fetch_mode="FLOAT") 27 | buffer = GPUVertBuf(attributes, vertex_count) 28 | buffer.attr_fill(id="position", data=positions) 29 | index_buffer = GPUIndexBuf(type='TRIS', seq=indices) 30 | 31 | return Mesh( 32 | GPUBatch(type='TRIS', buf=buffer, elem=index_buffer), 33 | vertex_count 34 | ) 35 | 36 | def make_batch(mesh: Mesh) -> GPUBatch: 37 | 38 | mesh.calc_loop_triangles() 39 | vertices = np.empty((len(mesh.vertices), 3), 'f') 40 | indices = np.empty((len(mesh.loop_triangles), 3), 'i') 41 | mesh.vertices.foreach_get("co", np.reshape(vertices, len(mesh.vertices) * 3)) 42 | mesh.loop_triangles.foreach_get("vertices", np.reshape(indices, len(mesh.loop_triangles) * 3)) 43 | vertex_count = len(mesh.vertices) 44 | 45 | attributes = GPUVertFormat() 46 | attributes.attr_add(id="position", comp_type="F32", len=3, fetch_mode="FLOAT") 47 | buffer = GPUVertBuf(attributes, vertex_count) 48 | buffer.attr_fill(id="position", data=vertices) 49 | index_buffer = GPUIndexBuf(type='TRIS', seq=indices) 50 | 51 | return Mesh( 52 | GPUBatch(type='TRIS', buf=buffer, elem=index_buffer), 53 | vertex_count 54 | ) 55 | 56 | def update(objects: Collection): 57 | 58 | for id in data.keys(): 59 | recycle = True 60 | for object in objects: 61 | if id == object.name: 62 | recycle = False 63 | break 64 | # actually recycle 65 | 66 | for object in objects: 67 | id = object.name 68 | if id in data: 69 | # check update 70 | if data[id].vertex_count != len(object.data.vertices): 71 | data[id] = make_batch(object.data) 72 | 73 | else: 74 | data[id] = make_batch(object.data) -------------------------------------------------------------------------------- /blender_manifest.toml: -------------------------------------------------------------------------------- 1 | schema_version = "1.0.0" 2 | 3 | # Example of manifest file for a Blender extension 4 | # Change the values according to your extension 5 | id = "blender_render_engine" 6 | version = "1.0.0" 7 | name = "Blender Render Engine" 8 | tagline = "This is another extension" 9 | maintainer = "Leon" 10 | # Supported types: "add-on", "theme" 11 | type = "add-on" 12 | 13 | # Optional link to documentation, support, source files, etc 14 | # website = "https://extensions.blender.org/add-ons/my-example-package/" 15 | 16 | # Optional list defined by Blender and server, see: 17 | # https://docs.blender.org/manual/en/dev/advanced/extensions/tags.html 18 | tags = ["Animation", "Sequencer"] 19 | 20 | blender_version_min = "4.2.0" 21 | # # Optional: Blender version that the extension does not support, earlier versions are supported. 22 | # # This can be omitted and defined later on the extensions platform if an issue is found. 23 | # blender_version_max = "5.1.0" 24 | 25 | # License conforming to https://spdx.org/licenses/ (use "SPDX: prefix) 26 | # https://docs.blender.org/manual/en/dev/advanced/extensions/licenses.html 27 | license = [ 28 | "SPDX:GPL-2.0-or-later", 29 | ] 30 | # Optional: required by some licenses. 31 | # copyright = [ 32 | # "2002-2024 Developer Name", 33 | # "1998 Company Name", 34 | # ] 35 | 36 | # Optional list of supported platforms. If omitted, the extension will be available in all operating systems. 37 | # platforms = ["windows-x64", "macos-arm64", "linux-x64"] 38 | # Other supported platforms: "windows-arm64", "macos-x64" 39 | 40 | # Optional: bundle 3rd party Python modules. 41 | # https://docs.blender.org/manual/en/dev/advanced/extensions/python_wheels.html 42 | # wheels = [ 43 | # "./wheels/hexdump-3.3-py3-none-any.whl", 44 | # "./wheels/jsmin-3.0.1-py3-none-any.whl", 45 | # ] 46 | 47 | # Optional: add-ons can list which resources they will require: 48 | # * files (for access of any filesystem operations) 49 | # * network (for internet access) 50 | # * clipboard (to read and/or write the system clipboard) 51 | # * camera (to capture photos and videos) 52 | # * microphone (to capture audio) 53 | # 54 | # If using network, remember to also check `bpy.app.online_access` 55 | # https://docs.blender.org/manual/en/dev/advanced/extensions/addons.html#internet-access 56 | # 57 | # For each permission it is important to also specify the reason why it is required. 58 | # Keep this a single short sentence without a period (.) at the end. 59 | # For longer explanations use the documentation or detail page. 60 | # 61 | # [permissions] 62 | # network = "Need to sync motion-capture data to server" 63 | # files = "Import/export FBX from/to disk" 64 | # clipboard = "Copy and paste bone transforms" 65 | 66 | # Optional: build settings. 67 | # https://docs.blender.org/manual/en/dev/advanced/extensions/command_line_arguments.html#command-line-args-extension-build 68 | # [build] 69 | # paths_exclude_pattern = [ 70 | # "__pycache__/", 71 | # "/.git/", 72 | # "/*.zip", 73 | # ] 74 | -------------------------------------------------------------------------------- /Engine.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import gpu 3 | from . import Render 4 | from . import Mesh 5 | from . import Material 6 | 7 | engines = [] 8 | 9 | class Engine(bpy.types.RenderEngine): 10 | 11 | bl_idname = "Shader" 12 | bl_label = "Shader" 13 | bl_use_preview = True 14 | bl_use_gpu_context = True 15 | 16 | def __init__(self): 17 | engines.append(self) 18 | 19 | def view_update(self, context, depsgraph): 20 | 21 | meshes = [] 22 | for object in context.scene.objects: 23 | if isinstance(object.data, bpy.types.Mesh): 24 | meshes.append(object) 25 | 26 | Mesh.update(meshes) 27 | Material.update(meshes) 28 | 29 | def view_draw(self, context, depsgraph): 30 | windowMatrix = context.region_data.window_matrix 31 | viewMatrix = bpy.context.region_data.view_matrix 32 | Render.update_matrix(windowMatrix, viewMatrix) 33 | self.bind_display_space_shader(depsgraph.scene) 34 | 35 | for object in context.scene.objects: 36 | if isinstance(object.data, bpy.types.Mesh): 37 | if object.visible_get(): 38 | Render.draw(object) 39 | 40 | Render.shared["tick"] += 1 41 | Render.shared["time"] = Engine.get_time(depsgraph) 42 | self.unbind_display_space_shader() 43 | 44 | def render(self, depsgraph): 45 | scene = depsgraph.scene 46 | width = self.render.resolution_x 47 | height = self.render.resolution_y 48 | offscreen = gpu.types.GPUOffScreen(width, height, format="RGBA32F") 49 | with offscreen.bind(): 50 | windowMatrix = scene.camera.calc_matrix_camera( 51 | depsgraph, 52 | x=width, 53 | y=height, 54 | ) 55 | viewMatrix = scene.camera.matrix_world.inverted() 56 | Render.update_matrix(windowMatrix, viewMatrix) 57 | self.bind_display_space_shader(depsgraph.scene) 58 | 59 | for object in scene.objects: 60 | if isinstance(object.data, bpy.types.Mesh): 61 | if object.visible_get(): 62 | Render.draw(object) 63 | 64 | Render.shared["tick"] += 1 65 | self.unbind_display_space_shader() 66 | 67 | result = self.begin_result(0, 0, width, height) 68 | layer = result.layers[0].passes["Combined"] 69 | array = offscreen.texture_color.read() 70 | array = [item for sub_list in array for item in sub_list] 71 | layer.rect = array 72 | self.end_result(result) 73 | offscreen.free() 74 | del offscreen 75 | 76 | # https://github.com/KoltesDigital/shiba/blob/master/blender/shiba/render_engine.py 77 | @staticmethod 78 | def get_time(depsgraph): 79 | scene = depsgraph.scene 80 | actual_fps = scene.render.fps / scene.render.fps_base 81 | time = scene.frame_current / actual_fps 82 | return time -------------------------------------------------------------------------------- /Panel.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from bpy.types import ( Object ) 4 | from os.path import ( splitext ) 5 | 6 | from . import Engine 7 | from . import Metadata 8 | from . import Material 9 | 10 | class RenderPanel(bpy.types.Panel): 11 | 12 | bl_label = "Render Settings" 13 | bl_idname = "RENDER_PT_Settings" 14 | bl_space_type = 'PROPERTIES' 15 | bl_region_type = 'WINDOW' 16 | bl_context = "render" 17 | 18 | def draw(self, context): 19 | 20 | layout = self.layout 21 | 22 | layout.use_property_split = True 23 | layout.use_property_decorate = False 24 | 25 | # resolution 26 | box = layout.box() 27 | keys = [ "resolution_x", "resolution_y", "resolution_percentage" ] 28 | for key in keys: 29 | box.prop(context.scene.render, key) 30 | 31 | # frame rate 32 | box = layout.box() 33 | keys = [ "fps" ] 34 | for key in keys: 35 | box.prop(context.scene.render, key) 36 | box.prop(context.scene.framedata, "frames") 37 | 38 | class DataPanel(bpy.types.Panel): 39 | 40 | bl_label = "Data Settings" 41 | bl_idname = "DATA_PT_Settings" 42 | bl_space_type = 'PROPERTIES' 43 | bl_region_type = 'WINDOW' 44 | bl_context = "data" 45 | 46 | def draw(self, context): 47 | 48 | layout = self.layout 49 | 50 | layout.use_property_split = True 51 | layout.use_property_decorate = False 52 | 53 | object = context.object 54 | 55 | if object != None: 56 | if isinstance(object.data, bpy.types.Camera): 57 | box = layout.box() 58 | box.prop(object.data, "lens") 59 | 60 | class MaterialPanel(bpy.types.Panel): 61 | 62 | bl_label = "Material Settings" 63 | bl_idname = "MATERIAL_PT_Settings" 64 | bl_space_type = 'PROPERTIES' 65 | bl_region_type = 'WINDOW' 66 | bl_context = "material" 67 | 68 | def draw(self, context): 69 | 70 | layout = self.layout 71 | object = context.object 72 | 73 | if object != None: 74 | layout.use_property_split = True 75 | layout.use_property_decorate = False 76 | 77 | # vertex shader 78 | box = layout.box() 79 | metadata = object.metadata 80 | box.prop(metadata, "instances") 81 | box.prop(metadata, "vertex") 82 | box.prop(metadata, "fragment") 83 | for setting in metadata.settings: 84 | box.prop(setting, "value", text=setting.name.capitalize()) 85 | 86 | def get_panels(): 87 | 88 | panels = [] 89 | exclude_panels = { 'VIEWLAYER_PT_filter', 'VIEWLAYER_PT_layer_passes', } 90 | include_panels = { 'EEVEE_MATERIAL_PT_context_material', 'MATERIAL_PT_preview', } 91 | for panel in bpy.types.Panel.__subclasses__(): 92 | if hasattr(panel, 'COMPAT_ENGINES'): 93 | if (('BLENDER_RENDER' in panel.COMPAT_ENGINES and panel.__name__ not in exclude_panels) 94 | or panel.__name__ in include_panels): 95 | panels.append(panel) 96 | 97 | return panels -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blender GLSL Render Engine 2 | 3 | This is a Blender add-on that adds a custom render engine. It is at the moment very basic but it can already display GLSL shaders like raymarching signed distance field fractals as seen in [ShaderToy](https://www.shadertoy.com/ "ShaderToy"). 4 | 5 | ![](./media/cavern.png) 6 | 7 | ## Basic usage 8 | 9 | At the moment, you can explore the examples and play with parameters. Download the repository, install the add-on archive from file in Blender and open the `shader.blend` to have a scene ready. 10 | 11 | ![](./media/city.png) 12 | 13 | ## Write your own shader 14 | 15 | To write your own shaders, you probably want to use [Visual Studio Code](https://code.visualstudio.com/download) and the packages [Blender Development](https://github.com/JacquesLucke/blender_vscode) and [GLSL Lint](https://github.com/hsimpson/vscode-glsllint) with [GLSLang validator](https://github.com/KhronosGroup/glslang) so you can hot reload the add-on while coding and have the details of shader compilation errors. 16 | 17 | The add-on is using Blender [GPU module](https://docs.blender.org/api/current/gpu.html), which does some shader file modification for the attributes, uniforms, varyings and fragment output. Thus there is small workaround in shader file to be compatible with both the GPU module and the GLSL Lint validator. 18 | 19 | ## About 20 | 21 | It is using [Shiba](https://github.com/KoltesDigital/shiba "Shiba") as a main reference and inspiration. It was the tool we used with Jonathan Giroux aka [Koltes](https://github.com/KoltesDigital/) to make productions in demoscene. This add-on is kind of the spiritual successor of Shiba. 22 | 23 | Making another rendering engine being a silly idea, the purpose is to learn basic and advanced graphic programming concepts and to play with pixels. Integration with Blender is great because it is an excellent tool to view, edit, animate and render. 24 | 25 | The project will evolve as a research project, experimentation tool and playground interface for exploring visual excentricities. Below is a list of things done and what would be nice to have: 26 | 27 | - [x] Learn Blender API to add UI panels and store data 28 | - [x] Set up basic OpenGL to draw a triangle 29 | - [x] Connect camera matrix to a raymaching shader 30 | - [x] Parse uniforms and integrate them in material panel 31 | - [x] Make vertex buffer from mesh 32 | - [ ] Parsing varying and attribute 33 | - [ ] Hot reload of shader files 34 | - [ ] UI hint of float default, min and max 35 | - [ ] Transform matrix slot in panel 36 | - [ ] Depth writing to mix SDF and meshes 37 | - [ ] Proper cache, reload and recycle system 38 | - [ ] Stereographic projection 39 | - [ ] Frame buffer feedback effect 40 | - [ ] Deferred shading 41 | - [ ] Integration of VSE scene clip 42 | - [ ] Export video 43 | - [ ] Export as an executable 44 | 45 | ## Alternatives 46 | 47 | This add-on is not a SDF editor with stack of primitives and layers of boolean operations. If you are looking for that, you can check out: 48 | - [MagicaCSG](https://ephtracy.github.io/index.html?page=magicacsg) by ephtracy 49 | - [Neo](https://projectneo.adobe.com/) by Inigo Quilez and Adobe 50 | - [Womp](https://womp.com/index) by Womp 3D 51 | - [Clayxels](https://www.clayxels.com/) by Andrea Interguglielmi 52 | - [Unbound](https://www.unbound.io/) by Florian Hoenig & Andrea Interguglielmi 53 | - [Clavicula](https://clavicula.link/) by hypnotau 54 | - [SculptGL](https://stephaneginier.com/archive/editSDF/) by Stéphane Ginier 55 | - [SDF Editor](https://joetech.itch.io/sdf-editor) by Joe Tech 56 | 57 | While the list above enumarates standalone softwares, there is [Ernst Renderer](https://github.com/iY0Yi/ErnstRenderer/) by iY0Yi that is a SDF editor Blender add-on. -------------------------------------------------------------------------------- /Material.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import gpu 3 | 4 | from mathutils import ( Matrix ) 5 | from gpu.types import ( GPUShader, GPUBatch ) 6 | from bpy.types import ( Collection ) 7 | 8 | data = {} 9 | 10 | class Material: 11 | 12 | shader: GPUShader 13 | uniforms = [] 14 | vertex = "" 15 | fragment = "" 16 | 17 | def __init__(self, shader, uniforms, vertex="", fragment=""): 18 | 19 | self.shader = shader 20 | self.uniforms = uniforms 21 | self.vertex = vertex 22 | self.fragment = fragment 23 | 24 | class Uniform: 25 | 26 | name: str 27 | type: str 28 | 29 | def __init__(self, name, type): 30 | 31 | self.name = name 32 | self.type = type 33 | 34 | def __repr__(self): return self.name + "(" + self.type + ")" 35 | def __str__(self): return self.name + "(" + self.type + ")" 36 | 37 | def make_from_path(vertex, fragment) -> Material: 38 | 39 | # shader container 40 | shader_info = gpu.types.GPUShaderCreateInfo() 41 | vert_out = gpu.types.GPUStageInterfaceInfo("shader") 42 | 43 | # vertex shader source 44 | source = open(bpy.path.abspath(vertex), "r").read() 45 | rows = source.split("\n") 46 | lines = [] 47 | uniforms = [] 48 | 49 | for row in rows: 50 | 51 | # manage uniforms 52 | if "uniform" in row: 53 | column = row.split(" ") 54 | type = column[1].upper() 55 | name = column[2].rstrip(';') 56 | shader_info.push_constant(type, name) 57 | uniforms.append(Uniform(name, type)) 58 | 59 | if "uniform" not in row and "attribute" not in row and "varying" not in row: 60 | lines.append(row) 61 | 62 | shader_info.vertex_source("\n".join(lines)) 63 | 64 | # vertex shader attributes 65 | shader_info.vertex_in(0, 'VEC3', "position") 66 | 67 | # vertex shader varying 68 | vert_out.smooth('VEC2', "uv") 69 | shader_info.vertex_out(vert_out) 70 | 71 | # fragment shader source 72 | source = open(bpy.path.abspath(fragment), "r").read() 73 | rows = source.split("\n") 74 | lines = [] 75 | images = 0 76 | 77 | for row in rows: 78 | 79 | if "uniform" in row: 80 | column = row.split(" ") 81 | type = column[1].upper() 82 | name = column[2].rstrip(';') 83 | 84 | already = False 85 | for uniform in uniforms: 86 | if uniform.name == name: 87 | already = True 88 | break 89 | 90 | if not already: 91 | uniforms.append(Uniform(name, type)) 92 | 93 | if "SAMPLER2D" in type: 94 | shader_info.sampler(images,"FLOAT_2D", name) 95 | images += 1 96 | 97 | else: 98 | shader_info.push_constant(type, name) 99 | 100 | elif "varying" not in row and "vec4 FragColor;" not in row: 101 | lines.append(row) 102 | 103 | shader_info.fragment_source("\n".join(lines)) 104 | 105 | # fragment shader out 106 | shader_info.fragment_out(0, 'VEC4', "FragColor") 107 | 108 | shader = gpu.shader.create_from_info(shader_info) 109 | 110 | del vert_out 111 | del shader_info 112 | 113 | return Material(shader, uniforms, vertex, fragment) 114 | 115 | def update(objects): 116 | 117 | # print(metadatas) 118 | # for id in data.keys(): 119 | # recycle = True 120 | # for object in objects: 121 | # if id == object.name: 122 | # recycle = False 123 | # break 124 | # # actually recycle 125 | 126 | for object in objects: 127 | id = object.name 128 | metadata = object.metadata 129 | if id in data: 130 | if data[id].fragment != metadata.fragment \ 131 | or data[id].vertex != metadata.vertex: 132 | data[id] = make_from_path(metadata.vertex, metadata.fragment) 133 | # check update 134 | 135 | else: 136 | 137 | # create and store material 138 | material = make_from_path(metadata.vertex, metadata.fragment) 139 | data[id] = material 140 | 141 | # create settings from uniforms 142 | for uniform in material.uniforms: 143 | if uniform.type == "FLOAT": 144 | already = False 145 | for setting in metadata.settings: 146 | if uniform.name == setting.name: 147 | uniform.value = setting.value 148 | already = True 149 | break 150 | 151 | if not already: 152 | setting = metadata.settings.add() 153 | setting.name = uniform.name 154 | setting.value = 1 155 | 156 | 157 | -------------------------------------------------------------------------------- /shader/shader.frag: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | varying vec2 uv; 4 | uniform float angle; 5 | uniform float count; 6 | uniform float size; 7 | uniform float range; 8 | uniform float falloff; 9 | uniform float noise; 10 | uniform float easing; 11 | uniform float blend; 12 | uniform float tick; 13 | uniform mat4 viewMatrix; 14 | uniform mat4 viewMatrixInvert; 15 | uniform mat4 windowMatrixInvert; 16 | vec4 FragColor; 17 | 18 | mat2 rot(float a) { float c=cos(a), s=sin(a); return mat2(c,s,-s,c); } 19 | 20 | // Inigo Quilez 21 | // https://iquilezles.org/articles/distfunctions/ 22 | float smin( float d1, float d2, float k ) 23 | { 24 | float h = clamp( 0.5 + 0.5*(d2-d1)/k, 0.0, 1.0 ); 25 | return mix( d2, d1, h ) - k*h*(1.0-h); 26 | } 27 | float smax( float d1, float d2, float k ) 28 | { 29 | float h = clamp( 0.5 - 0.5*(d2+d1)/k, 0.0, 1.0 ); 30 | return mix( d2, -d1, h ) + k*h*(1.0-h); 31 | } 32 | float sdBox( vec3 p, vec3 b ) 33 | { 34 | vec3 q = abs(p) - b; 35 | return length(max(q,0.0)) + min(max(q.x,max(q.y,q.z)),0.0); 36 | } 37 | 38 | // Dave Hoskins 39 | // https://www.shadertoy.com/view/4djSRW 40 | vec4 hash44(vec4 p4) 41 | { 42 | p4 = fract(p4 * vec4(.1031, .1030, .0973, .1099)); 43 | p4 += dot(p4, p4.wzxy+33.33); 44 | return fract((p4.xxyz+p4.yzzw)*p4.zywx); 45 | } 46 | 47 | // hornet 48 | // https://www.shadertoy.com/view/MslGR8 49 | // http://advances.realtimerendering.com/s2014/index.html 50 | float InterleavedGradientNoise( vec2 uv ) 51 | { 52 | const vec3 magic = vec3( 0.06711056, 0.00583715, 52.9829189 ); 53 | return fract( magic.z * fract( dot( uv, magic.xy ) ) ); 54 | } 55 | 56 | // blackle 57 | // https://suricrasia.online/blog/shader-functions/ 58 | vec3 erot(vec3 p, vec3 ax, float ro) { 59 | return mix(dot(ax, p)*ax, p, cos(ro)) + cross(ax,p)*sin(ro); 60 | } 61 | 62 | vec3 rndrot(vec3 p, vec4 rnd) { 63 | return erot(p, normalize(tan(rnd.xyz)), rnd.w*acos(-1.)); 64 | } 65 | 66 | float gyroid (vec3 p) { return dot(cos(p), sin(p.yzx)); } 67 | 68 | float fbm (vec3 p) { 69 | float r = 0.; 70 | float a = .5; 71 | for (float i = 0.; i < 6.; ++i) 72 | { 73 | p += r * .2; 74 | r += pow(abs(gyroid(p/a)), easing)*a; 75 | a /= 1.8; 76 | } 77 | return 1.-atan(r); 78 | } 79 | 80 | float map (vec3 p) 81 | { 82 | float d = 100.; 83 | float spice = fbm(p); 84 | float plane = p.z; 85 | float sphere = length(p)-size; 86 | 87 | float a = 1.; 88 | float t = angle; 89 | for (float i = 0.; i < count; ++i) 90 | { 91 | p.xy *= rot(t+a); 92 | p.yz *= rot(t+a); 93 | p = abs(p)-range*a; 94 | d = smin(d, length(p)-size*a, blend*a); 95 | a /= falloff; 96 | } 97 | 98 | d = smax(sphere, d, 1.); 99 | d += spice * noise; 100 | d = abs(d) - .01; 101 | 102 | return d; 103 | } 104 | 105 | void main () 106 | { 107 | vec3 color = vec3(0); 108 | float alpha = 0.; 109 | vec2 p = uv - 0.5; 110 | vec3 pos = viewMatrixInvert[3].xyz; 111 | vec3 ray = normalize(mat3(viewMatrixInvert) * (windowMatrixInvert * vec4(p*2.,-1,1)).xyz); 112 | vec4 rng = hash44(vec4(gl_FragCoord.xy, 196., tick)); 113 | float dither = InterleavedGradientNoise(gl_FragCoord.xy); 114 | 115 | float shade = 0.; 116 | float total = 0.; 117 | float dist = 0.; 118 | float max_dist = 1000.; 119 | for (shade = 1.; shade > 0.; shade -= 1./200.) 120 | { 121 | dist = map(pos); 122 | if (dist < 0.001 * total || total > max_dist) break; 123 | dist *= 0.5 + 0.1 * dither; 124 | total += dist; 125 | pos += ray * dist; 126 | } 127 | if (total < max_dist) 128 | { 129 | color = vec3(1); 130 | vec2 e1 = vec2(.5+.02*rng.y,0); 131 | vec2 e2 = vec2(.02,0); 132 | vec2 e3 = vec2(.1,0); 133 | vec2 e4 = vec2(5.,0); 134 | #define M(u) map(pos-u) 135 | vec3 n1 = normalize(dist - vec3(M(e1.xyy),M(e1.yxy),M(e1.yyx))); 136 | vec3 n2 = normalize(dist - vec3(M(e2.xyy),M(e2.yxy),M(e2.yyx))); 137 | vec3 n3 = normalize(dist - vec3(M(e3.xyy),M(e3.yxy),M(e3.yyx))); 138 | vec3 n4 = normalize(dist - vec3(M(e4.xyy),M(e4.yxy),M(e4.yyx))); 139 | n1 = normalize(mat3(viewMatrix) * n1); 140 | n2 = normalize(mat3(viewMatrix) * n2); 141 | n3 = normalize(mat3(viewMatrix) * n3); 142 | n4 = normalize(mat3(viewMatrix) * n4); 143 | color = vec3(0); 144 | color += vec3(0.5,.5,1)*pow(.5+.5*dot(n2, normalize(vec3(0,2,0))), 2.); 145 | color += vec3(1,.5,0)*(1.-pow(abs(n1.z), 1.)); 146 | color += vec3(.5,1,.5)*(1.-pow(abs(n2.z), 1.)); 147 | 148 | #define AO(u) clamp(abs((dist - M(ray*u))/u),0.,1.) 149 | color *= AO(.1); 150 | color *= AO(.5); 151 | color *= AO(1.); 152 | color *= .5+.5*smoothstep(.1,.9,n2.y+.5); 153 | color *= .5+.5*smoothstep(.0,.5,n3.z); 154 | color *= shade; 155 | } 156 | 157 | FragColor = vec4(color, 1.); 158 | } -------------------------------------------------------------------------------- /auto_load.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import typing 3 | import inspect 4 | import pkgutil 5 | import importlib 6 | from pathlib import Path 7 | 8 | __all__ = ( 9 | "init", 10 | "register", 11 | "unregister", 12 | ) 13 | 14 | blender_version = bpy.app.version 15 | 16 | modules = None 17 | ordered_classes = None 18 | 19 | 20 | def init(): 21 | global modules 22 | global ordered_classes 23 | 24 | modules = get_all_submodules(Path(__file__).parent) 25 | ordered_classes = get_ordered_classes_to_register(modules) 26 | 27 | 28 | def register(): 29 | for cls in ordered_classes: 30 | bpy.utils.register_class(cls) 31 | 32 | for module in modules: 33 | if module.__name__ == __name__: 34 | continue 35 | if hasattr(module, "register"): 36 | module.register() 37 | 38 | 39 | def unregister(): 40 | for cls in reversed(ordered_classes): 41 | bpy.utils.unregister_class(cls) 42 | 43 | for module in modules: 44 | if module.__name__ == __name__: 45 | continue 46 | if hasattr(module, "unregister"): 47 | module.unregister() 48 | 49 | 50 | # Import modules 51 | ################################################# 52 | 53 | 54 | def get_all_submodules(directory): 55 | return list(iter_submodules(directory, __package__)) 56 | 57 | 58 | def iter_submodules(path, package_name): 59 | for name in sorted(iter_submodule_names(path)): 60 | yield importlib.import_module("." + name, package_name) 61 | 62 | 63 | def iter_submodule_names(path, root=""): 64 | for _, module_name, is_package in pkgutil.iter_modules([str(path)]): 65 | if is_package: 66 | sub_path = path / module_name 67 | sub_root = root + module_name + "." 68 | yield from iter_submodule_names(sub_path, sub_root) 69 | else: 70 | yield root + module_name 71 | 72 | 73 | # Find classes to register 74 | ################################################# 75 | 76 | 77 | def get_ordered_classes_to_register(modules): 78 | return toposort(get_register_deps_dict(modules)) 79 | 80 | 81 | def get_register_deps_dict(modules): 82 | my_classes = set(iter_my_classes(modules)) 83 | my_classes_by_idname = {cls.bl_idname: cls for cls in my_classes if hasattr(cls, "bl_idname")} 84 | 85 | deps_dict = {} 86 | for cls in my_classes: 87 | deps_dict[cls] = set(iter_my_register_deps(cls, my_classes, my_classes_by_idname)) 88 | return deps_dict 89 | 90 | 91 | def iter_my_register_deps(cls, my_classes, my_classes_by_idname): 92 | yield from iter_my_deps_from_annotations(cls, my_classes) 93 | yield from iter_my_deps_from_parent_id(cls, my_classes_by_idname) 94 | 95 | 96 | def iter_my_deps_from_annotations(cls, my_classes): 97 | for value in typing.get_type_hints(cls, {}, {}).values(): 98 | dependency = get_dependency_from_annotation(value) 99 | if dependency is not None: 100 | if dependency in my_classes: 101 | yield dependency 102 | 103 | 104 | def get_dependency_from_annotation(value): 105 | if blender_version >= (2, 93): 106 | if isinstance(value, bpy.props._PropertyDeferred): 107 | return value.keywords.get("type") 108 | else: 109 | if isinstance(value, tuple) and len(value) == 2: 110 | if value[0] in (bpy.props.PointerProperty, bpy.props.CollectionProperty): 111 | return value[1]["type"] 112 | return None 113 | 114 | 115 | def iter_my_deps_from_parent_id(cls, my_classes_by_idname): 116 | if issubclass(cls, bpy.types.Panel): 117 | parent_idname = getattr(cls, "bl_parent_id", None) 118 | if parent_idname is not None: 119 | parent_cls = my_classes_by_idname.get(parent_idname) 120 | if parent_cls is not None: 121 | yield parent_cls 122 | 123 | 124 | def iter_my_classes(modules): 125 | base_types = get_register_base_types() 126 | for cls in get_classes_in_modules(modules): 127 | if any(issubclass(cls, base) for base in base_types): 128 | if not getattr(cls, "is_registered", False): 129 | yield cls 130 | 131 | 132 | def get_classes_in_modules(modules): 133 | classes = set() 134 | for module in modules: 135 | for cls in iter_classes_in_module(module): 136 | classes.add(cls) 137 | return classes 138 | 139 | 140 | def iter_classes_in_module(module): 141 | for value in module.__dict__.values(): 142 | if inspect.isclass(value): 143 | yield value 144 | 145 | 146 | def get_register_base_types(): 147 | return set( 148 | getattr(bpy.types, name) 149 | for name in [ 150 | "Panel", 151 | "Operator", 152 | "PropertyGroup", 153 | "AddonPreferences", 154 | "Header", 155 | "Menu", 156 | "Node", 157 | "NodeSocket", 158 | "NodeTree", 159 | "UIList", 160 | "RenderEngine", 161 | "Gizmo", 162 | "GizmoGroup", 163 | ] 164 | ) 165 | 166 | 167 | # Find order to register to solve dependencies 168 | ################################################# 169 | 170 | 171 | def toposort(deps_dict): 172 | sorted_list = [] 173 | sorted_values = set() 174 | while len(deps_dict) > 0: 175 | unsorted = [] 176 | sorted_list_sub = [] # helper for additional sorting by bl_order - in panels 177 | for value, deps in deps_dict.items(): 178 | if len(deps) == 0: 179 | sorted_list_sub.append(value) 180 | sorted_values.add(value) 181 | else: 182 | unsorted.append(value) 183 | deps_dict = {value: deps_dict[value] - sorted_values for value in unsorted} 184 | sorted_list_sub.sort(key=lambda cls: getattr(cls, "bl_order", 0)) 185 | sorted_list.extend(sorted_list_sub) 186 | return sorted_list 187 | --------------------------------------------------------------------------------