This commit is contained in:
2025-09-11 12:12:58 +03:00
parent 32fc626caf
commit 80a71c1dc1
33 changed files with 6051 additions and 6826 deletions

595
operators/generation_ops.py Normal file
View File

@@ -0,0 +1,595 @@
"""
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',
]