Skip to content

Commit

Permalink
converting lightmapper to modal for better baking.
Browse files Browse the repository at this point in the history
  • Loading branch information
SimonNordon4 committed Sep 11, 2024
1 parent ddc1ee2 commit 18eed16
Show file tree
Hide file tree
Showing 4 changed files with 222 additions and 48 deletions.
1 change: 1 addition & 0 deletions entry_lightmapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@
importlib.reload(lightmapper.lightmapper_panel)

lightmapper.__init__.register()

227 changes: 191 additions & 36 deletions lightmapper/lightmapper_operators.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,31 @@ class LIGHTMAPPER_OT_bake_lightmap(bpy.types.Operator):
bl_description = "Bake lightmap for selected objects"
bl_options = {'REGISTER', 'UNDO'}

def check_context(self, context):
_timer = None
progress = 0
instance = None



def get_bake_name(self, bake_target):
active_object = bpy.context.view_layer.objects.active

if bake_target == 'ACTIVE_OBJECT':
return active_object.name if active_object else "Unnamed"
elif bake_target == 'COLLECTION':
if active_object and active_object.users_collection:
return active_object.users_collection[0].name
else:
return "Unnamed"
else:
return "Unnamed"

def check_mesh_objects(self, context, mesh_objects):
""" Ensure the object is in a state that supports baking. """
mesh_objects = [obj for obj in context.selected_objects if obj.type == 'MESH']
if not mesh_objects:
self.report({'ERROR'}, "No mesh objects selected.")
return False

# Ensure we're in object mode.
if context.mode != 'OBJECT':
self.report({'ERROR'}, "Lightmap baking requires object mode.")
Expand All @@ -69,9 +91,8 @@ def check_context(self, context):

return True

def correct_context(self, context):
def correct_mesh_objects(self, context, mesh_objects):
""" Change the state of the selected objects to work for baking."""
mesh_objects = [obj for obj in context.selected_objects if obj.type == 'MESH']

self.report({'INFO'}, "Correcting UV Selections.")
# Ensure that the first UVMap is set to render, and that "Lightmap" UV is selected.
Expand All @@ -81,41 +102,175 @@ def correct_context(self, context):
# Ensure the lightmap is selected, as that's the UV we're baking to.
obj.data.uv_layers["Lightmap"].active = True

def execute(self, context):
if not self.check_context(context):
return {'CANCELLED'}
def combine_objects(self, context):
""" Combine selected objects into one for baking. """

self.correct_context(context)
print("Combining objects...")

bpy.ops.object.duplicate()
# set the active object to the first selected object
bpy.context.view_layer.objects.active = context.selected_objects[0]
# Join duplicated objects into one
bpy.ops.object.join()

# Store the combined object for later deletion
combined_object = context.active_object
# rename to temp_bake_name
combined_object.name = "temp_bake_object"
return combined_object

def create_lightmap_image(self, width=2048, height=2048):
""" Create a new lightmap image for baking. """
new_image = bpy.data.images.new("LightmapBake", width=width, height=height, float_buffer=True)
# give the image a name like temp_diffuse_bake_noise
new_image.name = "temp_lightmap_bake_image"
return new_image

def assign_lightmap_image(self, context, combined_object, lightmap_image):
# Ensure the object has an active material
if not combined_object.data.materials:
material = bpy.data.materials.new(name="TempBakeMaterial")
combined_object.data.materials.append(material)

# Get the active material
material = combined_object.active_material

# Ensure the material uses nodes
if not material.use_nodes:
material.use_nodes = True

# Access the material's node tree
nodes = material.node_tree.nodes
texture_node = nodes.get('Image Texture')

# If there's no existing Image Texture node, create one
if texture_node is None:
texture_node = nodes.new(type='ShaderNodeTexImage')

# Assign the lightmap image to the texture node
texture_node.image = lightmap_image

# Set the texture node as active for baking
material.node_tree.nodes.active = texture_node

def setup_bake_settings(self):
""" Set up bake settings for diffuse lightmap baking. """
bpy.context.scene.render.engine = 'CYCLES'
bpy.context.scene.cycles.samples = 128
bpy.context.scene.cycles.use_denoising = False

bpy.context.scene.cycles.bake_type = 'DIFFUSE'
bpy.context.scene.render.bake.use_pass_direct = True
bpy.context.scene.render.bake.use_pass_indirect = True
bpy.context.scene.render.bake.use_pass_color = False

bpy.context.scene.render.bake.use_selected_to_active = False

def perform_bake(self):
""" Perform the actual lightmap bake. """
bpy.ops.object.bake(type='DIFFUSE')

def setup_compositor(self):
""" Set up compositor nodes to denoise and save as HDR. """
bpy.context.scene.use_nodes = True
tree = bpy.context.scene.node_tree
tree.nodes.clear()

render_layer_node = tree.nodes.new('CompositorNodeRLayers')
denoise_node = tree.nodes.new('CompositorNodeDenoise')
file_output_node = tree.nodes.new('CompositorNodeOutputFile')

file_output_node.format.file_format = 'HDR'
file_output_node.base_path = "//"
file_output_node.file_slots[0].path = "LightmapResult"

tree.links.new(render_layer_node.outputs['Image'], denoise_node.inputs['Image'])
tree.links.new(denoise_node.outputs['Image'], file_output_node.inputs['Image'])

def render_and_save(self):
""" Render and save the denoised lightmap as an HDR. """
bpy.ops.render.render(use_viewport=True, write_still=True)

def modal(self, context, event):
if event.type in {'RIGHTMOUSE', 'ESC'}:
self.cancel(context)
return {'CANCELLED'}

if event.type == 'TIMER':
result = next(self.instance)
if result == -1:
self.cancel(context)
return {'CANCELLED'}
if result == 0:
self.finish(context)
return {'FINISHED'}

return {'RUNNING_MODAL'}

def bake(self, context):
yield 1
yield 0

return {""}

scene = context.scene
lightmapper_props = scene.lightmapper_properties
width = lightmapper_props.lightmap_width
height = lightmapper_props.lightmap_height
path = lightmapper_props.export_path
bake_target = lightmapper_props.bake_target

# 1. Set up and validate the bake.
bake_name = self.get_bake_name(bake_target)
bake_objects = [obj for obj in context.selected_objects if obj.type == 'MESH']
self.check_mesh_objects(context, bake_objects)
self.correct_mesh_objects(context, bake_objects)

# 2. Combine all selected meshes into 1 object for faster baking.
combined_object = self.combine_objects(context)

# 3. Create a new lightmap image for baking.
lightmap_image = self.create_lightmap_image(width, height)
self.assign_lightmap_image(context, combined_object, lightmap_image)

# 4. Start the initial bake.
self.setup_bake_settings()

# Start the timer
wm = context.window_manager
self._timer = wm.event_timer_add(0.1, window=context.window)
wm.modal_handler_add(self)

self.perform_bake()

return {'RUNNING_MODAL'}

def execute(self, context):
self.instance = self.bake(context)
wm = context.window_manager
self._timer = wm.event_timer_add(0.5, window=context.window)
wm.modal_handler_add(self)
return {'RUNNING_MODAL'}

def cancel(self, context):
# Stop timer on cancel
wm = context.window_manager
wm.event_timer_remove(self._timer)
return {'CANCELLED'}

def finish(self, context):
if self.instance.gi_running:
self.instance.close()
wm = context.window_manager
if self._timer:
wm.event_timer_remove(self._timer)

def draw(self, context):
layout = self.layout
row = layout.row()
row.label(text=f"Baking Progress: {self.progress}%")

self.report({'INFO'}, "Lightmap baking started.")
return {'FINISHED'}

for obj in context.selected_objects:
if obj.type == 'MESH':
# Set up lightmap UV if not present
if len(obj.data.uv_layers) < 2:
obj.data.uv_layers.new(name="Lightmap")

# Set up image for baking
image = bpy.data.images.new(name=f"{obj.name}_Lightmap", width=1024, height=1024)

# Set up material for baking
material = obj.active_material
if not material:
material = bpy.data.materials.new(name=f"{obj.name}_Lightmap_Material")
obj.data.materials.append(material)

# Set up node for baking
material.use_nodes = True
node_tree = material.node_tree
texture_node = node_tree.nodes.new('ShaderNodeTexImage')
texture_node.image = image

# Perform bake
bpy.ops.object.bake(type='COMBINED')

self.report({'INFO'}, "Lightmap baking completed")
return {'FINISHED'}

from bpy.utils import register_class, unregister_class
from .lightmapper_properties import LIGHTMAPPER_PT_properties
Expand Down
5 changes: 4 additions & 1 deletion lightmapper/lightmapper_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@ def draw(self, context):
layout.operator("lightmapper.create_lightmap_uv")


layout.prop(scene.lightmapper_properties, "lightmap_resolution")
layout.prop(scene.lightmapper_properties, "lightmap_width")
layout.prop(scene.lightmapper_properties, "lightmap_height")
layout.prop(scene.lightmapper_properties, "export_path")
layout.prop(scene.lightmapper_properties, "bake_target", expand=True)

layout.operator("lightmapper.bake_lightmap")

def register():
Expand Down
37 changes: 26 additions & 11 deletions lightmapper/lightmapper_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,20 @@
from bpy.props import EnumProperty, StringProperty # type: ignore

class LIGHTMAPPER_PT_properties(bpy.types.PropertyGroup):
lightmap_resolution: EnumProperty(
name="Lightmap Resolution",
description="Choose the resolution for the lightmap",
items=[
('512', "512", "512x512 pixels"),
('1024', "1k", "1024x1024 pixels"),
('2048', "2k", "2048x2048 pixels"),
('4096', "4k", "4096x4096 pixels"),
('8192', "8k", "8192x8192 pixels"),
],
default='2048'
lightmap_width: bpy.props.IntProperty(
name="Lightmap Width",
description="Width of the lightmap in pixels",
default=1024,
min=4,
max=8192
) # type: ignore

lightmap_height: bpy.props.IntProperty(
name="Lightmap Height",
description="Height of the lightmap in pixels",
default=1024,
min=4,
max=8192
) # type: ignore

export_path: StringProperty(
Expand All @@ -22,6 +25,18 @@ class LIGHTMAPPER_PT_properties(bpy.types.PropertyGroup):
maxlen=1024,
subtype='DIR_PATH'
) # type: ignore

bake_target: EnumProperty(
name="Bake Target",
description="Choose the bake target",
items=[
('ACTIVE_OBJECT', "Active Object", "Bake the active object"),
('COLLECTION', "Collection", "Bake the collection"),
],
default='ACTIVE_OBJECT'
) # type: ignore



def register():
print("Registering lightmapper_properties")
Expand Down

0 comments on commit 18eed16

Please sign in to comment.