├── README.md ├── MISC ├── scaleOptical.py ├── bakeAll.py ├── matchFPS.py ├── ffExport.py └── customNode.py ├── LICENSE ├── LF ├── lfAssets.py ├── pixelAnalyzer.py └── cameras.py └── VFX └── lightning.py /README.md: -------------------------------------------------------------------------------- 1 | # Some useful Blender scripts 2 | 3 | ## VFX 4 | lightning.py - compositor node for 2D lightning effect - working but not finished since custom nodes struggle with input sockets 5 | 6 | ## LF 7 | cameras.py - generates grid of cameras for lightfield or LKG\ 8 | pixelAnalyzer.py - analyzes pixels from LF data\ 9 | lfAssets.py - generates a virtual LF window from input grid - LF images - can be obtained from 3D scene with [this script](https://github.com/ichlubna/lfStreaming/blob/main/scripts/BlenderAddon.py) 10 | 11 | ## MISC 12 | bakeAll.py - bakes all simulations (usage: blender untitled.blend -b -P bakeAll.py)\ 13 | matchFPS.py - transforms the imported strip in VSE into the project FPS\ 14 | scaleOptical.py - scales the selected objects according to camera\ 15 | ffExport.py - connects Blender to external ffmpeg, allowing a direct encoding to all supported formats like gif of h.265\ 16 | customNode.py - example of creating a custom material node which renders the scene and uses the render as a material texture 17 | -------------------------------------------------------------------------------- /MISC/scaleOptical.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import mathutils 3 | 4 | focus = 0.19 5 | 6 | def mix(a,b, factor): 7 | r = a+b 8 | for i in 0,1,2: 9 | r[i] /= factor[i] 10 | return r 11 | 12 | def mixDepth(point, focus, cam, factor): 13 | y = factor*(point[1]-focus)+focus 14 | x = ((point[0]-cam[0])/(point[1]-cam[1]))*(y-cam[1])-cam[0] 15 | z = ((point[2]-cam[2])/(point[1]-cam[1]))*(y-cam[1])-cam[2] 16 | return mathutils.Vector((x,y,z)) 17 | 18 | def scaleOptical(objects, factor): 19 | camLoc = bpy.context.scene.camera.location 20 | for obj in objects: 21 | if obj.data: 22 | if hasattr(obj.data, 'vertices'): 23 | obj.data.vertices 24 | for vert in obj.data.vertices: 25 | #vert.co = obj.matrix_world.inverted() @ mix(obj.matrix_world @ vert.co, camLoc, factor) 26 | vert.co = obj.matrix_world.inverted() @ mixDepth(obj.matrix_world @ vert.co, focus, camLoc, factor) 27 | 28 | 29 | f = mathutils.Vector((10,0,10)) 30 | selected = bpy.context.selected_objects 31 | scaleOptical(selected, 0.25) 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 ichlubna 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MISC/bakeAll.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | for scene in bpy.data.scenes: 4 | for object in scene.objects: 5 | for modifier in object.modifiers: 6 | if modifier.type == 'FLUID': 7 | if modifier.fluid_type == 'DOMAIN': 8 | print("Baking fluid") 9 | object.select_set(True) 10 | bpy.context.view_layer.objects.active = object 11 | bpy.ops.fluid.bake_data() 12 | elif modifier.type == 'CLOTH': 13 | print("Baking cloth") 14 | override = {'scene': scene, 'active_object': object, 'point_cache': modifier.point_cache} 15 | bpy.ops.ptcache.free_bake(override) 16 | bpy.ops.ptcache.bake(override, bake=True) 17 | elif modifier.type == 'PARTICLE_SYSTEM': 18 | print("Baking particles") 19 | override = {'scene': scene, 'active_object': object, 'point_cache': modifier.particle_system.point_cache} 20 | bpy.ops.ptcache.free_bake(override) 21 | bpy.ops.ptcache.bake(override, bake=True) 22 | bpy.ops.wm.save_mainfile() 23 | -------------------------------------------------------------------------------- /MISC/matchFPS.py: -------------------------------------------------------------------------------- 1 | bl_info = { 2 | "name": "Match FPS", 3 | "author": "ichlubna", 4 | "version": (1, 0), 5 | "blender": (3, 0, 0), 6 | "location": "VSE > Strip > Match FPS", 7 | "description": "Matches the strip length and speed to render FPS settings. When having selected the strip, go to the Strip menu and hit Match FPS button.", 8 | "warning": "", 9 | "doc_url": "", 10 | "category": "Strip", 11 | } 12 | 13 | import bpy 14 | from bpy.types import Operator 15 | 16 | def matchFPS(): 17 | scene = bpy.context.scene 18 | editor = scene.sequence_editor 19 | clip = editor.active_strip 20 | scene.sequence_editor.sequences.new_effect(type='SPEED', name="FPS_FIX",frame_start=0, frame_end=0, channel=3, seq1=clip) 21 | ratio = scene.render.fps/clip.elements[0].orig_fps 22 | clip.frame_final_duration = clip.frame_duration*(ratio) 23 | bpy.ops.sequencer.meta_make() 24 | 25 | class STRIP_OT_match_fps(Operator): 26 | """Match the strip length according to scene FPS""" 27 | bl_idname = "strp.matchfps" 28 | bl_label = "Match FPS" 29 | bl_options = {'REGISTER', 'UNDO'} 30 | 31 | def execute(self, context): 32 | matchFPS() 33 | return {'FINISHED'} 34 | 35 | def add_object_button(self, context): 36 | scene = bpy.context.scene 37 | editor = scene.sequence_editor 38 | clip = editor.active_strip 39 | if isinstance(clip, bpy.types.MovieSequence): 40 | self.layout.operator( 41 | STRIP_OT_match_fps.bl_idname, 42 | text="Match FPS") 43 | 44 | def register(): 45 | bpy.utils.register_class(STRIP_OT_match_fps) 46 | bpy.types.SEQUENCER_MT_strip.append(add_object_button) 47 | 48 | 49 | def unregister(): 50 | bpy.utils.unregister_class(STRIP_OT_match_fps) 51 | bpy.types.SEQUENCER_MT_strip.remove(add_object_button) 52 | 53 | 54 | if __name__ == "__main__": 55 | register() 56 | -------------------------------------------------------------------------------- /LF/lfAssets.py: -------------------------------------------------------------------------------- 1 | bl_info = { 2 | "name": "Light Field Asset Generator", 3 | "author": "ichlubna", 4 | "version": (1, 0), 5 | "blender": (3, 0, 0), 6 | "location": "3D View side panel", 7 | "description": "Generates a plane with material based on the input light field grid", 8 | "warning": "", 9 | "doc_url": "", 10 | "category": "Material" 11 | } 12 | 13 | import bpy 14 | import os 15 | import mathutils 16 | import math 17 | import numpy as np 18 | from pathlib import Path 19 | 20 | class LFReader: 21 | cols = 0 22 | rows = 0 23 | files = [] 24 | path = "" 25 | 26 | def loadDir(self, path): 27 | self.path = path 28 | files = sorted(os.listdir(path)) 29 | length = Path(files[-1]).stem.split("_") 30 | if len(length) == 1: 31 | self.cols = len(files) 32 | self.rows = 1 33 | else: 34 | self.cols = int(length[1])+1 35 | self.rows = int(length[0])+1 36 | self.files = [files[i:i+self.cols] for i in range(0, len(files), self.cols)] 37 | 38 | def getColsRows(self): 39 | return [self.cols, self.rows] 40 | 41 | def getImagePath(self, row, col): 42 | filePath = os.path.join(self.path, self.files[row][col]) 43 | return filePath 44 | 45 | def getImage(self, col, row): 46 | image = bpy.data.images.load(self.getImagePath(row,col), check_existing=True) 47 | return image 48 | 49 | def getResolution(self): 50 | image = bpy.data.images.load(self.getImagePath(0,0), check_existing=True) 51 | return image.size 52 | 53 | class LFPanel(bpy.types.Panel): 54 | bl_space_type = "VIEW_3D" 55 | bl_region_type = "UI" 56 | bl_context = "objectmode" 57 | bl_category = "LFGenerator" 58 | bl_label = "Takes input LF grid and creates plane with LF material" 59 | 60 | def draw(self, context): 61 | col = self.layout.column(align=True) 62 | col.prop(context.scene, "LFInput") 63 | col.prop(context.scene, "LFOverrideCoords") 64 | if(context.scene.LFOverrideCoords): 65 | col.prop(context.scene, "LFViewCoords") 66 | col.operator("lf.generate", text="Generate") 67 | 68 | class LFGenerator(bpy.types.Operator): 69 | """Generates the LF asset""" 70 | bl_idname = "lf.generate" 71 | bl_label = "Generate" 72 | 73 | def cameraView(self, context): 74 | for area in context.screen.areas: 75 | if area.type == 'VIEW_3D': 76 | area.spaces[0].region_3d.view_perspective = 'CAMERA' 77 | 78 | def createTexture(self, context): 79 | lf = LFReader() 80 | lf.loadDir(context.scene.LFInput) 81 | colsRows = lf.getColsRows() 82 | resolution = lf.getResolution() 83 | CHANNELS = 4 84 | gridRes = [resolution[0], resolution[1]] 85 | gridRes[0] *= colsRows[0] 86 | gridRes[1] *= colsRows[1] 87 | lfGrid = bpy.data.images.new("LFGrid", width=gridRes[0], height=gridRes[1]) 88 | lfGridPx = np.array([], dtype=float).reshape(resolution[1]*colsRows[1], 0) 89 | 90 | for col in range(colsRows[0]): 91 | pixelsCol = np.array([], dtype=float).reshape(0, resolution[0]*CHANNELS) 92 | for row in range(colsRows[1]): 93 | image = lf.getImage(col, row) 94 | pixels = np.asarray(image.pixels) 95 | pixels = np.reshape(pixels, (-1, resolution[0]*CHANNELS)) 96 | pixelsCol = np.vstack([pixelsCol, pixels]) 97 | lfGridPx = np.hstack([lfGridPx, pixelsCol]) 98 | lfGrid.pixels = lfGridPx.flatten() 99 | 100 | def createMaterial(self, context): 101 | self.createTexture(context) 102 | 103 | def createPlane(self, context): 104 | camera = context.scene.camera 105 | direction = camera.matrix_world.to_quaternion() @ mathutils.Vector((0.0, 0.0, -1.0)) 106 | direction = direction.normalized() 107 | position = camera.location+direction 108 | xSize = 2*math.tan(camera.data.angle_x*0.5) 109 | renderInfo = bpy.context.scene.render 110 | aspectRatio = renderInfo.resolution_y / renderInfo.resolution_x 111 | bpy.ops.mesh.primitive_plane_add(size=(xSize), location=position, rotation=camera.rotation_euler) 112 | context.object.dimensions[1] = xSize*aspectRatio 113 | 114 | def invoke(self, context, event): 115 | self.cameraView(context) 116 | self.createPlane(context) 117 | self.createMaterial(context) 118 | return {"FINISHED"} 119 | 120 | def register(): 121 | bpy.utils.register_class(LFGenerator) 122 | bpy.utils.register_class(LFPanel) 123 | bpy.types.Scene.LFInput = bpy.props.StringProperty(name="Input", subtype="FILE_PATH", description="The path to the input views in format cols_rows.ext", default="") 124 | bpy.types.Scene.LFOverrideCoords = bpy.props.BoolProperty(name="Override view", description="Disables view-dependent changes and sets static coordinates for LF", default=False) 125 | bpy.types.Scene.LFViewCoords = bpy.props.FloatVectorProperty(name="View coordinates", size=2, description="Normalized view coordinates", default=(0.5,0.5), min=0, max=1) 126 | 127 | def unregister(): 128 | bpy.utils.unregister_class(LFGenerator) 129 | bpy.utils.unregister_class(LFPanel) 130 | 131 | if __name__ == "__main__" : 132 | register() -------------------------------------------------------------------------------- /LF/pixelAnalyzer.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import os 3 | import shutil 4 | import mathutils 5 | import math 6 | from functools import reduce 7 | 8 | sampleDensity = 3 9 | sampleDistance = 0.1 10 | renderInfo = bpy.data.scenes["Scene"].render 11 | tempRenderFile = bpy.app.tempdir+"test.png" 12 | originalFilePath = renderInfo.filepath 13 | 14 | def imagePath(x,y,path=originalFilePath,prefix=""): 15 | return path+prefix+str(x)+"_"+str(y)+renderInfo.file_extension 16 | 17 | def clamp(x): 18 | return max(min(x, 1.0), 0.0) 19 | 20 | def getPixel(x,y,image): 21 | i = (y * image.size[0] + x ) * 4 22 | return (image.pixels[i], image.pixels[i+1], image.pixels[i+2]) 23 | 24 | def writePixel(x,y,pixels,rx,value): 25 | i = (y * rx + x ) * 4 26 | pixels[i] = value[0] 27 | pixels[i+1] = value[1] 28 | pixels[i+2] = value[2] 29 | 30 | def saveImagePixels(pixels, width, height, path): 31 | pixelImage = bpy.data.images.new("pixelImage", width=width, height=height) 32 | pixelImage.pixels[:] = pixels 33 | pixelImage.update() 34 | pixelImage.save_render(path) 35 | pixelImage.buffers_free() 36 | bpy.data.images.remove(pixelImage) 37 | 38 | def renderSamplesAndSort(): 39 | #maybe copy camera and then delete to keep the original one in case of errors 40 | camera = bpy.context.scene.camera 41 | originalBasis = camera.matrix_basis 42 | cornerTranslation = (sampleDensity*sampleDistance)/2 43 | cornerBasis = originalBasis @ mathutils.Matrix.Translation((-cornerTranslation, -cornerTranslation, 0.0)) 44 | renderInfo.use_overwrite = True 45 | renderInfo.use_border = True 46 | renderInfo.use_crop_to_border = False 47 | 48 | for ry in range(renderInfo.resolution_y): 49 | pixels = [reduce(lambda x,y:x+y,[[0.0,0.0,0.0,1.0] for i in range(sampleDensity*sampleDensity)]) for i in range(renderInfo.resolution_x)] 50 | renderPadding = 0.03 51 | renderInfo.border_max_x = 1.0 52 | renderInfo.border_max_y = clamp(ry/renderInfo.resolution_y+renderPadding) 53 | renderInfo.border_min_x = 0.0 54 | renderInfo.border_min_y = clamp(ry/renderInfo.resolution_y-renderPadding) 55 | 56 | for x in range(sampleDensity): 57 | for y in range(sampleDensity): 58 | camera.matrix_basis = cornerBasis @ mathutils.Matrix.Translation((x*sampleDistance, y*sampleDistance, 0.0)) 59 | renderInfo.filepath = tempRenderFile 60 | bpy.ops.render.render( write_still=True ) 61 | image = bpy.data.images.load(tempRenderFile) 62 | for rx in range(renderInfo.resolution_x): 63 | pixel = getPixel(rx, ry, image) 64 | writePixel(x, y, pixels[rx], sampleDensity, pixel) 65 | image.buffers_free() 66 | bpy.data.images.remove(image) 67 | 68 | for p in range(renderInfo.resolution_x): 69 | saveImagePixels(pixels[p],sampleDensity,sampleDensity,imagePath(p,ry)) 70 | 71 | renderInfo.filepath = originalFilePath 72 | camera.matrix_basis = originalBasis 73 | renderInfo.use_border = False 74 | os.remove(tempRenderFile) 75 | 76 | def renderSamplesFull(path): 77 | camera = bpy.context.scene.camera 78 | originalBasis = camera.matrix_basis.copy() 79 | cornerTranslation = (sampleDensity*sampleDistance)/2 80 | cornerBasis = originalBasis @ mathutils.Matrix.Translation((-cornerTranslation, -cornerTranslation, 0.0)) 81 | renderInfo.use_overwrite = True 82 | renderInfo.use_border = False 83 | 84 | for x in range(sampleDensity): 85 | for y in range(sampleDensity): 86 | camera.matrix_basis = cornerBasis @ mathutils.Matrix.Translation((x*sampleDistance, y*sampleDistance, 0.0)) 87 | renderInfo.filepath = imagePath(x,y,path) 88 | bpy.ops.render.render( write_still=True ) 89 | camera.matrix_basis = originalBasis 90 | 91 | def sortPixels(inputPath, outputPath): 92 | for ry in range(renderInfo.resolution_y): 93 | pixels = [reduce(lambda x,y:x+y,[[0.0,0.0,0.0,1.0] for i in range(sampleDensity*sampleDensity)]) for i in range(renderInfo.resolution_x)] 94 | for x in range(sampleDensity): 95 | for y in range(sampleDensity): 96 | image = bpy.data.images.load(inputPath + imagePath(x,y)) 97 | for rx in range(renderInfo.resolution_x): 98 | pixel = getPixel(rx, ry, image) 99 | writePixel(x, y, pixels[rx], sampleDensity, pixel) 100 | image.buffers_free() 101 | bpy.data.images.remove(image) 102 | for p in range(renderInfo.resolution_x): 103 | saveImagePixels(pixels[p],sampleDensity,sampleDensity,imagePath(p,ry,outputPath)) 104 | 105 | def reconstruct(x,y,inputPath): 106 | pixels = reduce(lambda x,y:x+y,[[0.0,0.0,0.0,1.0] for i in range(renderInfo.resolution_x*renderInfo.resolution_y)]) 107 | for px in range(renderInfo.resolution_x): 108 | for py in range(renderInfo.resolution_y): 109 | image = bpy.data.images.load(imagePath(px,py,inputPath)) 110 | writePixel(px,py,pixels,renderInfo.resolution_x,getPixel(x,y,image)) 111 | image.buffers_free() 112 | bpy.data.images.remove(image) 113 | saveImagePixels(pixels,renderInfo.resolution_x,renderInfo.resolution_y, imagePath(x,y,prefix="reconstructed")) 114 | 115 | try: 116 | #renderSamplesAndSort() 117 | #renderInfo.filepath 118 | renderPath = "/home/ichlubna/Downloads/pav/"#bpy.app.tempdir + "render/" 119 | #sortPath = bpy.app.tempdir + "sort/" 120 | #os.mkdir(renderPath) 121 | #os.mkdir(sortPath) 122 | renderSamplesFull(renderPath) 123 | #sortPixels("/home/ichlubna/Downloads/lego/", sortPath) 124 | #reconstruct(2,2,sortPath) 125 | except Exception as e: 126 | renderInfo.filepath = originalFilePath 127 | print(e) 128 | renderInfo.filepath = originalFilePath -------------------------------------------------------------------------------- /LF/cameras.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import bmesh 3 | import mathutils 4 | import math 5 | import os 6 | 7 | class LFPanel(bpy.types.Panel): 8 | bl_space_type = "VIEW_3D" 9 | bl_region_type = "UI" 10 | bl_context = "objectmode" 11 | bl_category = "Lightfield" 12 | bl_label = "Generate lightfield array" 13 | 14 | def draw(self, context): 15 | col = self.layout.column(align=True) 16 | if context.scene.lfType == "plane": 17 | col.prop(context.scene, "lfAspect") 18 | col.prop(context.scene, "lfType") 19 | col.prop(context.scene, "lfSize") 20 | col.prop(context.scene, "lfDensity") 21 | col.prop(context.scene, "lfDepth") 22 | col.operator("mesh.generate", text="Generate") 23 | col.operator("mesh.render", text="Render") 24 | 25 | class LFArray(bpy.types.Operator): 26 | """ Generates the camera grid with given parameters. 27 | """ 28 | bl_idname = "mesh.generate" 29 | bl_label = "Generate LF array" 30 | bl_options = {"UNDO"} 31 | 32 | def invoke(self, context, event): 33 | bm = bmesh.new() 34 | mesh = bpy.data.meshes.new('Basic_Cube') 35 | lookAtCenter = False 36 | 37 | if context.scene.lfType == "row": 38 | position = [0,0,0] 39 | baseline = context.scene.lfSize/context.scene.lfDensity 40 | for i in range(context.scene.lfDensity): 41 | position = [0,0,0] 42 | position[1] = (context.scene.lfDensity/2.0 - i)*baseline 43 | bpy.ops.object.camera_add(location=position, rotation=(1.57,0,1.57)) 44 | camera = context.object 45 | camera.name = "LF_Cam_"+str(i) 46 | 47 | else: 48 | if context.scene.lfType == "plane": 49 | ratio = 1.0 50 | if context.scene.lfAspect == "16:9": 51 | ratio = 16.0/9 52 | elif context.scene.lfAspect == "4:3": 53 | ratio = 4.0/3.0 54 | mat = mathutils.Matrix.Scale(ratio, 4, (1.0, 0.0, 0.0)) @ mathutils.Matrix.Rotation(math.radians(180), 4, 'X') #rotation to place the first cam to top left corner 55 | bmesh.ops.create_grid(bm, x_segments= context.scene.lfDensity, y_segments=context.scene.lfDensity, size=context.scene.lfSize, matrix=mat), 56 | elif context.scene.lfType == "sphere": 57 | bmesh.ops.create_icosphere(bm, subdivisions=context.scene.lfDensity, radius=context.scene.lfSize) 58 | lookAtCenter = True 59 | 60 | bm.to_mesh(mesh) 61 | for v in mesh.vertices: 62 | direction = -v.co 63 | rotation = (0,0,0) 64 | if lookAtCenter: 65 | rotation = direction.to_track_quat('-Z', 'Y').to_euler() 66 | bpy.ops.object.camera_add(location=v.co, rotation=rotation) 67 | camera = context.object 68 | camera.name = "LF_Cam_"+str(v.index) 69 | 70 | objects = bpy.context.scene.objects 71 | for obj in objects: 72 | obj.select_set(obj.name[:6] == "LF_Cam") 73 | 74 | return {"FINISHED"} 75 | 76 | class LFRender(bpy.types.Operator): 77 | """ Renders whole animation (start-end frame), all frames for one cam in one folder. 78 | Takes format settings from Render options. 79 | """ 80 | bl_idname = "mesh.render" 81 | bl_label = "Render LF" 82 | bl_descripiton = "Render all views to the output folder" 83 | #TODO render depthmaps button 84 | 85 | def invoke(self, context, event): 86 | renderInfo = bpy.data.scenes["Scene"].render 87 | path = renderInfo.filepath[:] 88 | camCount = 0 89 | for obj in bpy.context.scene.objects: 90 | if obj.name[:6] == "LF_Cam": 91 | camCount += 1 92 | bpy.context.scene.camera = obj 93 | camPath = path+"/"+obj.name[7:] 94 | if not os.path.exists(camPath): 95 | os.makedirs(camPath) 96 | #context.window_manager.progress_begin(0,context.scene.lfS) 97 | for i in range(bpy.context.scene.frame_start, bpy.context.scene.frame_end+1): 98 | renderInfo.filepath = camPath+"/"+str(bpy.context.scene.frame_start-i) 99 | bpy.context.scene.frame_set(i) 100 | bpy.ops.render.render( write_still=True ) 101 | 102 | bpy.data.scenes["Scene"].render.filepath = path 103 | 104 | infoFile = open(path+"/info", "w") 105 | infoFile.write("Camera count: " + str(camCount)) 106 | infoFile.write("\nFPS: " + str(renderInfo.fps)) 107 | infoFile.write("\nFrame count: " + str(bpy.context.scene.frame_end - bpy.context.scene.frame_start)) 108 | infoFile.write("\nWidth: " + str(int((renderInfo.resolution_x * renderInfo.resolution_percentage)/100))) 109 | infoFile.write("\nHeight: " + str(int((renderInfo.resolution_y * renderInfo.resolution_percentage)/100))) 110 | #TODO camparams 111 | infoFile.close() 112 | 113 | 114 | return {"FINISHED"} 115 | 116 | def register(): 117 | bpy.utils.register_class(LFArray) 118 | bpy.utils.register_class(LFRender) 119 | bpy.utils.register_class(LFPanel) 120 | bpy.types.Scene.lfType = bpy.props.EnumProperty(name="Type", description="Shape of the LF camera array", items=[("row","Row","Horizontal line"), ("plane","Plane","Planar grid"), ("sphere","Sphere","Spherical grid (icosphere)")]) 121 | bpy.types.Scene.lfAspect = bpy.props.EnumProperty(name="Aspect", description="Aspect ratio for the camera grid", items=[("16:9", "16:9", ""), ("4:3", "4:3", ""), ("1:1", "1:1", "")]) 122 | bpy.types.Scene.lfSize = bpy.props.FloatProperty(name="Size", description="Scale of the array", default=1.0) 123 | bpy.types.Scene.lfDensity = bpy.props.IntProperty(name="Density", description="Density of the array", default=8) 124 | bpy.types.Scene.lfDepth = bpy.props.BoolProperty(name="Depth maps", description="Will render depth maps too", default=True) 125 | 126 | def unregister(): 127 | bpy.utils.unregister_class(LFArray) 128 | bpy.utils.unregister_class(LFRender) 129 | bpy.utils.unregister_class(LFPanel) 130 | del bpy.types.Scene.lfType 131 | 132 | if __name__ == "__main__" : 133 | register() 134 | -------------------------------------------------------------------------------- /MISC/ffExport.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import tempfile 3 | import shlex 4 | import shutil 5 | import subprocess 6 | 7 | bl_info = { 8 | "name": "FFmpeg Export", 9 | "description": "Allows to export the animation with external FFmpeg and all available formats.", 10 | "author": "ichlubna", 11 | "version": (1, 1), 12 | "blender": (4, 2, 3), 13 | "location": "Render Properties", 14 | "warning": "", 15 | "tracker_url": "https://github.com/ichlubna/blenderScripts", 16 | "support": "COMMUNITY", 17 | "category": "Import-Export" 18 | } 19 | 20 | class FFE_PT_Panel(bpy.types.Panel): 21 | bl_space_type = "PROPERTIES" 22 | bl_region_type = "WINDOW" 23 | bl_context = "render" 24 | bl_label = "FFmpeg Export" 25 | bl_options = {'DEFAULT_CLOSED'} 26 | 27 | def draw(self, context): 28 | self.layout.label(text="Exports the animation using full external FFmpeg") 29 | col = self.layout.column(align=True) 30 | col.prop(context.scene, "ffPath") 31 | col.prop(context.scene, "ffOutput") 32 | col.prop(context.scene, "ffParams") 33 | col.prop(context.scene, "ffImages") 34 | if context.scene.ffImages: 35 | col.prop(context.scene, "ffImagesPath") 36 | col.prop(context.scene, "ffImagesRender") 37 | col.operator("ffexport.render", text="Render") 38 | col.prop(context.scene, "ffExamples") 39 | 40 | class FFE_OT_Render(bpy.types.Operator): 41 | """ Renders the animation using FFmpeg 42 | """ 43 | bl_idname = "ffexport.render" 44 | bl_label = "Render animation" 45 | 46 | def getFPSStr(self): 47 | return str(round(bpy.context.scene.render.fps / bpy.context.scene.render.fps_base,2)) 48 | 49 | def encodeRender(self, context, renderInfo, tempDir, pipe): 50 | renderInfo.filepath = tempDir+"/frame.png" 51 | renderInfo.image_settings.file_format = 'PNG' 52 | for i in range(bpy.context.scene.frame_start, bpy.context.scene.frame_end+1): 53 | bpy.context.scene.frame_set(i) 54 | bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1) 55 | if context.scene.ffImages: 56 | renderInfo.filepath = context.scene.ffImagesPath+"/"+f'{i:05d}'+".png" 57 | bpy.ops.render.render( write_still=True ) 58 | with open(renderInfo.filepath, 'rb') as f: 59 | data = f.read() 60 | pipe.stdin.write(data) 61 | 62 | def bulkEncodeFrames(self, context): 63 | print(self.getFPSStr()) 64 | renderInfo = bpy.context.scene.render 65 | cmd = [context.scene.ffPath.encode('unicode_escape'), '-y', '-r', str(renderInfo.fps), '-start_number', 66 | str(bpy.context.scene.frame_start), 67 | '-i', context.scene.ffImagesPath+'/%05d.png', '-r', self.getFPSStr()] + \ 68 | shlex.split(context.scene.ffParams) + [context.scene.ffOutput] 69 | pipe = subprocess.Popen(cmd) 70 | pipe.wait() 71 | 72 | def encodeScene(self, context): 73 | renderInfo = bpy.context.scene.render 74 | tempDir = tempfile.mkdtemp() 75 | 76 | cmd = [context.scene.ffPath.encode('unicode_escape'), '-y', '-f', 'image2pipe', '-c:v', 'png', '-r', self.getFPSStr(), '-i', '-'] + \ 77 | shlex.split(context.scene.ffParams) + [context.scene.ffOutput] 78 | pipe = subprocess.Popen(cmd, stdin=subprocess.PIPE) 79 | 80 | self.encodeRender(context, renderInfo, tempDir, pipe) 81 | 82 | pipe.stdin.close() 83 | pipe.wait() 84 | if pipe.returncode != 0: 85 | raise subprocess.CalledProcessError(pipe.returncode, cmd) 86 | shutil.rmtree(tempDir) 87 | 88 | def invoke(self, context, event): 89 | if context.scene.ffImagesRender: 90 | self.bulkEncodeFrames(context) 91 | else: 92 | self.encodeScene(context) 93 | return {"FINISHED"} 94 | 95 | 96 | def updateExample(self, context): 97 | print(context.scene.ffExamples) 98 | if context.scene.ffExamples == "none": 99 | context.scene.ffOutput = "" 100 | context.scene.ffParams = "" 101 | if context.scene.ffExamples == "h.265": 102 | context.scene.ffParams = "-c:v libx265 -crf 28 -pix_fmt yuv420p" 103 | context.scene.ffOutput = "example.mkv" 104 | if context.scene.ffExamples == "hevc_nvenc": 105 | context.scene.ffParams = "-c:v hevc_nvenc -crf 28" 106 | context.scene.ffOutput = "example.mkv" 107 | if context.scene.ffExamples == "AV1": 108 | context.scene.ffParams = "-c:v libaom-av1 -crf 28 -pix_fmt yuv420p -cpu-used 4" 109 | context.scene.ffOutput = "example.mkv" 110 | if context.scene.ffExamples == "gif": 111 | context.scene.ffParams = "" 112 | context.scene.ffOutput = "example.gif" 113 | 114 | def register(): 115 | if bpy.app.background: 116 | return # avoid running in Blender CLI 117 | bpy.utils.register_class(FFE_PT_Panel) 118 | bpy.utils.register_class(FFE_OT_Render) 119 | bpy.types.Scene.ffPath = bpy.props.StringProperty(name="FFmpeg path", subtype="FILE_PATH", description="The path to the ffmpeg binary", default="ffmpeg") 120 | bpy.types.Scene.ffOutput = bpy.props.StringProperty(name="Output file", subtype="FILE_PATH", description="The output file with extension", default="myFile.mkv") 121 | bpy.types.Scene.ffParams = bpy.props.StringProperty(name="FFmpeg params", description="The ffmpeg parameters", default="-c:v libx265 -crf 28") 122 | bpy.types.Scene.ffImages = bpy.props.BoolProperty(name="Store frames", description="Will store the frames as well", default=False) 123 | bpy.types.Scene.ffImagesRender = bpy.props.BoolProperty(name="Render frames", description="Will encode the stored frames without rendering", default=False) 124 | bpy.types.Scene.ffImagesPath = bpy.props.StringProperty(name="Images path", subtype="DIR_PATH", description="Path for the frames to store", default="./myFrames") 125 | bpy.types.Scene.ffExamples = bpy.props.EnumProperty(name="Examples", description="Sets the params to the selected example", items=[("none","none", ""), ("h.265","h.265", ""), ("hevc_nvenc","hevc_nvenc", ""), ("AV1","AV1", ""), ("gif", "gif", "")], update=updateExample) 126 | 127 | def unregister(): 128 | bpy.utils.unregister_class(FFE_PT_Panel) 129 | bpy.utils.unregister_class(FFE_OT_Render) 130 | 131 | if __name__ == "__main__" : 132 | register() 133 | -------------------------------------------------------------------------------- /MISC/customNode.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bl_ui import node_add_menu 3 | import tempfile 4 | import string 5 | import random 6 | import os 7 | import shutil 8 | 9 | class RenderImage(bpy.types.Operator): 10 | """ Renders the scene, stores it in a file, loads the file back in Blender. 11 | """ 12 | bl_idname = "mesh.render" 13 | bl_label = "Render Image" 14 | percentage : bpy.props.IntProperty(default=100) 15 | fileName : bpy.props.StringProperty(default="") 16 | 17 | def invoke(self, context, event): 18 | renderInfo = context.scene.render 19 | backupPath = renderInfo.filepath[:] 20 | backupFormat = renderInfo.image_settings.file_format 21 | backupPercent = renderInfo.resolution_percentage 22 | 23 | renderInfo.image_settings.file_format = "PNG" 24 | renderInfo.resolution_percentage = self.percentage 25 | renderInfo.filepath = self.fileName 26 | bpy.ops.render.render(write_still=True) 27 | 28 | image = bpy.data.images.get(self.fileName, None) 29 | image.source = 'FILE' 30 | image.filepath = self.fileName 31 | image.reload() 32 | image.update() 33 | 34 | renderInfo.filepath = backupPath[:] 35 | renderInfo.image_settings.file_format = backupFormat 36 | renderInfo.resolution_percentage = backupPercent 37 | return {"FINISHED"} 38 | 39 | class TestNode (bpy.types.ShaderNodeCustomGroup): 40 | """ This is a new material node that uses the RenderImage operator, renders the scene and uses the rendered image as a texture which can be colorized by the input color. 41 | """ 42 | bl_name = 'TestNode' 43 | bl_label = 'TestNode' 44 | 45 | def update_effect(self, context): 46 | if (bpy.context.scene.use_nodes == False): 47 | return 48 | mixNode = self.node_tree.nodes.get("mixNode") 49 | mixNode.inputs[0].default_value = self.colorStrength 50 | mixNode.update() 51 | return 52 | 53 | percentage : bpy.props.IntProperty(name="Image size", 54 | description="Percentage of the render size", 55 | min=1, default=100, 56 | update=update_effect) 57 | fileName : bpy.props.StringProperty(name="Name of the image", 58 | description="This image is used to store the render", 59 | default="") 60 | 61 | def init(self, context): 62 | self.node_tree = bpy.data.node_groups.new(self.bl_name, 'ShaderNodeTree') 63 | inputColor = self.node_tree.interface.new_socket(name="Color", description="Color input", in_out='INPUT', socket_type='NodeSocketColor') 64 | outputColor = self.node_tree.interface.new_socket(name="Color", description="Color output", in_out='OUTPUT', socket_type='NodeSocketColor') 65 | 66 | inNode = self.node_tree.nodes.new(type='NodeGroupInput') 67 | outNode = self.node_tree.nodes.new(type='NodeGroupOutput') 68 | 69 | imageNode = self.node_tree.nodes.new("ShaderNodeTexEnvironment") 70 | self.fileName = str((''.join(random.choices(string.ascii_letters, k=5))) + ".png") 71 | temp = tempfile.TemporaryDirectory().name 72 | self.fileName = os.path.join(temp, self.fileName) 73 | imageNode.name = 'resultImageNode' 74 | for image in bpy.data.images: 75 | if image.name == self.fileName: 76 | bpy.data.images.remove(image) 77 | textureImage = bpy.data.images.new(self.fileName, 0, 0) 78 | imageNode.image = textureImage 79 | 80 | mixNode = self.node_tree.nodes.new("ShaderNodeMixRGB") 81 | mixNode.name = 'mixNode' 82 | mixNode.blend_type = 'COLOR' 83 | 84 | self.node_tree.links.new(imageNode.outputs[0], mixNode.inputs[1]) 85 | self.node_tree.links.new(inNode.outputs[0], mixNode.inputs[2]) 86 | self.node_tree.links.new(mixNode.outputs[0], outNode.inputs[0]) 87 | return 88 | 89 | def draw_buttons(self, context, layout): 90 | col = layout.column(align=True) 91 | textureImage = bpy.data.images.get("TestImage", None) 92 | col.prop(self, "percentage") 93 | op = col.operator("mesh.render", text="Render") 94 | op.percentage = self.percentage 95 | op.fileName = self.fileName 96 | 97 | def copy(self, node): 98 | self.init(bpy.context) 99 | return 100 | 101 | def free(self): 102 | bpy.data.node_groups.remove(self.node_tree, do_unlink=True) 103 | shutil.rmtree(os.path.dirname(self.fileName)) 104 | return 105 | 106 | class NODE_MT_category_shader_test(bpy.types.Menu): 107 | bl_idname = "NODE_MT_category_shader_test" 108 | bl_label = "Test" 109 | 110 | def draw(self, context): 111 | layout = self.layout 112 | node_add_menu.add_node_type(layout, "TestNode") 113 | node_add_menu.draw_assets_for_catalog(layout, self.bl_label) 114 | 115 | def testNodeDrawInNew(self, context): 116 | layout = self.layout 117 | layout.menu("NODE_MT_category_shader_test") 118 | 119 | def extendDraw(fn): 120 | def newDraw(self, context): 121 | fn(self, context) 122 | node_add_menu.add_node_type(self.layout, "TestNode") 123 | return newDraw 124 | 125 | backupDraw = bpy.types.NODE_MT_category_shader_output.draw 126 | 127 | def extendExistingCategory(): 128 | category = bpy.types.NODE_MT_category_shader_output 129 | global backupDraw 130 | backupDraw = category.draw 131 | category.draw = extendDraw(category.draw) 132 | 133 | def revertExistingCategory(): 134 | category = bpy.types.NODE_MT_category_shader_output 135 | global backupDraw 136 | category.draw = backupDraw 137 | 138 | def register(): 139 | bpy.utils.register_class(NODE_MT_category_shader_test) 140 | bpy.types.NODE_MT_shader_node_add_all.append(testNodeDrawInNew) 141 | #extendExistingCategory() 142 | bpy.utils.register_class(RenderImage) 143 | bpy.utils.register_class(TestNode) 144 | 145 | 146 | def unregister(): 147 | #revertExistingCategory() 148 | bpy.types.NODE_MT_shader_node_add_all.remove(testNodeDrawInNew) 149 | bpy.utils.unregister_class(NODE_MT_category_shader_test) 150 | bpy.utils.unregister_class(TestNode) 151 | bpy.utils.unregister_class(RenderImage) 152 | 153 | try: 154 | unregister() 155 | except: 156 | pass 157 | register() 158 | -------------------------------------------------------------------------------- /VFX/lightning.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import random 3 | import math 4 | import gpu 5 | import bgl 6 | import numpy as np 7 | import mathutils 8 | import bl_math 9 | from gpu_extras.batch import batch_for_shader 10 | import nodeitems_utils 11 | from nodeitems_builtins import CompositorNodeCategory 12 | from multiprocessing import Pool 13 | 14 | import time 15 | 16 | 17 | bl_info = { 18 | "name": "Compositor lightning generator node", 19 | "description": 20 | "Adds a new node in compositor that generates an electric lightning effect.", 21 | "author": "ichlubna", 22 | "version": (1, 0), 23 | "blender": (2, 80, 0), 24 | "location": "Compositing > Add > Generate > Lightning", 25 | "warning": "", 26 | "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/" 27 | "Scripts/My_Script", 28 | "tracker_url": 29 | "https://github.com/ichlubna/blenderScripts/tree/master/VFX", 30 | "support": "COMMUNITY", 31 | "category": "Compositing" 32 | } 33 | 34 | 35 | class LightningGen (bpy.types.CompositorNodeCustomGroup): 36 | 37 | bl_name = 'LightningGen' 38 | bl_label = 'Lightning' 39 | 40 | def generateBolt(self, p0, p1): 41 | # TODO add spatially consistent random generator so that moving the bolt doesn't change shape rapidly 42 | # https://docs.blender.org/api/current/mathutils.noise.html 43 | class Lines: 44 | class Segment: 45 | p0 = 0 46 | p1 = 0 47 | level = 0 48 | def __init__(self, a, b, l): 49 | self.p0 = a 50 | self.p1 = b 51 | self.level = l 52 | maxLevel = 0 53 | segments = [] 54 | vertices = [] 55 | initP0 = [] 56 | initP1 = [] 57 | initDirection = [] 58 | initLength = 0 59 | def __init__(self, p0, p1): 60 | self.initP0 = p0 61 | self.initP1 = p1 62 | self.initDirection = p1-p0 63 | self.initLength = np.linalg.norm(self.initDirection) 64 | self.addVertex(p0) 65 | self.addVertex(p1) 66 | self.addLine(0,1,0) 67 | def removeSegment(self, i): 68 | del self.segments[i] 69 | def getCoords(self,s): 70 | return [(self.vertices[s.p0], self.vertices[s.p1])] 71 | def addLine(self, a, b, level): 72 | if level > self.maxLevel: 73 | self.maxLevel = level 74 | self.segments.append(self.Segment(a, b, level)) 75 | def addVertex(self, v): 76 | self.vertices.append(v) 77 | return len(self.vertices)-1 78 | def getMaxLevel(self): 79 | return self.maxLevel; 80 | def getInitPts(self): 81 | return (self.initP1, self.initP0) 82 | def getInitLength(self): 83 | return self.initLength 84 | def getInitDirection(self): 85 | return self.initDirection 86 | 87 | lines = Lines(p0,p1) 88 | length = lines.getInitLength() 89 | random.seed(self.seed) 90 | randRange = int((1.0-self.stability)*int(length)) 91 | for i in range(0, self.complexity): 92 | for si in range (0, len(lines.segments)): 93 | segment = lines.segments[si] 94 | pts = lines.getCoords(segment)[0] 95 | vector = pts[1]-pts[0] 96 | normal = np.array([vector[1], -vector[0]]) 97 | normal = normal/np.linalg.norm(normal, ord=1) 98 | randOffset = normal*random.randint(-randRange, randRange) 99 | midpoint = ((pts[0]+pts[1])/2)+randOffset 100 | midpointID = lines.addVertex(midpoint) 101 | 102 | if random.uniform(0.0, 1.0) < self.forking and i < int(self.complexity/2): 103 | forkDirection = midpoint-pts[0] 104 | angle = random.uniform(0.1, 1.55*self.maxForkAngle) 105 | if random.uniform(0.0, 1.0) > 0.5: 106 | angle = -angle 107 | cosVal = math.cos(angle) 108 | sinVal = math.sin(angle) 109 | csd = np.array([cosVal, -sinVal])*forkDirection 110 | scd = np.array([sinVal, cosVal])*forkDirection 111 | forkEnd = midpoint+np.array([np.sum(csd), np.sum(scd)]) 112 | forkEndID = lines.addVertex(forkEnd) 113 | lines.addLine(midpointID, forkEndID, segment.level+1) 114 | 115 | lines.addLine(segment.p0, midpointID, segment.level) 116 | lines.addLine(midpointID, segment.p1, segment.level) 117 | lines.removeSegment(si) 118 | randRange = int(randRange/2) 119 | return lines 120 | 121 | def drawBolt(self, lines, pixels, coord, w, h): 122 | bitmap = [0.0, 0.0, 0.0, 1.0]*(w*h) 123 | def drawLine(start, end, thickness): 124 | def scaleRadius(radius, point, start, end, scale): 125 | if (scale == 1.0): 126 | return radius 127 | maxDistMultiplier = 1.0 128 | maxDist = math.dist((coord[0], coord[1]), (coord[2], coord[3]))*maxDistMultiplier 129 | currentDist = math.dist(point, (coord[0], coord[1])) 130 | if(currentDist == 0.0): 131 | return radius 132 | return int(round((currentDist/maxDist)*scale*radius+radius)) 133 | 134 | def setPixel(x, y): 135 | if (0 <= x < w) and (0 <= y < h): 136 | offset = (x + int(y*w))*4 137 | for i in range(4): 138 | try: 139 | bitmap[offset+i] = 1.0 140 | except: 141 | {} 142 | 143 | def drawPoint(x, y, thickness): 144 | radius = scaleRadius(thickness, [x,y], start, end, self.perspectiveScale) 145 | for X in range(-radius, radius+1): 146 | for Y in range(-radius, radius+1): 147 | if(X*X+Y*Y <= radius*radius): 148 | setPixel(X+x, Y+y) 149 | 150 | # Bressenham 151 | dx = abs(end[0] - start[0]) 152 | dy = abs(end[1] - start[1]) 153 | x, y = start[0], start[1] 154 | sx = -1 if start[0] > end[0] else 1 155 | sy = -1 if start[1] > end[1] else 1 156 | if dx > dy: 157 | err = dx / 2.0 158 | while x != end[0] and (0 <= x < w): 159 | drawPoint(x, y, thickness) 160 | err -= dy 161 | if err < 0: 162 | y += sy 163 | err += dx 164 | x += sx 165 | else: 166 | err = dy / 2.0 167 | while y != end[1] and (0 <= y < h): 168 | drawPoint(x, y, thickness) 169 | 170 | err -= dx 171 | if err < 0: 172 | x += sx 173 | err += dy 174 | y += sy 175 | drawPoint(x, y, thickness) 176 | 177 | for segment in lines.segments: 178 | pts = lines.getCoords(segment)[0] 179 | width = int(bl_math.clamp(self.thickness*(1.0-(segment.level/(self.falloff*lines.getMaxLevel()))), 0, self.thickness)) 180 | drawLine((int(pts[0][0]), int(pts[0][1])), (int(pts[1][0]), int(pts[1][1])), width) 181 | pixels.foreach_set(bitmap) 182 | 183 | def drawBoltGPU(self, lines, pixels, coord, w, h): 184 | vertexSource= ''' 185 | in vec3 position; 186 | void main() 187 | { 188 | gl_Position = vec4(position, 1.0); 189 | } 190 | ''' 191 | geometrySource= ''' 192 | layout(lines) in; 193 | layout(triangle_strip, max_vertices = 4) out; 194 | void main() 195 | { 196 | float width = gl_in[1].gl_Position.z; 197 | vec2 line = gl_in[1].gl_Position.xy - gl_in[0].gl_Position.xy; 198 | vec2 normal = normalize(vec2(line.y, -line.x))*(width/2.0); 199 | for(int i=0; i<4; i++) 200 | { 201 | vec2 coords = gl_in[i/2].gl_Position.xy+(1-2*(i%2))*normal; 202 | gl_Position = vec4(coords, 0.0, 1.0); 203 | EmitVertex(); 204 | } 205 | EndPrimitive(); 206 | } 207 | ''' 208 | fragmentSource = ''' 209 | out vec4 fragColor; 210 | void main() 211 | { 212 | fragColor = vec4(1.0,1.0,1.0,1.0); 213 | } 214 | ''' 215 | positions = [] 216 | for segment in lines.segments: 217 | pts = lines.getCoords(segment)[0] 218 | ptA = np.divide(pts[0], np.array([w,h]))*2-1.0 219 | ptB = np.divide(pts[1], np.array([w,h]))*2-1.0 220 | width = bl_math.clamp(self.thickness*(1.0-(segment.level/(self.falloff*lines.getMaxLevel()))), 0, self.thickness)/w 221 | positions.append((ptA[0], ptA[1], width)) 222 | positions.append((ptB[0], ptB[1], width)) 223 | offscreen = gpu.types.GPUOffScreen(w, h) 224 | shaders = gpu.types.GPUShader(vertexSource, fragmentSource, geocode=geometrySource) 225 | batch = batch_for_shader(shaders, 'LINES', {"position": tuple(positions)}) 226 | with offscreen.bind(): 227 | bgl.glClearColor(0.0, 0.0, 0.0, 1.0) 228 | bgl.glClear(bgl.GL_COLOR_BUFFER_BIT) 229 | with gpu.matrix.push_pop(): 230 | gpu.matrix.load_matrix(mathutils.Matrix.Identity(4)) 231 | gpu.matrix.load_projection_matrix(mathutils.Matrix.Identity(4)) 232 | shaders.bind() 233 | batch.draw(shaders) 234 | buffer = bgl.Buffer(bgl.GL_FLOAT, w * h * 4) 235 | bgl.glReadBuffer(bgl.GL_BACK) 236 | bgl.glReadPixels(0, 0, w, h, bgl.GL_RGBA, bgl.GL_FLOAT, buffer) 237 | pixels.foreach_set(buffer) 238 | offscreen.free() 239 | 240 | def update_effect(self, context): 241 | if (bpy.context.scene.use_nodes == False): 242 | return 243 | scene = bpy.context.scene 244 | img = bpy.data.images[self.name] 245 | coords = [0, 0, 0, 0] 246 | inputs = ['Start X', 'Start Y', 'End X', 'End Y'] 247 | for i in range(len(inputs)): 248 | coords[i] = self.inputs[inputs[i]].default_value 249 | if len(self.inputs[inputs[i]].links) != 0: 250 | inputNode = self.inputs[inputs[i]].links[0].from_node 251 | if isinstance(inputNode, bpy.types.CompositorNodeTrackPos): 252 | markerPosition = inputNode.clip.tracking.tracks[inputNode.track_name].markers.find_frame(bpy.context.scene.frame_current).co 253 | xy = 1 254 | if i % 2 == 0: 255 | xy = 0 256 | coords[i] = int(markerPosition[xy]*inputNode.clip.size[xy]) 257 | else: 258 | bpy.context.scene.node_tree.links.remove(self.inputs[inputs[i]].links[0]) 259 | start = time.time() 260 | lines = self.generateBolt(np.array([coords[0],coords[1]]), np.array([coords[2],coords[3]])) 261 | if(self.gpuComp): 262 | self.drawBoltGPU(lines, img.pixels, coords, img.size[0], img.size[1]) 263 | else: 264 | self.drawBolt(lines, img.pixels, coords, img.size[0], img.size[1]) 265 | 266 | end = time.time() 267 | print(end - start) 268 | img.update() 269 | coreBlurNode = self.node_tree.nodes.get('coreBlurNode') 270 | coreBlurNode.size_x = self.coreBlur 271 | coreBlurNode.size_y = self.coreBlur 272 | glowBlurNode = self.node_tree.nodes.get('glowBlurNode') 273 | glowBlurNode.size_x = self.glow 274 | glowBlurNode.size_y = self.glow 275 | return 276 | 277 | forking: bpy.props.FloatProperty(name="Forking", 278 | description="The probability of forking", 279 | min=0.0, max=1.0, default=0.5, 280 | update=update_effect) 281 | maxForkAngle: bpy.props.FloatProperty(name="Max fork angle", 282 | description="Maximal angle of the forks", 283 | min=0.0, max=1.0, default=0.5, 284 | update=update_effect) 285 | complexity: bpy.props.IntProperty(name="Complexity", 286 | description="Number of recursive segments (curves of the bolt)", 287 | min=5, max=17, default=8, 288 | update=update_effect) 289 | stability: bpy.props.FloatProperty(name="Stability", 290 | description="How much does the bolt wiggle", 291 | min=0.0, max=1.0, default=0.5, 292 | update=update_effect) 293 | falloff: bpy.props.FloatProperty(name="Falloff", 294 | description="Making the bolt thin at the end", 295 | min=0.5, max=2.0, default=0.5, 296 | update=update_effect) 297 | thickness: bpy.props.IntProperty(name="Thickness", 298 | description="Overall thickness of the bolt", 299 | min=0, max=100, default=3, 300 | update=update_effect) 301 | perspectiveScale: bpy.props.FloatProperty(name="Perspective scale", 302 | description="Scales the bolt in the direction of end point", 303 | min=1.0, max=10.0, default=1.0, 304 | update=update_effect) 305 | glow: bpy.props.IntProperty(name="Glow", 306 | description="The amount of glow/light emitted by the core", 307 | min=0, max=200, default=60, 308 | update=update_effect) 309 | coreBlur: bpy.props.IntProperty(name="Core blur", 310 | description="How sharp the core is", 311 | min=0, max=30, default=5, 312 | update=update_effect) 313 | seed: bpy.props.IntProperty(name="Seed", 314 | description="Random seed affecting the shape of the bolt", 315 | min=0, default=0, 316 | update=update_effect) 317 | gpuComp: bpy.props.BoolProperty(name="GPU compute", 318 | description="Use GPU acceleration (good for very thick and complex bolts)", 319 | default=0, 320 | update=update_effect) 321 | 322 | def init(self, context): 323 | scene = bpy.context.scene 324 | bpy.data.images.new(name=self.name, width=scene.render.resolution_x, height=scene.render.resolution_y) 325 | 326 | self.node_tree = bpy.data.node_groups.new(self.bl_name, 'CompositorNodeTree') 327 | inputs = self.node_tree.nodes.new('NodeGroupInput') 328 | outputs = self.node_tree.nodes.new('NodeGroupOutput') 329 | self.node_tree.inputs.new("NodeSocketColor", "Glow color") 330 | self.node_tree.inputs.new("NodeSocketInt", "Start X") 331 | self.node_tree.inputs.new("NodeSocketInt", "Start Y") 332 | self.node_tree.inputs.new("NodeSocketInt", "End X") 333 | self.node_tree.inputs.new("NodeSocketInt", "End Y") 334 | self.node_tree.outputs.new("NodeSocketColor", "Image") 335 | 336 | self.inputs["Start X"].default_value=scene.render.resolution_x/3 337 | self.inputs["Start Y"].default_value=scene.render.resolution_y/2 338 | self.inputs["End X"].default_value=scene.render.resolution_x-scene.render.resolution_x/3 339 | self.inputs["End Y"].default_value=scene.render.resolution_y/2 340 | self.inputs["Glow color"].default_value = [0.0, 0.27, 1.0, 1.0] 341 | 342 | imageNode = self.node_tree.nodes.new("CompositorNodeImage") 343 | imageNode.name = 'resultImageNode' 344 | imageNode.image = bpy.data.images[self.name] 345 | 346 | coreBlurNode = self.node_tree.nodes.new("CompositorNodeBlur") 347 | coreBlurNode.name = 'coreBlurNode' 348 | coreBlurNode.filter_type = 'FAST_GAUSS' 349 | 350 | glowBlurNode = self.node_tree.nodes.new("CompositorNodeBlur") 351 | glowBlurNode.name = 'glowBlurNode' 352 | glowBlurNode.filter_type = 'FAST_GAUSS' 353 | 354 | mixNode = self.node_tree.nodes.new("CompositorNodeMixRGB") 355 | mixNode.name = 'mixNode' 356 | mixNode.blend_type = 'ADD' 357 | 358 | colorizeNode = self.node_tree.nodes.new("CompositorNodeMixRGB") 359 | colorizeNode.name = 'colorizeNode' 360 | colorizeNode.blend_type = 'MIX' 361 | colorizeNode.inputs[1].default_value = (0.0, 0.0, 0.0, 1.0) 362 | colorizeNode.inputs[2].default_value = (0.0, 0.0, 1.0, 1.0) 363 | 364 | self.outputs['Image'].default_value = imageNode.outputs[0].default_value 365 | self.node_tree.links.new(imageNode.outputs[0], coreBlurNode.inputs[0]) 366 | self.node_tree.links.new(imageNode.outputs[0], glowBlurNode.inputs[0]) 367 | self.node_tree.links.new(coreBlurNode.outputs[0], mixNode.inputs[1]) 368 | self.node_tree.links.new(glowBlurNode.outputs[0], colorizeNode.inputs[0]) 369 | self.node_tree.links.new(self.node_tree.nodes['Group Input'].outputs[0], colorizeNode.inputs[2]) 370 | self.node_tree.links.new(colorizeNode.outputs[0], mixNode.inputs[2]) 371 | self.node_tree.links.new(mixNode.outputs[0], self.node_tree.nodes['Group Output'].inputs[0]) 372 | 373 | # WORKAROUND since socket update is not working 374 | def update(dummy): 375 | if self.name != "": 376 | self.update_effect(bpy.context) 377 | bpy.app.driver_namespace[self.name] = update 378 | bpy.app.handlers.depsgraph_update_pre.append(update) 379 | bpy.app.handlers.frame_change_post.append(update) 380 | bpy.app.handlers.render_post.append(update) 381 | 382 | def draw_buttons(self, context, layout): 383 | row = layout.row() 384 | row.prop(self, 'forking', text='Forking', slider=1) 385 | row = layout.row() 386 | row.prop(self, 'maxForkAngle', text='Max fork angle', slider=1) 387 | row = layout.row() 388 | row.prop(self, 'complexity', text='Complexity', slider=1) 389 | row = layout.row() 390 | row.prop(self, 'stability', text='Stability', slider=1) 391 | row = layout.row() 392 | row.prop(self, 'falloff', text='Falloff', slider=1) 393 | row = layout.row() 394 | row.prop(self, 'thickness', text='Thickness', slider=1) 395 | row = layout.row() 396 | row.prop(self, 'perspectiveScale', text='Perspective scale', slider=1) 397 | row = layout.row() 398 | row.prop(self, 'glow', text='Glow', slider=1) 399 | row = layout.row() 400 | row.prop(self, 'coreBlur', text='Core blur', slider=1) 401 | row = layout.row() 402 | row.prop(self, 'seed', text='Seed') 403 | row = layout.row() 404 | row.prop(self, 'gpuComp', text='GPU compute') 405 | 406 | def copy(self, node): 407 | self.init(bpy.context) 408 | return 409 | 410 | def free(self): 411 | bpy.data.node_groups.remove(self.node_tree, do_unlink=True) 412 | img = bpy.data.images[self.name] 413 | img.user_clear() 414 | bpy.data.images.remove(img) 415 | #WORKAROUND TO FIX NOT UPDATING OF CUSTOM PROPERTIES 416 | bpy.app.handlers.depsgraph_update_pre.remove(bpy.app.driver_namespace[self.name]) 417 | bpy.app.handlers.frame_change_post.remove(bpy.app.driver_namespace[self.name]) 418 | bpy.app.handlers.render_post.remove(bpy.app.driver_namespace[self.name]) 419 | del bpy.app.driver_namespace[self.name] 420 | 421 | 422 | def register(): 423 | bpy.utils.register_class(LightningGen) 424 | newcatlist = [CompositorNodeCategory( 425 | "CP_GENERATE", 426 | "Generate", 427 | items=[nodeitems_utils.NodeItem("LightningGen")])] 428 | nodeitems_utils.register_node_categories("GENERATE_NODES", newcatlist) 429 | 430 | 431 | def unregister(): 432 | nodeitems_utils.unregister_node_categories("GENERATE_NODES") 433 | bpy.utils.unregister_class(LightningGen) 434 | 435 | try: 436 | unregister() 437 | except: 438 | pass 439 | register() 440 | --------------------------------------------------------------------------------