Files
Text-Texture-Generator-for-…/operators/generation_ops.py
2025-09-11 12:12:58 +03:00

595 lines
30 KiB
Python

"""
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',
]