""" Generation operators for text texture creation. Contains operators for generating textures and complete shaders. """ import bpy from bpy.types import Operator import os import json # Import required functions from other modules from ..core.generation_engine import generate_texture_image, generate_preview class TEXT_TEXTURE_OT_generate(Operator): """Generate final texture and pack it""" bl_idname = "text_texture.generate" bl_label = "Generate Text Texture" bl_description = "Generate final high-quality texture and pack it into the blend file" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): try: props = context.scene.text_texture_props # IMPORTANT: Generate at EXACT user-specified dimensions final_width = props.texture_width final_height = props.texture_height print(f"Generating texture at {final_width}x{final_height}px") # Generate at FULL resolution specified by user result = generate_texture_image(props, final_width, final_height) if not result: self.report({'ERROR'}, "Failed to generate texture image") return {'CANCELLED'} # Extract diffuse and normal map images if isinstance(result, tuple): img, normal_map_img = result else: # Backward compatibility img = result normal_map_img = None if not img: self.report({'ERROR'}, "Failed to generate diffuse image") return {'CANCELLED'} # Create diffuse texture img_name = f"TextTexture_{props.text[:20].replace(' ', '_')}" # Remove existing diffuse texture if img_name in bpy.data.images: bpy.data.images.remove(bpy.data.images[img_name]) # Create new diffuse image with USER SPECIFIED dimensions blender_img = bpy.data.images.new(img_name, final_width, final_height, alpha=True) # Convert PIL to Blender for diffuse texture pixels = [] for y in range(final_height): for x in range(final_width): r, g, b, a = img.getpixel((x, final_height - 1 - y)) pixels.extend([r/255.0, g/255.0, b/255.0, a/255.0]) blender_img.pixels = pixels blender_img.pack() # PACK FINAL IMAGE # Create normal map texture if enabled and generated normal_map_blender_img = None if normal_map_img and props.generate_normal_map: normal_map_name = f"TextTexture_Normal_{props.text[:20].replace(' ', '_')}" # Remove existing normal map texture if normal_map_name in bpy.data.images: bpy.data.images.remove(bpy.data.images[normal_map_name]) # Create new normal map image normal_map_blender_img = bpy.data.images.new(normal_map_name, final_width, final_height, alpha=False) normal_map_blender_img.colorspace_settings.name = 'Non-Color' # Important for normal maps # Convert PIL to Blender for normal map normal_pixels = [] for y in range(final_height): for x in range(final_width): r, g, b = normal_map_img.getpixel((x, final_height - 1 - y)) normal_pixels.extend([r/255.0, g/255.0, b/255.0, 1.0]) # Alpha always 1.0 for normal maps normal_map_blender_img.pixels = normal_pixels normal_map_blender_img.pack() # PACK NORMAL MAP print(f"[Normal Map] Created and packed normal map texture: {normal_map_name}") # Add to shader if context.space_data and context.space_data.type == 'NODE_EDITOR': if context.space_data.tree_type == 'ShaderNodeTree': material = context.space_data.id if material and material.use_nodes: nodes = material.node_tree.nodes # Add diffuse texture node img_node = nodes.new(type='ShaderNodeTexImage') img_node.image = blender_img img_node.location = (0, 0) img_node.select = True nodes.active = img_node # Add normal map node if normal map was created if normal_map_blender_img: normal_node = nodes.new(type='ShaderNodeTexImage') normal_node.image = normal_map_blender_img normal_node.location = (0, -300) normal_node.label = "Normal Map" print(f"[Normal Map] Added normal map node to shader editor") # Provide comprehensive feedback if normal_map_blender_img: self.report({'INFO'}, f"Generated and packed: {img_name} + {normal_map_blender_img.name} ({final_width}x{final_height}px)") else: self.report({'INFO'}, f"Generated and packed: {img_name} ({final_width}x{final_height}px)") return {'FINISHED'} except Exception as e: self.report({'ERROR'}, f"Failed: {str(e)}") return {'CANCELLED'} class TEXT_TEXTURE_OT_generate_shader(Operator): """Generate complete shader with text texture""" bl_idname = "text_texture.generate_shader" bl_label = "Generate Shader" bl_description = "Generate a complete material shader with the text texture" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): try: props = context.scene.text_texture_props # Generate texture first final_width = props.texture_width final_height = props.texture_height print(f"Generating shader with texture at {final_width}x{final_height}px") result = generate_texture_image(props, final_width, final_height) if not result: self.report({'ERROR'}, "Failed to generate texture image") return {'CANCELLED'} # Extract diffuse and normal map images if isinstance(result, tuple): img, normal_map_img = result else: # Backward compatibility img = result normal_map_img = None if not img: self.report({'ERROR'}, "Failed to generate diffuse image") return {'CANCELLED'} # Create diffuse texture name img_name = f"TextTexture_{props.text[:20].replace(' ', '_')}" # Remove existing diffuse texture if it exists if img_name in bpy.data.images: bpy.data.images.remove(bpy.data.images[img_name]) # Create new diffuse texture blender_img = bpy.data.images.new(img_name, final_width, final_height, alpha=True) # Convert PIL to Blender for diffuse texture pixels = [] for y in range(final_height): for x in range(final_width): r, g, b, a = img.getpixel((x, final_height - 1 - y)) pixels.extend([r/255.0, g/255.0, b/255.0, a/255.0]) blender_img.pixels = pixels blender_img.pack() # Create normal map texture if enabled and generated normal_map_blender_img = None if normal_map_img and props.generate_normal_map: normal_map_name = f"TextTexture_Normal_{props.text[:20].replace(' ', '_')}" # Remove existing normal map texture if normal_map_name in bpy.data.images: bpy.data.images.remove(bpy.data.images[normal_map_name]) # Create new normal map image normal_map_blender_img = bpy.data.images.new(normal_map_name, final_width, final_height, alpha=False) normal_map_blender_img.colorspace_settings.name = 'Non-Color' # Important for normal maps # Convert PIL to Blender for normal map normal_pixels = [] for y in range(final_height): for x in range(final_width): r, g, b = normal_map_img.getpixel((x, final_height - 1 - y)) normal_pixels.extend([r/255.0, g/255.0, b/255.0, 1.0]) # Alpha always 1.0 for normal maps normal_map_blender_img.pixels = normal_pixels normal_map_blender_img.pack() print(f"[Normal Map] Created normal map texture for shader: {normal_map_name}") # Create or get material using custom name or fallback if props.custom_material_name.strip(): material_name = props.custom_material_name.strip() else: # Fallback to text-based naming clean_text = props.text[:15].replace(' ', '_').replace('\n', '_') material_name = f"TextTexture_Mat_{clean_text}" if material_name in bpy.data.materials: mat = bpy.data.materials[material_name] # Clear existing nodes mat.node_tree.nodes.clear() else: mat = bpy.data.materials.new(name=material_name) mat.use_nodes = True mat.node_tree.nodes.clear() # Set up shader nodes nodes = mat.node_tree.nodes links = mat.node_tree.links # Create nodes output_node = nodes.new(type='ShaderNodeOutputMaterial') output_node.location = (400, 0) # Shader type based on settings if props.shader_type == 'PRINCIPLED': shader_node = nodes.new(type='ShaderNodeBsdfPrincipled') shader_node.location = (200, 0) # Create diffuse texture node tex_node = nodes.new(type='ShaderNodeTexImage') tex_node.image = blender_img tex_node.location = (-200, 0) # Connect texture to shader if props.shader_connection == 'BASE_COLOR': links.new(tex_node.outputs['Color'], shader_node.inputs['Base Color']) elif props.shader_connection == 'EMISSION': links.new(tex_node.outputs['Color'], shader_node.inputs['Emission Color']) shader_node.inputs['Emission Strength'].default_value = props.emission_strength elif props.shader_connection == 'ALPHA': links.new(tex_node.outputs['Alpha'], shader_node.inputs['Alpha']) # Also connect color for visibility links.new(tex_node.outputs['Color'], shader_node.inputs['Base Color']) mat.blend_method = 'BLEND' # Connect alpha if enabled if props.use_alpha: links.new(tex_node.outputs['Alpha'], shader_node.inputs['Alpha']) mat.blend_method = 'BLEND' # Add normal map nodes if normal map was generated if normal_map_blender_img: # Create normal map texture node normal_tex_node = nodes.new(type='ShaderNodeTexImage') normal_tex_node.image = normal_map_blender_img normal_tex_node.location = (-200, -300) normal_tex_node.label = "Normal Map" # Create normal map node normal_map_node = nodes.new(type='ShaderNodeNormalMap') normal_map_node.location = (0, -300) # Connect normal map texture to normal map node links.new(normal_tex_node.outputs['Color'], normal_map_node.inputs['Color']) # Connect normal map node to shader links.new(normal_map_node.outputs['Normal'], shader_node.inputs['Normal']) print(f"[Normal Map] Added normal map nodes to Principled BSDF shader") # Handle shadow/reflection disabling current_shader = shader_node final_x_location = 400 # Disable shadows if enabled if props.disable_shadows: # Create light path node light_path_node = nodes.new(type='ShaderNodeLightPath') light_path_node.location = (50, 150) # Create transparent BSDF transparent_node = nodes.new(type='ShaderNodeBsdfTransparent') transparent_node.location = (200, 150) # Create mix shader for shadows shadow_mix_node = nodes.new(type='ShaderNodeMixShader') shadow_mix_node.location = (350, 50) # Connect light path "Is Shadow Ray" to mix factor links.new(light_path_node.outputs['Is Shadow Ray'], shadow_mix_node.inputs['Fac']) # Mix principled BSDF with transparent BSDF links.new(current_shader.outputs['BSDF'], shadow_mix_node.inputs[1]) # Shader 1 (not shadow) links.new(transparent_node.outputs['BSDF'], shadow_mix_node.inputs[2]) # Shader 2 (shadow) current_shader = shadow_mix_node final_x_location = 500 # Disable reflections if enabled if props.disable_reflections: # Create or reuse light path node if not props.disable_shadows: light_path_node = nodes.new(type='ShaderNodeLightPath') light_path_node.location = (50, 200) # Create transparent BSDF for reflections reflection_transparent_node = nodes.new(type='ShaderNodeBsdfTransparent') reflection_transparent_node.location = (200, 200) # Create mix shader for reflections reflection_mix_node = nodes.new(type='ShaderNodeMixShader') reflection_mix_node.location = (final_x_location - 50, 100) # Connect light path "Is Reflection Ray" to mix factor links.new(light_path_node.outputs['Is Reflection Ray'], reflection_mix_node.inputs['Fac']) # Mix current shader with transparent BSDF links.new(current_shader.outputs['Shader' if props.disable_shadows else 'BSDF'], reflection_mix_node.inputs[1]) # Shader 1 (not reflection) links.new(reflection_transparent_node.outputs['BSDF'], reflection_mix_node.inputs[2]) # Shader 2 (reflection) current_shader = reflection_mix_node final_x_location = final_x_location + 50 # Disable backfacing if enabled if props.disable_backfacing: # Create or reuse light path node if not props.disable_shadows and not props.disable_reflections: light_path_node = nodes.new(type='ShaderNodeLightPath') light_path_node.location = (50, 250) # Create geometry node for backfacing detection geometry_node = nodes.new(type='ShaderNodeNewGeometry') geometry_node.location = (50, 300) # Create transparent BSDF for backfaces backface_transparent_node = nodes.new(type='ShaderNodeBsdfTransparent') backface_transparent_node.location = (200, 250) # Create mix shader for backfaces backface_mix_node = nodes.new(type='ShaderNodeMixShader') backface_mix_node.location = (final_x_location - 50, 150) # Connect geometry "Backfacing" to mix factor links.new(geometry_node.outputs['Backfacing'], backface_mix_node.inputs['Fac']) # Mix current shader with transparent BSDF current_output = 'Shader' if (props.disable_shadows or props.disable_reflections) else 'BSDF' links.new(current_shader.outputs[current_output], backface_mix_node.inputs[1]) # Shader 1 (front face) links.new(backface_transparent_node.outputs['BSDF'], backface_mix_node.inputs[2]) # Shader 2 (backface) current_shader = backface_mix_node final_x_location = final_x_location + 50 # Update output node position output_node.location = (final_x_location, 0) # Connect final shader to output output_socket = 'Shader' if (props.disable_shadows or props.disable_reflections) else 'BSDF' links.new(current_shader.outputs[output_socket], output_node.inputs['Surface']) elif props.shader_type == 'EMISSION': shader_node = nodes.new(type='ShaderNodeEmission') shader_node.location = (200, 0) # Create texture node tex_node = nodes.new(type='ShaderNodeTexImage') tex_node.image = blender_img tex_node.location = (-200, 0) # Connect texture to emission links.new(tex_node.outputs['Color'], shader_node.inputs['Color']) shader_node.inputs['Strength'].default_value = props.emission_strength # Handle shadow/reflection disabling current_shader = shader_node final_x_location = 400 # Disable shadows if enabled if props.disable_shadows: # Create light path node light_path_node = nodes.new(type='ShaderNodeLightPath') light_path_node.location = (50, 150) # Create transparent BSDF transparent_node = nodes.new(type='ShaderNodeBsdfTransparent') transparent_node.location = (200, 150) # Create mix shader for shadows shadow_mix_node = nodes.new(type='ShaderNodeMixShader') shadow_mix_node.location = (350, 50) # Connect light path "Is Shadow Ray" to mix factor links.new(light_path_node.outputs['Is Shadow Ray'], shadow_mix_node.inputs['Fac']) # Mix emission with transparent BSDF links.new(current_shader.outputs['Emission'], shadow_mix_node.inputs[1]) # Shader 1 (not shadow) links.new(transparent_node.outputs['BSDF'], shadow_mix_node.inputs[2]) # Shader 2 (shadow) current_shader = shadow_mix_node final_x_location = 500 # Disable reflections if enabled if props.disable_reflections: # Create or reuse light path node if not props.disable_shadows: light_path_node = nodes.new(type='ShaderNodeLightPath') light_path_node.location = (50, 200) # Create transparent BSDF for reflections reflection_transparent_node = nodes.new(type='ShaderNodeBsdfTransparent') reflection_transparent_node.location = (200, 200) # Create mix shader for reflections reflection_mix_node = nodes.new(type='ShaderNodeMixShader') reflection_mix_node.location = (final_x_location - 50, 100) # Connect light path "Is Reflection Ray" to mix factor links.new(light_path_node.outputs['Is Reflection Ray'], reflection_mix_node.inputs['Fac']) # Mix current shader with transparent BSDF links.new(current_shader.outputs['Shader' if props.disable_shadows else 'Emission'], reflection_mix_node.inputs[1]) # Shader 1 (not reflection) links.new(reflection_transparent_node.outputs['BSDF'], reflection_mix_node.inputs[2]) # Shader 2 (reflection) current_shader = reflection_mix_node final_x_location = final_x_location + 50 # Update output node position output_node.location = (final_x_location, 0) # Connect final shader to output output_socket = 'Shader' if (props.disable_shadows or props.disable_reflections) else 'Emission' links.new(current_shader.outputs[output_socket], output_node.inputs['Surface']) elif props.shader_type == 'TRANSPARENT': shader_node = nodes.new(type='ShaderNodeBsdfTransparent') mix_node = nodes.new(type='ShaderNodeMixShader') principled_node = nodes.new(type='ShaderNodeBsdfPrincipled') shader_node.location = (200, 100) mix_node.location = (350, 0) principled_node.location = (200, -100) # Create texture node tex_node = nodes.new(type='ShaderNodeTexImage') tex_node.image = blender_img tex_node.location = (-200, 0) # Connect texture links.new(tex_node.outputs['Color'], principled_node.inputs['Base Color']) links.new(tex_node.outputs['Alpha'], mix_node.inputs['Fac']) # Connect shaders to mix links.new(shader_node.outputs['BSDF'], mix_node.inputs[1]) links.new(principled_node.outputs['BSDF'], mix_node.inputs[2]) # Handle shadow/reflection disabling current_shader = mix_node final_x_location = 500 # Disable shadows if enabled if props.disable_shadows: # Create light path node light_path_node = nodes.new(type='ShaderNodeLightPath') light_path_node.location = (50, 200) # Create transparent BSDF for shadows (reuse existing transparent node) shadow_transparent_node = nodes.new(type='ShaderNodeBsdfTransparent') shadow_transparent_node.location = (350, 150) # Create mix shader for shadows shadow_mix_node = nodes.new(type='ShaderNodeMixShader') shadow_mix_node.location = (500, 50) # Connect light path "Is Shadow Ray" to mix factor links.new(light_path_node.outputs['Is Shadow Ray'], shadow_mix_node.inputs['Fac']) # Mix current shader with transparent BSDF links.new(current_shader.outputs['Shader'], shadow_mix_node.inputs[1]) # Shader 1 (not shadow) links.new(shadow_transparent_node.outputs['BSDF'], shadow_mix_node.inputs[2]) # Shader 2 (shadow) current_shader = shadow_mix_node final_x_location = 650 # Disable reflections if enabled if props.disable_reflections: # Create or reuse light path node if not props.disable_shadows: light_path_node = nodes.new(type='ShaderNodeLightPath') light_path_node.location = (50, 250) # Create transparent BSDF for reflections reflection_transparent_node = nodes.new(type='ShaderNodeBsdfTransparent') reflection_transparent_node.location = (350, 200) # Create mix shader for reflections reflection_mix_node = nodes.new(type='ShaderNodeMixShader') reflection_mix_node.location = (final_x_location - 50, 100) # Connect light path "Is Reflection Ray" to mix factor links.new(light_path_node.outputs['Is Reflection Ray'], reflection_mix_node.inputs['Fac']) # Mix current shader with transparent BSDF links.new(current_shader.outputs['Shader'], reflection_mix_node.inputs[1]) # Shader 1 (not reflection) links.new(reflection_transparent_node.outputs['BSDF'], reflection_mix_node.inputs[2]) # Shader 2 (reflection) current_shader = reflection_mix_node final_x_location = final_x_location + 50 # Disable backfacing if enabled if props.disable_backfacing: # Create or reuse light path node if not props.disable_shadows and not props.disable_reflections: light_path_node = nodes.new(type='ShaderNodeLightPath') light_path_node.location = (50, 300) # Create geometry node for backfacing detection geometry_node = nodes.new(type='ShaderNodeNewGeometry') geometry_node.location = (50, 350) # Create transparent BSDF for backfaces backface_transparent_node = nodes.new(type='ShaderNodeBsdfTransparent') backface_transparent_node.location = (350, 300) # Create mix shader for backfaces backface_mix_node = nodes.new(type='ShaderNodeMixShader') backface_mix_node.location = (final_x_location + 50, 150) # Connect geometry "Backfacing" to mix factor links.new(geometry_node.outputs['Backfacing'], backface_mix_node.inputs['Fac']) # Mix current shader with transparent BSDF links.new(current_shader.outputs['Shader'], backface_mix_node.inputs[1]) # Shader 1 (front face) links.new(backface_transparent_node.outputs['BSDF'], backface_mix_node.inputs[2]) # Shader 2 (backface) current_shader = backface_mix_node final_x_location = final_x_location + 150 # Update output node position output_node.location = (final_x_location, 0) # Connect final shader to output links.new(current_shader.outputs['Shader'], output_node.inputs['Surface']) mat.blend_method = 'BLEND' # Assign material to active object if enabled and possible if props.auto_assign_material and context.active_object and context.active_object.type in ['MESH', 'CURVE']: obj = context.active_object # Check if material slot exists, reuse if found, otherwise create new one slot_found = False for i, slot in enumerate(obj.material_slots): if slot.material and slot.material.name == material_name: # Material already exists in a slot, update it obj.material_slots[i].material = mat slot_found = True break if not slot_found: # Create new material slot if not found obj.data.materials.append(mat) self.report({'INFO'}, f"Generated shader '{material_name}' and assigned to {obj.name}") else: self.report({'INFO'}, f"Generated shader '{material_name}' (assign manually to object)") # Switch to Shader Editor if available and set material for area in context.screen.areas: if area.type == 'NODE_EDITOR': for space in area.spaces: if space.type == 'NODE_EDITOR': space.shader_type = 'OBJECT' space.id = mat area.tag_redraw() break return {'FINISHED'} except Exception as e: self.report({'ERROR'}, f"Shader generation failed: {str(e)}") import traceback traceback.print_exc() return {'CANCELLED'} # Export all operator classes for wildcard imports __all__ = [ 'TEXT_TEXTURE_OT_generate', 'TEXT_TEXTURE_OT_generate_shader', ]