AvaLovelace's picture
loadldraw material error fix
49e7cc1
# -*- coding: utf-8 -*-
"""Load LDraw GPLv2 license.
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software Foundation,
Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
"""
"""
Import LDraw
This module loads LDraw compatible files into Blender. Set the
Options first, then call loadFromFile() function with the full
filepath of a file to load.
Accepts .io, .mpd, .ldr, .l3b, and .dat files.
Toby Nelson - [email protected]
"""
import os
import sys
import math
import mathutils
import traceback
import glob
import bpy
import datetime
import struct
import re
import bmesh
import copy
import platform
import itertools
import operator
import zipfile
import tempfile
from pprint import pprint
# **************************************************************************************
def linkToScene(ob):
if bpy.context.collection.objects.find(ob.name) < 0:
bpy.context.collection.objects.link(ob)
# **************************************************************************************
def linkToCollection(collectionName, ob):
# Add object to the appropriate collection
if hasCollections:
if bpy.data.collections[collectionName].objects.find(ob.name) < 0:
bpy.data.collections[collectionName].objects.link(ob)
else:
bpy.data.groups[collectionName].objects.link(ob)
# **************************************************************************************
def unlinkFromScene(ob):
if bpy.context.collection.objects.find(ob.name) >= 0:
bpy.context.collection.objects.unlink(ob)
# **************************************************************************************
def selectObject(ob):
ob.select_set(state=True)
bpy.context.view_layer.objects.active = ob
# **************************************************************************************
def deselectObject(ob):
ob.select_set(state=False)
bpy.context.view_layer.objects.active = None
# **************************************************************************************
def addPlane(location, size):
bpy.ops.mesh.primitive_plane_add(size=size, enter_editmode=False, location=location)
# **************************************************************************************
def useDenoising(scene, useDenoising):
if hasattr(getLayers(scene)[0], "cycles"):
getLayers(scene)[0].cycles.use_denoising = useDenoising
# **************************************************************************************
def getLayerNames(scene):
return list(map((lambda x: x.name), getLayers(scene)))
# **************************************************************************************
def deleteEdge(bm, edge):
bmesh.ops.delete(bm, geom=edge, context='EDGES')
# **************************************************************************************
def getLayers(scene):
# Get the render/view layers we are interested in:
return scene.view_layers
# **************************************************************************************
def getDiffuseColor(color):
return color + (1.0,)
# **************************************************************************************
def ShowMessageBox(message = "", title = "Message Box", icon = 'INFO'):
def draw(self, context):
self.layout.label(text=message)
bpy.context.window_manager.popup_menu(draw, title = title, icon = icon)
# **************************************************************************************
# **************************************************************************************
class Options:
"""User Options"""
# Full filepath to ldraw folder. If empty, some standard locations are attempted
ldrawDirectory = r"" # Full filepath to the ldraw parts library (searches some standard locations if left blank)
instructionsLook = False # Set up scene to look like Lego Instruction booklets
#scale = 0.01 # Size of the lego model to create. (0.04 is LeoCAD scale)
realScale = 1 # Scale of lego model to create (1 represents real Lego scale)
useUnofficialParts = True # Additionally searches <ldraw-dir>/unofficial/parts and /p for files
resolution = "Standard" # Choose from "High", "Standard", or "Low"
defaultColour = "4" # Default colour ("4" = red)
createInstances = True # Multiple bricks share geometry (recommended)
useColourScheme = "lgeo" # "ldraw", "alt", or "lgeo". LGEO gives the most true-to-life colours.
numberNodes = True # Each node's name has a numerical prefix eg. 00001_car.dat (keeps nodes listed in a fixed order)
removeDoubles = True # Remove duplicate vertices (recommended)
smoothShading = True # Smooth the surface normals (recommended)
edgeSplit = True # Edge split modifier (recommended if you use smoothShading)
gaps = True # Introduces a tiny space between each brick
realGapWidth = 0.0002 # Width of gap between bricks (in metres)
curvedWalls = True # Manipulate normals to make surfaces look slightly concave
importCameras = True # LeoCAD can specify cameras within the ldraw file format. Choose to load them or ignore them.
positionObjectOnGroundAtOrigin = True # Centre the object at the origin, sitting on the z=0 plane
flattenHierarchy = False # All parts are under the root object - no sub-models
minifigHierarchy = True # Parts of minifigs are automatically parented to each other in a hierarchy
flattenGroups = False # All LEOCad groups are ignored - no groups
usePrincipledShaderWhenAvailable = True # Use the new principled shader
scriptDirectory = os.path.dirname( os.path.realpath(__file__) )
# We have the option of including the 'LEGO' logo on each stud
useLogoStuds = False # Use the studs with the 'LEGO' logo on them
logoStudVersion = "4" # Which version of the logo to use ("3" (flat), "4" (rounded) or "5" (subtle rounded))
instanceStuds = False # Each stud is a new Blender object (slow)
# LSynth (http://www.holly-wood.it/lsynth/tutorial-en.html) is a collection of parts used to render string, hoses, cables etc
useLSynthParts = True # LSynth is used to render string, hoses etc.
LSynthDirectory = r"" # Full path to the lsynth parts (Defaults to <ldrawdir>/unofficial/lsynth if left blank)
studLogoDirectory = r"" # Optional full path to the stud logo parts (if not found in unofficial directory)
# Ambiguous Normals
# Older LDraw parts (parts not yet BFC certified) have ambiguous normals.
# We resolve this by creating double sided faces ("double") or by taking a best guess ("guess")
resolveAmbiguousNormals = "guess" # How to resolve ambiguous normals
overwriteExistingMaterials = True # If there's an existing material with the same name, do we overwrite it, or use it?
overwriteExistingMeshes = True # If there's an existing mesh with the same name, do we overwrite it, or use it?
verbose = 1 # 1 = Show messages while working, 0 = Only show warnings/errors
addBevelModifier = True # Adds a bevel modifier to each part (for rounded edges)
bevelWidth = 0.5 # Width of bevel
addWorldEnvironmentTexture = True # Add an environment texture
addGroundPlane = True # Add a ground plane
setRenderSettings = True # Set render percentage, denoising
removeDefaultObjects = True # Remove cube and lamp
positionCamera = True # Position the camera where so we get the whole object in shot
cameraBorderPercent = 0.05 # Add a border gap around the positioned object (0.05 = 5%) for the rendered image
def meshOptionsString():
"""These options change the mesh, so if they change, a new mesh needs to be cached"""
return "_".join([str(Options.realScale),
str(Options.useUnofficialParts),
str(Options.instructionsLook),
str(Options.resolution),
str(Options.defaultColour),
str(Options.createInstances),
str(Options.useColourScheme),
str(Options.removeDoubles),
str(Options.smoothShading),
str(Options.gaps),
str(Options.realGapWidth),
str(Options.curvedWalls),
str(Options.flattenHierarchy),
str(Options.minifigHierarchy),
str(Options.useLogoStuds),
str(Options.logoStudVersion),
str(Options.instanceStuds),
str(Options.useLSynthParts),
str(Options.LSynthDirectory),
str(Options.studLogoDirectory),
str(Options.resolveAmbiguousNormals),
str(Options.addBevelModifier),
str(Options.bevelWidth)])
# **************************************************************************************
# Globals
globalBrickCount = 0
globalObjectsToAdd = [] # Blender objects to add to the scene
globalCamerasToAdd = [] # Camera data to add to the scene
globalContext = None
globalPoints = []
globalScaleFactor = 0.0004
globalWeldDistance = 0.0005
hasCollections = None
lightName = "Light"
# **************************************************************************************
# Dictionary with as keys the part numbers (without any extension for decorations)
# of pieces that have grainy slopes, and as values a set containing the angles (in
# degrees) of the face's normal to the horizontal plane. Use a tuple to represent a
# range within which the angle must lie.
globalSlopeBricks = {
'962':{45},
'2341':{-45},
'2449':{-16},
'2875':{45},
'2876':{(40, 63)},
'3037':{45},
'3038':{45},
'3039':{45},
'3040':{45},
'3041':{45},
'3042':{45},
'3043':{45},
'3044':{45},
'3045':{45},
'3046':{45},
'3048':{45},
'3049':{45},
'3135':{45},
'3297':{63},
'3298':{63},
'3299':{63},
'3300':{63},
'3660':{-45},
'3665':{-45},
'3675':{63},
'3676':{-45},
'3678b':{24},
'3684':{15},
'3685':{16},
'3688':{15},
'3747':{-63},
'4089':{-63},
'4161':{63},
'4286':{63},
'4287':{-63},
'4445':{45},
'4460':{16},
'4509':{63},
'4854':{-45},
'4856':{(-60, -70), -45},
'4857':{45},
'4858':{72},
'4861':{45, 63},
'4871':{-45},
'4885':{72}, #blank
'6069':{72, 45},
'6153':{(60, 70), (26, 34)},
'6227':{45},
'6270':{45},
'13269':{(40, 63)},
'13548':{(45, 35)},
'15571':{45},
'18759':{-45},
'22390':{(40, 55)}, #blank
'22391':{(40, 55)},
'22889':{-45},
'28192':{45}, #blank
'30180':{47},
'30182':{45},
'30183':{-45},
'30249':{35},
'30283':{-45},
'30363':{72},
'30373':{-24},
'30382':{11, 45},
'30390':{-45},
'30499':{16},
'32083':{45},
'43708':{(64, 72)},
'43710':{72, 45},
'43711':{72, 45},
'47759':{(40, 63)},
'52501':{-45},
'60219':{-45},
'60477':{72},
'60481':{24},
'63341':{45},
'72454':{-45},
'92946':{45},
'93348':{72},
'95188':{65},
'99301':{63},
'303923':{45},
'303926':{45},
'304826':{45},
'329826':{64},
'374726':{-64},
'428621':{64},
'4162628':{17},
'4195004':{45},
}
globalLightBricks = {
'62930.dat':(1.0,0.373,0.059,1.0),
'54869.dat':(1.0,0.052,0.017,1.0)
}
# Create a regular dictionary of parts with ranges of angles to check
margin = 5 # Allow 5 degrees either way to compensate for measuring inaccuracies
globalSlopeAngles = {}
for part, angles in globalSlopeBricks.items():
globalSlopeAngles[part] = {(c-margin, c+margin) if type(c) is not tuple else (min(c)-margin,max(c)+margin) for c in angles}
# **************************************************************************************
def internalPrint(message):
"""Debug print with identification timestamp."""
# Current timestamp (with milliseconds trimmed to two places)
timestamp = datetime.datetime.now().strftime("%H:%M:%S.%f")[:-4]
message = "{0} [importldraw] {1}".format(timestamp, message)
print("{0}".format(message))
global globalContext
if globalContext is not None:
globalContext.report({'INFO'}, message)
# **************************************************************************************
def debugPrint(message):
"""Debug print with identification timestamp."""
if Options.verbose > 0:
internalPrint(message)
# **************************************************************************************
def printWarningOnce(key, message=None):
if message is None:
message = key
if key not in Configure.warningSuppression:
internalPrint("WARNING: {0}".format(message))
Configure.warningSuppression[key] = True
global globalContext
if globalContext is not None:
globalContext.report({'WARNING'}, message)
# **************************************************************************************
def printError(message):
internalPrint("ERROR: {0}".format(message))
global globalContext
if globalContext is not None:
globalContext.report({'ERROR'}, message)
# **************************************************************************************
# **************************************************************************************
class Math:
identityMatrix = mathutils.Matrix((
(1.0, 0.0, 0.0, 0.0),
(0.0, 1.0, 0.0, 0.0),
(0.0, 0.0, 1.0, 0.0),
(0.0, 0.0, 0.0, 1.0)
))
rotationMatrix = mathutils.Matrix.Rotation(math.radians(-90), 4, 'X')
reflectionMatrix = mathutils.Matrix((
(1.0, 0.0, 0.0, 0.0),
(0.0, 1.0, 0.0, 0.0),
(0.0, 0.0, -1.0, 0.0),
(0.0, 0.0, 0.0, 1.0)
))
def clamp01(value):
return max(min(value, 1.0), 0.0)
def __init__(self):
global globalScaleFactor
# Rotation and scale matrices that convert LDraw coordinate space to Blender coordinate space
Math.scaleMatrix = mathutils.Matrix((
(globalScaleFactor, 0.0, 0.0, 0.0),
(0.0, globalScaleFactor, 0.0, 0.0),
(0.0, 0.0, globalScaleFactor, 0.0),
(0.0, 0.0, 0.0, 1.0)
))
# **************************************************************************************
# **************************************************************************************
class Configure:
"""Configuration.
Attempts to find the ldraw directory (platform specific directories are searched).
Stores the list of paths to parts libraries that we search for individual parts.
Stores warning messages we have already seen so we don't see them again.
"""
searchPaths = []
warningSuppression = {}
tempDir = None
def appendPath(path):
if os.path.exists(path):
Configure.searchPaths.append(path)
def __setSearchPaths():
Configure.searchPaths = []
# Always search for parts in the 'models' folder
Configure.appendPath(os.path.join(Configure.ldrawInstallDirectory, "models"))
# Search for stud logo parts
if Options.useLogoStuds and Options.studLogoDirectory != "":
if Options.resolution == "Low":
Configure.appendPath(os.path.join(Options.studLogoDirectory, "8"))
Configure.appendPath(Options.studLogoDirectory)
# Search unofficial parts
if Options.useUnofficialParts:
Configure.appendPath(os.path.join(Configure.ldrawInstallDirectory, "unofficial", "parts"))
if Options.resolution == "High":
Configure.appendPath(os.path.join(Configure.ldrawInstallDirectory, "unofficial", "p", "48"))
elif Options.resolution == "Low":
Configure.appendPath(os.path.join(Configure.ldrawInstallDirectory, "unofficial", "p", "8"))
Configure.appendPath(os.path.join(Configure.ldrawInstallDirectory, "unofficial", "p"))
# Add 'Tente' parts too
Configure.appendPath(os.path.join(Configure.ldrawInstallDirectory, "tente", "parts"))
if Options.resolution == "High":
Configure.appendPath(os.path.join(Configure.ldrawInstallDirectory, "tente", "p", "48"))
elif Options.resolution == "Low":
Configure.appendPath(os.path.join(Configure.ldrawInstallDirectory, "tente", "p", "8"))
Configure.appendPath(os.path.join(Configure.ldrawInstallDirectory, "tente", "p"))
# Search LSynth parts
if Options.useLSynthParts:
if Options.LSynthDirectory != "":
Configure.appendPath(Options.LSynthDirectory)
else:
Configure.appendPath(os.path.join(Configure.ldrawInstallDirectory, "unofficial", "lsynth"))
debugPrint("Use LSynth Parts requested")
# Search official parts
Configure.appendPath(os.path.join(Configure.ldrawInstallDirectory, "parts"))
if Options.resolution == "High":
Configure.appendPath(os.path.join(Configure.ldrawInstallDirectory, "p", "48"))
debugPrint("High-res primitives selected")
elif Options.resolution == "Low":
Configure.appendPath(os.path.join(Configure.ldrawInstallDirectory, "p", "8"))
debugPrint("Low-res primitives selected")
else:
debugPrint("Standard-res primitives selected")
Configure.appendPath(os.path.join(Configure.ldrawInstallDirectory, "p"))
def isWindows():
return platform.system() == "Windows"
def isMac():
return platform.system() == "Darwin"
def isLinux():
return platform.system() == "Linux"
def findDefaultLDrawDirectory():
result = ""
# Get list of possible ldraw installation directories for the platform
if Configure.isWindows():
ldrawPossibleDirectories = [
"C:\\LDraw",
"C:\\Program Files\\LDraw",
"C:\\Program Files (x86)\\LDraw",
"C:\\Program Files\\Studio 2.0\\ldraw",
]
elif Configure.isMac():
ldrawPossibleDirectories = [
"~/ldraw/",
"/Applications/LDraw/",
"/Applications/ldraw/",
"/usr/local/share/ldraw",
"/Applications/Studio 2.0/ldraw",
]
else: # Default to Linux if not Windows or Mac
ldrawPossibleDirectories = [
"~/LDraw",
"~/ldraw",
"~/.LDraw",
"~/.ldraw",
"/usr/local/share/ldraw",
]
# Search possible directories
for dir in ldrawPossibleDirectories:
dir = os.path.expanduser(dir)
if os.path.isfile(os.path.join(dir, "LDConfig.ldr")):
result = dir
break
return result
def setLDrawDirectory():
if Options.ldrawDirectory == "":
Configure.ldrawInstallDirectory = Configure.findDefaultLDrawDirectory()
else:
Configure.ldrawInstallDirectory = os.path.expanduser(Options.ldrawDirectory)
debugPrint("The LDraw Parts Library path to be used is: {0}".format(Configure.ldrawInstallDirectory))
Configure.__setSearchPaths()
def __init__(self):
Configure.setLDrawDirectory()
# **************************************************************************************
# **************************************************************************************
class LegoColours:
"""Parses and stores a table of colour / material definitions. Converts colour space."""
colours = {}
def __getValue(line, value):
"""Parses a colour value from the ldConfig.ldr file"""
if value in line:
n = line.index(value)
return line[n + 1]
def __sRGBtoRGBValue(value):
# See https://en.wikipedia.org/wiki/SRGB#The_reverse_transformation
if value < 0.04045:
return value / 12.92
return ((value + 0.055)/1.055)**2.4
def isDark(colour):
R = colour[0]
G = colour[1]
B = colour[2]
# Measure the perceived brightness of colour
brightness = math.sqrt( 0.299*R*R + 0.587*G*G + 0.114*B*B )
# Dark colours have white lines
if brightness < 0.03:
return True
return False
def sRGBtoLinearRGB(sRGBColour):
# See https://en.wikipedia.org/wiki/SRGB#The_reverse_transformation
(sr, sg, sb) = sRGBColour
r = LegoColours.__sRGBtoRGBValue(sr)
g = LegoColours.__sRGBtoRGBValue(sg)
b = LegoColours.__sRGBtoRGBValue(sb)
return (r,g,b)
def hexDigitsToLinearRGBA(hexDigits, alpha):
# String is "RRGGBB" format
int_tuple = struct.unpack('BBB', bytes.fromhex(hexDigits))
sRGB = tuple([val / 255 for val in int_tuple])
linearRGB = LegoColours.sRGBtoLinearRGB(sRGB)
return (linearRGB[0], linearRGB[1], linearRGB[2], alpha)
def hexStringToLinearRGBA(hexString):
"""Convert colour hex value to RGB value."""
# Handle direct colours
# Direct colours are documented here: http://www.hassings.dk/l3/l3p.html
match = re.fullmatch(r"0x0*([0-9])((?:[A-F0-9]{2}){3})", hexString)
if match is not None:
digit = match.group(1)
rgb_str = match.group(2)
interleaved = False
if digit == "2": # Opaque
alpha = 1.0
elif digit == "3": # Transparent
alpha = 0.5
elif digit == "4": # Opaque
alpha = 1.0
interleaved = True
elif digit == "5": # More Transparent
alpha = 0.333
interleaved = True
elif digit == "6": # Less transparent
alpha = 0.666
interleaved = True
elif digit == "7": # Invisible
alpha = 0.0
interleaved = True
else:
alpha = 1.0
if interleaved:
# Input string is six hex digits of two colours "RGBRGB".
# This was designed to be a dithered colour.
# Take the average of those two colours (R+R,G+G,B+B) * 0.5
r = float(int(rgb_str[0], 16)) / 15
g = float(int(rgb_str[1], 16)) / 15
b = float(int(rgb_str[2], 16)) / 15
colour1 = LegoColours.sRGBtoLinearRGB((r,g,b))
r = float(int(rgb_str[3], 16)) / 15
g = float(int(rgb_str[4], 16)) / 15
b = float(int(rgb_str[5], 16)) / 15
colour2 = LegoColours.sRGBtoLinearRGB((r,g,b))
return (0.5 * (colour1[0] + colour2[0]),
0.5 * (colour1[1] + colour2[1]),
0.5 * (colour1[2] + colour2[2]), alpha)
# String is "RRGGBB" format
return LegoColours.hexDigitsToLinearRGBA(rgb_str, alpha)
return None
def __overwriteColour(index, sRGBColour):
if index in LegoColours.colours:
# Colour Space Management: Convert sRGB colour values to Blender's linear RGB colour space
LegoColours.colours[index]["colour"] = LegoColours.sRGBtoLinearRGB(sRGBColour)
def __readColourTable():
"""Reads the colour values from the LDConfig.ldr file. For details of the
Ldraw colour system see: http://www.ldraw.org/article/547"""
if Options.useColourScheme == "alt":
configFilename = "LDCfgalt.ldr"
else:
configFilename = "LDConfig.ldr"
configFilepath = os.path.join(Configure.ldrawInstallDirectory, configFilename)
ldconfig_lines = ""
if os.path.exists(configFilepath):
with open(configFilepath, "rt", encoding="utf_8") as ldconfig:
ldconfig_lines = ldconfig.readlines()
for line in ldconfig_lines:
if len(line) > 3:
if line[2:4].lower() == '!c':
line_split = line.split()
name = line_split[2]
code = int(line_split[4])
linearRGBA = LegoColours.hexDigitsToLinearRGBA(line_split[6][1:], 1.0)
colour = {
"name": name,
"colour": linearRGBA[0:3],
"alpha": linearRGBA[3],
"luminance": 0.0,
"material": "BASIC"
}
if "ALPHA" in line_split:
colour["alpha"] = int(LegoColours.__getValue(line_split, "ALPHA")) / 256.0
if "LUMINANCE" in line_split:
colour["luminance"] = int(LegoColours.__getValue(line_split, "LUMINANCE"))
if "CHROME" in line_split:
colour["material"] = "CHROME"
if "PEARLESCENT" in line_split:
colour["material"] = "PEARLESCENT"
if "RUBBER" in line_split:
colour["material"] = "RUBBER"
if "METAL" in line_split:
colour["material"] = "METAL"
if "MATERIAL" in line_split:
subline = line_split[line_split.index("MATERIAL"):]
colour["material"] = LegoColours.__getValue(subline, "MATERIAL")
# current `FABRIC [VELVET | CANVAS | STRING | FUR]` is not yet supported.
if colour["material"] == "FABRIC":
debugPrint(f"Unsupported material finish: {colour['material']} for [colour: {name} code: {code}] in line: {subline}")
# Note, not all finishes have a secondary value
finishValue = LegoColours.__getValue(subline, "VALUE")
if finishValue is not None:
hexDigits = finishValue[1:]
colour["secondary_colour"] = LegoColours.hexDigitsToLinearRGBA(hexDigits, 1.0)
colour["fraction"] = LegoColours.__getValue(subline, "FRACTION")
colour["vfraction"] = LegoColours.__getValue(subline, "VFRACTION")
colour["size"] = LegoColours.__getValue(subline, "SIZE")
colour["minsize"] = LegoColours.__getValue(subline, "MINSIZE")
colour["maxsize"] = LegoColours.__getValue(subline, "MAXSIZE")
LegoColours.colours[code] = colour
if Options.useColourScheme == "lgeo":
# LGEO is a parts library for rendering LEGO using the povray rendering software.
# It has a list of LEGO colours suitable for realistic rendering.
# I've extracted the following colours from the LGEO file: lg_color.inc
# LGEO is downloadable from http://ldraw.org/downloads-2/downloads.html
# We overwrite the standard LDraw colours if we have better LGEO colours.
LegoColours.__overwriteColour(0, ( 33/255, 33/255, 33/255))
LegoColours.__overwriteColour(1, ( 13/255, 105/255, 171/255))
LegoColours.__overwriteColour(2, ( 40/255, 127/255, 70/255))
LegoColours.__overwriteColour(3, ( 0/255, 143/255, 155/255))
LegoColours.__overwriteColour(4, (196/255, 40/255, 27/255))
LegoColours.__overwriteColour(5, (205/255, 98/255, 152/255))
LegoColours.__overwriteColour(6, ( 98/255, 71/255, 50/255))
LegoColours.__overwriteColour(7, (161/255, 165/255, 162/255))
LegoColours.__overwriteColour(8, (109/255, 110/255, 108/255))
LegoColours.__overwriteColour(9, (180/255, 210/255, 227/255))
LegoColours.__overwriteColour(10, ( 75/255, 151/255, 74/255))
LegoColours.__overwriteColour(11, ( 85/255, 165/255, 175/255))
LegoColours.__overwriteColour(12, (242/255, 112/255, 94/255))
LegoColours.__overwriteColour(13, (252/255, 151/255, 172/255))
LegoColours.__overwriteColour(14, (245/255, 205/255, 47/255))
LegoColours.__overwriteColour(15, (242/255, 243/255, 242/255))
LegoColours.__overwriteColour(17, (194/255, 218/255, 184/255))
LegoColours.__overwriteColour(18, (249/255, 233/255, 153/255))
LegoColours.__overwriteColour(19, (215/255, 197/255, 153/255))
LegoColours.__overwriteColour(20, (193/255, 202/255, 222/255))
LegoColours.__overwriteColour(21, (224/255, 255/255, 176/255))
LegoColours.__overwriteColour(22, (107/255, 50/255, 123/255))
LegoColours.__overwriteColour(23, ( 35/255, 71/255, 139/255))
LegoColours.__overwriteColour(25, (218/255, 133/255, 64/255))
LegoColours.__overwriteColour(26, (146/255, 57/255, 120/255))
LegoColours.__overwriteColour(27, (164/255, 189/255, 70/255))
LegoColours.__overwriteColour(28, (149/255, 138/255, 115/255))
LegoColours.__overwriteColour(29, (228/255, 173/255, 200/255))
LegoColours.__overwriteColour(30, (172/255, 120/255, 186/255))
LegoColours.__overwriteColour(31, (225/255, 213/255, 237/255))
LegoColours.__overwriteColour(32, ( 0/255, 20/255, 20/255))
LegoColours.__overwriteColour(33, (123/255, 182/255, 232/255))
LegoColours.__overwriteColour(34, (132/255, 182/255, 141/255))
LegoColours.__overwriteColour(35, (217/255, 228/255, 167/255))
LegoColours.__overwriteColour(36, (205/255, 84/255, 75/255))
LegoColours.__overwriteColour(37, (228/255, 173/255, 200/255))
LegoColours.__overwriteColour(38, (255/255, 43/255, 0/225))
LegoColours.__overwriteColour(40, (166/255, 145/255, 130/255))
LegoColours.__overwriteColour(41, (170/255, 229/255, 255/255))
LegoColours.__overwriteColour(42, (198/255, 255/255, 0/255))
LegoColours.__overwriteColour(43, (193/255, 223/255, 240/255))
LegoColours.__overwriteColour(44, (150/255, 112/255, 159/255))
LegoColours.__overwriteColour(46, (247/255, 241/255, 141/255))
LegoColours.__overwriteColour(47, (252/255, 252/255, 252/255))
LegoColours.__overwriteColour(52, (156/255, 149/255, 199/255))
LegoColours.__overwriteColour(54, (255/255, 246/255, 123/255))
LegoColours.__overwriteColour(57, (226/255, 176/255, 96/255))
LegoColours.__overwriteColour(65, (236/255, 201/255, 53/255))
LegoColours.__overwriteColour(66, (202/255, 176/255, 0/255))
LegoColours.__overwriteColour(67, (255/255, 255/255, 255/255))
LegoColours.__overwriteColour(68, (243/255, 207/255, 155/255))
LegoColours.__overwriteColour(69, (142/255, 66/255, 133/255))
LegoColours.__overwriteColour(70, (105/255, 64/255, 39/255))
LegoColours.__overwriteColour(71, (163/255, 162/255, 164/255))
LegoColours.__overwriteColour(72, ( 99/255, 95/255, 97/255))
LegoColours.__overwriteColour(73, (110/255, 153/255, 201/255))
LegoColours.__overwriteColour(74, (161/255, 196/255, 139/255))
LegoColours.__overwriteColour(77, (220/255, 144/255, 149/255))
LegoColours.__overwriteColour(78, (246/255, 215/255, 179/255))
LegoColours.__overwriteColour(79, (255/255, 255/255, 255/255))
LegoColours.__overwriteColour(80, (140/255, 140/255, 140/255))
LegoColours.__overwriteColour(82, (219/255, 172/255, 52/255))
LegoColours.__overwriteColour(84, (170/255, 125/255, 85/255))
LegoColours.__overwriteColour(85, ( 52/255, 43/255, 117/255))
LegoColours.__overwriteColour(86, (124/255, 92/255, 69/255))
LegoColours.__overwriteColour(89, (155/255, 178/255, 239/255))
LegoColours.__overwriteColour(92, (204/255, 142/255, 104/255))
LegoColours.__overwriteColour(100, (238/255, 196/255, 182/255))
LegoColours.__overwriteColour(115, (199/255, 210/255, 60/255))
LegoColours.__overwriteColour(134, (174/255, 122/255, 89/255))
LegoColours.__overwriteColour(135, (171/255, 173/255, 172/255))
LegoColours.__overwriteColour(137, (106/255, 122/255, 150/255))
LegoColours.__overwriteColour(142, (220/255, 188/255, 129/255))
LegoColours.__overwriteColour(148, ( 62/255, 60/255, 57/255))
LegoColours.__overwriteColour(151, ( 14/255, 94/255, 77/255))
LegoColours.__overwriteColour(179, (160/255, 160/255, 160/255))
LegoColours.__overwriteColour(183, (242/255, 243/255, 242/255))
LegoColours.__overwriteColour(191, (248/255, 187/255, 61/255))
LegoColours.__overwriteColour(212, (159/255, 195/255, 233/255))
LegoColours.__overwriteColour(216, (143/255, 76/255, 42/255))
LegoColours.__overwriteColour(226, (253/255, 234/255, 140/255))
LegoColours.__overwriteColour(232, (125/255, 187/255, 221/255))
LegoColours.__overwriteColour(256, ( 33/255, 33/255, 33/255))
LegoColours.__overwriteColour(272, ( 32/255, 58/255, 86/255))
LegoColours.__overwriteColour(273, ( 13/255, 105/255, 171/255))
LegoColours.__overwriteColour(288, ( 39/255, 70/255, 44/255))
LegoColours.__overwriteColour(294, (189/255, 198/255, 173/255))
LegoColours.__overwriteColour(297, (170/255, 127/255, 46/255))
LegoColours.__overwriteColour(308, ( 53/255, 33/255, 0/255))
LegoColours.__overwriteColour(313, (171/255, 217/255, 255/255))
LegoColours.__overwriteColour(320, (123/255, 46/255, 47/255))
LegoColours.__overwriteColour(321, ( 70/255, 155/255, 195/255))
LegoColours.__overwriteColour(322, (104/255, 195/255, 226/255))
LegoColours.__overwriteColour(323, (211/255, 242/255, 234/255))
LegoColours.__overwriteColour(324, (196/255, 0/255, 38/255))
LegoColours.__overwriteColour(326, (226/255, 249/255, 154/255))
LegoColours.__overwriteColour(330, (119/255, 119/255, 78/255))
LegoColours.__overwriteColour(334, (187/255, 165/255, 61/255))
LegoColours.__overwriteColour(335, (149/255, 121/255, 118/255))
LegoColours.__overwriteColour(366, (209/255, 131/255, 4/255))
LegoColours.__overwriteColour(373, (135/255, 124/255, 144/255))
LegoColours.__overwriteColour(375, (193/255, 194/255, 193/255))
LegoColours.__overwriteColour(378, (120/255, 144/255, 129/255))
LegoColours.__overwriteColour(379, ( 94/255, 116/255, 140/255))
LegoColours.__overwriteColour(383, (224/255, 224/255, 224/255))
LegoColours.__overwriteColour(406, ( 0/255, 29/255, 104/255))
LegoColours.__overwriteColour(449, (129/255, 0/255, 123/255))
LegoColours.__overwriteColour(450, (203/255, 132/255, 66/255))
LegoColours.__overwriteColour(462, (226/255, 155/255, 63/255))
LegoColours.__overwriteColour(484, (160/255, 95/255, 52/255))
LegoColours.__overwriteColour(490, (215/255, 240/255, 0/255))
LegoColours.__overwriteColour(493, (101/255, 103/255, 97/255))
LegoColours.__overwriteColour(494, (208/255, 208/255, 208/255))
LegoColours.__overwriteColour(496, (163/255, 162/255, 164/255))
LegoColours.__overwriteColour(503, (199/255, 193/255, 183/255))
LegoColours.__overwriteColour(504, (137/255, 135/255, 136/255))
LegoColours.__overwriteColour(511, (250/255, 250/255, 250/255))
def lightenRGBA(colour, scale):
# Moves the linear RGB values closer to white
# scale = 0 means full white
# scale = 1 means color stays same
colour = ((1.0 - colour[0]) * scale,
(1.0 - colour[1]) * scale,
(1.0 - colour[2]) * scale,
colour[3])
return (Math.clamp01(1.0 - colour[0]),
Math.clamp01(1.0 - colour[1]),
Math.clamp01(1.0 - colour[2]),
colour[3])
def isFluorescentTransparent(colName):
if (colName == "Trans_Neon_Orange"):
return True
if (colName == "Trans_Neon_Green"):
return True
if (colName == "Trans_Neon_Yellow"):
return True
if (colName == "Trans_Bright_Green"):
return True
return False
def __init__(self):
LegoColours.__readColourTable()
# **************************************************************************************
# **************************************************************************************
class FileSystem:
"""
Reads text files in different encodings. Locates full filepath for a part.
"""
# Takes a case-insensitive filepath and constructs a case sensitive version (based on an actual existing file)
# See https://stackoverflow.com/questions/8462449/python-case-insensitive-file-name/8462613#8462613
def pathInsensitive(path):
"""
Get a case-insensitive path for use on a case sensitive system.
>>> path_insensitive('/Home')
'/home'
>>> path_insensitive('/Home/chris')
'/home/chris'
>>> path_insensitive('/HoME/CHris/')
'/home/chris/'
>>> path_insensitive('/home/CHRIS')
'/home/chris'
>>> path_insensitive('/Home/CHRIS/.gtk-bookmarks')
'/home/chris/.gtk-bookmarks'
>>> path_insensitive('/home/chris/.GTK-bookmarks')
'/home/chris/.gtk-bookmarks'
>>> path_insensitive('/HOME/Chris/.GTK-bookmarks')
'/home/chris/.gtk-bookmarks'
>>> path_insensitive("/HOME/Chris/I HOPE this doesn't exist")
"/HOME/Chris/I HOPE this doesn't exist"
"""
return FileSystem.__pathInsensitive(path) or path
def __pathInsensitive(path):
"""
Recursive part of path_insensitive to do the work.
"""
if path == '' or os.path.exists(path):
return path
base = os.path.basename(path) # may be a directory or a file
dirname = os.path.dirname(path)
suffix = ''
if not base: # dir ends with a slash?
if len(dirname) < len(path):
suffix = path[:len(path) - len(dirname)]
base = os.path.basename(dirname)
dirname = os.path.dirname(dirname)
if not os.path.exists(dirname):
debug_dirname = dirname
dirname = FileSystem.__pathInsensitive(dirname)
if not dirname:
return
# at this point, the directory exists but not the file
try: # we are expecting dirname to be a directory, but it could be a file
files = CachedDirectoryFilenames.getCached(dirname)
if files is None:
files = os.listdir(dirname)
CachedDirectoryFilenames.addToCache(dirname, files)
except OSError:
return
baselow = base.lower()
try:
basefinal = next(fl for fl in files if fl.lower() == baselow)
except StopIteration:
return
if basefinal:
return os.path.join(dirname, basefinal) + suffix
else:
return
def __checkEncoding(filepath):
"""Check the encoding of a file for Endian encoding."""
filepath = FileSystem.pathInsensitive(filepath)
# Open it, read just the area containing a possible byte mark
with open(filepath, "rb") as encode_check:
encoding = encode_check.readline(3)
# The file uses UCS-2 (UTF-16) Big Endian encoding
if encoding == b"\xfe\xff\x00":
return "utf_16_be"
# The file uses UCS-2 (UTF-16) Little Endian
elif encoding == b"\xff\xfe0":
return "utf_16_le"
# Use LDraw model standard UTF-8
else:
return "utf_8"
def readTextFile(filepath):
"""Read a text file, with various checks for type of encoding"""
filepath = FileSystem.pathInsensitive(filepath)
lines = None
if os.path.exists(filepath):
# Try to read using the suspected encoding
file_encoding = FileSystem.__checkEncoding(filepath)
try:
with open(filepath, "rt", encoding=file_encoding) as f_in:
lines = f_in.readlines()
except:
# If all else fails, read using Latin 1 encoding
with open(filepath, "rt", encoding="latin_1") as f_in:
lines = f_in.readlines()
return lines
def locate(filename, rootPath = None):
"""Given a file name of an ldraw file, find the full path"""
partName = filename.replace("\\", os.path.sep)
partName = os.path.expanduser(partName)
if rootPath is None:
rootPath = os.path.dirname(filename)
allSearchPaths = Configure.searchPaths[:]
if rootPath not in allSearchPaths:
allSearchPaths.append(rootPath)
for path in allSearchPaths:
fullPathName = os.path.join(path, partName)
fullPathName = FileSystem.pathInsensitive(fullPathName)
if os.path.exists(fullPathName):
return fullPathName
return None
# **************************************************************************************
# **************************************************************************************
class CachedDirectoryFilenames:
"""Cached dictionary of directory filenames keyed by directory path"""
__cache = {} # Dictionary
def getCached(key):
if key in CachedDirectoryFilenames.__cache:
return CachedDirectoryFilenames.__cache[key]
return None
def addToCache(key, value):
CachedDirectoryFilenames.__cache[key] = value
def clearCache():
CachedDirectoryFilenames.__cache = {}
# **************************************************************************************
# **************************************************************************************
class CachedFiles:
"""Cached dictionary of LDrawFile objects keyed by filename"""
__cache = {} # Dictionary of exact filenames as keys, and file contents as values
__lowercache = {} # Dictionary of lowercase filenames as keys, and file contents as values
def getCached(key):
# Look for an exact match in the cache first
if key in CachedFiles.__cache:
return CachedFiles.__cache[key]
# Look for a case-insensitive match next
if key.lower() in CachedFiles.__lowercache:
return CachedFiles.__lowercache[key.lower()]
return None
def addToCache(key, value):
CachedFiles.__cache[key] = value
CachedFiles.__lowercache[key.lower()] = value
def clearCache():
CachedFiles.__cache = {}
CachedFiles.__lowercache = {}
# **************************************************************************************
# **************************************************************************************
class CachedGeometry:
"""Cached dictionary of LDrawGeometry objects"""
__cache = {} # Dictionary
def getCached(key):
if key in CachedGeometry.__cache:
return CachedGeometry.__cache[key]
return None
def addToCache(key, value):
CachedGeometry.__cache[key] = value
def clearCache():
CachedGeometry.__cache = {}
# **************************************************************************************
# **************************************************************************************
class FaceInfo:
def __init__(self, faceColour, culling, windingCCW, isGrainySlopeAllowed):
self.faceColour = faceColour
self.culling = culling
self.windingCCW = windingCCW
self.isGrainySlopeAllowed = isGrainySlopeAllowed
# **************************************************************************************
# **************************************************************************************
class LDrawGeometry:
"""Stores the geometry for an LDrawFile"""
def __init__(self):
self.points = []
self.faces = []
self.faceInfo = []
self.edges = []
self.edgeIndices = []
def parseFace(self, parameters, cull, ccw, isGrainySlopeAllowed):
"""Parse a face from parameters"""
num_points = int(parameters[0])
colourName = parameters[1]
newPoints = []
for i in range(num_points):
blenderPos = Math.scaleMatrix @ mathutils.Vector( (float(parameters[i * 3 + 2]),
float(parameters[i * 3 + 3]),
float(parameters[i * 3 + 4])) )
newPoints.append(blenderPos)
# Fix "bowtie" quadrilaterals (see http://wiki.ldraw.org/index.php?title=LDraw_technical_restrictions#Complex_quadrilaterals)
if num_points == 4:
nA = (newPoints[1] - newPoints[0]).cross(newPoints[2] - newPoints[0])
nB = (newPoints[2] - newPoints[1]).cross(newPoints[3] - newPoints[1])
nC = (newPoints[3] - newPoints[2]).cross(newPoints[0] - newPoints[2])
if (nA.dot(nB) < 0):
newPoints[2], newPoints[3] = newPoints[3], newPoints[2]
elif (nB.dot(nC) < 0):
newPoints[2], newPoints[1] = newPoints[1], newPoints[2]
pointCount = len(self.points)
newFace = list(range(pointCount, pointCount + num_points))
self.points.extend(newPoints)
self.faces.append(newFace)
self.faceInfo.append(FaceInfo(colourName, cull, ccw, isGrainySlopeAllowed))
def parseEdge(self, parameters):
"""Parse an edge from parameters"""
colourName = parameters[1]
if colourName == "24":
blenderPos1 = Math.scaleMatrix @ mathutils.Vector( (float(parameters[2]),
float(parameters[3]),
float(parameters[4])) )
blenderPos2 = Math.scaleMatrix @ mathutils.Vector( (float(parameters[5]),
float(parameters[6]),
float(parameters[7])) )
self.edges.append((blenderPos1, blenderPos2))
def verify(self, face, numPoints):
for i in face:
assert i < numPoints
assert i >= 0
def appendGeometry(self, geometry, matrix, isStud, isStudLogo, parentMatrix, cull, invert):
combinedMatrix = parentMatrix @ matrix
isReflected = combinedMatrix.determinant() < 0.0
reflectStudLogo = isStudLogo and isReflected
fixedMatrix = matrix.copy()
if reflectStudLogo:
fixedMatrix = matrix @ Math.reflectionMatrix
invert = not invert
# Append face information
pointCount = len(self.points)
newFaceInfo = []
for index, face in enumerate(geometry.faces):
# Gather points for this face (and transform points)
newPoints = []
for i in face:
newPoints.append(fixedMatrix @ geometry.points[i])
# Add clockwise and/or anticlockwise sets of points as appropriate
newFace = face.copy()
for i in range(len(newFace)):
newFace[i] += pointCount
faceInfo = geometry.faceInfo[index]
faceCCW = faceInfo.windingCCW != invert
faceCull = faceInfo.culling and cull
# If we are going to resolve ambiguous normals by "best guess" we will let
# Blender calculate that for us later. Just cull with arbitrary winding for now.
if not faceCull:
if Options.resolveAmbiguousNormals == "guess":
faceCull = True
if faceCCW or not faceCull:
self.points.extend(newPoints)
self.faces.append(newFace)
newFaceInfo.append(FaceInfo(faceInfo.faceColour, True, True, not isStud and faceInfo.isGrainySlopeAllowed))
self.verify(newFace, len(self.points))
if not faceCull:
newFace = newFace.copy()
pointCount += len(newPoints)
for i in range(len(newFace)):
newFace[i] += len(newPoints)
if not faceCCW or not faceCull:
self.points.extend(newPoints[::-1])
self.faces.append(newFace)
newFaceInfo.append(FaceInfo(faceInfo.faceColour, True, True, not isStud and faceInfo.isGrainySlopeAllowed))
self.verify(newFace, len(self.points))
self.faceInfo.extend(newFaceInfo)
assert len(self.faces) == len(self.faceInfo)
# Append edge information
newEdges = []
for edge in geometry.edges:
newEdges.append( (fixedMatrix @ edge[0], fixedMatrix @ edge[1]) )
self.edges.extend(newEdges)
# **************************************************************************************
# **************************************************************************************
class LDrawNode:
"""A node in the hierarchy. References one LDrawFile"""
def __init__(self, filename, isFullFilepath, parentFilepath, colourName=Options.defaultColour, matrix=Math.identityMatrix, bfcCull=True, bfcInverted=False, isLSynthPart=False, isSubPart=False, isRootNode=True, groupNames=[]):
self.filename = filename
self.isFullFilepath = isFullFilepath
self.parentFilepath = parentFilepath
self.matrix = matrix
self.colourName = colourName
self.bfcInverted = bfcInverted
self.bfcCull = bfcCull
self.file = None
self.isLSynthPart = isLSynthPart
self.isSubPart = isSubPart
self.isRootNode = isRootNode
self.groupNames = groupNames.copy()
def look_at(obj_camera, target, up_vector):
bpy.context.view_layer.update()
loc_camera = obj_camera.matrix_world.to_translation()
#print("CamLoc = " + str(loc_camera[0]) + "," + str(loc_camera[1]) + "," + str(loc_camera[2]))
#print("TarLoc = " + str(target[0]) + "," + str(target[1]) + "," + str(target[2]))
#print("UpVec = " + str(up_vector[0]) + "," + str(up_vector[1]) + "," + str(up_vector[2]))
# back vector is a vector pointing from the target to the camera
back = loc_camera - target;
back.normalize()
# If our back and up vectors are very close to pointing the same way (or opposite), choose a different up_vector
if (abs(back.dot(up_vector)) > 0.9999):
up_vector=mathutils.Vector((0.0,0.0,1.0))
if (abs(back.dot(up_vector)) > 0.9999):
up_vector=mathutils.Vector((1.0,0.0,0.0))
right = up_vector.cross(back)
right.normalize()
up = back.cross(right)
up.normalize()
row1 = [ right.x, up.x, back.x, loc_camera.x ]
row2 = [ right.y, up.y, back.y, loc_camera.y ]
row3 = [ right.z, up.z, back.z, loc_camera.z ]
row4 = [ 0.0, 0.0, 0.0, 1.0 ]
#bpy.ops.mesh.primitive_ico_sphere_add(location=loc_camera+up,size=0.1)
#bpy.ops.mesh.primitive_cylinder_add(location=loc_camera+back,radius = 0.1, depth=0.2)
#bpy.ops.mesh.primitive_cone_add(location=loc_camera+right,radius1=0.1, radius2=0, depth=0.2)
obj_camera.matrix_world = mathutils.Matrix((row1, row2, row3, row4))
#print(obj_camera.matrix_world)
def isBlenderObjectNode(self):
"""
Calculates if this node should become a Blender object.
Some nodes will become objects in Blender, some will not.
Typically nodes that reference a model or a part become Blender Objects, but not nodes that reference subparts.
"""
# The root node is always a Blender node
if self.isRootNode:
return True
# General rule: We are a Blender object if we are a part or higher (ie. if we are not a subPart)
isBON = not self.isSubPart
# Exception #1 - If flattening the hierarchy, we only want parts (not models)
if Options.flattenHierarchy:
isBON = self.file.isPart and not self.isSubPart
# Exception #2 - We are not a Blender Object if we are an LSynth part (so that all LSynth parts become a single mesh)
if self.isLSynthPart:
isBON = False
# Exception #3 - We are a Blender Object if we are a stud to be instanced
if Options.instanceStuds and self.file.isStud:
isBON = True
return isBON
def load(self):
# Is this file in the cache?
self.file = CachedFiles.getCached(self.filename)
if self.file is None:
# Not in cache, so load file
self.file = LDrawFile(self.filename, self.isFullFilepath, self.parentFilepath, None, self.isSubPart)
assert self.file is not None
# Add the new file to the cache
CachedFiles.addToCache(self.filename, self.file)
# Load any children
for child in self.file.childNodes:
child.load()
def resolveColour(colourName, realColourName):
if colourName == "16":
return realColourName
return colourName
def printBFC(self, depth=0):
# For debugging, displays BFC information
debugPrint("{0}Node {1} has cull={2} and invert={3} det={4}".format(" "*(depth*4), self.filename, self.bfcCull, self.bfcInverted, self.matrix.determinant()))
for child in self.file.childNodes:
child.printBFC(depth + 1)
def getBFCCode(accumCull, accumInvert, bfcCull, bfcInverted):
index = (8 if accumCull else 0) + (4 if accumInvert else 0) + (2 if bfcCull else 0) + (1 if bfcInverted else 0)
# Normally meshes are culled and not inverted, so don't bother with a code in this case
if index == 10:
return ""
# If this is out of the ordinary, add a code that makes it a unique name to cache the mesh properly
return "_{0}".format(index)
def getBlenderGeometry(self, realColourName, basename, parentMatrix=Math.identityMatrix, accumCull=True, accumInvert=False):
"""
Returns the geometry for the Blender Object at this node.
It accumulates the geometry of itself with all the geometry of it's children
recursively (specifically - those children that are not Blender Object nodes).
The result will become a single mesh in Blender.
"""
assert self.file is not None
accumCull = accumCull and self.bfcCull
accumInvert = accumInvert != self.bfcInverted
ourColourName = LDrawNode.resolveColour(self.colourName, realColourName)
code = LDrawNode.getBFCCode(accumCull, accumInvert, self.bfcCull, self.bfcInverted)
meshName = "Mesh_{0}_{1}{2}".format(basename, ourColourName, code)
key = (self.filename, ourColourName, accumCull, accumInvert, self.bfcCull, self.bfcInverted)
bakedGeometry = CachedGeometry.getCached(key)
if bakedGeometry is None:
combinedMatrix = parentMatrix @ self.matrix
# Start with a copy of our file's geometry
assert len(self.file.geometry.faces) == len(self.file.geometry.faceInfo)
bakedGeometry = LDrawGeometry()
bakedGeometry.appendGeometry(self.file.geometry, Math.identityMatrix, self.file.isStud, self.file.isStudLogo, combinedMatrix, self.bfcCull, self.bfcInverted)
# Replaces the default colour 16 in our faceColours list with a specific colour
for faceInfo in bakedGeometry.faceInfo:
faceInfo.faceColour = LDrawNode.resolveColour(faceInfo.faceColour, ourColourName)
# Append each child's geometry
for child in self.file.childNodes:
assert child.file is not None
if not child.isBlenderObjectNode():
childColourName = LDrawNode.resolveColour(child.colourName, ourColourName)
childMeshName, bg = child.getBlenderGeometry(childColourName, basename, combinedMatrix, accumCull, accumInvert)
isStud = child.file.isStud
isStudLogo = child.file.isStudLogo
bakedGeometry.appendGeometry(bg, child.matrix, isStud, isStudLogo, combinedMatrix, self.bfcCull, self.bfcInverted)
CachedGeometry.addToCache(key, bakedGeometry)
assert len(bakedGeometry.faces) == len(bakedGeometry.faceInfo)
return (meshName, bakedGeometry)
# **************************************************************************************
# **************************************************************************************
class LDrawCamera:
"""Data about a camera"""
def __init__(self):
self.vert_fov_degrees = 30.0
self.near = 0.01
self.far = 100.0
self.position = mathutils.Vector((0.0, 0.0, 0.0))
self.target_position = mathutils.Vector((1.0, 0.0, 0.0))
self.up_vector = mathutils.Vector((0.0, 1.0, 0.0))
self.name = "Camera"
self.orthographic = False
self.hidden = False
def createCameraNode(self):
camData = bpy.data.cameras.new(self.name)
camera = bpy.data.objects.new(self.name, camData)
# Add to scene
camera.location = self.position
camera.data.sensor_fit = 'VERTICAL'
camera.data.angle = self.vert_fov_degrees * 3.1415926 / 180.0
camera.data.clip_end = self.far
camera.data.clip_start = self.near
camera.hide_set(self.hidden)
self.hidden = False
if self.orthographic:
dist_target_to_camera = (self.position - self.target_position).length
camera.data.ortho_scale = dist_target_to_camera / 1.92
camera.data.type = 'ORTHO'
self.orthographic = False
else:
camera.data.type = 'PERSP'
linkToScene(camera)
LDrawNode.look_at(camera, self.target_position, self.up_vector)
return camera
# **************************************************************************************
# **************************************************************************************
class LDrawFile:
"""Stores the contents of a single LDraw file.
Specifically this represents an IO, LDR, L3B, DAT or one '0 FILE' section of an MPD.
Splits up an MPD file into '0 FILE' sections and caches them."""
def __loadLegoFile(self, filepath, isFullFilepath, parentFilepath):
# Resolve full filepath if necessary
if isFullFilepath is False:
if parentFilepath == "":
parentDir = os.path.dirname(filepath)
else:
parentDir = os.path.dirname(parentFilepath)
result = FileSystem.locate(filepath, parentDir)
if result is None:
printWarningOnce("Missing file {0}".format(filepath))
return False
filepath = result
if os.path.splitext(filepath)[1] == ".io":
# Check if the file is encrypted (password protected)
is_encrypted = False
zf = zipfile.ZipFile(filepath)
for zinfo in zf.infolist():
is_encrypted |= zinfo.flag_bits & 0x1
if is_encrypted:
ShowMessageBox("Oops, this .io file is password protected", "Password protected files are not supported", 'ERROR')
return False
# Get a temporary directory. Store the TemporaryDirectory object in Configure so it's scope lasts long enough
Configure.tempDir = tempfile.TemporaryDirectory()
directory_to_extract_to = Configure.tempDir.name
# Decompress to temporary directory
with zipfile.ZipFile(filepath, 'r') as zip_ref:
zip_ref.extractall(directory_to_extract_to)
# It's the 'model.ldr' file we want to use
filepath = os.path.join(directory_to_extract_to, "model.ldr")
# Add the subdirectories of the directory to the search paths
Configure.appendPath(os.path.join(directory_to_extract_to, "CustomParts"))
Configure.appendPath(os.path.join(directory_to_extract_to, "CustomParts", "parts"))
if Options.resolution == "High":
Configure.appendPath(os.path.join(directory_to_extract_to, "CustomParts", "p", "48"))
elif Options.resolution == "Low":
Configure.appendPath(os.path.join(directory_to_extract_to, "CustomParts", "p", "8"))
Configure.appendPath(os.path.join(directory_to_extract_to, "CustomParts", "p"))
Configure.appendPath(os.path.join(directory_to_extract_to, "CustomParts", "s"))
Configure.appendPath(os.path.join(directory_to_extract_to, "CustomParts", "s", "s"))
self.fullFilepath = filepath
# Load text into local lines variable
lines = FileSystem.readTextFile(filepath)
if lines is None:
printWarningOnce("Could not read file {0}".format(filepath))
lines = []
# MPD files have separate sections between '0 FILE' and '0 NOFILE' lines.
# Split into sections between "0 FILE" and "0 NOFILE" lines
sections = []
startLine = 0
endLine = 0
lineCount = 0
sectionFilename = filepath
foundEnd = False
for line in lines:
parameters = line.strip().split()
if len(parameters) > 2:
if parameters[0] == "0" and parameters[1] == "FILE":
if foundEnd == False:
endLine = lineCount
if endLine > startLine:
sections.append((sectionFilename, lines[startLine:endLine]))
startLine = lineCount
foundEnd = False
sectionFilename = " ".join(parameters[2:])
if parameters[0] == "0" and parameters[1] == "NOFILE":
endLine = lineCount
foundEnd = True
sections.append((sectionFilename, lines[startLine:endLine]))
lineCount += 1
if foundEnd == False:
endLine = lineCount
if endLine > startLine:
sections.append((sectionFilename, lines[startLine:endLine]))
if len(sections) == 0:
return False
# First section is the main one
self.filename = sections[0][0]
self.lines = sections[0][1]
# Remaining sections are loaded into the cached files
for (sectionFilename, lines) in sections[1:]:
# Load section
file = LDrawFile(sectionFilename, False, filepath, lines, False)
assert file is not None
# Cache section
CachedFiles.addToCache(sectionFilename, file)
return True
def __isStud(filename):
"""Is this file a stud?"""
if LDrawFile.__isStudLogo(filename):
return True
# Extract just the filename, in lower case
filename = filename.replace("\\", os.path.sep)
name = os.path.basename(filename).lower()
return name in (
"stud2.dat",
"stud6.dat",
"stud6a.dat",
"stud7.dat",
"stud10.dat",
"stud13.dat",
"stud15.dat",
"stud20.dat",
"studa.dat",
"teton.dat", # TENTE
"stud-logo3.dat", "stud-logo4.dat", "stud-logo5.dat",
"stud2-logo3.dat", "stud2-logo4.dat", "stud2-logo5.dat",
"stud6-logo3.dat", "stud6-logo4.dat", "stud6-logo5.dat",
"stud6a-logo3.dat", "stud6a-logo4.dat", "stud6a-logo5.dat",
"stud7-logo3.dat", "stud7-logo4.dat", "stud7-logo5.dat",
"stud10-logo3.dat", "stud10-logo4.dat", "stud10-logo5.dat",
"stud13-logo3.dat", "stud13-logo4.dat", "stud13-logo5.dat",
"stud15-logo3.dat", "stud15-logo4.dat", "stud15-logo5.dat",
"stud20-logo3.dat", "stud20-logo4.dat", "stud20-logo5.dat",
"studa-logo3.dat", "studa-logo4.dat", "studa-logo5.dat",
"studtente-logo.dat" # TENTE
)
def __isStudLogo(filename):
"""Is this file a stud logo?"""
# Extract just the filename, in lower case
filename = filename.replace("\\", os.path.sep)
name = os.path.basename(filename).lower()
return name in ("logo3.dat", "logo4.dat", "logo5.dat", "logotente.dat")
def __init__(self, filename, isFullFilepath, parentFilepath, lines = None, isSubPart=False):
"""Loads an LDraw file (IO, LDR, L3B, DAT or MPD)"""
global globalCamerasToAdd
global globalScaleFactor
self.filename = filename
self.lines = lines
self.isPart = False
self.isSubPart = isSubPart
self.isStud = LDrawFile.__isStud(filename)
self.isStudLogo = LDrawFile.__isStudLogo(filename)
self.isLSynthPart = False
self.isDoubleSided = False
self.geometry = LDrawGeometry()
self.childNodes = []
self.bfcCertified = None
self.isModel = False
isGrainySlopeAllowed = not self.isStud
if self.lines is None:
# Load the file into self.lines
if not self.__loadLegoFile(self.filename, isFullFilepath, parentFilepath):
return
else:
# We are loading a section of our parent document, so full filepath is that of the parent
self.fullFilepath = parentFilepath
# BFC = Back face culling. The rules are arcane and complex, but at least
# it's kind of documented: http://www.ldraw.org/article/415.html
bfcLocalCull = True
bfcWindingCCW = True
bfcInvertNext = False
processingLSynthParts = False
camera = LDrawCamera()
currentGroupNames = []
#debugPrint("Processing file {0}, isSubPart = {1}, found {2} lines".format(self.filename, self.isSubPart, len(self.lines)))
for line in self.lines:
parameters = line.strip().split()
# Skip empty lines
if len(parameters) == 0:
continue
# Pad with empty values to simplify parsing code
while len(parameters) < 9:
parameters.append("")
# Parse LDraw comments (some of which have special significance)
if parameters[0] == "0":
if parameters[1] == "!LDRAW_ORG":
partType = parameters[2].lower()
if 'part' in partType:
self.isPart = True
if 'subpart' in partType:
self.isSubPart = True
if 'primitive' in partType:
self.isSubPart = True
#if 'shortcut' in partType:
# self.isPart = True
if parameters[1] == "BFC":
# If unsure about being certified yet...
if self.bfcCertified is None:
if parameters[2] == "NOCERTIFY":
self.bfcCertified = False
else:
self.bfcCertified = True
if "CW" in parameters:
bfcWindingCCW = False
if "CCW" in parameters:
bfcWindingCCW = True
if "CLIP" in parameters:
bfcLocalCull = True
if "NOCLIP" in parameters:
bfcLocalCull = False
if "INVERTNEXT" in parameters:
bfcInvertNext = True
if parameters[1] == "SYNTH":
if parameters[2] == "SYNTHESIZED":
if parameters[3] == "BEGIN":
processingLSynthParts = True
if parameters[3] == "END":
processingLSynthParts = False
if parameters[1] == "!LDCAD":
if parameters[2] == "GENERATED":
processingLSynthParts = True
if parameters[1] == "!LEOCAD":
if parameters[2] == "GROUP":
if parameters[3] == "BEGIN":
currentGroupNames.append(" ".join(parameters[4:]))
elif parameters[3] == "END":
currentGroupNames.pop(-1)
if parameters[2] == "CAMERA":
if Options.importCameras:
parameters = parameters[3:]
while( len(parameters) > 0):
if parameters[0] == "FOV":
camera.vert_fov_degrees = float(parameters[1])
parameters = parameters[2:]
elif parameters[0] == "ZNEAR":
camera.near = globalScaleFactor * float(parameters[1])
parameters = parameters[2:]
elif parameters[0] == "ZFAR":
camera.far = globalScaleFactor * float(parameters[1])
parameters = parameters[2:]
elif parameters[0] == "POSITION":
camera.position = Math.scaleMatrix @ mathutils.Vector((float(parameters[1]), float(parameters[2]), float(parameters[3])))
parameters = parameters[4:]
elif parameters[0] == "TARGET_POSITION":
camera.target_position = Math.scaleMatrix @ mathutils.Vector((float(parameters[1]), float(parameters[2]), float(parameters[3])))
parameters = parameters[4:]
elif parameters[0] == "UP_VECTOR":
camera.up_vector = mathutils.Vector((float(parameters[1]), float(parameters[2]), float(parameters[3])))
parameters = parameters[4:]
elif parameters[0] == "ORTHOGRAPHIC":
camera.orthographic = True
parameters = parameters[1:]
elif parameters[0] == "HIDDEN":
camera.hidden = True
parameters = parameters[1:]
elif parameters[0] == "NAME":
camera.name = line.split(" NAME ",1)[1].strip()
globalCamerasToAdd.append(camera)
camera = LDrawCamera()
# By definition this is the last of the parameters
parameters = []
else:
parameters = parameters[1:]
else:
if self.bfcCertified is None:
self.bfcCertified = False
self.isModel = (not self.isPart) and (not self.isSubPart)
# Parse a File reference
if parameters[0] == "1":
(x, y, z, a, b, c, d, e, f, g, h, i) = map(float, parameters[2:14])
(x, y, z) = Math.scaleMatrix @ mathutils.Vector((x, y, z))
localMatrix = mathutils.Matrix( ((a, b, c, x), (d, e, f, y), (g, h, i, z), (0, 0, 0, 1)) )
new_filename = " ".join(parameters[14:])
new_colourName = parameters[1]
det = localMatrix.determinant()
if det < 0:
bfcInvertNext = not bfcInvertNext
canCullChildNode = (self.bfcCertified or self.isModel) and bfcLocalCull and (det != 0)
if new_filename != "":
newNode = LDrawNode(new_filename, False, self.fullFilepath, new_colourName, localMatrix, canCullChildNode, bfcInvertNext, processingLSynthParts, not self.isModel, False, currentGroupNames)
self.childNodes.append(newNode)
else:
printWarningOnce("In file '{0}', the line '{1}' is not formatted corectly (ignoring).".format(self.fullFilepath, line))
# Parse an edge
elif parameters[0] == "2":
self.geometry.parseEdge(parameters)
# Parse a Face (either a triangle or a quadrilateral)
elif parameters[0] == "3" or parameters[0] == "4":
if self.bfcCertified is None:
self.bfcCertified = False
if not self.bfcCertified or not bfcLocalCull:
printWarningOnce("Found double-sided polygons in file {0}".format(self.filename))
self.isDoubleSided = True
assert len(self.geometry.faces) == len(self.geometry.faceInfo)
self.geometry.parseFace(parameters, self.bfcCertified and bfcLocalCull, bfcWindingCCW, isGrainySlopeAllowed)
assert len(self.geometry.faces) == len(self.geometry.faceInfo)
bfcInvertNext = False
#debugPrint("File {0} is part = {1}, is subPart = {2}, isModel = {3}".format(filename, self.isPart, isSubPart, self.isModel))
# **************************************************************************************
# **************************************************************************************
class BlenderMaterials:
"""Creates and stores a cache of materials for Blender"""
__material_list = {}
if bpy.app.version >= (4, 0, 0):
__hasPrincipledShader = True
else:
__hasPrincipledShader = "ShaderNodeBsdfPrincipled" in [node.nodetype for node in getattr(bpy.types, "NODE_MT_category_SH_NEW_SHADER").category.items(None)]
def __getGroupName(name):
if Options.instructionsLook:
return name + " Instructions"
return name
def __createNodeBasedMaterial(blenderName, col, isSlopeMaterial=False):
"""Set Cycles Material Values."""
# Reuse current material if it exists, otherwise create a new material
if bpy.data.materials.get(blenderName) is None:
material = bpy.data.materials.new(blenderName)
else:
material = bpy.data.materials[blenderName]
# Use nodes
material.use_nodes = True
if col is not None:
if len(col["colour"]) == 3:
colour = col["colour"] + (1.0,)
material.diffuse_color = getDiffuseColor(col["colour"][0:3])
if Options.instructionsLook:
material.blend_method = 'BLEND'
material.show_transparent_back = False
if col is not None:
# Dark colours have white lines
if LegoColours.isDark(colour):
material.line_color = (1.0, 1.0, 1.0, 1.0)
nodes = material.node_tree.nodes
links = material.node_tree.links
# Remove any existing nodes
for n in nodes:
nodes.remove(n)
if col is not None:
isTransparent = col["alpha"] < 1.0
if Options.instructionsLook:
BlenderMaterials.__createCyclesBasic(nodes, links, colour, col["alpha"], "")
elif col["name"] == "Milky_White":
BlenderMaterials.__createCyclesMilkyWhite(nodes, links, colour)
elif col["luminance"] > 0:
BlenderMaterials.__createCyclesEmission(nodes, links, colour, col["alpha"], col["luminance"])
elif col["material"] == "CHROME":
BlenderMaterials.__createCyclesChrome(nodes, links, colour)
elif col["material"] == "PEARLESCENT":
BlenderMaterials.__createCyclesPearlescent(nodes, links, colour)
elif col["material"] == "METAL":
BlenderMaterials.__createCyclesMetal(nodes, links, colour)
elif col["material"] == "GLITTER":
BlenderMaterials.__createCyclesGlitter(nodes, links, colour, col["secondary_colour"])
elif col["material"] == "SPECKLE":
BlenderMaterials.__createCyclesSpeckle(nodes, links, colour, col["secondary_colour"])
elif col["material"] == "RUBBER":
BlenderMaterials.__createCyclesRubber(nodes, links, colour, col["alpha"])
else:
BlenderMaterials.__createCyclesBasic(nodes, links, colour, col["alpha"], col["name"])
if isSlopeMaterial and not Options.instructionsLook:
BlenderMaterials.__createCyclesSlopeTexture(nodes, links, 0.6)
elif Options.curvedWalls and not Options.instructionsLook:
BlenderMaterials.__createCyclesConcaveWalls(nodes, links, 20 * globalScaleFactor)
material["Lego.isTransparent"] = isTransparent
return material
BlenderMaterials.__createCyclesBasic(nodes, links, (1.0, 1.0, 0.0, 1.0), 1.0, "")
material["Lego.isTransparent"] = False
return material
def __nodeConcaveWalls(nodes, strength, x, y):
node = nodes.new('ShaderNodeGroup')
node.node_tree = bpy.data.node_groups[BlenderMaterials.__getGroupName('Concave Walls')]
node.location = x, y
node.inputs['Strength'].default_value = strength
return node
def __nodeSlopeTexture(nodes, strength, x, y):
node = nodes.new('ShaderNodeGroup')
node.node_tree = bpy.data.node_groups[BlenderMaterials.__getGroupName('Slope Texture')]
node.location = x, y
node.inputs['Strength'].default_value = strength
return node
def __nodeLegoStandard(nodes, colour, x, y):
node = nodes.new('ShaderNodeGroup')
node.node_tree = bpy.data.node_groups[BlenderMaterials.__getGroupName('Lego Standard')]
node.location = x, y
node.inputs['Color'].default_value = colour
return node
def __nodeLegoTransparentFluorescent(nodes, colour, x, y):
node = nodes.new('ShaderNodeGroup')
node.node_tree = bpy.data.node_groups[BlenderMaterials.__getGroupName('Lego Transparent Fluorescent')]
node.location = x, y
node.inputs['Color'].default_value = colour
return node
def __nodeLegoTransparent(nodes, colour, x, y):
node = nodes.new('ShaderNodeGroup')
node.node_tree = bpy.data.node_groups[BlenderMaterials.__getGroupName('Lego Transparent')]
node.location = x, y
node.inputs['Color'].default_value = colour
return node
def __nodeLegoRubberSolid(nodes, colour, x, y):
node = nodes.new('ShaderNodeGroup')
node.node_tree = bpy.data.node_groups[BlenderMaterials.__getGroupName('Lego Rubber Solid')]
node.location = x, y
node.inputs['Color'].default_value = colour
return node
def __nodeLegoRubberTranslucent(nodes, colour, x, y):
node = nodes.new('ShaderNodeGroup')
node.node_tree = bpy.data.node_groups[BlenderMaterials.__getGroupName('Lego Rubber Translucent')]
node.location = x, y
node.inputs['Color'].default_value = colour
return node
def __nodeLegoEmission(nodes, colour, luminance, x, y):
node = nodes.new('ShaderNodeGroup')
node.node_tree = bpy.data.node_groups[BlenderMaterials.__getGroupName('Lego Emission')]
node.location = x, y
node.inputs['Color'].default_value = colour
node.inputs['Luminance'].default_value = luminance
return node
def __nodeLegoChrome(nodes, colour, x, y):
node = nodes.new('ShaderNodeGroup')
node.node_tree = bpy.data.node_groups[BlenderMaterials.__getGroupName('Lego Chrome')]
node.location = x, y
node.inputs['Color'].default_value = colour
return node
def __nodeLegoPearlescent(nodes, colour, x, y):
node = nodes.new('ShaderNodeGroup')
node.node_tree = bpy.data.node_groups[BlenderMaterials.__getGroupName('Lego Pearlescent')]
node.location = x, y
node.inputs['Color'].default_value = colour
return node
def __nodeLegoMetal(nodes, colour, x, y):
node = nodes.new('ShaderNodeGroup')
node.node_tree = bpy.data.node_groups[BlenderMaterials.__getGroupName('Lego Metal')]
node.location = x, y
node.inputs['Color'].default_value = colour
return node
def __nodeLegoGlitter(nodes, colour, glitterColour, x, y):
node = nodes.new('ShaderNodeGroup')
node.node_tree = bpy.data.node_groups[BlenderMaterials.__getGroupName('Lego Glitter')]
node.location = x, y
node.inputs['Color'].default_value = colour
node.inputs['Glitter Color'].default_value = glitterColour
return node
def __nodeLegoSpeckle(nodes, colour, speckleColour, x, y):
node = nodes.new('ShaderNodeGroup')
node.node_tree = bpy.data.node_groups[BlenderMaterials.__getGroupName('Lego Speckle')]
node.location = x, y
node.inputs['Color'].default_value = colour
node.inputs['Speckle Color'].default_value = speckleColour
return node
def __nodeLegoMilkyWhite(nodes, colour, x, y):
node = nodes.new('ShaderNodeGroup')
node.node_tree = bpy.data.node_groups[BlenderMaterials.__getGroupName('Lego Milky White')]
node.location = x, y
node.inputs['Color'].default_value = colour
return node
def __nodeMix(nodes, factor, x, y):
node = nodes.new('ShaderNodeMixShader')
node.location = x, y
node.inputs['Fac'].default_value = factor
return node
def __nodeOutput(nodes, x, y):
node = nodes.new('ShaderNodeOutputMaterial')
node.location = x, y
return node
def __nodeDielectric(nodes, roughness, reflection, transparency, ior, x, y):
node = nodes.new('ShaderNodeGroup')
node.node_tree = bpy.data.node_groups['PBR-Dielectric']
node.location = x, y
node.inputs['Roughness'].default_value = roughness
node.inputs['Reflection'].default_value = reflection
node.inputs['Transparency'].default_value = transparency
node.inputs['IOR'].default_value = ior
return node
def __nodePrincipled(nodes, subsurface, sub_rad, metallic, roughness, clearcoat, clearcoat_roughness, ior, transmission, x, y):
node = nodes.new('ShaderNodeBsdfPrincipled')
node.location = x, y
# Some inputs are renamed in Blender 4
if bpy.app.version >= (4, 0, 0):
node.inputs['Subsurface Weight'].default_value = subsurface
node.inputs['Coat Weight'].default_value = clearcoat
node.inputs['Coat Roughness'].default_value = clearcoat_roughness
node.inputs['Transmission Weight'].default_value = transmission
else:
# Blender 3.X or earlier
node.inputs['Subsurface'].default_value = subsurface
node.inputs['Clearcoat'].default_value = clearcoat
node.inputs['Clearcoat Roughness'].default_value = clearcoat_roughness
node.inputs['Transmission'].default_value = transmission
node.inputs['Subsurface Radius'].default_value = mathutils.Vector( (sub_rad, sub_rad, sub_rad) )
node.inputs['Metallic'].default_value = metallic
node.inputs['Roughness'].default_value = roughness
node.inputs['IOR'].default_value = ior
return node
def __nodeHSV(nodes, h, s, v, x, y):
node = nodes.new('ShaderNodeHueSaturation')
node.location = x, y
node.inputs[0].default_value = h
node.inputs[1].default_value = s
node.inputs[2].default_value = v
return node
def __nodeSeparateHSV(nodes, x, y):
node = nodes.new('ShaderNodeSeparateHSV')
node.location = x, y
return node
def __nodeCombineHSV(nodes, x, y):
node = nodes.new('ShaderNodeCombineHSV')
node.location = x, y
return node
def __nodeTexCoord(nodes, x, y):
node = nodes.new('ShaderNodeTexCoord')
node.location = x, y
return node
def __nodeTexWave(nodes, wave_type, wave_profile, scale, distortion, detail, detailScale, x, y):
node = nodes.new('ShaderNodeTexWave')
node.wave_type = wave_type
node.wave_profile = wave_profile
node.inputs[1].default_value = scale
node.inputs[2].default_value = distortion
node.inputs[3].default_value = detail
node.inputs[4].default_value = detailScale
node.location = x, y
return node
def __nodeDiffuse(nodes, roughness, x, y):
node = nodes.new('ShaderNodeBsdfDiffuse')
node.location = x, y
node.inputs['Color'].default_value = (1,1,1,1)
node.inputs['Roughness'].default_value = roughness
return node
def __nodeGlass(nodes, roughness, ior, distribution, x, y):
node = nodes.new('ShaderNodeBsdfGlass')
node.location = x, y
node.distribution = distribution
node.inputs['Color'].default_value = (1,1,1,1)
node.inputs['Roughness'].default_value = roughness
node.inputs['IOR'].default_value = ior
return node
def __nodeFresnel(nodes, ior, x, y):
node = nodes.new('ShaderNodeFresnel')
node.location = x, y
node.inputs['IOR'].default_value = ior
return node
def __nodeGlossy(nodes, colour, roughness, distribution, x, y):
node = nodes.new('ShaderNodeBsdfGlossy')
node.location = x, y
node.distribution = distribution
node.inputs['Color'].default_value = colour
node.inputs['Roughness'].default_value = roughness
return node
def __nodeTranslucent(nodes, x, y):
node = nodes.new('ShaderNodeBsdfTranslucent')
node.location = x, y
return node
def __nodeTransparent(nodes, x, y):
node = nodes.new('ShaderNodeBsdfTransparent')
node.location = x, y
return node
def __nodeAddShader(nodes, x, y):
node = nodes.new('ShaderNodeAddShader')
node.location = x, y
return node
def __nodeVolume(nodes, density, x, y):
node = nodes.new('ShaderNodeVolumeAbsorption')
node.inputs['Density'].default_value = density
node.location = x, y
return node
def __nodeLightPath(nodes, x, y):
node = nodes.new('ShaderNodeLightPath')
node.location = x, y
return node
def __nodeMath(nodes, operation, x, y):
node = nodes.new('ShaderNodeMath')
node.operation = operation
node.location = x, y
return node
def __nodeVectorMath(nodes, operation, x, y):
node = nodes.new('ShaderNodeVectorMath')
node.operation = operation
node.location = x, y
return node
def __nodeEmission(nodes, x, y):
node = nodes.new('ShaderNodeEmission')
node.location = x, y
return node
def __nodeVoronoi(nodes, scale, x, y):
node = nodes.new('ShaderNodeTexVoronoi')
node.location = x, y
node.inputs['Scale'].default_value = scale
return node
def __nodeGamma(nodes, gamma, x, y):
node = nodes.new('ShaderNodeGamma')
node.location = x, y
node.inputs['Gamma'].default_value = gamma
return node
def __nodeColorRamp(nodes, pos1, colour1, pos2, colour2, x, y):
node = nodes.new('ShaderNodeValToRGB')
node.location = x, y
node.color_ramp.elements[0].position = pos1
node.color_ramp.elements[0].color = colour1
node.color_ramp.elements[1].position = pos2
node.color_ramp.elements[1].color = colour2
return node
def __nodeNoiseTexture(nodes, scale, detail, distortion, x, y):
node = nodes.new('ShaderNodeTexNoise')
node.location = x, y
node.inputs['Scale'].default_value = scale
node.inputs['Detail'].default_value = detail
node.inputs['Distortion'].default_value = distortion
return node
def __nodeBumpShader(nodes, strength, distance, x, y):
node = nodes.new('ShaderNodeBump')
node.location = x, y
node.inputs[0].default_value = strength
node.inputs[1].default_value = distance
return node
def __nodeRefraction(nodes, roughness, ior, x, y):
node = nodes.new('ShaderNodeBsdfRefraction')
node.inputs['Roughness'].default_value = roughness
node.inputs['IOR'].default_value = ior
node.location = x, y
return node
def __getGroup(nodes):
out = None
for x in nodes:
if x.type == 'GROUP':
return x
return None
def __createCyclesConcaveWalls(nodes, links, strength):
"""Concave wall normals for Cycles render engine"""
node = BlenderMaterials.__nodeConcaveWalls(nodes, strength, -200, 5)
out = BlenderMaterials.__getGroup(nodes)
if out is not None:
links.new(node.outputs['Normal'], out.inputs['Normal'])
def __createCyclesSlopeTexture(nodes, links, strength):
"""Slope face normals for Cycles render engine"""
node = BlenderMaterials.__nodeSlopeTexture(nodes, strength, -200, 5)
out = BlenderMaterials.__getGroup(nodes)
if out is not None:
links.new(node.outputs['Normal'], out.inputs['Normal'])
def __createCyclesBasic(nodes, links, diffColour, alpha, colName):
"""Basic Material for Cycles render engine."""
if alpha < 1:
if LegoColours.isFluorescentTransparent(colName):
node = BlenderMaterials.__nodeLegoTransparentFluorescent(nodes, diffColour, 0, 5)
else:
node = BlenderMaterials.__nodeLegoTransparent(nodes, diffColour, 0, 5)
else:
node = BlenderMaterials.__nodeLegoStandard(nodes, diffColour, 0, 5)
out = BlenderMaterials.__nodeOutput(nodes, 200, 0)
links.new(node.outputs['Shader'], out.inputs[0])
def __createCyclesEmission(nodes, links, diffColour, alpha, luminance):
"""Emission material for Cycles render engine."""
node = BlenderMaterials.__nodeLegoEmission(nodes, diffColour, luminance/100.0, 0, 5)
out = BlenderMaterials.__nodeOutput(nodes, 200, 0)
links.new(node.outputs['Shader'], out.inputs[0])
def __createCyclesChrome(nodes, links, diffColour):
"""Chrome material for Cycles render engine."""
node = BlenderMaterials.__nodeLegoChrome(nodes, diffColour, 0, 5)
out = BlenderMaterials.__nodeOutput(nodes, 200, 0)
links.new(node.outputs['Shader'], out.inputs[0])
def __createCyclesPearlescent(nodes, links, diffColour):
"""Pearlescent material for Cycles render engine."""
node = BlenderMaterials.__nodeLegoPearlescent(nodes, diffColour, 0, 5)
out = BlenderMaterials.__nodeOutput(nodes, 200, 0)
links.new(node.outputs['Shader'], out.inputs[0])
def __createCyclesMetal(nodes, links, diffColour):
"""Metal material for Cycles render engine."""
node = BlenderMaterials.__nodeLegoMetal(nodes, diffColour, 0, 5)
out = BlenderMaterials.__nodeOutput(nodes, 200, 0)
links.new(node.outputs['Shader'], out.inputs[0])
def __createCyclesGlitter(nodes, links, diffColour, glitterColour):
"""Glitter material for Cycles render engine."""
glitterColour = LegoColours.lightenRGBA(glitterColour, 0.5)
node = BlenderMaterials.__nodeLegoGlitter(nodes, diffColour, glitterColour, 0, 5)
out = BlenderMaterials.__nodeOutput(nodes, 200, 0)
links.new(node.outputs['Shader'], out.inputs[0])
def __createCyclesSpeckle(nodes, links, diffColour, speckleColour):
"""Speckle material for Cycles render engine."""
speckleColour = LegoColours.lightenRGBA(speckleColour, 0.5)
node = BlenderMaterials.__nodeLegoSpeckle(nodes, diffColour, speckleColour, 0, 5)
out = BlenderMaterials.__nodeOutput(nodes, 200, 0)
links.new(node.outputs['Shader'], out.inputs[0])
def __createCyclesRubber(nodes, links, diffColour, alpha):
"""Rubber material colours for Cycles render engine."""
out = BlenderMaterials.__nodeOutput(nodes, 200, 0)
if alpha < 1.0:
rubber = BlenderMaterials.__nodeLegoRubberTranslucent(nodes, diffColour, 0, 5)
else:
rubber = BlenderMaterials.__nodeLegoRubberSolid(nodes, diffColour, 0, 5)
links.new(rubber.outputs[0], out.inputs[0])
def __createCyclesMilkyWhite(nodes, links, diffColour):
"""Milky White material for Cycles render engine."""
node = BlenderMaterials.__nodeLegoMilkyWhite(nodes, diffColour, 0, 5)
out = BlenderMaterials.__nodeOutput(nodes, 200, 0)
links.new(node.outputs['Shader'], out.inputs[0])
def __is_int(s):
try:
int(s)
return True
except ValueError:
return False
def __getColourData(colourName):
"""Get the colour data associated with the colour name"""
# Try the LDraw defined colours
if BlenderMaterials.__is_int(colourName):
colourInt = int(colourName)
if colourInt in LegoColours.colours:
return LegoColours.colours[colourInt]
# Handle direct colours
# Direct colours are documented here: http://www.hassings.dk/l3/l3p.html
linearRGBA = LegoColours.hexStringToLinearRGBA(colourName)
if linearRGBA is None:
printWarningOnce("Could not decode {0} to a colour".format(colourName))
return None
return {
"name": colourName,
"colour": linearRGBA[0:3],
"alpha": linearRGBA[3],
"luminance": 0.0,
"material": "BASIC"
}
# **********************************************************************************
def getMaterial(colourName, isSlopeMaterial):
pureColourName = colourName
if isSlopeMaterial:
colourName = colourName + "_s"
# If it's already in the cache, use that
if (colourName in BlenderMaterials.__material_list):
result = BlenderMaterials.__material_list[colourName]
return result
# Create a name for the material based on the colour
if Options.instructionsLook:
blenderName = "MatInst_{0}".format(colourName)
elif Options.curvedWalls and not isSlopeMaterial:
blenderName = "Material_{0}_c".format(colourName)
else:
blenderName = "Material_{0}".format(colourName)
# If the name already exists in Blender, use that
if Options.overwriteExistingMaterials is False:
if blenderName in bpy.data.materials:
return bpy.data.materials[blenderName]
# Create new material
col = BlenderMaterials.__getColourData(pureColourName)
material = BlenderMaterials.__createNodeBasedMaterial(blenderName, col, isSlopeMaterial)
if material is None:
printWarningOnce("Could not create material for blenderName {0}".format(blenderName))
# Add material to cache
BlenderMaterials.__material_list[colourName] = material
return material
# **********************************************************************************
def clearCache():
BlenderMaterials.__material_list = {}
# **********************************************************************************
def addInputSocket(group, my_socket_type, myname):
if bpy.app.version >= (4, 0, 0):
if my_socket_type.endswith("FloatFactor"):
my_socket_type = my_socket_type[:-6]
elif my_socket_type.endswith("VectorDirection"):
my_socket_type = my_socket_type[:-9]
group.interface.new_socket(name=myname, in_out="INPUT", socket_type=my_socket_type)
else:
if my_socket_type.endswith("Vector"):
my_socket_type += "Direction"
group.inputs.new(my_socket_type, myname)
# **********************************************************************************
def addOutputSocket(group, my_socket_type, myname):
if bpy.app.version >= (4, 0, 0):
if my_socket_type.endswith("FloatFactor"):
my_socket_type = my_socket_type[:-6]
elif my_socket_type.endswith("VectorDirection"):
my_socket_type = my_socket_type[:-9]
group.interface.new_socket(name=myname, in_out="OUTPUT", socket_type=my_socket_type)
else:
if my_socket_type.endswith("Vector"):
my_socket_type += "Direction"
group.outputs.new(my_socket_type, myname)
# **********************************************************************************
def setDefaults(group, name, default_value, min_value, max_value):
if bpy.app.version >= (4, 0, 0):
group_inputs = group.nodes["Group Input"].outputs
group_inputs[name].default_value = default_value
# TODO: How to set min_value and max_value?
else:
group_inputs = group.inputs
group_inputs[name].default_value = default_value
group_inputs[name].min_value = min_value
group_inputs[name].max_value = max_value
# **********************************************************************************
def __createGroup(name, x1, y1, x2, y2, createShaderOutput):
group = bpy.data.node_groups.new(name, 'ShaderNodeTree')
# create input node
node_input = group.nodes.new('NodeGroupInput')
node_input.location = (x1,y1)
# create output node
node_output = group.nodes.new('NodeGroupOutput')
node_output.location = (x2,y2)
if createShaderOutput:
BlenderMaterials.addOutputSocket(group, 'NodeSocketShader', 'Shader')
return (group, node_input, node_output)
# **********************************************************************************
def __createBlenderDistanceToCenterNodeGroup():
if bpy.data.node_groups.get('Distance-To-Center') is None:
debugPrint("createBlenderDistanceToCenterNodeGroup #create")
# create a group
group, node_input, node_output = BlenderMaterials.__createGroup('Distance-To-Center', -930, 0, 240, 0, False)
BlenderMaterials.addOutputSocket(group, 'NodeSocketVectorDirection', 'Vector')
# create nodes
node_texture_coordinate = BlenderMaterials.__nodeTexCoord(group.nodes, -730, 0)
node_vector_subtraction1 = BlenderMaterials.__nodeVectorMath(group.nodes, 'SUBTRACT', -535, 0)
node_vector_subtraction1.inputs[1].default_value[0] = 0.5
node_vector_subtraction1.inputs[1].default_value[1] = 0.5
node_vector_subtraction1.inputs[1].default_value[2] = 0.5
node_normalize = BlenderMaterials.__nodeVectorMath(group.nodes, 'NORMALIZE', -535, -245)
node_dot_product = BlenderMaterials.__nodeVectorMath(group.nodes, 'DOT_PRODUCT', -340, -125)
node_multiply = group.nodes.new('ShaderNodeMixRGB')
node_multiply.blend_type = 'MULTIPLY'
node_multiply.inputs['Fac'].default_value = 1.0
node_multiply.location = -145, -125
node_vector_subtraction2 = BlenderMaterials.__nodeVectorMath(group.nodes, 'SUBTRACT', 40, 0)
# link nodes together
group.links.new(node_texture_coordinate.outputs['Generated'], node_vector_subtraction1.inputs[0])
group.links.new(node_texture_coordinate.outputs['Normal'], node_normalize.inputs[0])
group.links.new(node_vector_subtraction1.outputs['Vector'], node_dot_product.inputs[0])
group.links.new(node_normalize.outputs['Vector'], node_dot_product.inputs[1])
group.links.new(node_dot_product.outputs['Value'], node_multiply.inputs['Color1'])
group.links.new(node_normalize.outputs['Vector'], node_multiply.inputs['Color2'])
group.links.new(node_vector_subtraction1.outputs['Vector'], node_vector_subtraction2.inputs[0])
group.links.new(node_multiply.outputs['Color'], node_vector_subtraction2.inputs[1])
group.links.new(node_vector_subtraction2.outputs['Vector'], node_output.inputs[0])
# **********************************************************************************
def __createBlenderVectorElementPowerNodeGroup():
if bpy.data.node_groups.get('Vector-Element-Power') is None:
debugPrint("createBlenderVectorElementPowerNodeGroup #create")
# create a group
group, node_input, node_output = BlenderMaterials.__createGroup('Vector-Element-Power', -580, 0, 400, 0, False)
BlenderMaterials.addInputSocket(group, 'NodeSocketFloat', 'Exponent')
BlenderMaterials.addInputSocket(group, 'NodeSocketVectorDirection', 'Vector')
BlenderMaterials.addOutputSocket(group, 'NodeSocketVectorDirection', 'Vector')
# create nodes
node_separate_xyz = group.nodes.new('ShaderNodeSeparateXYZ')
node_separate_xyz.location = -385, -140
node_abs_x = BlenderMaterials.__nodeMath(group.nodes, 'ABSOLUTE', -180, 180)
node_abs_y = BlenderMaterials.__nodeMath(group.nodes, 'ABSOLUTE', -180, 0)
node_abs_z = BlenderMaterials.__nodeMath(group.nodes, 'ABSOLUTE', -180, -180)
node_power_x = BlenderMaterials.__nodeMath(group.nodes, 'POWER', 20, 180)
node_power_y = BlenderMaterials.__nodeMath(group.nodes, 'POWER', 20, 0)
node_power_z = BlenderMaterials.__nodeMath(group.nodes, 'POWER', 20, -180)
node_combine_xyz = group.nodes.new('ShaderNodeCombineXYZ')
node_combine_xyz.location = 215, 0
# link nodes together
group.links.new(node_input.outputs['Vector'], node_separate_xyz.inputs[0])
group.links.new(node_separate_xyz.outputs['X'], node_abs_x.inputs[0])
group.links.new(node_separate_xyz.outputs['Y'], node_abs_y.inputs[0])
group.links.new(node_separate_xyz.outputs['Z'], node_abs_z.inputs[0])
group.links.new(node_abs_x.outputs['Value'], node_power_x.inputs[0])
group.links.new(node_input.outputs['Exponent'], node_power_x.inputs[1])
group.links.new(node_abs_y.outputs['Value'], node_power_y.inputs[0])
group.links.new(node_input.outputs['Exponent'], node_power_y.inputs[1])
group.links.new(node_abs_z.outputs['Value'], node_power_z.inputs[0])
group.links.new(node_input.outputs['Exponent'], node_power_z.inputs[1])
group.links.new(node_power_x.outputs['Value'], node_combine_xyz.inputs['X'])
group.links.new(node_power_y.outputs['Value'], node_combine_xyz.inputs['Y'])
group.links.new(node_power_z.outputs['Value'], node_combine_xyz.inputs['Z'])
group.links.new(node_combine_xyz.outputs['Vector'], node_output.inputs[0])
# **********************************************************************************
def __createBlenderConvertToNormalsNodeGroup():
if bpy.data.node_groups.get('Convert-To-Normals') is None:
debugPrint("createBlenderConvertToNormalsNodeGroup #create")
# create a group
group, node_input, node_output = BlenderMaterials.__createGroup('Convert-To-Normals', -490, 0, 400, 0, False)
BlenderMaterials.addInputSocket(group, 'NodeSocketFloat', 'Vector Length')
BlenderMaterials.addInputSocket(group, 'NodeSocketFloat', 'Smoothing')
BlenderMaterials.addInputSocket(group, 'NodeSocketFloat', 'Strength')
BlenderMaterials.addInputSocket(group, 'NodeSocketVectorDirection', 'Normal')
BlenderMaterials.addOutputSocket(group, 'NodeSocketVectorDirection', 'Normal')
# create nodes
node_power = BlenderMaterials.__nodeMath(group.nodes, 'POWER', -290, 150)
node_colorramp = group.nodes.new('ShaderNodeValToRGB')
node_colorramp.color_ramp.color_mode = 'RGB'
node_colorramp.color_ramp.interpolation = 'EASE'
node_colorramp.color_ramp.elements[0].color = (1, 1, 1, 1)
node_colorramp.color_ramp.elements[1].color = (0, 0, 0, 1)
node_colorramp.color_ramp.elements[1].position = 0.45
node_colorramp.location = -95, 150
node_bump = group.nodes.new('ShaderNodeBump')
node_bump.inputs['Distance'].default_value = 0.02
node_bump.location = 200, 0
# link nodes together
group.links.new(node_input.outputs['Vector Length'], node_power.inputs[0])
group.links.new(node_input.outputs['Smoothing'], node_power.inputs[1])
group.links.new(node_power.outputs['Value'], node_colorramp.inputs[0])
group.links.new(node_input.outputs['Strength'], node_bump.inputs['Strength'])
group.links.new(node_colorramp.outputs['Color'], node_bump.inputs['Height'])
group.links.new(node_input.outputs['Normal'], node_bump.inputs['Normal'])
group.links.new(node_bump.outputs['Normal'], node_output.inputs[0])
# **********************************************************************************
def __createBlenderConcaveWallsNodeGroup():
if bpy.data.node_groups.get('Concave Walls') is None:
debugPrint("createBlenderConcaveWallsNodeGroup #create")
# create a group
group, node_input, node_output = BlenderMaterials.__createGroup('Concave Walls', -530, 0, 300, 0, False)
BlenderMaterials.addInputSocket(group, 'NodeSocketFloat', 'Strength')
BlenderMaterials.addInputSocket(group, 'NodeSocketVectorDirection', 'Normal')
BlenderMaterials.addOutputSocket(group, 'NodeSocketVectorDirection', 'Normal')
# create nodes
node_distance_to_center = group.nodes.new('ShaderNodeGroup')
node_distance_to_center.node_tree = bpy.data.node_groups['Distance-To-Center']
node_distance_to_center.location = (-340,105)
node_vector_elements_power = group.nodes.new('ShaderNodeGroup')
node_vector_elements_power.node_tree = bpy.data.node_groups['Vector-Element-Power']
node_vector_elements_power.location = (-120,105)
node_vector_elements_power.inputs['Exponent'].default_value = 4.0
node_convert_to_normals = group.nodes.new('ShaderNodeGroup')
node_convert_to_normals.node_tree = bpy.data.node_groups['Convert-To-Normals']
node_convert_to_normals.location = (90,0)
node_convert_to_normals.inputs['Strength'].default_value = 0.2
node_convert_to_normals.inputs['Smoothing'].default_value = 0.3
# link nodes together
group.links.new(node_distance_to_center.outputs['Vector'], node_vector_elements_power.inputs['Vector'])
group.links.new(node_vector_elements_power.outputs['Vector'], node_convert_to_normals.inputs['Vector Length'])
group.links.new(node_input.outputs['Strength'], node_convert_to_normals.inputs['Strength'])
group.links.new(node_input.outputs['Normal'], node_convert_to_normals.inputs['Normal'])
group.links.new(node_convert_to_normals.outputs['Normal'], node_output.inputs['Normal'])
# **********************************************************************************
def __createBlenderSlopeTextureNodeGroup():
global globalScaleFactor
if bpy.data.node_groups.get('Slope Texture') is None:
debugPrint("createBlenderSlopeTextureNodeGroup #create")
# create a group
group, node_input, node_output = BlenderMaterials.__createGroup('Slope Texture', -530, 0, 300, 0, False)
BlenderMaterials.addInputSocket(group, 'NodeSocketFloat', 'Strength')
BlenderMaterials.addInputSocket(group, 'NodeSocketVectorDirection', 'Normal')
BlenderMaterials.addOutputSocket(group, 'NodeSocketVectorDirection', 'Normal')
# create nodes
node_texture_coordinate = BlenderMaterials.__nodeTexCoord(group.nodes, -300, 240)
node_voronoi = BlenderMaterials.__nodeVoronoi(group.nodes, 3.0/globalScaleFactor, -100, 155)
node_bump = BlenderMaterials.__nodeBumpShader(group.nodes, 0.3, 0.08, 90, 50)
node_bump.invert = True
# link nodes together
group.links.new(node_texture_coordinate.outputs['Object'], node_voronoi.inputs['Vector'])
group.links.new(node_voronoi.outputs['Distance'], node_bump.inputs['Height'])
group.links.new(node_input.outputs['Strength'], node_bump.inputs['Strength'])
group.links.new(node_input.outputs['Normal'], node_bump.inputs['Normal'])
group.links.new(node_bump.outputs['Normal'], node_output.inputs['Normal'])
# **********************************************************************************
def __createBlenderFresnelNodeGroup():
if bpy.data.node_groups.get('PBR-Fresnel-Roughness') is None:
debugPrint("createBlenderFresnelNodeGroup #create")
# create a group
group, node_input, node_output = BlenderMaterials.__createGroup('PBR-Fresnel-Roughness', -530, 0, 300, 0, False)
BlenderMaterials.addInputSocket(group, 'NodeSocketFloatFactor', 'Roughness')
BlenderMaterials.addInputSocket(group, 'NodeSocketFloat', 'IOR')
BlenderMaterials.addInputSocket(group, 'NodeSocketVectorDirection', 'Normal')
BlenderMaterials.addOutputSocket(group, 'NodeSocketFloatFactor', 'Fresnel Factor')
# create nodes
node_fres = group.nodes.new('ShaderNodeFresnel')
node_fres.location = (110,0)
node_mix = group.nodes.new('ShaderNodeMixRGB')
node_mix.location = (-80,-75)
node_bump = group.nodes.new('ShaderNodeBump')
node_bump.location = (-320,-172)
# node_bump.hide = True
node_geom = group.nodes.new('ShaderNodeNewGeometry')
node_geom.location = (-320,-360)
# node_geom.hide = True
# link nodes together
group.links.new(node_input.outputs['Roughness'], node_mix.inputs['Fac']) # Input Roughness -> Mix Fac
group.links.new(node_input.outputs['IOR'], node_fres.inputs['IOR']) # Input IOR -> Fres IOR
group.links.new(node_input.outputs['Normal'], node_bump.inputs['Normal']) # Input Normal -> Bump Normal
group.links.new(node_bump.outputs['Normal'], node_mix.inputs['Color1']) # Bump Normal -> Mix Color1
group.links.new(node_geom.outputs['Incoming'], node_mix.inputs['Color2']) # Geom Incoming -> Mix Colour2
group.links.new(node_mix.outputs['Color'], node_fres.inputs['Normal']) # Mix Color -> Fres Normal
group.links.new(node_fres.outputs['Fac'], node_output.inputs['Fresnel Factor']) # Fres Fac -> Group Output Fresnel Factor
# **********************************************************************************
def __createBlenderReflectionNodeGroup():
if bpy.data.node_groups.get('PBR-Reflection') is None:
debugPrint("createBlenderReflectionNodeGroup #create")
# create a group
group, node_input, node_output = BlenderMaterials.__createGroup('PBR-Reflection', -530, 0, 300, 0, True)
BlenderMaterials.addInputSocket(group, 'NodeSocketShader', 'Shader')
BlenderMaterials.addInputSocket(group, 'NodeSocketFloatFactor', 'Roughness')
BlenderMaterials.addInputSocket(group, 'NodeSocketFloatFactor', 'Reflection')
BlenderMaterials.addInputSocket(group, 'NodeSocketFloat', 'IOR')
BlenderMaterials.addInputSocket(group, 'NodeSocketVectorDirection', 'Normal')
node_fresnel_roughness = group.nodes.new('ShaderNodeGroup')
node_fresnel_roughness.node_tree = bpy.data.node_groups['PBR-Fresnel-Roughness']
node_fresnel_roughness.location = (-290,145)
node_mixrgb = group.nodes.new('ShaderNodeMixRGB')
node_mixrgb.location = (-80,115)
node_mixrgb.inputs['Color2'].default_value = (0.0, 0.0, 0.0, 1.0)
node_mix_shader = group.nodes.new('ShaderNodeMixShader')
node_mix_shader.location = (100,0)
node_glossy = group.nodes.new('ShaderNodeBsdfGlossy')
node_glossy.inputs['Color'].default_value = (1.0, 1.0, 1.0, 1.0)
node_glossy.location = (-290,-95)
# link nodes together
group.links.new(node_input.outputs['Shader'], node_mix_shader.inputs[1])
group.links.new(node_input.outputs['Roughness'], node_fresnel_roughness.inputs['Roughness'])
group.links.new(node_input.outputs['Roughness'], node_glossy.inputs['Roughness'])
group.links.new(node_input.outputs['Reflection'], node_mixrgb.inputs['Color1'])
group.links.new(node_input.outputs['IOR'], node_fresnel_roughness.inputs['IOR'])
group.links.new(node_input.outputs['Normal'], node_fresnel_roughness.inputs['Normal'])
group.links.new(node_input.outputs['Normal'], node_glossy.inputs['Normal'])
group.links.new(node_fresnel_roughness.outputs[0], node_mixrgb.inputs[0])
group.links.new(node_mixrgb.outputs[0], node_mix_shader.inputs[0])
group.links.new(node_glossy.outputs[0], node_mix_shader.inputs[2])
group.links.new(node_mix_shader.outputs[0], node_output.inputs['Shader'])
# **********************************************************************************
def __createBlenderDielectricNodeGroup():
if bpy.data.node_groups.get('PBR-Dielectric') is None:
debugPrint("createBlenderDielectricNodeGroup #create")
# create a group
group, node_input, node_output = BlenderMaterials.__createGroup('PBR-Dielectric', -530, 70, 500, 0, True)
BlenderMaterials.addInputSocket(group, 'NodeSocketColor','Color')
BlenderMaterials.addInputSocket(group, 'NodeSocketFloatFactor','Roughness')
BlenderMaterials.addInputSocket(group, 'NodeSocketFloatFactor','Reflection')
BlenderMaterials.addInputSocket(group, 'NodeSocketFloatFactor','Transparency')
BlenderMaterials.addInputSocket(group, 'NodeSocketFloat','IOR')
BlenderMaterials.addInputSocket(group, 'NodeSocketVectorDirection','Normal')
BlenderMaterials.setDefaults(group, 'IOR', 1.46, 0.0, 100.0)
BlenderMaterials.setDefaults(group, 'Roughness', 0.2, 0.0, 1.0)
BlenderMaterials.setDefaults(group, 'Reflection', 0.1, 0.0, 1.0)
BlenderMaterials.setDefaults(group, 'Transparency', 0.0, 0.0, 1.0)
node_diffuse = group.nodes.new('ShaderNodeBsdfDiffuse')
node_diffuse.location = (-110,145)
node_reflection = group.nodes.new('ShaderNodeGroup')
node_reflection.node_tree = bpy.data.node_groups['PBR-Reflection']
node_reflection.location = (100,115)
node_power = BlenderMaterials.__nodeMath(group.nodes, 'POWER', -330, -105)
node_power.inputs[1].default_value = 2.0
node_glass = group.nodes.new('ShaderNodeBsdfGlass')
node_glass.location = (100,-105)
node_mix_shader = group.nodes.new('ShaderNodeMixShader')
node_mix_shader.location = (300,5)
# link nodes together
group.links.new(node_input.outputs['Color'], node_diffuse.inputs['Color'])
group.links.new(node_input.outputs['Roughness'], node_power.inputs[0])
group.links.new(node_input.outputs['Reflection'], node_reflection.inputs['Reflection'])
group.links.new(node_input.outputs['IOR'], node_reflection.inputs['IOR'])
group.links.new(node_input.outputs['Normal'], node_diffuse.inputs['Normal'])
group.links.new(node_input.outputs['Normal'], node_reflection.inputs['Normal'])
group.links.new(node_power.outputs[0], node_diffuse.inputs['Roughness'])
group.links.new(node_power.outputs[0], node_reflection.inputs['Roughness'])
group.links.new(node_diffuse.outputs[0], node_reflection.inputs['Shader'])
group.links.new(node_reflection.outputs['Shader'], node_mix_shader.inputs['Shader'])
group.links.new(node_input.outputs['Color'], node_glass.inputs['Color'])
group.links.new(node_input.outputs['IOR'], node_glass.inputs['IOR'])
group.links.new(node_input.outputs['Normal'], node_glass.inputs['Normal'])
group.links.new(node_power.outputs[0], node_glass.inputs['Roughness'])
group.links.new(node_input.outputs['Transparency'], node_mix_shader.inputs[0])
group.links.new(node_glass.outputs[0], node_mix_shader.inputs[2])
group.links.new(node_mix_shader.outputs['Shader'], node_output.inputs['Shader'])
# **********************************************************************************
def __getSubsurfaceColor(node):
if 'Subsurface Color' in node.inputs:
# Blender 3
return node.inputs['Subsurface Color']
# Blender 4 - Subsurface Colour has been removed, so just use the base colour instead
return node.inputs['Base Color']
# **********************************************************************************
def __createBlenderLegoStandardNodeGroup():
groupName = BlenderMaterials.__getGroupName('Lego Standard')
if bpy.data.node_groups.get(groupName) is None:
debugPrint("createBlenderLegoStandardNodeGroup #create")
# create a group
group, node_input, node_output = BlenderMaterials.__createGroup(groupName, -250, 0, 300, 0, True)
BlenderMaterials.addInputSocket(group,'NodeSocketColor','Color')
BlenderMaterials.addInputSocket(group,'NodeSocketVectorDirection','Normal')
if Options.instructionsLook:
node_emission = BlenderMaterials.__nodeEmission(group.nodes, 0, 0)
group.links.new(node_input.outputs['Color'], node_emission.inputs['Color'])
group.links.new(node_emission.outputs['Emission'], node_output.inputs['Shader'])
else:
if BlenderMaterials.usePrincipledShader:
node_main = BlenderMaterials.__nodePrincipled(group.nodes, 5 * globalScaleFactor, 0.05, 0.0, 0.1, 0.0, 0.0, 1.45, 0.0, 0, 0)
output_name = 'BSDF'
color_name = 'Base Color'
group.links.new(node_input.outputs['Color'], BlenderMaterials.__getSubsurfaceColor(node_main))
else:
node_main = BlenderMaterials.__nodeDielectric(group.nodes, 0.2, 0.1, 0.0, 1.46, 0, 0)
output_name = 'Shader'
color_name = 'Color'
# link nodes together
group.links.new(node_input.outputs['Color'], node_main.inputs[color_name])
group.links.new(node_input.outputs['Normal'], node_main.inputs['Normal'])
group.links.new(node_main.outputs[output_name], node_output.inputs['Shader'])
# **********************************************************************************
def __createBlenderLegoTransparentNodeGroup():
groupName = BlenderMaterials.__getGroupName('Lego Transparent')
if bpy.data.node_groups.get(groupName) is None:
debugPrint("createBlenderLegoTransparentNodeGroup #create")
# create a group
group, node_input, node_output = BlenderMaterials.__createGroup(groupName, -250, 0, 300, 0, True)
BlenderMaterials.addInputSocket(group,'NodeSocketColor','Color')
BlenderMaterials.addInputSocket(group,'NodeSocketVectorDirection','Normal')
if Options.instructionsLook:
node_emission = BlenderMaterials.__nodeEmission(group.nodes, 0, 0)
node_transparent = BlenderMaterials.__nodeTransparent(group.nodes, 0, 100)
node_mix1 = BlenderMaterials.__nodeMix(group.nodes, 0.5, 400, 100)
node_light = BlenderMaterials.__nodeLightPath(group.nodes, 200, 400)
node_less = BlenderMaterials.__nodeMath(group.nodes, 'LESS_THAN', 400, 400)
node_mix2 = BlenderMaterials.__nodeMix(group.nodes, 0.5, 600, 300)
node_output.location = (800,0)
group.links.new(node_input.outputs['Color'], node_emission.inputs['Color'])
group.links.new(node_transparent.outputs[0], node_mix1.inputs[1])
group.links.new(node_emission.outputs['Emission'], node_mix1.inputs[2])
group.links.new(node_transparent.outputs[0], node_mix2.inputs[1])
group.links.new(node_mix1.outputs[0], node_mix2.inputs[2])
group.links.new(node_light.outputs['Transparent Depth'], node_less.inputs[0])
group.links.new(node_less.outputs[0], node_mix2.inputs['Fac'])
group.links.new(node_mix2.outputs[0], node_output.inputs['Shader'])
else:
if BlenderMaterials.usePrincipledShader:
node_principled = BlenderMaterials.__nodePrincipled(group.nodes, 0.0, 0.0, 0.0, 0.05, 0.0, 0.0, 1.585, 1.0, 45, 340)
# link nodes together
group.links.new(node_input.outputs['Color'], node_principled.inputs['Base Color'])
group.links.new(node_input.outputs['Normal'], node_principled.inputs['Normal'])
group.links.new(node_principled.outputs['BSDF'], node_output.inputs['Shader'])
else:
node_main = BlenderMaterials.__nodeDielectric(group.nodes, 0.15, 0.1, 0.97, 1.46, 0, 0)
# link nodes together
group.links.new(node_input.outputs['Color'], node_main.inputs['Color'])
group.links.new(node_input.outputs['Normal'], node_main.inputs['Normal'])
group.links.new(node_main.outputs['Shader'], node_output.inputs['Shader'])
# **********************************************************************************
def __createBlenderLegoTransparentFluorescentNodeGroup():
groupName = BlenderMaterials.__getGroupName('Lego Transparent Fluorescent')
if bpy.data.node_groups.get(groupName) is None:
debugPrint("createBlenderLegoTransparentFluorescentNodeGroup #create")
# create a group
group, node_input, node_output = BlenderMaterials.__createGroup(groupName, -250, 0, 300, 0, True)
BlenderMaterials.addInputSocket(group,'NodeSocketColor','Color')
BlenderMaterials.addInputSocket(group,'NodeSocketVectorDirection','Normal')
if Options.instructionsLook:
node_emission = BlenderMaterials.__nodeEmission(group.nodes, 0, 0)
node_transparent = BlenderMaterials.__nodeTransparent(group.nodes, 0, 100)
node_mix1 = BlenderMaterials.__nodeMix(group.nodes, 0.5, 400, 100)
node_light = BlenderMaterials.__nodeLightPath(group.nodes, 200, 400)
node_less = BlenderMaterials.__nodeMath(group.nodes, 'LESS_THAN', 400, 400)
node_mix2 = BlenderMaterials.__nodeMix(group.nodes, 0.5, 600, 300)
node_output.location = (800,0)
group.links.new(node_input.outputs['Color'], node_emission.inputs['Color'])
group.links.new(node_transparent.outputs[0], node_mix1.inputs[1])
group.links.new(node_emission.outputs['Emission'], node_mix1.inputs[2])
group.links.new(node_transparent.outputs[0], node_mix2.inputs[1])
group.links.new(node_mix1.outputs[0], node_mix2.inputs[2])
group.links.new(node_light.outputs['Transparent Depth'], node_less.inputs[0])
group.links.new(node_less.outputs[0], node_mix2.inputs['Fac'])
group.links.new(node_mix2.outputs[0], node_output.inputs['Shader'])
else:
if BlenderMaterials.usePrincipledShader:
node_principled = BlenderMaterials.__nodePrincipled(group.nodes, 0.0, 0.0, 0.0, 0.05, 0.0, 0.0, 1.585, 1.0, 45, 340)
node_emission = BlenderMaterials.__nodeEmission(group.nodes, 45, -160)
node_mix = BlenderMaterials.__nodeMix(group.nodes, 0.03, 300, 290)
node_output.location = 500, 290
# link nodes together
group.links.new(node_input.outputs['Color'], node_principled.inputs['Base Color'])
group.links.new(node_input.outputs['Color'], node_emission.inputs['Color'])
group.links.new(node_input.outputs['Normal'], node_principled.inputs['Normal'])
group.links.new(node_principled.outputs['BSDF'], node_mix.inputs[1])
group.links.new(node_emission.outputs['Emission'], node_mix.inputs[2])
group.links.new(node_mix.outputs[0], node_output.inputs['Shader'])
else:
node_main = BlenderMaterials.__nodeDielectric(group.nodes, 0.15, 0.1, 0.97, 1.46, 0, 0)
# link nodes together
group.links.new(node_input.outputs['Color'], node_main.inputs['Color'])
group.links.new(node_input.outputs['Normal'], node_main.inputs['Normal'])
group.links.new(node_main.outputs['Shader'], node_output.inputs['Shader'])
# **********************************************************************************
def __createBlenderLegoRubberNodeGroup():
groupName = BlenderMaterials.__getGroupName('Lego Rubber Solid')
if bpy.data.node_groups.get(groupName) is None:
debugPrint("createBlenderLegoRubberNodeGroup #create")
# create a group
group, node_input, node_output = BlenderMaterials.__createGroup(groupName, 45-950, 340-50, 45+200, 340-5, True)
BlenderMaterials.addInputSocket(group,'NodeSocketColor','Color')
BlenderMaterials.addInputSocket(group,'NodeSocketVectorDirection','Normal')
if BlenderMaterials.usePrincipledShader:
node_noise = BlenderMaterials.__nodeNoiseTexture(group.nodes, 250, 2, 0.0, 45-770, 340-200)
node_bump1 = BlenderMaterials.__nodeBumpShader(group.nodes, 1.0, 0.3, 45-366, 340-200)
node_bump2 = BlenderMaterials.__nodeBumpShader(group.nodes, 1.0, 0.1, 45-184, 340-115)
node_subtract = BlenderMaterials.__nodeMath(group.nodes, 'SUBTRACT', 45-570, 340-216)
node_principled = BlenderMaterials.__nodePrincipled(group.nodes, 0.0, 0.0, 0.0, 0.4, 0.03, 0.0, 1.45, 0.0, 45, 340)
node_subtract.inputs[1].default_value = 0.4
group.links.new(node_input.outputs['Color'], node_principled.inputs['Base Color'])
group.links.new(node_principled.outputs['BSDF'], node_output.inputs[0])
group.links.new(node_noise.outputs['Color'], node_subtract.inputs[0])
group.links.new(node_subtract.outputs[0], node_bump1.inputs['Height'])
group.links.new(node_bump1.outputs['Normal'], node_bump2.inputs['Normal'])
group.links.new(node_bump2.outputs['Normal'], node_principled.inputs['Normal'])
else:
node_dielectric = BlenderMaterials.__nodeDielectric(group.nodes, 0.5, 0.07, 0.0, 1.52, 0, 0)
# link nodes together
group.links.new(node_input.outputs['Color'], node_dielectric.inputs['Color'])
group.links.new(node_input.outputs['Normal'], node_dielectric.inputs['Normal'])
group.links.new(node_dielectric.outputs['Shader'], node_output.inputs['Shader'])
# **********************************************************************************
def __createBlenderLegoRubberTranslucentNodeGroup():
groupName = BlenderMaterials.__getGroupName('Lego Rubber Translucent')
if bpy.data.node_groups.get(groupName) is None:
debugPrint("createBlenderLegoRubberTranslucentNodeGroup #create")
# create a group
group, node_input, node_output = BlenderMaterials.__createGroup(groupName, -250, 0, 250, 0, True)
BlenderMaterials.addInputSocket(group,'NodeSocketColor','Color')
BlenderMaterials.addInputSocket(group,'NodeSocketVectorDirection','Normal')
if BlenderMaterials.usePrincipledShader:
node_noise = BlenderMaterials.__nodeNoiseTexture(group.nodes, 250, 2, 0.0, 45-770, 340-200)
node_bump1 = BlenderMaterials.__nodeBumpShader(group.nodes, 1.0, 0.3, 45-366, 340-200)
node_bump2 = BlenderMaterials.__nodeBumpShader(group.nodes, 1.0, 0.1, 45-184, 340-115)
node_subtract = BlenderMaterials.__nodeMath(group.nodes, 'SUBTRACT', 45-570, 340-216)
node_principled = BlenderMaterials.__nodePrincipled(group.nodes, 0.0, 0.0, 0.0, 0.4, 0.03, 0.0, 1.45, 0.0, 45, 340)
node_mix = BlenderMaterials.__nodeMix(group.nodes, 0.8, 300, 290)
node_refraction = BlenderMaterials.__nodeRefraction(group.nodes, 0.0, 1.45, 290-242, 154-330)
node_input.location = -320, 290
node_output.location = 530, 285
node_subtract.inputs[1].default_value = 0.4
group.links.new(node_input.outputs['Normal'], node_refraction.inputs['Normal'])
group.links.new(node_refraction.outputs[0], node_mix.inputs[2])
group.links.new(node_principled.outputs[0], node_mix.inputs[1])
group.links.new(node_mix.outputs[0], node_output.inputs[0])
group.links.new(node_input.outputs['Color'], node_principled.inputs['Base Color'])
group.links.new(node_noise.outputs['Color'], node_subtract.inputs[0])
group.links.new(node_subtract.outputs[0], node_bump1.inputs['Height'])
group.links.new(node_bump1.outputs['Normal'], node_bump2.inputs['Normal'])
group.links.new(node_bump2.outputs['Normal'], node_principled.inputs['Normal'])
group.links.new(node_mix.outputs[0], node_output.inputs[0])
else:
node_dielectric = BlenderMaterials.__nodeDielectric(group.nodes, 0.15, 0.1, 0.97, 1.46, 0, 0)
# link nodes together
group.links.new(node_input.outputs['Color'], node_dielectric.inputs['Color'])
group.links.new(node_input.outputs['Normal'], node_dielectric.inputs['Normal'])
group.links.new(node_dielectric.outputs['Shader'], node_output.inputs['Shader'])
# **************************************************************************************
def __createBlenderLegoEmissionNodeGroup():
groupName = BlenderMaterials.__getGroupName('Lego Emission')
if bpy.data.node_groups.get(groupName) is None:
debugPrint("createBlenderLegoEmissionNodeGroup #create")
# create a group
group, node_input, node_output = BlenderMaterials.__createGroup(groupName, -450, 90, 250, 0, True)
BlenderMaterials.addInputSocket(group,'NodeSocketColor','Color')
BlenderMaterials.addInputSocket(group,'NodeSocketFloatFactor','Luminance')
BlenderMaterials.addInputSocket(group,'NodeSocketVectorDirection','Normal')
node_emit = BlenderMaterials.__nodeEmission(group.nodes, -242, -123)
node_mix = BlenderMaterials.__nodeMix(group.nodes, 0.5, 0, 90)
if BlenderMaterials.usePrincipledShader:
node_main = BlenderMaterials.__nodePrincipled(group.nodes, 1.0, 0.05, 0.0, 0.5, 0.0, 0.03, 1.45, 0.0, -242, 154+240)
group.links.new(node_input.outputs['Color'], BlenderMaterials.__getSubsurfaceColor(node_main))
group.links.new(node_input.outputs['Color'], node_emit.inputs['Color'])
main_colour = 'Base Color'
else:
node_main = BlenderMaterials.__nodeTranslucent(group.nodes, -242, 154)
main_colour = 'Color'
# link nodes together
group.links.new(node_input.outputs['Color'], node_main.inputs[main_colour])
group.links.new(node_input.outputs['Normal'], node_main.inputs['Normal'])
group.links.new(node_input.outputs['Luminance'], node_mix.inputs[0])
group.links.new(node_main.outputs[0], node_mix.inputs[1])
group.links.new(node_emit.outputs[0], node_mix.inputs[2])
group.links.new(node_mix.outputs[0], node_output.inputs[0])
# **********************************************************************************
def __createBlenderLegoChromeNodeGroup():
groupName = BlenderMaterials.__getGroupName('Lego Chrome')
if bpy.data.node_groups.get(groupName) is None:
debugPrint("createBlenderLegoChromeNodeGroup #create")
# create a group
group, node_input, node_output = BlenderMaterials.__createGroup(groupName, -450, 90, 250, 0, True)
BlenderMaterials.addInputSocket(group,'NodeSocketColor','Color')
BlenderMaterials.addInputSocket(group,'NodeSocketVectorDirection','Normal')
if BlenderMaterials.usePrincipledShader:
node_hsv = BlenderMaterials.__nodeHSV(group.nodes, 0.5, 0.9, 2.0, -90, 0)
node_principled = BlenderMaterials.__nodePrincipled(group.nodes, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 2.4, 0.0, 100, 0)
node_output.location = (575, -140)
# link nodes together
group.links.new(node_input.outputs['Color'], node_hsv.inputs['Color'])
group.links.new(node_input.outputs['Normal'], node_principled.inputs['Normal'])
group.links.new(node_hsv.outputs['Color'], node_principled.inputs['Base Color'])
group.links.new(node_principled.outputs['BSDF'], node_output.inputs[0])
else:
node_glossyOne = BlenderMaterials.__nodeGlossy(group.nodes, (1,1,1,1), 0.03, 'GGX', -242, 154)
node_glossyTwo = BlenderMaterials.__nodeGlossy(group.nodes, (1.0, 1.0, 1.0, 1.0), 0.03, 'BECKMANN', -242, -23)
node_mix = BlenderMaterials.__nodeMix(group.nodes, 0.01, 0, 90)
# link nodes together
group.links.new(node_input.outputs['Color'], node_glossyOne.inputs['Color'])
group.links.new(node_input.outputs['Normal'], node_glossyOne.inputs['Normal'])
group.links.new(node_input.outputs['Normal'], node_glossyTwo.inputs['Normal'])
group.links.new(node_glossyOne.outputs[0], node_mix.inputs[1])
group.links.new(node_glossyTwo.outputs[0], node_mix.inputs[2])
group.links.new(node_mix.outputs[0], node_output.inputs[0])
# **********************************************************************************
def __createBlenderLegoPearlescentNodeGroup():
groupName = BlenderMaterials.__getGroupName('Lego Pearlescent')
if bpy.data.node_groups.get(groupName) is None:
debugPrint("createBlenderLegoPearlescentNodeGroup #create")
# create a group
group, node_input, node_output = BlenderMaterials.__createGroup(groupName, -450, 90, 630, 95, True)
BlenderMaterials.addInputSocket(group,'NodeSocketColor','Color')
BlenderMaterials.addInputSocket(group,'NodeSocketVectorDirection','Normal')
if BlenderMaterials.usePrincipledShader:
node_principled = BlenderMaterials.__nodePrincipled(group.nodes, 1.0, 0.25, 0.5, 0.2, 1.0, 0.2, 1.6, 0.0, 310, 95)
node_sep_hsv = BlenderMaterials.__nodeSeparateHSV(group.nodes, -240, 75)
node_multiply = BlenderMaterials.__nodeMath(group.nodes, 'MULTIPLY', -60, 0)
node_com_hsv = BlenderMaterials.__nodeCombineHSV(group.nodes, 110, 95)
node_tex_coord = BlenderMaterials.__nodeTexCoord(group.nodes, -730, -223)
node_tex_wave = BlenderMaterials.__nodeTexWave(group.nodes, 'BANDS', 'SIN', 0.5, 40, 1, 1.5, -520, -190)
node_color_ramp = BlenderMaterials.__nodeColorRamp(group.nodes, 0.329, (0.89, 0.89, 0.89, 1), 0.820, (1, 1, 1, 1), -340, -70)
element = node_color_ramp.color_ramp.elements.new(1.0)
element.color = (1.118, 1.118, 1.118, 1)
# link nodes together
group.links.new(node_input.outputs['Color'], node_sep_hsv.inputs['Color'])
group.links.new(node_input.outputs['Normal'], node_principled.inputs['Normal'])
group.links.new(node_sep_hsv.outputs['H'], node_com_hsv.inputs['H'])
group.links.new(node_sep_hsv.outputs['S'], node_com_hsv.inputs['S'])
group.links.new(node_sep_hsv.outputs['V'], node_multiply.inputs[0])
group.links.new(node_com_hsv.outputs['Color'], node_principled.inputs['Base Color'])
group.links.new(node_com_hsv.outputs['Color'], BlenderMaterials.__getSubsurfaceColor(node_principled))
group.links.new(node_tex_coord.outputs['Object'], node_tex_wave.inputs['Vector'])
group.links.new(node_tex_wave.outputs['Fac'], node_color_ramp.inputs['Fac'])
group.links.new(node_color_ramp.outputs['Color'], node_multiply.inputs[1])
group.links.new(node_multiply.outputs[0], node_com_hsv.inputs['V'])
group.links.new(node_principled.outputs['BSDF'], node_output.inputs[0])
else:
node_diffuse = BlenderMaterials.__nodeDiffuse(group.nodes, 0.0, -242, -23)
node_glossy = BlenderMaterials.__nodeGlossy(group.nodes, (1,1,1,1), 0.05, 'BECKMANN', -242, 154)
node_mix = BlenderMaterials.__nodeMix(group.nodes, 0.4, 0, 90)
# link nodes together
group.links.new(node_input.outputs['Color'], node_diffuse.inputs['Color'])
group.links.new(node_input.outputs['Color'], node_glossy.inputs['Color'])
group.links.new(node_input.outputs['Normal'], node_diffuse.inputs['Normal'])
group.links.new(node_input.outputs['Normal'], node_glossy.inputs['Normal'])
group.links.new(node_glossy.outputs[0], node_mix.inputs[1])
group.links.new(node_diffuse.outputs[0], node_mix.inputs[2])
group.links.new(node_mix.outputs[0], node_output.inputs[0])
# **********************************************************************************
def __createBlenderLegoMetalNodeGroup():
groupName = BlenderMaterials.__getGroupName('Lego Metal')
if bpy.data.node_groups.get(groupName) is None:
debugPrint("createBlenderLegoMetalNodeGroup #create")
# create a group
group, node_input, node_output = BlenderMaterials.__createGroup(groupName, -450, 90, 250, 0, True)
BlenderMaterials.addInputSocket(group,'NodeSocketColor','Color')
BlenderMaterials.addInputSocket(group,'NodeSocketVectorDirection','Normal')
if BlenderMaterials.usePrincipledShader:
node_principled = BlenderMaterials.__nodePrincipled(group.nodes, 0.0, 0.0, 0.8, 0.2, 0.0, 0.03, 1.45, 0.0, 310, 95)
group.links.new(node_input.outputs['Color'], node_principled.inputs['Base Color'])
group.links.new(node_input.outputs['Normal'], node_principled.inputs['Normal'])
group.links.new(node_principled.outputs[0], node_output.inputs['Shader'])
else:
node_dielectric = BlenderMaterials.__nodeDielectric(group.nodes, 0.05, 0.2, 0.0, 1.46, -242, 0)
node_glossy = BlenderMaterials.__nodeGlossy(group.nodes, (1,1,1,1), 0.2, 'BECKMANN', -242, 154)
node_mix = BlenderMaterials.__nodeMix(group.nodes, 0.4, 0, 90)
# link nodes together
group.links.new(node_input.outputs['Color'], node_glossy.inputs['Color'])
group.links.new(node_input.outputs['Color'], node_dielectric.inputs['Color'])
group.links.new(node_input.outputs['Normal'], node_glossy.inputs['Normal'])
group.links.new(node_input.outputs['Normal'], node_dielectric.inputs['Normal'])
group.links.new(node_glossy.outputs[0], node_mix.inputs[1])
group.links.new(node_dielectric.outputs[0], node_mix.inputs[2])
group.links.new(node_mix.outputs[0], node_output.inputs[0])
# **********************************************************************************
def __createBlenderLegoGlitterNodeGroup():
groupName = BlenderMaterials.__getGroupName('Lego Glitter')
if bpy.data.node_groups.get(groupName) is None:
debugPrint("createBlenderLegoGlitterNodeGroup #create")
# create a group
group, node_input, node_output = BlenderMaterials.__createGroup(groupName, -450, 0, 410, 0, True)
BlenderMaterials.addInputSocket(group,'NodeSocketColor','Color')
BlenderMaterials.addInputSocket(group,'NodeSocketColor','Glitter Color')
BlenderMaterials.addInputSocket(group,'NodeSocketVectorDirection','Normal')
if BlenderMaterials.usePrincipledShader:
node_voronoi = BlenderMaterials.__nodeVoronoi(group.nodes, 100, -222, 310)
node_gamma = BlenderMaterials.__nodeGamma(group.nodes, 50, 0, 200)
node_mix = BlenderMaterials.__nodeMix(group.nodes, 0.05, 210, 90+25)
node_principled1 = BlenderMaterials.__nodePrincipled(group.nodes, 0.0, 0.0, 0.0, 0.2, 0.0, 0.03, 1.585, 1.0, 45-270, 340-210)
node_principled2 = BlenderMaterials.__nodePrincipled(group.nodes, 0.0, 0.0, 0.0, 0.5, 0.0, 0.03, 1.45, 0.0, 45-270, 340-750)
group.links.new(node_input.outputs['Color'], node_principled1.inputs['Base Color'])
group.links.new(node_input.outputs['Glitter Color'], node_principled2.inputs['Base Color'])
group.links.new(node_input.outputs['Normal'], node_principled1.inputs['Normal'])
group.links.new(node_input.outputs['Normal'], node_principled2.inputs['Normal'])
group.links.new(node_voronoi.outputs['Color'], node_gamma.inputs['Color'])
group.links.new(node_gamma.outputs[0], node_mix.inputs[0])
group.links.new(node_principled1.outputs['BSDF'], node_mix.inputs[1])
group.links.new(node_principled2.outputs['BSDF'], node_mix.inputs[2])
group.links.new(node_mix.outputs[0], node_output.inputs[0])
else:
node_glass = BlenderMaterials.__nodeGlass(group.nodes, 0.05, 1.46, 'BECKMANN', -242, 154)
node_glossy = BlenderMaterials.__nodeGlossy(group.nodes, (1,1,1,1), 0.05, 'BECKMANN', -242, -23)
node_diffuse = BlenderMaterials.__nodeDiffuse(group.nodes, 0.0, -12, -49)
node_voronoi = BlenderMaterials.__nodeVoronoi(group.nodes, 100, -232, 310)
node_gamma = BlenderMaterials.__nodeGamma(group.nodes, 50, 0, 200)
node_mixOne = BlenderMaterials.__nodeMix(group.nodes, 0.05, 0, 90)
node_mixTwo = BlenderMaterials.__nodeMix(group.nodes, 0.5, 200, 90)
# link nodes together
group.links.new(node_input.outputs['Color'], node_glass.inputs['Color'])
group.links.new(node_input.outputs['Glitter Color'], node_diffuse.inputs['Color'])
group.links.new(node_input.outputs['Normal'], node_glass.inputs['Normal'])
group.links.new(node_input.outputs['Normal'], node_glossy.inputs['Normal'])
group.links.new(node_input.outputs['Normal'], node_diffuse.inputs['Normal'])
group.links.new(node_glass.outputs[0], node_mixOne.inputs[1])
group.links.new(node_glossy.outputs[0], node_mixOne.inputs[2])
group.links.new(node_voronoi.outputs[0], node_gamma.inputs[0])
group.links.new(node_gamma.outputs[0], node_mixTwo.inputs[0])
group.links.new(node_mixOne.outputs[0], node_mixTwo.inputs[1])
group.links.new(node_diffuse.outputs[0], node_mixTwo.inputs[2])
group.links.new(node_mixTwo.outputs[0], node_output.inputs[0])
# **********************************************************************************
def __createBlenderLegoSpeckleNodeGroup():
groupName = BlenderMaterials.__getGroupName('Lego Speckle')
if bpy.data.node_groups.get(groupName) is None:
debugPrint("createBlenderLegoSpeckleNodeGroup #create")
# create a group
group, node_input, node_output = BlenderMaterials.__createGroup(groupName, -450, 0, 410, 0, True)
BlenderMaterials.addInputSocket(group,'NodeSocketColor','Color')
BlenderMaterials.addInputSocket(group,'NodeSocketColor','Speckle Color')
BlenderMaterials.addInputSocket(group,'NodeSocketVectorDirection','Normal')
if BlenderMaterials.usePrincipledShader:
node_voronoi = BlenderMaterials.__nodeVoronoi(group.nodes, 50, -222, 310)
node_gamma = BlenderMaterials.__nodeGamma(group.nodes, 3.5, 0, 200)
node_mix = BlenderMaterials.__nodeMix(group.nodes, 0.05, 210, 90+25)
node_principled1 = BlenderMaterials.__nodePrincipled(group.nodes, 0.0, 0.0, 0.0, 0.1, 0.0, 0.03, 1.45, 0.0, 45-270, 340-210)
node_principled2 = BlenderMaterials.__nodePrincipled(group.nodes, 0.0, 0.0, 1.0, 0.5, 0.0, 0.03, 1.45, 0.0, 45-270, 340-750)
group.links.new(node_input.outputs['Color'], node_principled1.inputs['Base Color'])
group.links.new(node_input.outputs['Speckle Color'], node_principled2.inputs['Base Color'])
group.links.new(node_input.outputs['Normal'], node_principled1.inputs['Normal'])
group.links.new(node_input.outputs['Normal'], node_principled2.inputs['Normal'])
group.links.new(node_voronoi.outputs['Color'], node_gamma.inputs['Color'])
group.links.new(node_gamma.outputs[0], node_mix.inputs[0])
group.links.new(node_principled1.outputs['BSDF'], node_mix.inputs[1])
group.links.new(node_principled2.outputs['BSDF'], node_mix.inputs[2])
group.links.new(node_mix.outputs[0], node_output.inputs[0])
else:
node_diffuseOne = BlenderMaterials.__nodeDiffuse(group.nodes, 0.0, -242, 131)
node_glossy = BlenderMaterials.__nodeGlossy(group.nodes, (0.333, 0.333, 0.333, 1.0), 0.2, 'BECKMANN', -242, -23)
node_diffuseTwo = BlenderMaterials.__nodeDiffuse(group.nodes, 0.0, -12, -49)
node_voronoi = BlenderMaterials.__nodeVoronoi(group.nodes, 100, -232, 310)
node_gamma = BlenderMaterials.__nodeGamma(group.nodes, 20, 0, 200)
node_mixOne = BlenderMaterials.__nodeMix(group.nodes, 0.2, 0, 90)
node_mixTwo = BlenderMaterials.__nodeMix(group.nodes, 0.5, 200, 90)
# link nodes together
group.links.new(node_input.outputs['Color'], node_diffuseOne.inputs['Color'])
group.links.new(node_input.outputs['Speckle Color'], node_diffuseTwo.inputs['Color'])
group.links.new(node_input.outputs['Normal'], node_diffuseOne.inputs['Normal'])
group.links.new(node_input.outputs['Normal'], node_glossy.inputs['Normal'])
group.links.new(node_input.outputs['Normal'], node_diffuseTwo.inputs['Normal'])
group.links.new(node_voronoi.outputs[0], node_gamma.inputs[0])
group.links.new(node_diffuseOne.outputs[0], node_mixOne.inputs[1])
group.links.new(node_glossy.outputs[0], node_mixOne.inputs[2])
group.links.new(node_gamma.outputs[0], node_mixTwo.inputs[0])
group.links.new(node_mixOne.outputs[0], node_mixTwo.inputs[1])
group.links.new(node_diffuseTwo.outputs[0], node_mixTwo.inputs[2])
group.links.new(node_mixTwo.outputs[0], node_output.inputs[0])
# **********************************************************************************
def __createBlenderLegoMilkyWhiteNodeGroup():
groupName = BlenderMaterials.__getGroupName('Lego Milky White')
if bpy.data.node_groups.get(groupName) is None:
debugPrint("createBlenderLegoMilkyWhiteNodeGroup #create")
# create a group
group, node_input, node_output = BlenderMaterials.__createGroup(groupName, -450, 0, 350, 0, True)
BlenderMaterials.addInputSocket(group,'NodeSocketColor','Color')
BlenderMaterials.addInputSocket(group,'NodeSocketVectorDirection','Normal')
if BlenderMaterials.usePrincipledShader:
node_principled = BlenderMaterials.__nodePrincipled(group.nodes, 1.0, 0.05, 0.0, 0.5, 0.0, 0.03, 1.45, 0.0, 45-270, 340-210)
node_translucent = BlenderMaterials.__nodeTranslucent(group.nodes, -225, -382)
node_mix = BlenderMaterials.__nodeMix(group.nodes, 0.5, 65, -40)
group.links.new(node_input.outputs['Color'], node_principled.inputs['Base Color'])
group.links.new(node_input.outputs['Color'], BlenderMaterials.__getSubsurfaceColor(node_principled))
group.links.new(node_input.outputs['Normal'], node_principled.inputs['Normal'])
group.links.new(node_input.outputs['Normal'], node_translucent.inputs['Normal'])
group.links.new(node_principled.outputs[0], node_mix.inputs[1])
group.links.new(node_translucent.outputs[0], node_mix.inputs[2])
group.links.new(node_mix.outputs[0], node_output.inputs[0])
else:
node_diffuse = BlenderMaterials.__nodeDiffuse(group.nodes, 0.0, -242, 90)
node_trans = BlenderMaterials.__nodeTranslucent(group.nodes, -242, -46)
node_glossy = BlenderMaterials.__nodeGlossy(group.nodes, (1,1,1,1), 0.5, 'BECKMANN', -42, -54)
node_mixOne = BlenderMaterials.__nodeMix(group.nodes, 0.4, -35, 90)
node_mixTwo = BlenderMaterials.__nodeMix(group.nodes, 0.2, 175, 90)
# link nodes together
group.links.new(node_input.outputs['Color'], node_diffuse.inputs['Color'])
group.links.new(node_input.outputs['Color'], node_trans.inputs['Color'])
group.links.new(node_input.outputs['Color'], node_glossy.inputs['Color'])
group.links.new(node_input.outputs['Normal'], node_diffuse.inputs['Normal'])
group.links.new(node_input.outputs['Normal'], node_trans.inputs['Normal'])
group.links.new(node_input.outputs['Normal'], node_glossy.inputs['Normal'])
group.links.new(node_diffuse.outputs[0], node_mixOne.inputs[1])
group.links.new(node_trans.outputs[0], node_mixOne.inputs[2])
group.links.new(node_mixOne.outputs[0], node_mixTwo.inputs[1])
group.links.new(node_glossy.outputs[0], node_mixTwo.inputs[2])
group.links.new(node_mixTwo.outputs[0], node_output.inputs[0])
# **********************************************************************************
def createBlenderNodeGroups():
BlenderMaterials.usePrincipledShader = BlenderMaterials.__hasPrincipledShader and Options.usePrincipledShaderWhenAvailable
BlenderMaterials.__createBlenderDistanceToCenterNodeGroup()
BlenderMaterials.__createBlenderVectorElementPowerNodeGroup()
BlenderMaterials.__createBlenderConvertToNormalsNodeGroup()
BlenderMaterials.__createBlenderConcaveWallsNodeGroup()
BlenderMaterials.__createBlenderSlopeTextureNodeGroup()
# Originally based on ideas from https://www.youtube.com/watch?v=V3wghbZ-Vh4
# "Create your own PBR Material [Fixed!]" by BlenderGuru
# Updated with Principled Shader, if available
BlenderMaterials.__createBlenderFresnelNodeGroup()
BlenderMaterials.__createBlenderReflectionNodeGroup()
BlenderMaterials.__createBlenderDielectricNodeGroup()
BlenderMaterials.__createBlenderLegoStandardNodeGroup()
BlenderMaterials.__createBlenderLegoTransparentNodeGroup()
BlenderMaterials.__createBlenderLegoTransparentFluorescentNodeGroup()
BlenderMaterials.__createBlenderLegoRubberNodeGroup()
BlenderMaterials.__createBlenderLegoRubberTranslucentNodeGroup()
BlenderMaterials.__createBlenderLegoEmissionNodeGroup()
BlenderMaterials.__createBlenderLegoChromeNodeGroup()
BlenderMaterials.__createBlenderLegoPearlescentNodeGroup()
BlenderMaterials.__createBlenderLegoMetalNodeGroup()
BlenderMaterials.__createBlenderLegoGlitterNodeGroup()
BlenderMaterials.__createBlenderLegoSpeckleNodeGroup()
BlenderMaterials.__createBlenderLegoMilkyWhiteNodeGroup()
# **************************************************************************************
def addSharpEdges(bm, geometry, filename):
if geometry.edges:
global globalWeldDistance
epsilon = globalWeldDistance
bm.faces.ensure_lookup_table()
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
# Create kd tree for fast "find nearest points" calculation
kd = mathutils.kdtree.KDTree(len(bm.verts))
for i, v in enumerate(bm.verts):
kd.insert(v.co, i)
kd.balance()
# Create edgeIndices dictionary, which is the list of edges as pairs of indicies into our bm.verts array
edgeIndices = {}
for ind, geomEdge in enumerate(geometry.edges):
# Find index of nearest points in bm.verts to geomEdge[0] and geomEdge[1]
edges0 = [index for (co, index, dist) in kd.find_range(geomEdge[0], epsilon)]
edges1 = [index for (co, index, dist) in kd.find_range(geomEdge[1], epsilon)]
#if (len(edges0) > 2):
# printWarningOnce("Found {1} vertices near {0} in file {2}".format(geomEdge[0], len(edges0), filename))
#if (len(edges1) > 2):
# printWarningOnce("Found {1} vertices near {0} in file {2}".format(geomEdge[1], len(edges1), filename))
for e0 in edges0:
for e1 in edges1:
edgeIndices[(e0, e1)] = True
edgeIndices[(e1, e0)] = True
# Find the appropriate mesh edges and make them sharp (i.e. not smooth)
for meshEdge in bm.edges:
v0 = meshEdge.verts[0].index
v1 = meshEdge.verts[1].index
if (v0, v1) in edgeIndices:
# Make edge sharp
meshEdge.smooth = False
# Set bevel weights
if bpy.app.version < (4, 0, 0):
# Blender 3
# Find layer for bevel weights
if 'BevelWeight' in bm.edges.layers.bevel_weight:
bwLayer = bm.edges.layers.bevel_weight['BevelWeight']
elif '' in bm.edges.layers.bevel_weight:
bwLayer = bm.edges.layers.bevel_weight['']
else:
bwLayer = None
for meshEdge in bm.edges:
v0 = meshEdge.verts[0].index
v1 = meshEdge.verts[1].index
if (v0, v1) in edgeIndices:
# Add bevel weight
if bwLayer is not None:
meshEdge[bwLayer] = 1.0
return edgeIndices
# Commented this next section out as it fails for certain pieces.
# Look for any pair of colinear edges emanating from a single vertex, where each edge is connected to exactly one face.
# Subdivide the longer edge to include the shorter edge's vertex.
# Repeat until there's nothing left to subdivide.
# This helps create better (more manifold) geometry in general, and in particular solves issues with technic pieces with holes.
# verts = set(bm.verts)
#
# while(verts):
# v = verts.pop()
# edges = [e for e in v.link_edges if len(e.link_faces) == 1]
# for e1, e2 in itertools.combinations(edges, 2):
#
# # ensure e1 is always the longer edge
# if e1.calc_length() < e2.calc_length():
# e1, e2 = e2, e1
#
# v1 = e1.other_vert(v)
# v2 = e2.other_vert(v)
# vec1 = v1.co - v.co
# vec2 = v2.co - v.co
#
# # test for colinear
# if vec1.angle(vec2) < 0.02:
# old_face = e1.link_faces[0]
# new_verts = old_face.verts[:]
#
# e2.smooth &= e1.smooth
# if bwLayer is not None:
# e2[bwLayer] = max(e1[bwLayer], e2[bwLayer])
#
# # insert the shorter edge's vertex
# i = new_verts.index(v)
# i1 = new_verts.index(v1)
# if i1 - i in [1, -1]:
# new_verts.insert(max(i,i1), v2)
# else:
# new_verts.insert(0, v2)
#
# # create a new face that includes the newly inserted vertex
# new_face = bm.faces.new(new_verts)
#
# # copy material to new face
# new_face.material_index = old_face.material_index
#
# # copy metadata to the new edge
# for e in v2.link_edges:
# if e.other_vert(v2) is v1:
# e.smooth &= e1.smooth
# if bwLayer is not None:
# e[bwLayer] = max(e1[bwLayer], e[bwLayer])
#
# # delete the old edge
# deleteEdge(bm, [e1])
#
# # re-check the vertices we modified
# verts.add(v)
# verts.add(v2)
# break
bm.faces.ensure_lookup_table()
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
# **************************************************************************************
def meshIsReusable(meshName, geometry):
meshExists = meshName in bpy.data.meshes
#debugPrint("meshIsReusable says {0} exists = {1}.".format(meshName, meshExists))
if meshExists and not Options.overwriteExistingMeshes:
mesh = bpy.data.meshes[meshName]
#debugPrint("meshIsReusable testing")
# A mesh loses it's materials information when it is no longer in use.
# We must check the number of faces matches, otherwise we can't re-set the
# materials.
if mesh.users == 0 and (len(mesh.polygons) != len(geometry.faces)):
#debugPrint("meshIsReusable says no users and num faces changed.")
return False
# If options have changed (e.g. scale) we should not reuse the same mesh.
if 'customMeshOptions' in mesh.keys():
#debugPrint("meshIsReusable found custom options.")
#debugPrint("mesh['customMeshOptions'] = {0}".format(mesh['customMeshOptions']))
#debugPrint("Options.meshOptionsString() = {0}".format(Options.meshOptionsString()))
if mesh['customMeshOptions'] == Options.meshOptionsString():
#debugPrint("meshIsReusable found custom options - match OK.")
return True
#debugPrint("meshIsReusable found custom options - DON'T match.")
return False
# **************************************************************************************
def addNodeToParentWithGroups(parentObject, groupNames, newObject):
if not Options.flattenGroups:
# Create groups as needed
for groupName in groupNames:
# The max length of a Blender node name appears to be 63 bytes when encoded as UTF-8. We make sure it fits.
while len(groupName.encode("utf8")) > 63:
groupName = groupName[:-1]
# Check if we already have this node name, or if we need to create a new node
groupObj = None
for obj in bpy.data.objects:
if (obj.name == groupName):
groupObj = obj
if (groupObj is None):
groupObj = bpy.data.objects.new(groupName, None)
groupObj.parent = parentObject
globalObjectsToAdd.append(groupObj)
parentObject = groupObj
newObject.parent = parentObject
globalObjectsToAdd.append(newObject)
# **************************************************************************************
parent = None
attach_points = []
children = []
partsHierarchy = {}
macro_name = None
macros = {}
# **************************************************************************************
def parseParentsFile(file):
global parent
global attach_points
global children
global partsHierarchy
global macro_name
global macros
# See https://stackoverflow.com/a/53870514
number_pattern = "[+-]?((\d+(\.\d*)?)|(\.\d+))"
pattern = "(" + number_pattern + ")(.*)"
compiled = re.compile(pattern)
def number_split(s):
match = compiled.match(s)
if match is None:
return None, s
groups = match.groups()
return groups[0], groups[-1].strip()
parent = None
attach_points = []
children = []
partsHierarchy = {}
macro_name = None
macros = {}
def finishParent():
global parent
global attach_points
global children
global partsHierarchy
global macro_name
if macro_name:
macros[macro_name] = children
# print("Adding macro ", macro_name)
parent = None
attach_points = []
children = []
macro_name = None
if parent:
partsHierarchy[parent] = (attach_points, children)
parent = None
attach_points = []
children = []
macro_name = None
with open(file) as f:
lines = f.readlines() # list containing lines of file
line_number = 0
for line in lines:
line_number += 1
line = line.strip() # remove leading/trailing white spaces
line = line.split("#")[0]
if line:
line = line.strip()
original_line = line
if line.startswith("Group "):
# Found group definition
finishParent()
macro_name = line[6:].strip().strip(":")
# print("Found group definition ", macro_name)
continue
if line.startswith("Parent "):
# Found parent definition
finishParent()
parent = line[7:].strip().strip(":")
# print("Found parent definition ", parent)
continue
if line in macros:
# found instance of a macro
# add children to definition
children += macros[line]
continue
# check for three floating point numbers of an attach point
number1, line = number_split(line)
if number1:
number3 = None
number2, line = number_split(line)
if number2:
number3, line = number_split(line)
if number3:
# Got three numbers for an attach point
try:
attachPoint = (float(number1), float(number2), float(number3))
except:
attachPoint = None
if attachPoint:
# Attach point
attach_points.append(attachPoint)
continue
else:
debugPrint("ERROR: Bad attach point found on line %d" % (line_number,))
partsHierarchy = None
return
# child part number?
children.append(original_line)
finishParent()
# print("Macros:")
# pprint(macros)
# print("End of Macros")
return
# **************************************************************************************
def setupImplicitParents():
global globalScaleFactor
if not Options.minifigHierarchy:
return
parseParentsFile(Options.scriptDirectory + '/parents.txt')
# print(partsHierarchy)
if not partsHierarchy:
return
bpy.context.view_layer.update()
# create a set of the parent parts and a set of child parts from the partsHierarchy
parentParts = set()
childParts = set()
for parent, childrenData in partsHierarchy.items():
parentParts.add(parent)
childParts.update(childrenData[1])
# create a flat set of all interesting parts (parents and children together)
interestingParts = set()
interestingParts.update(parentParts)
interestingParts.update(childParts)
# print('Parent parts: %s' % (parentParts,))
# print('Child parts: %s' % (childParts,))
# print('Interesting parts: %s' % (interestingParts,))
tolerance = globalScaleFactor * 5 # in LDraw units
squaredTolerance = tolerance * tolerance
# print(" Squared tolerance: %s" % (squaredTolerance,))
# For each interesting mesh in the scene, remember the bare part number and the children
parentMeshParts = {} # bare part numbers of the parents
childMeshParts = {} # bare part numbers of the children
parentableMeshes = {} # interesting children
lego_part_pattern = "([A-Za-z]?\d+)($|\D)"
# for each object in the scene
for obj in bpy.data.objects:
if obj.type != 'MESH':
continue
name = obj.data.name
if not name.startswith('Mesh_'):
continue
# skip 'Mesh_' and get part of name that is just digits (possibly with a letter in front)
test_name = name[5:]
if " - " in test_name:
test_name = test_name.split(" - ",1)[1]
partName = ''
m = re.match(lego_part_pattern, test_name)
if m:
partName = m.group(1)
# For each interesting parent mesh in the scene, remember the bare part number and the children
if partName in parentParts:
# remember the bare part number for each interesting mesh in the scene
parentMeshParts[name] = partName
# remember possible children of the mesh in the scene
children = partsHierarchy.get(partName)
if children:
parentableMeshes[name] = children
# For each interesting child mesh in the scene, remember the bare part number
if partName in childParts:
# remember the bare part number for each interesting mesh in the scene
childMeshParts[name] = partName
# Now, iterate through the objects in the scene and gather the interesting ones
parentObjects = []
childObjects = []
for obj in bpy.data.objects:
if obj.type != 'MESH':
continue
meshName = obj.data.name
if meshName in parentMeshParts:
parentObjects.append(obj)
# print("Possible parent object %s has matrix %s" % (obj.name, obj.matrix_world))
if meshName in childMeshParts:
childObjects.append(obj)
# for each interesting parent object
for obj in parentObjects:
meshName = obj.data.name
childrenData = parentableMeshes.get(meshName)
if not childrenData:
continue
# parentLocation = obj.matrix_world @ mathutils.Vector((0, 0, 0))
# parentMatrixInverted = obj.matrix_world.inverted()
# print("Looking for children of %s (at %s)" % (obj.name, parentLocation))
slotLocations = []
for slot in childrenData[0]:
loc = obj.matrix_world @ (mathutils.Vector(slot) * globalScaleFactor)
slotLocations.append(loc)
# print(" Slot locations: %s" % (slotLocations,))
# for each interesting child object
for childObj in childObjects:
childMeshName = childObj.data.name
childPartName = childMeshParts[childMeshName]
if childPartName not in childrenData[1]:
continue
childLocation = childObj.matrix_world.to_translation()
# print(" Found possible child %s" % (childObj.name,))
for slotLocation in slotLocations:
# print(" Slot location:%s Child Location:%s" % (slotLocation, childLocation))
diff = slotLocation - childLocation
squaredDistance = diff.length_squared
# print(" location: %s (squared distance: %s)" % (childLocation, squaredDistance))
if squaredDistance <= squaredTolerance:
temp = childObj.matrix_world
childObj.parent = obj
# childObj.matrix_parent_inverse = parentMatrixInverted
childObj.matrix_world = temp
# print(" Got it! Parent '%s' now has child '%s'" % (obj.name, childObj.name))
# **************************************************************************************
def slopeAnglesForPart(partName):
"""
Gets the allowable slope angles for a given part.
"""
global globalSlopeAngles
# Check for a part number with or without a subsequent letter
match = re.match(r'\D*(\d+)([A-Za-z]?)', partName)
if match:
partNumberWithoutLetter = match.group(1)
partNumberWithLetter = partNumberWithoutLetter + match.group(2)
if partNumberWithLetter in globalSlopeAngles:
return globalSlopeAngles[partNumberWithLetter]
if partNumberWithoutLetter in globalSlopeAngles:
return globalSlopeAngles[partNumberWithoutLetter]
return None
# **************************************************************************************
def isSlopeFace(slopeAngles, isGrainySlopeAllowed, faceVertices):
"""
Checks whether a given face should receive a grainy slope material.
"""
# Step 1: Ignore some faces (studs) when checking for a grainy face
if not isGrainySlopeAllowed:
return False
# Step 2: Calculate angle of face normal to the ground
faceNormal = (faceVertices[1] - faceVertices[0]).cross(faceVertices[2]-faceVertices[0])
faceNormal.normalize()
# Clamp value to range -1 to 1 (ensure we are in the strict range of the acos function, taking account of rounding errors)
cosine = min(max(faceNormal.y, -1.0), 1.0)
# Calculate angle of face normal to the ground (-90 to 90 degrees)
angleToGroundDegrees = math.degrees(math.acos(cosine)) - 90
# debugPrint("Angle to ground {0}".format(angleToGroundDegrees))
# Step 3: Check angle of normal to ground is within one of the acceptable ranges for this part
return any(c[0] <= angleToGroundDegrees <= c[1] for c in slopeAngles)
# **************************************************************************************
def createMesh(name, meshName, geometry):
# Are there any points?
if not geometry.points:
return (None, False)
newMeshCreated = False
# Have we already cached this mesh?
if Options.createInstances and hasattr(geometry, 'mesh'):
mesh = geometry.mesh
else:
# Does this mesh already exist in Blender?
if meshIsReusable(meshName, geometry):
mesh = bpy.data.meshes[meshName]
else:
# Create new mesh
# debugPrint("Creating Mesh for node {0}".format(node.filename))
mesh = bpy.data.meshes.new(meshName)
points = [p.to_tuple() for p in geometry.points]
mesh.from_pydata(points, [], geometry.faces)
mesh.validate()
mesh.update()
# Set a custom parameter to record the options used to create this mesh
# Used for caching.
mesh['customMeshOptions'] = Options.meshOptionsString()
newMeshCreated = True
# Create materials and assign material to each polygon
if mesh.users == 0:
assert len(mesh.polygons) == len(geometry.faces)
assert len(geometry.faces) == len(geometry.faceInfo)
slopeAngles = slopeAnglesForPart(name)
isSloped = slopeAngles is not None
for i, f in enumerate(mesh.polygons):
faceInfo = geometry.faceInfo[i]
isSlopeMaterial = isSloped and isSlopeFace(slopeAngles, faceInfo.isGrainySlopeAllowed, [geometry.points[j] for j in geometry.faces[i]])
faceColour = faceInfo.faceColour
# For debugging purposes, we can make sloped faces blue:
# if isSlopeMaterial:
# faceColour = "1"
material = BlenderMaterials.getMaterial(faceColour, isSlopeMaterial)
if material is not None:
if mesh.materials.get(material.name) is None:
mesh.materials.append(material)
f.material_index = mesh.materials.find(material.name)
else:
printWarningOnce("Could not find material '{0}' in mesh '{1}'.".format(faceColour, name))
# Cache mesh
if newMeshCreated:
geometry.mesh = mesh
return (mesh, newMeshCreated)
# **************************************************************************************
def addModifiers(ob):
global globalScaleFactor
# Add Bevel modifier to each instance
if Options.addBevelModifier:
bevelModifier = ob.modifiers.new("Bevel", type='BEVEL')
bevelModifier.width = Options.bevelWidth * globalScaleFactor
bevelModifier.segments = 4
bevelModifier.profile = 0.5
bevelModifier.limit_method = 'WEIGHT'
bevelModifier.use_clamp_overlap = True
# Add edge split modifier to each instance
if Options.edgeSplit:
edgeModifier = ob.modifiers.new("Edge Split", type='EDGE_SPLIT')
edgeModifier.use_edge_sharp = True
edgeModifier.split_angle = math.radians(30.0)
# **************************************************************************************
def smoothShadingAndFreestyleEdges(ob):
# We would like to avoid using bpy.ops functions altogether since it
# slows down progressively as more objects are added to the scene, but
# we have no choice but to use it here (a) for smoothing and (b) for
# marking freestyle edges (no bmesh options exist currently). To minimise
# the performance drop, we add one object only to the scene, smooth it,
# then remove it again. Only at the end of the import process are all the
# objects properly added to the scene.
# Temporarily add object to scene
linkToScene(ob)
# Select object
selectObject(ob)
# Smooth shading
if Options.smoothShading:
# Smooth the mesh
bpy.ops.object.shade_smooth()
if Options.instructionsLook:
# Mark all sharp edges as freestyle edges
me = bpy.context.object.data
for e in me.edges:
e.use_freestyle_mark = e.use_edge_sharp
# Deselect object
deselectObject(ob)
# Remove object from scene
unlinkFromScene(ob)
# **************************************************************************************
def createBlenderObjectsFromNode(node,
localMatrix,
name,
realColourName=Options.defaultColour,
blenderParentTransform=Math.identityMatrix,
localToWorldSpaceMatrix=Math.identityMatrix,
blenderNodeParent=None):
"""
Creates a Blender Object for the node given and (recursively) for all it's children as required.
Creates and optimises the mesh for each object too.
"""
global globalBrickCount
global globalObjectsToAdd
global globalWeldDistance
global globalPoints
ob = None
if node.isBlenderObjectNode():
ourColourName = LDrawNode.resolveColour(node.colourName, realColourName)
meshName, geometry = node.getBlenderGeometry(ourColourName, name)
mesh, newMeshCreated = createMesh(name, meshName, geometry)
# Format a name for the Blender Object
if Options.numberNodes:
blenderName = str(globalBrickCount).zfill(5) + "_" + name
else:
blenderName = name
globalBrickCount = globalBrickCount + 1
# Create Blender Object
ob = bpy.data.objects.new(blenderName, mesh)
ob.matrix_local = blenderParentTransform @ localMatrix
if newMeshCreated:
# For performance reasons we try to avoid using bpy.ops.* methods
# (e.g. we use bmesh.* operations instead).
# See discussion: http://blender.stackexchange.com/questions/7358/python-performance-with-blender-operators
# Use bevel weights (added to sharp edges) - Only available for Blender version < 3.4
if hasattr(ob.data, "use_customdata_edge_bevel"):
ob.data.use_customdata_edge_bevel = True
else:
if bpy.app.version < (4, 0, 0):
# Add to scene
linkToScene(ob)
# Blender 3.4 removed 'ob.data.use_customdata_edge_bevel', so this seems to be the alternative:
# See https://blender.stackexchange.com/a/270716
area_type = 'VIEW_3D' # change this to use the correct Area Type context you want to process in
areas = [area for area in bpy.context.window.screen.areas if area.type == area_type]
if len(areas) <= 0:
raise Exception(f"Make sure an Area of type {area_type} is open or visible on your screen!")
selectObject(ob)
bpy.ops.object.mode_set(mode='EDIT')
with bpy.context.temp_override(
window=bpy.context.window,
area=areas[0],
regions=[region for region in areas[0].regions if region.type == 'WINDOW'][0],
screen=bpy.context.window.screen):
bpy.ops.mesh.customdata_bevel_weight_edge_add()
bpy.ops.object.mode_set(mode='OBJECT')
unlinkFromScene(ob)
# The lines out of an empty shown in the viewport are scaled to a reasonable size
ob.empty_display_size = 250.0 * globalScaleFactor
# Mark object as transparent if any polygon is transparent
ob["Lego.isTransparent"] = False
if mesh is not None:
for faceInfo in geometry.faceInfo:
material = BlenderMaterials.getMaterial(faceInfo.faceColour, False)
if material is not None:
if "Lego.isTransparent" in material:
if material["Lego.isTransparent"]:
ob["Lego.isTransparent"] = True
break
# Add any (LeoCAD) group nodes as parents of 'ob' (the new node), and as children of 'blenderNodeParent'.
# Also add all objects to 'globalObjectsToAdd'.
addNodeToParentWithGroups(blenderNodeParent, node.groupNames, ob)
# Node to which our children will be attached
blenderNodeParent = ob
blenderParentTransform = Math.identityMatrix
# debugPrint("NAME = {0}".format(name))
# Add light to light bricks
if (name in globalLightBricks):
lights = bpy.data.lights
lamp_data = lights.new(name="LightLamp", type='POINT')
lamp_data.shadow_soft_size = 0.05
lamp_data.use_nodes = True
emission_node = lamp_data.node_tree.nodes.get('Emission')
if emission_node:
emission_node.inputs['Color'].default_value = globalLightBricks[name]
emission_node.inputs['Strength'].default_value = 100.0
lamp_object = bpy.data.objects.new(name="LightLamp", object_data=lamp_data)
lamp_object.location = (-0.27, 0.0, -0.18)
addNodeToParentWithGroups(blenderNodeParent, [], lamp_object)
if newMeshCreated:
# Calculate what we need to do next
recalculateNormals = node.file.isDoubleSided and (Options.resolveAmbiguousNormals == "guess")
keepDoubleSided = node.file.isDoubleSided and (Options.resolveAmbiguousNormals == "double")
removeDoubles = Options.removeDoubles and not keepDoubleSided
bm = bmesh.new()
bm.from_mesh(ob.data)
bm.faces.ensure_lookup_table()
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
# Remove doubles
# Note: This doesn't work properly with a low distance value
# So we scale up the vertices beforehand and scale them down afterwards
for v in bm.verts:
v.co = v.co * 1000
if removeDoubles:
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=globalWeldDistance)
for v in bm.verts:
v.co = v.co / 1000
# Recalculate normals
if recalculateNormals:
bmesh.ops.recalc_face_normals(bm, faces=bm.faces[:])
# Add sharp edges (and edge weights in Blender 3)
edgeIndices = addSharpEdges(bm, geometry, name)
bm.to_mesh(ob.data)
# In Blender 4, set the edge weights (on ob.data rather than bm these days)
if (bpy.app.version >= (4, 0, 0)) and edgeIndices:
# Blender 4
bevel_weight_attr = ob.data.attributes.new("bevel_weight_edge", "FLOAT", "EDGE")
for idx, meshEdge in enumerate(bm.edges):
v0 = meshEdge.verts[0].index
v1 = meshEdge.verts[1].index
if (v0, v1) in edgeIndices:
bevel_weight_attr.data[idx].value = 1.0
bm.clear()
bm.free()
# Show the sharp edges in Edit Mode
for area in bpy.context.screen.areas: # iterate through areas in current screen
if area.type == 'VIEW_3D':
for space in area.spaces: # iterate through spaces in current VIEW_3D area
if space.type == 'VIEW_3D': # check if space is a 3D view
space.overlay.show_edge_sharp = True
# Scale for Gaps
if Options.gaps and node.file.isPart:
# Distance between gaps is controlled by Options.realGapWidth
# Gap height is set smaller than realGapWidth since empirically, stacked bricks tend
# to be pressed more tightly together
gapHeight = 0.33 * Options.realGapWidth
objScale = ob.scale
dim = ob.dimensions
# Checks whether the object isn't flat in a certain direction
# to avoid division by zero.
# Else, the scale factor is set proportional to the inverse of
# the dimension so that the mesh shrinks a fixed distance
# (determined by the gap_width and the scale of the object)
# in every direction, creating a uniform gap.
scaleFac = mathutils.Vector( (1.0, 1.0, 1.0) )
if dim.x != 0:
scaleFac.x = 1 - Options.realGapWidth * abs(objScale.x) / dim.x
if dim.y != 0:
scaleFac.y = 1 - gapHeight * abs(objScale.y) / dim.y
if dim.z != 0:
scaleFac.z = 1 - Options.realGapWidth * abs(objScale.z) / dim.z
# A safety net: Don't distort the part too much (e.g. -ve scale would not look good)
if scaleFac.x < 0.95:
scaleFac.x = 0.95
if scaleFac.y < 0.95:
scaleFac.y = 0.95
if scaleFac.z < 0.95:
scaleFac.z = 0.95
# Scale all vertices in the mesh
gapsScaleMatrix = mathutils.Matrix((
(scaleFac.x, 0.0, 0.0, 0.0),
(0.0, scaleFac.y, 0.0, 0.0),
(0.0, 0.0, scaleFac.z, 0.0),
(0.0, 0.0, 0.0, 1.0)
))
mesh.transform(gapsScaleMatrix)
smoothShadingAndFreestyleEdges(ob)
# Keep track of all vertices in global space, for positioning the camera and/or root object at the end
# Notice that we do this after scaling for Options.gaps
if Options.positionObjectOnGroundAtOrigin or Options.positionCamera:
if mesh and mesh.vertices:
localTransform = localToWorldSpaceMatrix @ localMatrix
points = [localTransform @ p.co for p in mesh.vertices]
# Remember all the points
globalPoints.extend(points)
# Hide selection of studs
if node.file.isStud:
ob.hide_select = True
# Add bevel and edge split modifiers as needed
if mesh:
addModifiers(ob)
else:
blenderParentTransform = blenderParentTransform @ localMatrix
# Create children and parent them
for childNode in node.file.childNodes:
# Create sub-objects recursively
childColourName = LDrawNode.resolveColour(childNode.colourName, realColourName)
createBlenderObjectsFromNode(childNode, childNode.matrix, childNode.filename, childColourName, blenderParentTransform, localToWorldSpaceMatrix @ localMatrix, blenderNodeParent)
return ob
# **************************************************************************************
def addFileToCache(relativePath, name):
"""Loads and caches an LDraw file in the cache of files"""
file = LDrawFile(relativePath, False, "", None, True)
CachedFiles.addToCache(name, file)
return True
# **************************************************************************************
def setupLineset(lineset, thickness, group):
lineset.select_silhouette = True
lineset.select_border = False
lineset.select_contour = False
lineset.select_suggestive_contour = False
lineset.select_ridge_valley = False
lineset.select_crease = False
lineset.select_edge_mark = True
lineset.select_external_contour = False
lineset.select_material_boundary = False
lineset.edge_type_combination = 'OR'
lineset.edge_type_negation = 'INCLUSIVE'
lineset.select_by_collection = True
lineset.collection = bpy.data.collections[bpy.data.collections.find(group)]
# Set line color
lineset.linestyle.color = (0.0, 0.0, 0.0)
# Set material to override color
if 'LegoMaterial' not in lineset.linestyle.color_modifiers:
lineset.linestyle.color_modifiers.new('LegoMaterial', 'MATERIAL')
# Use square caps
lineset.linestyle.caps = 'SQUARE' # Can be 'ROUND', 'BUTT', or 'SQUARE'
# Draw inside the edge of the object
lineset.linestyle.thickness_position = 'INSIDE'
# Set Thickness
lineset.linestyle.thickness = thickness
# **************************************************************************************
def setupRealisticLook():
scene = bpy.context.scene
render = scene.render
# Use cycles render
scene.render.engine = 'CYCLES'
# Add environment texture for world
if Options.addWorldEnvironmentTexture:
scene.world.use_nodes = True
nodes = scene.world.node_tree.nodes
links = scene.world.node_tree.links
worldNodeNames = [node.name for node in scene.world.node_tree.nodes]
if "LegoEnvMap" in worldNodeNames:
env_tex = nodes["LegoEnvMap"]
else:
env_tex = nodes.new('ShaderNodeTexEnvironment')
env_tex.location = (-250, 300)
env_tex.name = "LegoEnvMap"
env_tex.image = bpy.data.images.load(Options.scriptDirectory + "/background.exr", check_existing=True)
if "Background" in worldNodeNames:
background = nodes["Background"]
links.new(env_tex.outputs[0],background.inputs[0])
else:
scene.world.color = (1.0, 1.0, 1.0)
if Options.setRenderSettings:
useDenoising(scene, True)
if (scene.cycles.samples < 400):
scene.cycles.samples = 400
if (scene.cycles.diffuse_bounces < 20):
scene.cycles.diffuse_bounces = 20
if (scene.cycles.glossy_bounces < 20):
scene.cycles.glossy_bounces = 20
# Check layer names to see if we were previously rendering instructions and change settings back.
layerNames = getLayerNames(scene)
if ("SolidBricks" in layerNames) or ("TransparentBricks" in layerNames):
render.use_freestyle = False
# Change camera back to Perspective
if scene.camera is not None:
scene.camera.data.type = 'PERSP'
# For Blender Render, reset to opaque background (Not available in Blender 3.5.1 or higher.)
if hasattr(render, "alpha_mode"):
render.alpha_mode = 'SKY'
# Turn off cycles transparency
scene.cycles.film_transparent = False
# Get the render/view layers we are interested in:
layers = getLayers(scene)
# If we have previously added render layers for instructions look, re-enable any disabled render layers
for i in range(len(layers)):
layers[i].use = True
# Un-name SolidBricks and TransparentBricks layers
if "SolidBricks" in layerNames:
layers.remove(layers["SolidBricks"])
if "TransparentBricks" in layerNames:
layers.remove(layers["TransparentBricks"])
# Re-enable all layers
for i in range(len(layers)):
layers[i].use = True
# Create Compositing Nodes
scene.use_nodes = True
# If scene nodes exist for compositing instructions look, remove them
nodeNames = [node.name for node in scene.node_tree.nodes]
if "Solid" in nodeNames:
scene.node_tree.nodes.remove(scene.node_tree.nodes["Solid"])
if "Trans" in nodeNames:
scene.node_tree.nodes.remove(scene.node_tree.nodes["Trans"])
if "Z Combine" in nodeNames:
scene.node_tree.nodes.remove(scene.node_tree.nodes["Z Combine"])
# Set up standard link from Render Layers to Composite
if "Render Layers" in nodeNames:
if "Composite" in nodeNames:
rl = scene.node_tree.nodes["Render Layers"]
zCombine = scene.node_tree.nodes["Composite"]
links = scene.node_tree.links
links.new(rl.outputs[0], zCombine.inputs[0])
removeCollection('Black Edged Bricks Collection')
removeCollection('White Edged Bricks Collection')
removeCollection('Solid Bricks Collection')
removeCollection('Transparent Bricks Collection')
# **************************************************************************************
def removeCollection(name, remove_collection_objects=False):
coll = bpy.data.collections.get(name)
if coll:
if remove_collection_objects:
obs = [o for o in coll.objects if o.users == 1]
while obs:
bpy.data.objects.remove(obs.pop())
bpy.data.collections.remove(coll)
# **************************************************************************************
def createCollection(scene, name):
if bpy.data.collections.find(name) < 0:
# Create collection
bpy.data.collections.new(name)
# Add collection to scene
scene.collection.children.link(bpy.data.collections[name])
# **************************************************************************************
def setupInstructionsLook():
scene = bpy.context.scene
render = scene.render
render.use_freestyle = True
# Use Blender Eevee (or Eevee Next) for instructions look
try:
render.engine = 'BLENDER_EEVEE'
except:
render.engine = 'BLENDER_EEVEE_NEXT'
# Change camera to Orthographic
if scene.camera is not None:
scene.camera.data.type = 'ORTHO'
# For Blender Render, set transparent background. (Not available in Blender 3.5.1 or higher.)
if hasattr(render, "alpha_mode"):
render.alpha_mode = 'TRANSPARENT'
# Turn on cycles transparency
scene.cycles.film_transparent = True
# Increase max number of transparency bounces to at least 80
# This avoids artefacts when multiple transparent objects are behind each other
if scene.cycles.transparent_max_bounces < 80:
scene.cycles.transparent_max_bounces = 80
# Add collections / groups, if not already present
if hasCollections:
createCollection(scene, 'Black Edged Bricks Collection')
createCollection(scene, 'White Edged Bricks Collection')
createCollection(scene, 'Solid Bricks Collection')
createCollection(scene, 'Transparent Bricks Collection')
else:
if bpy.data.groups.find('Black Edged Bricks Collection') < 0:
bpy.data.groups.new('Black Edged Bricks Collection')
if bpy.data.groups.find('White Edged Bricks Collection') < 0:
bpy.data.groups.new('White Edged Bricks Collection')
# Find or create the render/view layers we are interested in:
layers = getLayers(scene)
# Remember current view layer
current_view_layer = bpy.context.view_layer
# Add layers as needed
layerNames = list(map((lambda x: x.name), layers))
if "SolidBricks" not in layerNames:
bpy.ops.scene.view_layer_add()
layers[-1].name = "SolidBricks"
layers[-1].use = True
layerNames.append("SolidBricks")
solidLayer = layerNames.index("SolidBricks")
if "TransparentBricks" not in layerNames:
bpy.ops.scene.view_layer_add()
layers[-1].name = "TransparentBricks"
layers[-1].use = True
layerNames.append("TransparentBricks")
transLayer = layerNames.index("TransparentBricks")
# Restore current view layer
bpy.context.window.view_layer = current_view_layer
# Use Z layer (defaults to off in Blender 3.5.1)
if hasattr(layers[transLayer], "use_pass_z"):
layers[transLayer].use_pass_z = True
if hasattr(layers[solidLayer], "use_pass_z"):
layers[solidLayer].use_pass_z = True
# Disable any render/view layers that are not needed
for i in range(len(layers)):
if i not in [solidLayer, transLayer]:
layers[i].use = False
layers[solidLayer].use = True
layers[transLayer].use = True
# Include or exclude collections for each layer
for collection in layers[solidLayer].layer_collection.children:
collection.exclude = collection.name != 'Solid Bricks Collection'
for collection in layers[transLayer].layer_collection.children:
collection.exclude = collection.name != 'Transparent Bricks Collection'
#layers[solidLayer].layer_collection.children['Black Edged Bricks Collection'].exclude = True
#layers[solidLayer].layer_collection.children['White Edged Bricks Collection'].exclude = True
#layers[solidLayer].layer_collection.children['Solid Bricks Collection'].exclude = False
#layers[solidLayer].layer_collection.children['Transparent Bricks Collection'].exclude = True
#layers[transLayer].layer_collection.children['Black Edged Bricks Collection'].exclude = True
#layers[transLayer].layer_collection.children['White Edged Bricks Collection'].exclude = True
#layers[transLayer].layer_collection.children['Solid Bricks Collection'].exclude = True
#layers[transLayer].layer_collection.children['Transparent Bricks Collection'].exclude = False
# Move each part to appropriate collection
for object in scene.objects:
isTransparent = False
if "Lego.isTransparent" in object:
isTransparent = object["Lego.isTransparent"]
# Add objects to the appropriate layers
if isTransparent:
linkToCollection('Transparent Bricks Collection', object)
else:
linkToCollection('Solid Bricks Collection', object)
# Add object to the appropriate group
if object.data != None:
colour = object.data.materials[0].diffuse_color
# Dark colours have white lines
if LegoColours.isDark(colour):
linkToCollection('White Edged Bricks Collection', object)
else:
linkToCollection('Black Edged Bricks Collection', object)
# Find or create linesets
solidBlackLineset = None
solidWhiteLineset = None
transBlackLineset = None
transWhiteLineset = None
for lineset in layers[solidLayer].freestyle_settings.linesets:
if lineset.name == "LegoSolidBlackLines":
solidBlackLineset = lineset
if lineset.name == "LegoSolidWhiteLines":
solidWhiteLineset = lineset
for lineset in layers[transLayer].freestyle_settings.linesets:
if lineset.name == "LegoTransBlackLines":
transBlackLineset = lineset
if lineset.name == "LegoTransWhiteLines":
transWhiteLineset = lineset
if solidBlackLineset == None:
layers[solidLayer].freestyle_settings.linesets.new("LegoSolidBlackLines")
solidBlackLineset = layers[solidLayer].freestyle_settings.linesets[-1]
setupLineset(solidBlackLineset, 2.25, 'Black Edged Bricks Collection')
if solidWhiteLineset == None:
layers[solidLayer].freestyle_settings.linesets.new("LegoSolidWhiteLines")
solidWhiteLineset = layers[solidLayer].freestyle_settings.linesets[-1]
setupLineset(solidWhiteLineset, 2, 'White Edged Bricks Collection')
if transBlackLineset == None:
layers[transLayer].freestyle_settings.linesets.new("LegoTransBlackLines")
transBlackLineset = layers[transLayer].freestyle_settings.linesets[-1]
setupLineset(transBlackLineset, 2.25, 'Black Edged Bricks Collection')
if transWhiteLineset == None:
layers[transLayer].freestyle_settings.linesets.new("LegoTransWhiteLines")
transWhiteLineset = layers[transLayer].freestyle_settings.linesets[-1]
setupLineset(transWhiteLineset, 2, 'White Edged Bricks Collection')
# Create Compositing Nodes
scene.use_nodes = True
if "Solid" in scene.node_tree.nodes:
solidLayer = scene.node_tree.nodes["Solid"]
else:
solidLayer = scene.node_tree.nodes.new('CompositorNodeRLayers')
solidLayer.name = "Solid"
solidLayer.layer = 'SolidBricks'
if "Trans" in scene.node_tree.nodes:
transLayer = scene.node_tree.nodes["Trans"]
else:
transLayer = scene.node_tree.nodes.new('CompositorNodeRLayers')
transLayer.name = "Trans"
transLayer.layer = 'TransparentBricks'
if "Z Combine" in scene.node_tree.nodes:
zCombine = scene.node_tree.nodes["Z Combine"]
else:
zCombine = scene.node_tree.nodes.new('CompositorNodeZcombine')
zCombine.use_alpha = True
zCombine.use_antialias_z = True
if "Set Alpha" in scene.node_tree.nodes:
setAlpha = scene.node_tree.nodes["Set Alpha"]
else:
setAlpha = scene.node_tree.nodes.new('CompositorNodeSetAlpha')
setAlpha.inputs[1].default_value = 0.75
composite = scene.node_tree.nodes["Composite"]
composite.location = (950, 400)
zCombine.location = (750, 500)
transLayer.location = (300, 300)
solidLayer.location = (300, 600)
setAlpha.location = (580, 370)
links = scene.node_tree.links
links.new(solidLayer.outputs[0], zCombine.inputs[0])
links.new(solidLayer.outputs[2], zCombine.inputs[1])
links.new(transLayer.outputs[0], setAlpha.inputs[0])
links.new(setAlpha.outputs[0], zCombine.inputs[2])
links.new(transLayer.outputs[2], zCombine.inputs[3])
links.new(zCombine.outputs[0], composite.inputs[0])
# Blender 3 only: link the Z from the Z Combine to the composite. This is not present in Blender 4.
if bpy.app.version < (4, 0, 0):
links.new(zCombine.outputs[1], composite.inputs[2])
# **************************************************************************************
def iterateCameraPosition(camera, render, vcentre3d, moveCamera):
global globalPoints
bpy.context.view_layer.update()
minX = sys.float_info.max
maxX = -sys.float_info.max
minY = sys.float_info.max
maxY = -sys.float_info.max
# Calculate matrix to take 3d points into normalised camera space
modelview_matrix = camera.matrix_world.inverted()
get_depsgraph_method = getattr(bpy.context, "evaluated_depsgraph_get", None)
if callable(get_depsgraph_method):
depsgraph = get_depsgraph_method()
else:
depsgraph = bpy.context.depsgraph
projection_matrix = camera.calc_matrix_camera(
depsgraph,
x=render.resolution_x,
y=render.resolution_y,
scale_x=render.pixel_aspect_x,
scale_y=render.pixel_aspect_y)
mp_matrix = projection_matrix @ modelview_matrix
mpinv_matrix = mp_matrix.copy()
mpinv_matrix.invert()
isOrtho = bpy.context.scene.camera.data.type == 'ORTHO'
# Convert 3d points to camera space, calculating the min and max extents in 2d normalised camera space.
minDistToCamera = sys.float_info.max
for point in globalPoints:
p1 = mp_matrix @ mathutils.Vector((point.x, point.y, point.z, 1))
if isOrtho:
point2d = (p1.x, p1.y)
elif abs(p1.w)<1e-8:
continue
else:
point2d = (p1.x/p1.w, p1.y/p1.w)
minX = min(point2d[0], minX)
minY = min(point2d[1], minY)
maxX = max(point2d[0], maxX)
maxY = max(point2d[1], maxY)
disttocamera = (point - camera.location).length
minDistToCamera = min(minDistToCamera, disttocamera)
#debugPrint("minX,maxX: " + ('%.5f' % minX) + "," + ('%.5f' % maxX))
#debugPrint("minY,maxY: " + ('%.5f' % minY) + "," + ('%.5f' % maxY))
# Calculate distance d from camera to centre of the model
d = (vcentre3d - camera.location).length
# Which axis is filling most of the display?
largestSpan = max(maxX-minX, maxY-minY)
# Force option to be in range
if Options.cameraBorderPercent > 0.99999:
Options.cameraBorderPercent = 0.99999
# How far should the camera be away from the object?
# Zoom in or out to make the coverage close to 1 (or 1-border if theres a border amount specified)
scale = largestSpan/(2 - 2 * Options.cameraBorderPercent)
desiredMinDistToCamera = scale * minDistToCamera
# Adjust d to be the change in distance from the centre of the object
offsetD = minDistToCamera - desiredMinDistToCamera
# Calculate centre of object on screen
centre2d = mathutils.Vector(((minX + maxX)*0.5, (minY+maxY)*0.5))
# Get the forward vector of the camera
tempMatrix = camera.matrix_world.copy()
tempMatrix.invert()
forwards4d = -tempMatrix[2]
forwards3d = mathutils.Vector((forwards4d.x, forwards4d.y, forwards4d.z))
# Transform the 2d centre of object back into 3d space
if isOrtho:
centre3d = mpinv_matrix @ mathutils.Vector((centre2d.x, centre2d.y, 0, 1))
centre3d = mathutils.Vector((centre3d.x, centre3d.y, centre3d.z))
# Move centre3d a distance d from the camera plane
v = centre3d - camera.location
dist = v.dot(forwards3d)
centre3d = centre3d + (d - dist) * forwards3d
else:
centre3d = mpinv_matrix @ mathutils.Vector((centre2d.x, centre2d.y, -1, 1))
centre3d = mathutils.Vector((centre3d.x / centre3d.w, centre3d.y / centre3d.w, centre3d.z / centre3d.w))
# Make sure the 3d centre of the object is distance d from the camera location
forwards = centre3d - camera.location
forwards.normalize()
centre3d = camera.location + d * forwards
# Get the centre of the viewing area in 3d space at distance d from the camera
# This is where we want to move the object to
origin3d = camera.location + d * forwards3d
#debugPrint("d: " + ('%.5f' % d))
#debugPrint("camloc: " + ('%.5f' % camera.location.x) + "," + ('%.5f' % camera.location.y) + "," + ('%.5f' % camera.location.z))
#debugPrint("forwards3d: " + ('%.5f' % forwards3d.x) + "," + ('%.5f' % forwards3d.y) + "," + ('%.5f' % forwards3d.z))
#debugPrint("Origin3d: " + ('%.5f' % origin3d.x) + "," + ('%.5f' % origin3d.y) + "," + ('%.5f' % origin3d.z))
#debugPrint("Centre3d: " + ('%.5f' % centre3d.x) + "," + ('%.5f' % centre3d.y) + "," + ('%.5f' % centre3d.z))
# bpy.context.scene.cursor_location = centre3d
# bpy.context.scene.cursor_location = origin3d
if moveCamera:
if isOrtho:
offset3d = (centre3d - origin3d)
camera.data.ortho_scale *= scale
else:
# How much do we want to move the camera?
# We want to move the camera by the same amount as if we moved the centre of the object to the centre of the viewing area.
# In practice, this is not completely accurate, since the perspective projection changes the objects silhouette in 2d space
# when we move the camera, but it's close in practice. We choose to move it conservatively by 93% of our calculated amount,
# a figure obtained by some quick practical observations of the convergence on a few test models.
offset3d = 0.93 * (centre3d - origin3d) + offsetD * forwards3d
# debugPrint("offset3d: " + ('%.5f' % offset3d.x) + "," + ('%.5f' % offset3d.y) + "," + ('%.5f' % offset3d.z) + " length:" + ('%.5f' % offset3d.length))
# debugPrint("move by: " + ('%.5f' % offset3d.length))
camera.location += mathutils.Vector((offset3d.x, offset3d.y, offset3d.z))
return offset3d.length
return 0.0
# **************************************************************************************
def getConvexHull(minPoints = 3):
global globalPoints
if len(globalPoints) >= minPoints:
bm = bmesh.new()
[bm.verts.new(v) for v in globalPoints]
bm.verts.ensure_lookup_table()
ret = bmesh.ops.convex_hull(bm, input=bm.verts, use_existing_faces=False)
globalPoints = [vert.co.copy() for vert in ret["geom"] if isinstance(vert, bmesh.types.BMVert)]
del ret
bm.clear()
bm.free()
# **************************************************************************************
def loadFromFile(context, filename, isFullFilepath=True):
global globalCamerasToAdd
global globalContext
global globalScaleFactor
# Set global scale factor
# -----------------------
#
# 1. The size of Lego pieces:
#
# Lego scale: https://www.lugnet.com/~330/FAQ/Build/dimensions
#
# 1 Lego draw unit = 0.4 mm, in an idealised world.
#
# In real life, actual Lego pieces have been measured as 0.3993 mm +/- 0.0002,
# which makes 0.4mm accurate enough for all practical purposes (The difference
# being just 7 microns).
#
# 2. Blender coordinates:
#
# Blender reports coordinates in metres by default. So the
# scale factor to convert from Lego units to Blender coordinates
# is 0.0004.
#
# This calculation does not adjust for any gap between the pieces.
# This is (optionally) done later in the calculations, where we
# reduce the size of each piece by 0.2mm (default amount) to allow
# for a small gap between pieces. This matches real piece sizes.
#
# 3. Blender Scene Unit Scale:
#
# Blender has a 'Scene Unit Scale' value which by default is set
# to 1.0. By changing the 'Unit Scale' after import the size of
# everything in the scene can be adjusted.
globalScaleFactor = 0.0004 * Options.realScale
globalWeldDistance = 0.01 * globalScaleFactor
globalCamerasToAdd = []
globalContext = context
# Make sure we have the latest configuration, including the latest ldraw directory
# and the colours derived from that.
Configure()
LegoColours()
Math()
if Configure.ldrawInstallDirectory == "":
printError("Could not find LDraw Part Library")
return None
# Clear caches
CachedDirectoryFilenames.clearCache()
CachedFiles.clearCache()
CachedGeometry.clearCache()
BlenderMaterials.clearCache()
Configure.warningSuppression = {}
if Options.useLogoStuds:
debugPrint("Loading stud files")
# Load stud logo files into cache
addFileToCache("stud-logo" + Options.logoStudVersion + ".dat", "stud.dat")
addFileToCache("stud2-logo" + Options.logoStudVersion + ".dat", "stud2.dat")
addFileToCache("stud6-logo" + Options.logoStudVersion + ".dat", "stud6.dat")
addFileToCache("stud6a-logo" + Options.logoStudVersion + ".dat", "stud6a.dat")
addFileToCache("stud7-logo" + Options.logoStudVersion + ".dat", "stud7.dat")
addFileToCache("stud10-logo" + Options.logoStudVersion + ".dat", "stud10.dat")
addFileToCache("stud13-logo" + Options.logoStudVersion + ".dat", "stud13.dat")
addFileToCache("stud15-logo" + Options.logoStudVersion + ".dat", "stud15.dat")
addFileToCache("stud20-logo" + Options.logoStudVersion + ".dat", "stud20.dat")
addFileToCache("studa-logo" + Options.logoStudVersion + ".dat", "studa.dat")
addFileToCache("studtente-logo.dat", "s\\teton.dat") # TENTE
# Load and parse file to create geometry
filename = os.path.expanduser(filename)
debugPrint("Loading files")
node = LDrawNode(filename, isFullFilepath, os.path.dirname(filename))
node.load()
# node.printBFC()
if node.file.isModel:
# Fix top level rotation from LDraw coordinate space to Blender coordinate space
node.file.geometry.points = [Math.rotationMatrix * p for p in node.file.geometry.points]
node.file.geometry.edges = [(Math.rotationMatrix @ e[0], Math.rotationMatrix @ e[1]) for e in node.file.geometry.edges]
for childNode in node.file.childNodes:
childNode.matrix = Math.rotationMatrix @ childNode.matrix
# Switch to Object mode and deselect all
if bpy.ops.object.mode_set.poll():
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
name = os.path.basename(filename)
global globalBrickCount
global globalObjectsToAdd
global globalPoints
globalBrickCount = 0
globalObjectsToAdd = []
globalPoints = []
debugPrint("Creating NodeGroups")
BlenderMaterials.createBlenderNodeGroups()
# Create Blender objects from the loaded file
debugPrint("Creating Blender objects")
rootOb = createBlenderObjectsFromNode(node, node.matrix, name)
if not node.file.isModel:
if rootOb.data:
rootOb.data.transform(Math.rotationMatrix)
scene = bpy.context.scene
camera = scene.camera
render = scene.render
debugPrint("Number of vertices: " + str(len(globalPoints)))
# Take the convex hull of all the points in the scene (operation must have at least three vertices)
# This results in far fewer points to consider when adjusting the object and/or camera position.
getConvexHull()
debugPrint("Number of convex hull vertices: " + str(len(globalPoints)))
# Set camera type
if scene.camera is not None:
if Options.instructionsLook:
scene.camera.data.type = 'ORTHO'
else:
scene.camera.data.type = 'PERSP'
# Centre object only if root node is a model
if node.file.isModel and globalPoints:
# Calculate our bounding box in global coordinate space
boundingBoxMin = mathutils.Vector((0, 0, 0))
boundingBoxMax = mathutils.Vector((0, 0, 0))
boundingBoxMin[0] = min(p[0] for p in globalPoints)
boundingBoxMin[1] = min(p[1] for p in globalPoints)
boundingBoxMin[2] = min(p[2] for p in globalPoints)
boundingBoxMax[0] = max(p[0] for p in globalPoints)
boundingBoxMax[1] = max(p[1] for p in globalPoints)
boundingBoxMax[2] = max(p[2] for p in globalPoints)
# Length of bounding box diagonal
boundingBoxDistance = (boundingBoxMax - boundingBoxMin).length
boundingBoxCentre = (boundingBoxMax + boundingBoxMin) * 0.5
vcentre = (boundingBoxMin + boundingBoxMax) * 0.5
offsetToCentreModel = mathutils.Vector((-vcentre.x, -vcentre.y, -boundingBoxMin.z))
if Options.positionObjectOnGroundAtOrigin:
debugPrint("Centre object")
rootOb.location += offsetToCentreModel
# Offset bounding box
boundingBoxMin += offsetToCentreModel
boundingBoxMax += offsetToCentreModel
boundingBoxCentre += offsetToCentreModel
# Offset all points
globalPoints = [p + offsetToCentreModel for p in globalPoints]
offsetToCentreModel = mathutils.Vector((0, 0, 0))
if camera is not None:
if Options.positionCamera:
debugPrint("Positioning Camera")
camera.data.clip_start = 25 * globalScaleFactor # 0.01 at normal scale
camera.data.clip_end = 250000 * globalScaleFactor # 100 at normal scale
# Set up a default camera position and rotation
camera.location = mathutils.Vector((6.5, -6.5, 4.75))
camera.location.normalize()
camera.location = camera.location * boundingBoxDistance
camera.rotation_mode = 'XYZ'
camera.rotation_euler = mathutils.Euler((1.0471975803375244, 0.0, 0.7853981852531433), 'XYZ')
# Must have at least three vertices to move the camera
if len(globalPoints) >= 3:
isOrtho = camera.data.type == 'ORTHO'
if isOrtho:
iterateCameraPosition(camera, render, vcentre, True)
else:
for i in range(20):
error = iterateCameraPosition(camera, render, vcentre, True)
if (error < 0.001):
break
# Find the (first) 3D View, then set the view's 'look at' and 'distance'
# Note: Not a camera object, but the point of view in the UI.
areas = [area for area in bpy.context.window.screen.areas if area.type == 'VIEW_3D']
if len(areas) > 0:
area = areas[0]
with bpy.context.temp_override(area=area):
view3d = bpy.context.space_data
view3d.region_3d.view_location = boundingBoxCentre # Where to look at
view3d.region_3d.view_distance = boundingBoxDistance # How far from target
# Get existing object names
sceneObjectNames = [x.name for x in scene.objects]
# Remove default objects
if Options.removeDefaultObjects:
if "Cube" in sceneObjectNames:
cube = scene.objects['Cube']
if (cube.location.length < 0.001):
unlinkFromScene(cube)
if lightName in sceneObjectNames:
light = scene.objects[lightName]
lampVector = light.location - mathutils.Vector((4.076245307922363, 1.0054539442062378, 5.903861999511719))
if (lampVector.length < 0.001):
unlinkFromScene(light)
# Finally add each object to the scene
debugPrint("Adding {0} objects to scene".format(len(globalObjectsToAdd)))
for ob in globalObjectsToAdd:
linkToScene(ob)
# Parent only once everything has been added to the scene, otherwise the matrix_world's are
# sometimes not updated properly - some are erroneously still the identity matrix.
setupImplicitParents()
# Add cameras to the scene
for ob in globalCamerasToAdd:
cam = ob.createCameraNode()
cam.parent = rootOb
globalObjectsToAdd = []
globalCamerasToAdd = []
# Select the newly created root object
selectObject(rootOb)
# Get existing object names
sceneObjectNames = [x.name for x in scene.objects]
# Add ground plane with white material
if Options.addGroundPlane and not Options.instructionsLook:
if "LegoGroundPlane" not in sceneObjectNames:
addPlane((0,0,0), 100000 * globalScaleFactor)
blenderName = "Mat_LegoGroundPlane"
# Reuse current material if it exists, otherwise create a new material
if bpy.data.materials.get(blenderName) is None:
material = bpy.data.materials.new(blenderName)
else:
material = bpy.data.materials[blenderName]
# Use nodes
material.use_nodes = True
nodes = material.node_tree.nodes
links = material.node_tree.links
# Remove any existing nodes
for n in nodes:
nodes.remove(n)
node = nodes.new('ShaderNodeBsdfDiffuse')
node.location = 0, 5
node.inputs['Color'].default_value = (1,1,1,1)
node.inputs['Roughness'].default_value = 1.0
out = nodes.new('ShaderNodeOutputMaterial')
out.location = 200, 0
links.new(node.outputs[0], out.inputs[0])
for obj in bpy.context.selected_objects:
obj.name = "LegoGroundPlane"
if obj.data.materials:
obj.data.materials[0] = material
else:
obj.data.materials.append(material)
# Set to render at full resolution
if Options.setRenderSettings:
scene.render.resolution_percentage = 100
# Setup scene as appropriate
if Options.instructionsLook:
setupInstructionsLook()
else:
setupRealisticLook()
# Delete the temporary directory if there was one
if Configure.tempDir:
Configure.tempDir.cleanup()
debugPrint("Load Done")
return rootOb