diff --git "a/ImportLDraw/loadldraw/loadldraw.py" "b/ImportLDraw/loadldraw/loadldraw.py" deleted file mode 100644--- "a/ImportLDraw/loadldraw/loadldraw.py" +++ /dev/null @@ -1,4787 +0,0 @@ -# -*- 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 - tobymnelson@gmail.com -""" - -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 /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 /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 \ No newline at end of file