""" 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 with proper sanitization import re # Remove non-ASCII and non-alphanumeric characters, keep spaces temporarily sanitized_text = re.sub(r'[^\w\s-]', '', props.text[:20]) # Replace spaces and multiple underscores/dashes with single underscores sanitized_text = re.sub(r'[\s_-]+', '_', sanitized_text).strip('_') # Fallback if text becomes empty if not sanitized_text: sanitized_text = "Texture" img_name = f"TextTexture_{sanitized_text}" # 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) # Directly copy pixel data from generated image to Blender image # The generated image is already in the correct format, just copy it blender_img.pixels[:] = img.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_{sanitized_text}" # 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 # Directly copy normal map pixel data # Copy RGB channels and set alpha to 1.0 temp_pixels = list(normal_map_img.pixels[:]) for i in range(3, len(temp_pixels), 4): temp_pixels[i] = 1.0 # Set alpha to 1.0 for normal maps normal_map_blender_img.pixels[:] = temp_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: print(f"[TTG SHADER DEBUG] === SHADER GENERATION STARTED ===") props = context.scene.text_texture_props # Generate texture first final_width = props.texture_width final_height = props.texture_height print(f"[TTG SHADER DEBUG] Step 1: Starting texture generation at {final_width}x{final_height}px") result = generate_texture_image(props, final_width, final_height) print(f"[TTG SHADER DEBUG] Step 2: Texture generation returned: {type(result)}") if not result: self.report({'ERROR'}, "Failed to generate texture image") return {'CANCELLED'} print(f"[TTG SHADER DEBUG] Step 3: Processing result...") # Extract diffuse and normal map images if isinstance(result, tuple): img, normal_map_img = result print(f"[TTG SHADER DEBUG] Step 3a: Got tuple result - img: {type(img)}, normal: {type(normal_map_img)}") else: # Backward compatibility img = result normal_map_img = None print(f"[TTG SHADER DEBUG] Step 3b: Got single result - img: {type(img)}") if not img: self.report({'ERROR'}, "Failed to generate diffuse image") return {'CANCELLED'} print(f"[TTG SHADER DEBUG] Step 4: Image validation passed, proceeding with shader creation...") print(f"[TTG SHADER DEBUG] Step 5: Creating diffuse texture...") # Create diffuse texture name with proper sanitization import re # Remove non-ASCII and non-alphanumeric characters, keep spaces temporarily sanitized_text = re.sub(r'[^\w\s-]', '', props.text[:20]) # Replace spaces and multiple underscores/dashes with single underscores sanitized_text = re.sub(r'[\s_-]+', '_', sanitized_text).strip('_') # Fallback if text becomes empty if not sanitized_text: sanitized_text = "Texture" img_name = f"TextTexture_{sanitized_text}" print(f"[TTG SHADER DEBUG] Step 5a: Sanitized texture name: {img_name}") # Remove existing diffuse texture if it exists if img_name in bpy.data.images: print(f"[TTG SHADER DEBUG] Step 5b: Removing existing texture") bpy.data.images.remove(bpy.data.images[img_name]) print(f"[TTG SHADER DEBUG] Step 5c: Creating new Blender image...") # Create new diffuse texture blender_img = bpy.data.images.new(img_name, final_width, final_height, alpha=True) print(f"[TTG SHADER DEBUG] Step 5d: New image created, copying pixel data...") # Directly copy pixel data from generated image to Blender image # The generated image is already in the correct format, just copy it blender_img.pixels[:] = img.pixels[:] print(f"[TTG SHADER DEBUG] Step 5e: Pixel data copied, packing image...") blender_img.pack() print(f"[TTG SHADER DEBUG] Step 5f: Image packed successfully") print(f"[TTG SHADER DEBUG] Step 6: Processing normal maps...") # Create normal map texture if enabled and generated normal_map_blender_img = None if normal_map_img and props.generate_normal_map: print(f"[TTG SHADER DEBUG] Step 6a: Normal map enabled and generated, processing...") # Use the same sanitized text for consistency normal_map_name = f"TextTexture_Normal_{sanitized_text}" print(f"[TTG SHADER DEBUG] Step 6b: Sanitized normal map name: {normal_map_name}") # Remove existing normal map texture if normal_map_name in bpy.data.images: print(f"[TTG SHADER DEBUG] Step 6c: Removing existing normal map") bpy.data.images.remove(bpy.data.images[normal_map_name]) print(f"[TTG SHADER DEBUG] Step 6d: Creating normal map image...") # 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 print(f"[TTG SHADER DEBUG] Step 6e: Normal map image created, copying pixels...") # Directly copy normal map pixel data # Copy RGB channels and set alpha to 1.0 temp_pixels = list(normal_map_img.pixels[:]) for i in range(3, len(temp_pixels), 4): temp_pixels[i] = 1.0 # Set alpha to 1.0 for normal maps normal_map_blender_img.pixels[:] = temp_pixels print(f"[TTG SHADER DEBUG] Step 6f: Normal map pixels copied, packing...") normal_map_blender_img.pack() print(f"[TTG SHADER DEBUG] Step 6g: Normal map packed successfully") else: print(f"[TTG SHADER DEBUG] Step 6: Normal map generation skipped") print(f"[TTG SHADER DEBUG] Step 7: Creating material...") # Create or get material using custom name or fallback if props.custom_material_name.strip(): material_name = props.custom_material_name.strip() print(f"[TTG SHADER DEBUG] Step 7a: Using custom material name: {material_name}") else: # Use the same sanitized text for consistency material_name = f"TextTexture_Mat_{sanitized_text}" print(f"[TTG SHADER DEBUG] Step 7b: Using sanitized material name: {material_name}") 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', ]