1021 lines
47 KiB
Python
1021 lines
47 KiB
Python
import json
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import numpy as np
|
|
from PIL import Image, ImageEnhance, ImageFilter
|
|
import cv2
|
|
import torch
|
|
from io import BytesIO
|
|
import base64
|
|
|
|
class QManualGLBMaterialModifier:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"glb_path": ("STRING", {"default": "", "multiline": False}),
|
|
"texture_image": ("IMAGE", {"forceInput": True}),
|
|
"output_suffix": ("STRING", {"default": "_modified", "multiline": False}),
|
|
"metallic_factor": ("FLOAT", {"default": 0.85, "min": 0.0, "max": 1.0, "step": 0.01}),
|
|
"smoothness": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
|
|
"base_color_r": ("FLOAT", {"default": 0.7, "min": 0.0, "max": 1.0, "step": 0.01}),
|
|
"base_color_g": ("FLOAT", {"default": 0.8, "min": 0.0, "max": 1.0, "step": 0.01}),
|
|
"base_color_b": ("FLOAT", {"default": 0.9, "min": 0.0, "max": 1.0, "step": 0.01}),
|
|
"emissive_brightness_threshold": ("FLOAT", {"default": 0.2, "min": 0.05, "max": 0.5, "step": 0.01}),
|
|
"emissive_percentage": ("FLOAT", {"default": 0.05, "min": 0.01, "max": 0.15, "step": 0.01}),
|
|
"absolute_min_brightness": ("INT", {"default": 128, "min": 50, "max": 200, "step": 1}),
|
|
"emissive_strength": ("FLOAT", {"default": 0.8, "min": 0.0, "max": 2.0, "step": 0.1}),
|
|
"emissive_mode": (["No Emissive", "With Emissive", "Both Versions"], {"default": "With Emissive"}),
|
|
"render": ("BOOLEAN", {"default": True}),
|
|
"normal_mode_1": (["No Normal Map", "Use Input Texture", "AutoGen Subtle", "AutoGen Normal", "AutoGen Enhanced", "AutoGen Dramatic", "AutoGen Smooth"], {"default": "No Normal Map"}),
|
|
"normal_mode_2": (["Disable", "No Normal Map", "Use Input Texture", "AutoGen Subtle", "AutoGen Normal", "AutoGen Enhanced", "AutoGen Dramatic", "AutoGen Smooth"], {"default": "Disable"}),
|
|
"normal_algorithm": (["Sobel Filter", "Simple Gradient"], {"default": "Sobel Filter"}),
|
|
"normal_scale": (["1x (Original)", "2x (Half Size)", "4x (Quarter Size)"], {"default": "1x (Original)"}),
|
|
"normal_compression": ("BOOLEAN", {"default": True}),
|
|
"normal_noise": ("FLOAT", {"default": 0.25, "min": 0.0, "max": 0.5, "step": 0.01}),
|
|
},
|
|
"optional": {
|
|
"normal_texture": ("IMAGE", {"forceInput": True}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING", "IMAGE", "IMAGE")
|
|
RETURN_NAMES = ("modified_glb_path", "emissive_mask_preview", "normal_map_preview")
|
|
FUNCTION = "modify_material_smart"
|
|
CATEGORY = "3d/q_material"
|
|
|
|
def create_emissive_texture(self, original_image, emissive_mask, emissive_strength):
|
|
"""Creates a proper colored emissive texture that preserves original colors in emissive areas"""
|
|
# Convert mask to 3-channel if needed
|
|
if len(emissive_mask.shape) == 2:
|
|
emissive_mask_3d = np.stack([emissive_mask, emissive_mask, emissive_mask], axis=-1)
|
|
else:
|
|
emissive_mask_3d = emissive_mask
|
|
|
|
# Normalize mask to [0,1]
|
|
mask_normalized = emissive_mask_3d.astype(np.float32) / 255.0
|
|
|
|
# Create colored emissive texture that preserves original colors in bright areas
|
|
emissive_texture = original_image * mask_normalized * emissive_strength
|
|
|
|
# Clip to valid range
|
|
emissive_texture = np.clip(emissive_texture, 0, 1)
|
|
|
|
return emissive_texture
|
|
|
|
def analyze_texture_brightness(self, image_tensor, brightness_threshold, percentage, absolute_min_threshold=128):
|
|
"""Analyzes texture and creates smart emissive mask"""
|
|
# Check if it's a tensor or already a numpy array
|
|
if hasattr(image_tensor, 'cpu'):
|
|
image_np_raw = image_tensor.cpu().numpy()
|
|
else:
|
|
image_np_raw = image_tensor
|
|
|
|
# Remove batch dimension if it exists
|
|
if len(image_np_raw.shape) == 4:
|
|
image_np_raw = image_np_raw[0]
|
|
|
|
# Convert from float [0,1] to uint8 [0,255]
|
|
image_np = (image_np_raw * 255).astype(np.uint8)
|
|
|
|
# Create PIL Image
|
|
pil_image = Image.fromarray(image_np)
|
|
|
|
# Convert to grayscale for brightness analysis
|
|
gray = pil_image.convert('L')
|
|
gray_np = np.array(gray)
|
|
|
|
# Calculate brightness statistics
|
|
mean_brightness = np.mean(gray_np) / 255.0
|
|
|
|
# Find percentile for brightest pixels
|
|
bright_threshold_value = np.percentile(gray_np, (1.0 - percentage) * 100)
|
|
|
|
# Use higher value from two thresholds
|
|
final_threshold = max(bright_threshold_value, absolute_min_threshold)
|
|
|
|
# Check if brightest pixels are significantly brighter than average
|
|
brightness_contrast = (final_threshold / 255.0) - mean_brightness
|
|
|
|
# Create emissive mask only if contrast is sufficient
|
|
if brightness_contrast >= brightness_threshold:
|
|
# Create mask for brightest pixels
|
|
emissive_mask = (gray_np >= final_threshold).astype(np.uint8) * 255
|
|
|
|
# Optional: slight blur of mask for smoother transition
|
|
emissive_mask = cv2.GaussianBlur(emissive_mask, (3, 3), 0)
|
|
|
|
# Normalize mask
|
|
if np.max(emissive_mask) > 0:
|
|
emissive_mask = (emissive_mask / np.max(emissive_mask) * 255).astype(np.uint8)
|
|
else:
|
|
emissive_mask = np.zeros_like(gray_np, dtype=np.uint8)
|
|
return emissive_mask, False
|
|
|
|
return emissive_mask, True
|
|
else:
|
|
return np.zeros_like(gray_np, dtype=np.uint8), False
|
|
|
|
def image_to_data_uri(self, image_array, format='png', texture_type='color'):
|
|
"""Convert image array to data URI with optimized compression"""
|
|
if len(image_array.shape) == 3:
|
|
# Convert to PIL Image
|
|
if image_array.dtype == np.float32 or image_array.dtype == np.float64:
|
|
# Convert float [0,1] to uint8 [0,255]
|
|
image_array = (image_array * 255).astype(np.uint8)
|
|
pil_image = Image.fromarray(image_array)
|
|
|
|
# Save to BytesIO with optimized settings
|
|
buffer = BytesIO()
|
|
|
|
if format.lower() == 'png':
|
|
# Optimize PNG compression based on texture type
|
|
if texture_type == 'normal':
|
|
pil_image.save(buffer, format='PNG', optimize=True, compress_level=9)
|
|
elif texture_type == 'emissive':
|
|
pil_image.save(buffer, format='PNG', optimize=True, compress_level=6)
|
|
else:
|
|
pil_image.save(buffer, format='PNG', optimize=True, compress_level=6)
|
|
elif format.lower() == 'jpeg':
|
|
pil_image.save(buffer, format='JPEG', optimize=True, quality=85)
|
|
else:
|
|
pil_image.save(buffer, format=format)
|
|
|
|
buffer.seek(0)
|
|
|
|
# Convert to base64
|
|
mime_type = f"image/{format.lower()}"
|
|
encoded = base64.b64encode(buffer.read()).decode('ascii')
|
|
return f"data:{mime_type};base64,{encoded}"
|
|
return None
|
|
|
|
def get_normal_mode_suffix(self, normal_mode):
|
|
"""Get filename suffix for normal mode"""
|
|
suffix_map = {
|
|
"No Normal Map": "_nonormal",
|
|
"Use Input Texture": "_inputnormal",
|
|
"AutoGen Subtle": "_normalautogen_sub",
|
|
"AutoGen Normal": "_normalautogen_nor",
|
|
"AutoGen Enhanced": "_normalautogen_enc",
|
|
"AutoGen Dramatic": "_normalautogen_dra",
|
|
"AutoGen Smooth": "_normalautogen_smo",
|
|
"Disable": "" # No suffix for disabled
|
|
}
|
|
return suffix_map.get(normal_mode, "_nonormal")
|
|
|
|
def get_normal_preset_params(self, preset_name):
|
|
"""Get parameters for normal map generation presets"""
|
|
presets = {
|
|
"AutoGen Subtle": {
|
|
"strength": 0.7,
|
|
"blur_radius": 1.0,
|
|
"contrast": 1.0,
|
|
"detail_boost": 1.0
|
|
},
|
|
"AutoGen Normal": {
|
|
"strength": 1.0,
|
|
"blur_radius": 0.5,
|
|
"contrast": 1.0,
|
|
"detail_boost": 1.0
|
|
},
|
|
"AutoGen Enhanced": {
|
|
"strength": 1.3,
|
|
"blur_radius": 0.3,
|
|
"contrast": 1.2,
|
|
"detail_boost": 1.2
|
|
},
|
|
"AutoGen Dramatic": {
|
|
"strength": 1.8,
|
|
"blur_radius": 0.1,
|
|
"contrast": 1.5,
|
|
"detail_boost": 1.5
|
|
},
|
|
"AutoGen Smooth": {
|
|
"strength": 1.0,
|
|
"blur_radius": 2.0,
|
|
"contrast": 0.8,
|
|
"detail_boost": 0.8
|
|
}
|
|
}
|
|
return presets.get(preset_name, presets["AutoGen Normal"])
|
|
|
|
def resize_texture(self, texture_array, scale_option):
|
|
"""Resize texture based on scale option"""
|
|
scale_factor = 1
|
|
if scale_option == "2x (Half Size)":
|
|
scale_factor = 2
|
|
elif scale_option == "4x (Quarter Size)":
|
|
scale_factor = 4
|
|
|
|
if scale_factor == 1:
|
|
return texture_array
|
|
|
|
# Calculate new dimensions
|
|
if len(texture_array.shape) == 3:
|
|
height, width, channels = texture_array.shape
|
|
new_height = height // scale_factor
|
|
new_width = width // scale_factor
|
|
|
|
# Use PIL for high-quality resizing
|
|
from PIL import Image
|
|
pil_image = Image.fromarray(texture_array)
|
|
resized_pil = pil_image.resize((new_width, new_height), Image.LANCZOS)
|
|
return np.array(resized_pil)
|
|
else:
|
|
return texture_array
|
|
|
|
def generate_normal_from_diffuse(self, diffuse_texture, method="Sobel Filter", strength=1.0,
|
|
blur_radius=0.5, contrast=1.0, detail_boost=1.0, scale_option="1x (Original)"):
|
|
# Konwersja tensora na numpy, jeśli potrzebne
|
|
if hasattr(diffuse_texture, 'cpu'):
|
|
img_np = diffuse_texture.cpu().numpy()
|
|
else:
|
|
img_np = diffuse_texture
|
|
|
|
# Usunięcie wymiaru batch, jeśli istnieje
|
|
if len(img_np.shape) == 4:
|
|
img_np = img_np[0]
|
|
|
|
# Upewnienie się, że dane są w zakresie [0,1] typu float
|
|
if img_np.dtype == np.uint8:
|
|
img_np = img_np.astype(np.float64) / 255.0
|
|
|
|
# Konwersja na mapę wysokości (grayscale) przy użyciu formuły luminancji
|
|
if len(img_np.shape) == 3:
|
|
heightmap = np.dot(img_np[...,:3], [0.299, 0.587, 0.114])
|
|
else:
|
|
heightmap = img_np
|
|
|
|
# Normalizacja mapy wysokości do [0,1]
|
|
height_min = np.min(heightmap)
|
|
height_max = np.max(heightmap)
|
|
if height_max > height_min:
|
|
heightmap = (heightmap - height_min) / (height_max - height_min)
|
|
else:
|
|
heightmap = np.zeros_like(heightmap)
|
|
|
|
# Opcjonalne przetwarzanie wstępne
|
|
if blur_radius > 0:
|
|
sigma = blur_radius
|
|
heightmap = cv2.GaussianBlur(heightmap, (0, 0), sigma)
|
|
|
|
# Wzmacnianie kontrastu, jeśli podano
|
|
if contrast != 1.0:
|
|
heightmap = np.clip((heightmap - 0.5) * contrast + 0.5, 0, 1)
|
|
|
|
height, width = heightmap.shape
|
|
|
|
print(f"Generowanie mapy normalnych metodą {method}")
|
|
|
|
if method == "Sobel Filter":
|
|
# Obliczanie gradientów na mapie wysokości typu float
|
|
grad_x = cv2.Sobel(heightmap, cv2.CV_64F, 1, 0, ksize=3)
|
|
grad_y = cv2.Sobel(heightmap, cv2.CV_64F, 0, 1, ksize=3)
|
|
grad_x *= strength
|
|
grad_y *= strength
|
|
normal_x = -grad_x
|
|
normal_y = -grad_y
|
|
normal_z = np.ones_like(normal_x)
|
|
|
|
elif method == "Simple Gradient":
|
|
normal_x = np.zeros_like(heightmap)
|
|
normal_y = np.zeros_like(heightmap)
|
|
normal_z = np.ones_like(heightmap)
|
|
for y in range(1, height-1):
|
|
for x in range(1, width-1):
|
|
h_left = heightmap[y, x-1]
|
|
h_right = heightmap[y, x+1]
|
|
h_up = heightmap[y-1, x]
|
|
h_down = heightmap[y+1, x]
|
|
dx = (h_right - h_left) / 2.0 * strength
|
|
dy = (h_down - h_up) / 2.0 * strength
|
|
normal_x[y, x] = -dx
|
|
normal_y[y, x] = -dy
|
|
|
|
# Normalizacja wektorów normalnych
|
|
length = np.sqrt(normal_x**2 + normal_y**2 + normal_z**2)
|
|
length = np.maximum(length, 1e-8) # Unikanie dzielenia przez zero
|
|
normal_x /= length
|
|
normal_y /= length
|
|
normal_z /= length
|
|
|
|
# Konwersja z [-1,1] na [0,1] dla zapisu
|
|
normal_x = (normal_x + 1.0) * 0.5
|
|
normal_y = (normal_y + 1.0) * 0.5
|
|
normal_z = (normal_z + 1.0) * 0.5
|
|
|
|
# Upewnienie się, że kanał Z jest odpowiedni (> 0.5 dla normalnych skierowanych na zewnątrz)
|
|
normal_z = np.maximum(normal_z, 0.5)
|
|
|
|
# Połączenie w obraz RGB
|
|
normal_map = np.stack([normal_x, normal_y, normal_z], axis=2)
|
|
|
|
# Konwersja na 8-bitowy format
|
|
normal_map_8bit = (normal_map * 255).astype(np.uint8)
|
|
|
|
print(f"Wygenerowano mapę normalnych {width}x{height} metodą {method}")
|
|
|
|
return normal_map_8bit
|
|
|
|
def apply_normal_noise(self, normal_texture, noise_strength):
|
|
"""Apply procedural noise to normal map for surface roughness/detail"""
|
|
if noise_strength <= 0:
|
|
return normal_texture
|
|
|
|
# Ensure we're working with numpy array
|
|
if hasattr(normal_texture, 'cpu'):
|
|
normal_np = normal_texture.cpu().numpy()
|
|
else:
|
|
normal_np = normal_texture.copy()
|
|
|
|
# Ensure float32 for processing
|
|
if normal_np.dtype == np.uint8:
|
|
normal_np = normal_np.astype(np.float32) / 255.0
|
|
was_uint8 = True
|
|
else:
|
|
was_uint8 = False
|
|
|
|
height, width = normal_np.shape[:2]
|
|
|
|
# Set random seed for reproducibility
|
|
np.random.seed(42)
|
|
|
|
# Generate multi-octave Perlin-like noise using OpenCV
|
|
# We'll combine multiple scales for more natural detail
|
|
noise_map = np.zeros((height, width), dtype=np.float32)
|
|
|
|
# Octave 1: Large scale features
|
|
scale1 = 50.0
|
|
noise1 = np.random.rand(int(height/scale1) + 2, int(width/scale1) + 2)
|
|
noise1 = cv2.resize(noise1, (width, height), interpolation=cv2.INTER_CUBIC)
|
|
noise1 = cv2.GaussianBlur(noise1, (0, 0), scale1/4)
|
|
noise_map += noise1 * 0.5
|
|
|
|
# Octave 2: Medium details
|
|
scale2 = 20.0
|
|
noise2 = np.random.rand(int(height/scale2) + 2, int(width/scale2) + 2)
|
|
noise2 = cv2.resize(noise2, (width, height), interpolation=cv2.INTER_CUBIC)
|
|
noise2 = cv2.GaussianBlur(noise2, (0, 0), scale2/4)
|
|
noise_map += noise2 * 0.3
|
|
|
|
# Octave 3: Fine details
|
|
scale3 = 10.0
|
|
noise3 = np.random.rand(int(height/scale3) + 2, int(width/scale3) + 2)
|
|
noise3 = cv2.resize(noise3, (width, height), interpolation=cv2.INTER_CUBIC)
|
|
noise3 = cv2.GaussianBlur(noise3, (0, 0), scale3/4)
|
|
noise_map += noise3 * 0.2
|
|
|
|
# Normalize noise to [-1, 1]
|
|
noise_map = (noise_map - 0.5) * 2.0
|
|
|
|
# Apply noise to normal map
|
|
# Convert normal map from [0,1] to [-1,1]
|
|
normal_vectors = normal_np * 2.0 - 1.0
|
|
|
|
# Apply noise as perturbation to X and Y channels
|
|
# Noise strength controls how much the normals are perturbed
|
|
normal_vectors[:, :, 0] += noise_map * noise_strength * 0.5 # X (red)
|
|
normal_vectors[:, :, 1] += noise_map * noise_strength * 0.5 # Y (green)
|
|
|
|
# Apply slight noise to Z channel too, but much less
|
|
z_noise = cv2.GaussianBlur(np.random.rand(height, width), (0, 0), 5) - 0.5
|
|
normal_vectors[:, :, 2] += z_noise * noise_strength * 0.1 # Z (blue)
|
|
|
|
# Renormalize the vectors
|
|
length = np.sqrt(np.sum(normal_vectors**2, axis=2, keepdims=True))
|
|
length = np.maximum(length, 1e-8)
|
|
normal_vectors /= length
|
|
|
|
# Convert back to [0,1] range
|
|
normal_np = (normal_vectors + 1.0) * 0.5
|
|
|
|
# Ensure Z is still pointing outward (> 0.5)
|
|
normal_np[:, :, 2] = np.maximum(normal_np[:, :, 2], 0.5)
|
|
|
|
# Convert back to original dtype if needed
|
|
if was_uint8:
|
|
normal_np = (normal_np * 255).astype(np.uint8)
|
|
|
|
return normal_np
|
|
|
|
def process_normal_texture(self, normal_texture, enable_compression=True):
|
|
"""Process normal texture to ensure it's in the correct format for glTF"""
|
|
# Check if it's a tensor or already a numpy array
|
|
if hasattr(normal_texture, 'cpu'):
|
|
normal_np = normal_texture.cpu().numpy()
|
|
else:
|
|
normal_np = normal_texture
|
|
|
|
# Remove batch dimension if it exists
|
|
if len(normal_np.shape) == 4:
|
|
normal_np = normal_np[0]
|
|
|
|
# Ensure it's in [0,1] range for processing
|
|
if normal_np.dtype == np.uint8:
|
|
normal_np = normal_np.astype(np.float32) / 255.0
|
|
|
|
# If the image is grayscale, convert to a flat normal map
|
|
if len(normal_np.shape) == 2 or (len(normal_np.shape) == 3 and normal_np.shape[2] == 1):
|
|
height, width = normal_np.shape[:2]
|
|
flat_normal = np.zeros((height, width, 3), dtype=np.float32)
|
|
flat_normal[:, :, 0] = 0.5 # X = 0 (neutral)
|
|
flat_normal[:, :, 1] = 0.5 # Y = 0 (neutral)
|
|
flat_normal[:, :, 2] = 1.0 # Z = 1 (pointing up)
|
|
normal_np = flat_normal
|
|
|
|
# Ensure we have exactly 3 channels
|
|
if normal_np.shape[2] > 3:
|
|
normal_np = normal_np[:, :, :3]
|
|
|
|
# SIMPLIFIED RG compression (only if enabled)
|
|
if enable_compression:
|
|
print("Applying light RG optimization to normal map")
|
|
|
|
# Ensure blue channel (Z) is reasonable for normal maps
|
|
blue_channel = normal_np[:, :, 2]
|
|
|
|
# If blue channel is too low, boost it slightly
|
|
min_blue = np.min(blue_channel)
|
|
if min_blue < 0.3:
|
|
normal_np[:, :, 2] = np.maximum(blue_channel, 0.5)
|
|
print(f"Boosted blue channel from min {min_blue:.3f} to ensure proper normal mapping")
|
|
else:
|
|
print("RG compression disabled - using original normal map data")
|
|
|
|
# Convert to 8-bit for saving
|
|
normal_8bit = (normal_np * 255).astype(np.uint8)
|
|
|
|
return normal_8bit
|
|
|
|
def process_single_normal_mode(self, normal_mode, texture_image, normal_texture,
|
|
normal_algorithm, normal_scale, normal_compression, normal_noise):
|
|
"""Process a single normal mode and return the data URI and suffix"""
|
|
if normal_mode == "Disable" or normal_mode == "No Normal Map":
|
|
return None, False, None
|
|
|
|
normal_data_uri = None
|
|
has_normal = False
|
|
normal_preview = None
|
|
|
|
print(f"Processing normal mode: {normal_mode}")
|
|
|
|
if normal_mode == "Use Input Texture" and normal_texture is not None:
|
|
print("Processing provided normal texture")
|
|
try:
|
|
processed_normal = self.process_normal_texture(normal_texture, normal_compression)
|
|
|
|
# Apply scaling if requested
|
|
if normal_scale != "1x (Original)":
|
|
print(f"Scaling normal texture: {normal_scale}")
|
|
processed_normal = self.resize_texture(processed_normal, normal_scale)
|
|
|
|
# Apply noise if requested
|
|
if normal_noise > 0:
|
|
print(f"Applying normal noise with strength: {normal_noise}")
|
|
processed_normal = self.apply_normal_noise(processed_normal, normal_noise)
|
|
|
|
print(f"Processed normal shape: {processed_normal.shape}")
|
|
normal_data_uri = self.image_to_data_uri(processed_normal, texture_type='normal')
|
|
has_normal = True
|
|
|
|
# Create preview
|
|
if processed_normal.dtype == np.uint8:
|
|
normal_preview = processed_normal.astype(np.float32) / 255.0
|
|
else:
|
|
normal_preview = processed_normal
|
|
|
|
print("Normal texture processed successfully")
|
|
except Exception as e:
|
|
print(f"Error processing normal texture: {e}")
|
|
has_normal = False
|
|
|
|
elif normal_mode.startswith("AutoGen"):
|
|
print(f"Auto-generating normal map using preset: {normal_mode}")
|
|
print(f"Algorithm: {normal_algorithm}")
|
|
|
|
try:
|
|
# Get preset parameters
|
|
preset_params = self.get_normal_preset_params(normal_mode)
|
|
print(f"Preset params: {preset_params}")
|
|
|
|
# Generate normal map from diffuse texture
|
|
generated_normal = self.generate_normal_from_diffuse(
|
|
texture_image,
|
|
method=normal_algorithm,
|
|
strength=preset_params["strength"],
|
|
blur_radius=preset_params["blur_radius"],
|
|
contrast=preset_params["contrast"],
|
|
detail_boost=preset_params["detail_boost"],
|
|
scale_option=normal_scale
|
|
)
|
|
|
|
# Apply scaling if requested and not already done
|
|
if normal_scale != "1x (Original)":
|
|
print(f"Scaling generated normal texture: {normal_scale}")
|
|
generated_normal = self.resize_texture(generated_normal, normal_scale)
|
|
|
|
# Apply noise if requested
|
|
if normal_noise > 0:
|
|
print(f"Applying normal noise with strength: {normal_noise}")
|
|
generated_normal = self.apply_normal_noise(generated_normal, normal_noise)
|
|
|
|
print(f"Generated normal shape: {generated_normal.shape}")
|
|
normal_data_uri = self.image_to_data_uri(generated_normal, texture_type='normal')
|
|
has_normal = True
|
|
|
|
# Create preview
|
|
if generated_normal.dtype == np.uint8:
|
|
normal_preview = generated_normal.astype(np.float32) / 255.0
|
|
else:
|
|
normal_preview = generated_normal
|
|
|
|
print("Normal map auto-generated successfully")
|
|
|
|
except Exception as e:
|
|
print(f"Error auto-generating normal map: {e}")
|
|
has_normal = False
|
|
|
|
return normal_data_uri, has_normal, normal_preview
|
|
|
|
def modify_material_smart(self, glb_path, texture_image, output_suffix, metallic_factor, smoothness,
|
|
base_color_r, base_color_g, base_color_b,
|
|
emissive_brightness_threshold, emissive_percentage, absolute_min_brightness,
|
|
emissive_strength, emissive_mode, render, normal_mode_1, normal_mode_2, normal_algorithm,
|
|
normal_scale, normal_compression, normal_noise, normal_texture=None):
|
|
|
|
try:
|
|
from pygltflib import GLTF2, TextureInfo, Image as GLTFImage, Texture, Sampler
|
|
except ImportError:
|
|
print("ERROR: pygltflib not installed. Install with: pip install pygltflib")
|
|
black_mask = np.zeros((512, 512, 3), dtype=np.float32)
|
|
flat_normal = np.zeros((512, 512, 3), dtype=np.float32)
|
|
flat_normal[:, :, 0] = 0.5
|
|
flat_normal[:, :, 1] = 0.5
|
|
flat_normal[:, :, 2] = 1.0
|
|
return ("PYGLTFLIB_NOT_INSTALLED", torch.from_numpy(black_mask[None, ...]), torch.from_numpy(flat_normal[None, ...]))
|
|
|
|
# Check if rendering is disabled
|
|
if not render:
|
|
print("Rendering disabled, skipping GLB processing")
|
|
if hasattr(texture_image, 'cpu'):
|
|
shape = texture_image[0].cpu().numpy().shape
|
|
else:
|
|
shape = texture_image[0].shape if len(texture_image.shape) == 4 else texture_image.shape
|
|
|
|
black_mask = np.zeros(shape, dtype=np.float32)
|
|
flat_normal = np.zeros(shape, dtype=np.float32)
|
|
flat_normal[:, :, 0] = 0.5
|
|
flat_normal[:, :, 1] = 0.5
|
|
flat_normal[:, :, 2] = 1.0
|
|
return ("RENDER_DISABLED", torch.from_numpy(black_mask[None, ...]), torch.from_numpy(flat_normal[None, ...]))
|
|
|
|
# Path validation and clean-up
|
|
if not glb_path or glb_path.strip() == "":
|
|
print("ERROR: No GLB file path provided")
|
|
black_mask = np.zeros((512, 512, 3), dtype=np.float32)
|
|
flat_normal = np.zeros((512, 512, 3), dtype=np.float32)
|
|
flat_normal[:, :, 0] = 0.5
|
|
flat_normal[:, :, 1] = 0.5
|
|
flat_normal[:, :, 2] = 1.0
|
|
return ("NO_PATH_PROVIDED", torch.from_numpy(black_mask[None, ...]), torch.from_numpy(flat_normal[None, ...]))
|
|
|
|
# Clean up path - remove quotes and whitespace
|
|
glb_path = glb_path.strip().strip('"\'')
|
|
|
|
# Find the actual GLB file
|
|
actual_glb_path = None
|
|
if os.path.exists(glb_path) and os.path.isfile(glb_path):
|
|
actual_glb_path = glb_path
|
|
else:
|
|
# Check various possible locations
|
|
if os.path.dirname(glb_path) == "":
|
|
possible_paths = [
|
|
os.path.join(os.getcwd(), glb_path),
|
|
os.path.join("output", glb_path),
|
|
os.path.join("ComfyUI", "output", glb_path),
|
|
os.path.join("..", "output", glb_path),
|
|
]
|
|
else:
|
|
normalized_path = os.path.normpath(glb_path)
|
|
possible_paths = [normalized_path]
|
|
if not normalized_path.lower().endswith('.glb'):
|
|
possible_paths.append(f"{normalized_path}.glb")
|
|
|
|
# Check all possible locations
|
|
for path in possible_paths:
|
|
if os.path.exists(path) and os.path.isfile(path):
|
|
actual_glb_path = path
|
|
break
|
|
|
|
if not actual_glb_path:
|
|
# Wait and try again
|
|
import time
|
|
time.sleep(2)
|
|
|
|
for path in possible_paths:
|
|
if os.path.exists(path) and os.path.isfile(path):
|
|
actual_glb_path = path
|
|
break
|
|
|
|
if not actual_glb_path:
|
|
print(f"File not found: {glb_path}")
|
|
black_preview = np.zeros((1, 512, 512, 3), dtype=np.float32)
|
|
flat_normal = np.zeros((1, 512, 512, 3), dtype=np.float32)
|
|
flat_normal[:, :, :, 0] = 0.5
|
|
flat_normal[:, :, :, 1] = 0.5
|
|
flat_normal[:, :, :, 2] = 1.0
|
|
return ("FILE_NOT_FOUND", torch.from_numpy(black_preview), torch.from_numpy(flat_normal))
|
|
|
|
# Process emissive texture
|
|
emissive_mask = None
|
|
has_emissive = False
|
|
emissive_data_uri = None
|
|
emissive_preview = None
|
|
|
|
print(f"Emissive mode: {emissive_mode}")
|
|
|
|
if emissive_mode in ["With Emissive", "Both Versions"]:
|
|
# Generate emissive mask
|
|
emissive_mask, has_emissive = self.analyze_texture_brightness(
|
|
texture_image, emissive_brightness_threshold, emissive_percentage, absolute_min_brightness
|
|
)
|
|
|
|
if has_emissive:
|
|
print(f"Creating emissive texture")
|
|
|
|
# Get original texture for color reference
|
|
if hasattr(texture_image, 'cpu'):
|
|
original_rgb = texture_image[0].cpu().numpy()
|
|
else:
|
|
original_rgb = texture_image[0] if len(texture_image.shape) == 4 else texture_image
|
|
|
|
# Create colored emissive texture
|
|
emissive_texture = self.create_emissive_texture(
|
|
original_rgb, emissive_mask, emissive_strength
|
|
)
|
|
|
|
# Convert to 8-bit for saving
|
|
emissive_texture_8bit = (emissive_texture * 255).astype(np.uint8)
|
|
|
|
# Convert emissive texture to data URI
|
|
emissive_data_uri = self.image_to_data_uri(emissive_texture_8bit, texture_type='emissive')
|
|
|
|
# Save for preview
|
|
emissive_preview = emissive_texture[None, ...] # Add batch dimension
|
|
else:
|
|
print(f"No emissive areas detected")
|
|
|
|
# Process both normal modes
|
|
normal_data_uri_1, has_normal_1, normal_preview_1 = self.process_single_normal_mode(
|
|
normal_mode_1, texture_image, normal_texture,
|
|
normal_algorithm, normal_scale, normal_compression, normal_noise
|
|
)
|
|
|
|
normal_data_uri_2, has_normal_2, normal_preview_2 = self.process_single_normal_mode(
|
|
normal_mode_2, texture_image, normal_texture,
|
|
normal_algorithm, normal_scale, normal_compression, normal_noise
|
|
)
|
|
|
|
# Generate files based on combinations
|
|
output_files = []
|
|
base_path, ext = os.path.splitext(actual_glb_path)
|
|
|
|
if emissive_mode == "Both Versions":
|
|
# Generate all combinations
|
|
print("Generating both emissive and non-emissive versions")
|
|
|
|
# With emissive
|
|
if has_emissive:
|
|
# Normal mode 1 + emissive
|
|
normal_suffix_1 = self.get_normal_mode_suffix(normal_mode_1)
|
|
em_suffix_1 = output_suffix + normal_suffix_1 + "_EM"
|
|
em_file_1 = self.generate_single_glb(actual_glb_path, em_suffix_1, metallic_factor, smoothness,
|
|
base_color_r, base_color_g, base_color_b,
|
|
True, emissive_data_uri, has_normal_1, normal_data_uri_1)
|
|
output_files.append(em_file_1)
|
|
print(f"Generated emissive version with normal mode 1: {em_file_1}")
|
|
|
|
# Normal mode 2 + emissive (if not disabled)
|
|
if normal_mode_2 != "Disable":
|
|
normal_suffix_2 = self.get_normal_mode_suffix(normal_mode_2)
|
|
em_suffix_2 = output_suffix + normal_suffix_2 + "_EM"
|
|
em_file_2 = self.generate_single_glb(actual_glb_path, em_suffix_2, metallic_factor, smoothness,
|
|
base_color_r, base_color_g, base_color_b,
|
|
True, emissive_data_uri, has_normal_2, normal_data_uri_2)
|
|
output_files.append(em_file_2)
|
|
print(f"Generated emissive version with normal mode 2: {em_file_2}")
|
|
|
|
# Without emissive
|
|
# Normal mode 1 + no emissive
|
|
normal_suffix_1 = self.get_normal_mode_suffix(normal_mode_1)
|
|
noem_suffix_1 = output_suffix + normal_suffix_1 + "_noEM"
|
|
noem_file_1 = self.generate_single_glb(actual_glb_path, noem_suffix_1, metallic_factor, smoothness,
|
|
base_color_r, base_color_g, base_color_b,
|
|
False, None, has_normal_1, normal_data_uri_1)
|
|
output_files.append(noem_file_1)
|
|
print(f"Generated non-emissive version with normal mode 1: {noem_file_1}")
|
|
|
|
# Normal mode 2 + no emissive (if not disabled)
|
|
if normal_mode_2 != "Disable":
|
|
normal_suffix_2 = self.get_normal_mode_suffix(normal_mode_2)
|
|
noem_suffix_2 = output_suffix + normal_suffix_2 + "_noEM"
|
|
noem_file_2 = self.generate_single_glb(actual_glb_path, noem_suffix_2, metallic_factor, smoothness,
|
|
base_color_r, base_color_g, base_color_b,
|
|
False, None, has_normal_2, normal_data_uri_2)
|
|
output_files.append(noem_file_2)
|
|
print(f"Generated non-emissive version with normal mode 2: {noem_file_2}")
|
|
|
|
main_output = output_files[0] if output_files else "ERROR"
|
|
|
|
elif emissive_mode == "With Emissive":
|
|
# Generate only emissive versions
|
|
# Normal mode 1 + emissive
|
|
normal_suffix_1 = self.get_normal_mode_suffix(normal_mode_1)
|
|
em_suffix_1 = output_suffix + normal_suffix_1 + "_EM"
|
|
main_output = self.generate_single_glb(actual_glb_path, em_suffix_1, metallic_factor, smoothness,
|
|
base_color_r, base_color_g, base_color_b,
|
|
has_emissive, emissive_data_uri, has_normal_1, normal_data_uri_1)
|
|
print(f"Generated emissive version with normal mode 1: {main_output}")
|
|
|
|
# Normal mode 2 + emissive (if not disabled)
|
|
if normal_mode_2 != "Disable":
|
|
normal_suffix_2 = self.get_normal_mode_suffix(normal_mode_2)
|
|
em_suffix_2 = output_suffix + normal_suffix_2 + "_EM"
|
|
em_file_2 = self.generate_single_glb(actual_glb_path, em_suffix_2, metallic_factor, smoothness,
|
|
base_color_r, base_color_g, base_color_b,
|
|
has_emissive, emissive_data_uri, has_normal_2, normal_data_uri_2)
|
|
print(f"Generated emissive version with normal mode 2: {em_file_2}")
|
|
|
|
else: # "No Emissive"
|
|
# Generate only non-emissive versions
|
|
# Normal mode 1 + no emissive
|
|
normal_suffix_1 = self.get_normal_mode_suffix(normal_mode_1)
|
|
noem_suffix_1 = output_suffix + normal_suffix_1 + "_noEM"
|
|
main_output = self.generate_single_glb(actual_glb_path, noem_suffix_1, metallic_factor, smoothness,
|
|
base_color_r, base_color_g, base_color_b,
|
|
False, None, has_normal_1, normal_data_uri_1)
|
|
print(f"Generated non-emissive version with normal mode 1: {main_output}")
|
|
|
|
# Normal mode 2 + no emissive (if not disabled)
|
|
if normal_mode_2 != "Disable":
|
|
normal_suffix_2 = self.get_normal_mode_suffix(normal_mode_2)
|
|
noem_suffix_2 = output_suffix + normal_suffix_2 + "_noEM"
|
|
noem_file_2 = self.generate_single_glb(actual_glb_path, noem_suffix_2, metallic_factor, smoothness,
|
|
base_color_r, base_color_g, base_color_b,
|
|
False, None, has_normal_2, normal_data_uri_2)
|
|
print(f"Generated non-emissive version with normal mode 2: {noem_file_2}")
|
|
|
|
# Ensure emissive_preview is always defined
|
|
if emissive_preview is None:
|
|
# Create a black preview image
|
|
if hasattr(texture_image, 'cpu'):
|
|
shape = texture_image[0].cpu().numpy().shape
|
|
else:
|
|
shape = texture_image[0].shape if len(texture_image.shape) == 4 else texture_image.shape
|
|
emissive_preview = np.zeros(shape, dtype=np.float32)[None, ...]
|
|
|
|
# Ensure normal_preview is always defined
|
|
# Use normal_preview_1 as the main normal preview
|
|
if normal_preview_1 is None:
|
|
# Create a flat normal map preview
|
|
if hasattr(texture_image, 'cpu'):
|
|
shape = texture_image[0].cpu().numpy().shape
|
|
else:
|
|
shape = texture_image[0].shape if len(texture_image.shape) == 4 else texture_image.shape
|
|
normal_preview = np.zeros(shape, dtype=np.float32)
|
|
normal_preview[:, :, 0] = 0.5 # X
|
|
normal_preview[:, :, 1] = 0.5 # Y
|
|
normal_preview[:, :, 2] = 1.0 # Z
|
|
normal_preview = normal_preview[None, ...]
|
|
else:
|
|
normal_preview = normal_preview_1[None, ...] if len(normal_preview_1.shape) == 3 else normal_preview_1
|
|
|
|
# Return all three outputs
|
|
return (main_output, torch.from_numpy(emissive_preview), torch.from_numpy(normal_preview))
|
|
def generate_single_glb(self, source_path, suffix, metallic_factor, smoothness,
|
|
base_color_r, base_color_g, base_color_b,
|
|
use_emissive, emissive_data_uri, use_normal, normal_data_uri):
|
|
"""Generate a single GLB file with specified parameters"""
|
|
try:
|
|
from pygltflib import GLTF2, TextureInfo, Image as GLTFImage, Texture, Sampler
|
|
except ImportError:
|
|
return "PYGLTFLIB_NOT_INSTALLED"
|
|
|
|
# Create output path
|
|
base_path, ext = os.path.splitext(source_path)
|
|
output_path = f"{base_path}{suffix}{ext}"
|
|
|
|
# Load the GLB file
|
|
gltf = GLTF2().load(source_path)
|
|
|
|
# Update materials in the GLTF
|
|
roughness_factor = 1.0 - smoothness # Convert Unity smoothness to glTF roughness
|
|
|
|
# Add textures to glTF if needed
|
|
emissive_texture_index = None
|
|
normal_texture_index = None
|
|
|
|
# Add emissive texture if requested
|
|
if use_emissive and emissive_data_uri:
|
|
# Create an image for the emissive texture
|
|
emissive_image = GLTFImage()
|
|
emissive_image.uri = emissive_data_uri
|
|
emissive_image.mimeType = "image/png"
|
|
emissive_image_index = len(gltf.images)
|
|
gltf.images.append(emissive_image)
|
|
|
|
# Make sure we have a sampler
|
|
if not gltf.samplers:
|
|
default_sampler = Sampler()
|
|
default_sampler.magFilter = 9729 # LINEAR
|
|
default_sampler.minFilter = 9987 # LINEAR_MIPMAP_LINEAR
|
|
default_sampler.wrapS = 10497 # REPEAT
|
|
default_sampler.wrapT = 10497 # REPEAT
|
|
gltf.samplers.append(default_sampler)
|
|
|
|
# Create a texture referencing the image
|
|
emissive_texture = Texture()
|
|
emissive_texture.source = emissive_image_index
|
|
emissive_texture.sampler = 0 # Use first sampler
|
|
emissive_texture_index = len(gltf.textures)
|
|
gltf.textures.append(emissive_texture)
|
|
|
|
# Add normal texture if available
|
|
if use_normal and normal_data_uri:
|
|
# Create an image for the normal texture
|
|
normal_image = GLTFImage()
|
|
normal_image.uri = normal_data_uri
|
|
normal_image.mimeType = "image/png"
|
|
normal_image_index = len(gltf.images)
|
|
gltf.images.append(normal_image)
|
|
|
|
# Make sure we have a sampler
|
|
if not gltf.samplers:
|
|
default_sampler = Sampler()
|
|
default_sampler.magFilter = 9729 # LINEAR
|
|
default_sampler.minFilter = 9987 # LINEAR_MIPMAP_LINEAR
|
|
default_sampler.wrapS = 10497 # REPEAT
|
|
default_sampler.wrapT = 10497 # REPEAT
|
|
gltf.samplers.append(default_sampler)
|
|
|
|
# Create a texture referencing the image
|
|
normal_texture_obj = Texture()
|
|
normal_texture_obj.source = normal_image_index
|
|
normal_texture_obj.sampler = 0 # Use first sampler
|
|
normal_texture_index = len(gltf.textures)
|
|
gltf.textures.append(normal_texture_obj)
|
|
|
|
# Update all materials
|
|
for i, material in enumerate(gltf.materials):
|
|
# Set base PBR properties
|
|
if not hasattr(material, 'pbrMetallicRoughness'):
|
|
from pygltflib import PbrMetallicRoughness
|
|
material.pbrMetallicRoughness = PbrMetallicRoughness()
|
|
|
|
material.pbrMetallicRoughness.baseColorFactor = [
|
|
base_color_r, base_color_g, base_color_b, 1.0
|
|
]
|
|
material.pbrMetallicRoughness.metallicFactor = metallic_factor
|
|
material.pbrMetallicRoughness.roughnessFactor = roughness_factor
|
|
|
|
# Set emissive properties
|
|
if use_emissive and emissive_texture_index is not None:
|
|
# Create a TextureInfo for the emissive texture
|
|
emissive_texture_info = TextureInfo()
|
|
emissive_texture_info.index = emissive_texture_index
|
|
material.emissiveTexture = emissive_texture_info
|
|
material.emissiveFactor = [1.0, 1.0, 1.0]
|
|
else:
|
|
# No emissive
|
|
material.emissiveFactor = [0.0, 0.0, 0.0]
|
|
|
|
# Set normal texture properties
|
|
if use_normal and normal_texture_index is not None:
|
|
# Create a TextureInfo for the normal texture
|
|
normal_texture_info = TextureInfo()
|
|
normal_texture_info.index = normal_texture_index
|
|
normal_strength = 1.0
|
|
|
|
if hasattr(normal_texture_info, 'scale'):
|
|
normal_texture_info.scale = normal_strength
|
|
else:
|
|
normal_texture_info = {
|
|
"index": normal_texture_index,
|
|
"scale": normal_strength
|
|
}
|
|
|
|
material.normalTexture = normal_texture_info
|
|
|
|
# Save the modified GLB file
|
|
gltf.save(output_path)
|
|
|
|
# Return only the filename (not full path)
|
|
return os.path.basename(output_path)
|
|
|
|
|
|
# Simplified presets for Unity-style with normal texture support
|
|
class QPresetGLBMaterialModifier:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"glb_path": ("STRING", {"default": "", "multiline": False}),
|
|
"texture_image": ("IMAGE", {"forceInput": True}),
|
|
"preset": (["Spaceship Metal", "Brushed Steel", "Chrome Hull", "Chrome Hull Lite", "Titanium", "Combat Metal", "Alien Tech"], {"default": "Spaceship Metal"}),
|
|
"output_suffix": ("STRING", {"default": "_modified", "multiline": False}),
|
|
"emissive_mode": (["No Emissive", "With Emissive", "Both Versions"], {"default": "With Emissive"}),
|
|
"absolute_min_brightness": ("INT", {"default": 128, "min": 50, "max": 200, "step": 1}),
|
|
"render": ("BOOLEAN", {"default": True}),
|
|
"normal_mode_1": (["No Normal Map", "Use Input Texture", "AutoGen Subtle", "AutoGen Normal", "AutoGen Enhanced", "AutoGen Dramatic", "AutoGen Smooth"], {"default": "No Normal Map"}),
|
|
"normal_mode_2": (["Disable", "No Normal Map", "Use Input Texture", "AutoGen Subtle", "AutoGen Normal", "AutoGen Enhanced", "AutoGen Dramatic", "AutoGen Smooth"], {"default": "Disable"}),
|
|
"normal_algorithm": (["Sobel Filter", "Simple Gradient"], {"default": "Sobel Filter"}),
|
|
"normal_scale": (["1x (Original)", "2x (Half Size)", "4x (Quarter Size)"], {"default": "1x (Original)"}),
|
|
"normal_compression": ("BOOLEAN", {"default": True}),
|
|
},
|
|
"optional": {
|
|
"normal_texture": ("IMAGE", {"forceInput": True}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING", "IMAGE", "IMAGE")
|
|
RETURN_NAMES = ("modified_glb_path", "emissive_mask_preview", "normal_map_preview")
|
|
FUNCTION = "apply_material_preset"
|
|
CATEGORY = "3d/q_material"
|
|
|
|
def apply_material_preset(self, glb_path, texture_image, preset, output_suffix, emissive_mode,
|
|
absolute_min_brightness, render, normal_mode_1, normal_mode_2, normal_algorithm,
|
|
normal_scale, normal_compression, normal_texture=None):
|
|
# Unity-style presets
|
|
presets = {
|
|
"Spaceship Metal": {
|
|
"metallic": 0.85, "smoothness": 0.5,
|
|
"color": [0.7, 0.8, 0.9], "emissive_strength": 0.8,
|
|
"brightness_threshold": 0.2, "percentage": 0.05,
|
|
"normal_noise": 0.1
|
|
},
|
|
"Brushed Steel": {
|
|
"metallic": 0.9, "smoothness": 0.3,
|
|
"color": [0.8, 0.8, 0.8], "emissive_strength": 0.6,
|
|
"brightness_threshold": 0.25, "percentage": 0.03,
|
|
"normal_noise": 0.25
|
|
},
|
|
"Chrome Hull": {
|
|
"metallic": 1.0, "smoothness": 0.9,
|
|
"color": [0.95, 0.95, 0.95], "emissive_strength": 0.9,
|
|
"brightness_threshold": 0.15, "percentage": 0.08,
|
|
"normal_noise": 0.05
|
|
},
|
|
"Chrome Hull Lite": {
|
|
"metallic": 1.0, "smoothness": 0.72,
|
|
"color": [0.95, 0.95, 0.95], "emissive_strength": 0.9,
|
|
"brightness_threshold": 0.15, "percentage": 0.08,
|
|
"normal_noise": 0.12
|
|
},
|
|
"Titanium": {
|
|
"metallic": 0.8, "smoothness": 0.4,
|
|
"color": [0.6, 0.6, 0.7], "emissive_strength": 0.7,
|
|
"brightness_threshold": 0.22, "percentage": 0.04,
|
|
"normal_noise": 0.15
|
|
},
|
|
"Combat Metal": {
|
|
"metallic": 0.7, "smoothness": 0.2,
|
|
"color": [0.5, 0.5, 0.6], "emissive_strength": 1.0,
|
|
"brightness_threshold": 0.18, "percentage": 0.06,
|
|
"normal_noise": 0.3
|
|
},
|
|
"Alien Tech": {
|
|
"metallic": 0.6, "smoothness": 0.7,
|
|
"color": [0.4, 0.8, 0.6], "emissive_strength": 1.2,
|
|
"brightness_threshold": 0.12, "percentage": 0.1,
|
|
"normal_noise": 0.2
|
|
}
|
|
}
|
|
|
|
settings = presets[preset]
|
|
|
|
# Use the main node - pass all parameters including normal texture
|
|
modifier = QManualGLBMaterialModifier()
|
|
return modifier.modify_material_smart(
|
|
glb_path=glb_path,
|
|
texture_image=texture_image,
|
|
output_suffix=output_suffix,
|
|
metallic_factor=settings["metallic"],
|
|
smoothness=settings["smoothness"],
|
|
base_color_r=settings["color"][0],
|
|
base_color_g=settings["color"][1],
|
|
base_color_b=settings["color"][2],
|
|
emissive_brightness_threshold=settings["brightness_threshold"],
|
|
emissive_percentage=settings["percentage"],
|
|
absolute_min_brightness=absolute_min_brightness,
|
|
emissive_strength=settings["emissive_strength"],
|
|
emissive_mode=emissive_mode,
|
|
render=render,
|
|
normal_mode_1=normal_mode_1,
|
|
normal_mode_2=normal_mode_2,
|
|
normal_algorithm=normal_algorithm,
|
|
normal_scale=normal_scale,
|
|
normal_compression=normal_compression,
|
|
normal_noise=settings["normal_noise"],
|
|
normal_texture=normal_texture
|
|
) |