|
|
|
"""Load LDraw GPLv2 license. |
|
|
|
This program is free software; you can redistribute it and/or |
|
modify it under the terms of the GNU General Public License |
|
as published by the Free Software Foundation; either version 2 |
|
of the License, or (at your option) any later version. |
|
|
|
This program is distributed in the hope that it will be useful, |
|
but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
GNU General Public License for more details. |
|
|
|
You should have received a copy of the GNU General Public License |
|
along with this program; if not, write to the Free Software Foundation, |
|
Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
|
|
""" |
|
|
|
""" |
|
Import LDraw |
|
|
|
This module loads LDraw compatible files into Blender. Set the |
|
Options first, then call loadFromFile() function with the full |
|
filepath of a file to load. |
|
|
|
Accepts .io, .mpd, .ldr, .l3b, and .dat files. |
|
|
|
Toby Nelson - [email protected] |
|
""" |
|
|
|
import os |
|
import sys |
|
import math |
|
import mathutils |
|
import traceback |
|
import glob |
|
import bpy |
|
import datetime |
|
import struct |
|
import re |
|
import bmesh |
|
import copy |
|
import platform |
|
import itertools |
|
import operator |
|
import zipfile |
|
import tempfile |
|
|
|
from pprint import pprint |
|
|
|
|
|
def linkToScene(ob): |
|
if bpy.context.collection.objects.find(ob.name) < 0: |
|
bpy.context.collection.objects.link(ob) |
|
|
|
|
|
def linkToCollection(collectionName, ob): |
|
|
|
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): |
|
|
|
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""" |
|
|
|
|
|
ldrawDirectory = r"" |
|
instructionsLook = False |
|
|
|
realScale = 1 |
|
useUnofficialParts = True |
|
resolution = "Standard" |
|
defaultColour = "4" |
|
createInstances = True |
|
useColourScheme = "lgeo" |
|
numberNodes = True |
|
removeDoubles = True |
|
smoothShading = True |
|
edgeSplit = True |
|
gaps = True |
|
realGapWidth = 0.0002 |
|
curvedWalls = True |
|
importCameras = True |
|
positionObjectOnGroundAtOrigin = True |
|
flattenHierarchy = False |
|
minifigHierarchy = True |
|
flattenGroups = False |
|
usePrincipledShaderWhenAvailable = True |
|
scriptDirectory = os.path.dirname( os.path.realpath(__file__) ) |
|
|
|
|
|
useLogoStuds = False |
|
logoStudVersion = "4" |
|
instanceStuds = False |
|
|
|
|
|
useLSynthParts = True |
|
LSynthDirectory = r"" |
|
studLogoDirectory = r"" |
|
|
|
|
|
|
|
|
|
resolveAmbiguousNormals = "guess" |
|
|
|
overwriteExistingMaterials = True |
|
overwriteExistingMeshes = True |
|
verbose = 1 |
|
|
|
addBevelModifier = True |
|
bevelWidth = 0.5 |
|
|
|
addWorldEnvironmentTexture = True |
|
addGroundPlane = True |
|
setRenderSettings = True |
|
removeDefaultObjects = True |
|
positionCamera = True |
|
cameraBorderPercent = 0.05 |
|
|
|
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)]) |
|
|
|
|
|
|
|
globalBrickCount = 0 |
|
globalObjectsToAdd = [] |
|
globalCamerasToAdd = [] |
|
globalContext = None |
|
globalPoints = [] |
|
globalScaleFactor = 0.0004 |
|
globalWeldDistance = 0.0005 |
|
|
|
hasCollections = None |
|
lightName = "Light" |
|
|
|
|
|
|
|
|
|
|
|
|
|
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}, |
|
'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)}, |
|
'22391':{(40, 55)}, |
|
'22889':{-45}, |
|
'28192':{45}, |
|
'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) |
|
} |
|
|
|
|
|
margin = 5 |
|
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.""" |
|
|
|
|
|
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 |
|
|
|
|
|
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 = [] |
|
|
|
|
|
Configure.appendPath(os.path.join(Configure.ldrawInstallDirectory, "models")) |
|
|
|
|
|
if Options.useLogoStuds and Options.studLogoDirectory != "": |
|
if Options.resolution == "Low": |
|
Configure.appendPath(os.path.join(Options.studLogoDirectory, "8")) |
|
Configure.appendPath(Options.studLogoDirectory) |
|
|
|
|
|
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")) |
|
|
|
|
|
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")) |
|
|
|
|
|
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") |
|
|
|
|
|
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 = "" |
|
|
|
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: |
|
ldrawPossibleDirectories = [ |
|
"~/LDraw", |
|
"~/ldraw", |
|
"~/.LDraw", |
|
"~/.ldraw", |
|
"/usr/local/share/ldraw", |
|
] |
|
|
|
|
|
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): |
|
|
|
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] |
|
|
|
|
|
brightness = math.sqrt( 0.299*R*R + 0.587*G*G + 0.114*B*B ) |
|
|
|
|
|
if brightness < 0.03: |
|
return True |
|
return False |
|
|
|
def sRGBtoLinearRGB(sRGBColour): |
|
|
|
(sr, sg, sb) = sRGBColour |
|
r = LegoColours.__sRGBtoRGBValue(sr) |
|
g = LegoColours.__sRGBtoRGBValue(sg) |
|
b = LegoColours.__sRGBtoRGBValue(sb) |
|
return (r,g,b) |
|
|
|
def hexDigitsToLinearRGBA(hexDigits, alpha): |
|
|
|
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.""" |
|
|
|
|
|
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": |
|
alpha = 1.0 |
|
elif digit == "3": |
|
alpha = 0.5 |
|
elif digit == "4": |
|
alpha = 1.0 |
|
interleaved = True |
|
elif digit == "5": |
|
alpha = 0.333 |
|
interleaved = True |
|
elif digit == "6": |
|
alpha = 0.666 |
|
interleaved = True |
|
elif digit == "7": |
|
alpha = 0.0 |
|
interleaved = True |
|
else: |
|
alpha = 1.0 |
|
|
|
if interleaved: |
|
|
|
|
|
|
|
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) |
|
|
|
|
|
return LegoColours.hexDigitsToLinearRGBA(rgb_str, alpha) |
|
return None |
|
|
|
def __overwriteColour(index, sRGBColour): |
|
if index in LegoColours.colours: |
|
|
|
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") |
|
|
|
|
|
if colour["material"] == "FABRIC": |
|
debugPrint(f"Unsupported material finish: {colour['material']} for [colour: {name} code: {code}] in line: {subline}") |
|
|
|
|
|
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": |
|
|
|
|
|
|
|
|
|
|
|
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): |
|
|
|
|
|
|
|
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. |
|
""" |
|
|
|
|
|
|
|
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) |
|
dirname = os.path.dirname(path) |
|
|
|
suffix = '' |
|
if not base: |
|
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 |
|
|
|
|
|
|
|
try: |
|
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) |
|
|
|
|
|
with open(filepath, "rb") as encode_check: |
|
encoding = encode_check.readline(3) |
|
|
|
|
|
if encoding == b"\xfe\xff\x00": |
|
return "utf_16_be" |
|
|
|
|
|
elif encoding == b"\xff\xfe0": |
|
return "utf_16_le" |
|
|
|
|
|
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): |
|
|
|
file_encoding = FileSystem.__checkEncoding(filepath) |
|
try: |
|
with open(filepath, "rt", encoding=file_encoding) as f_in: |
|
lines = f_in.readlines() |
|
except: |
|
|
|
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 = {} |
|
|
|
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 = {} |
|
__lowercache = {} |
|
|
|
def getCached(key): |
|
|
|
if key in CachedFiles.__cache: |
|
return CachedFiles.__cache[key] |
|
|
|
|
|
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 = {} |
|
|
|
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) |
|
|
|
|
|
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 |
|
|
|
|
|
pointCount = len(self.points) |
|
newFaceInfo = [] |
|
for index, face in enumerate(geometry.faces): |
|
|
|
newPoints = [] |
|
for i in face: |
|
newPoints.append(fixedMatrix @ geometry.points[i]) |
|
|
|
|
|
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 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) |
|
|
|
|
|
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() |
|
|
|
|
|
|
|
|
|
|
|
|
|
back = loc_camera - target; |
|
back.normalize() |
|
|
|
|
|
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 ] |
|
|
|
|
|
|
|
|
|
|
|
obj_camera.matrix_world = mathutils.Matrix((row1, row2, row3, row4)) |
|
|
|
|
|
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. |
|
""" |
|
|
|
|
|
if self.isRootNode: |
|
return True |
|
|
|
|
|
isBON = not self.isSubPart |
|
|
|
|
|
if Options.flattenHierarchy: |
|
isBON = self.file.isPart and not self.isSubPart |
|
|
|
|
|
if self.isLSynthPart: |
|
isBON = False |
|
|
|
|
|
if Options.instanceStuds and self.file.isStud: |
|
isBON = True |
|
|
|
return isBON |
|
|
|
def load(self): |
|
|
|
self.file = CachedFiles.getCached(self.filename) |
|
if self.file is None: |
|
|
|
self.file = LDrawFile(self.filename, self.isFullFilepath, self.parentFilepath, None, self.isSubPart) |
|
assert self.file is not None |
|
|
|
|
|
CachedFiles.addToCache(self.filename, self.file) |
|
|
|
|
|
for child in self.file.childNodes: |
|
child.load() |
|
|
|
def resolveColour(colourName, realColourName): |
|
if colourName == "16": |
|
return realColourName |
|
return colourName |
|
|
|
def printBFC(self, depth=0): |
|
|
|
|
|
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) |
|
|
|
if index == 10: |
|
return "" |
|
|
|
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 |
|
|
|
|
|
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) |
|
|
|
|
|
for faceInfo in bakedGeometry.faceInfo: |
|
faceInfo.faceColour = LDrawNode.resolveColour(faceInfo.faceColour, ourColourName) |
|
|
|
|
|
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) |
|
|
|
|
|
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): |
|
|
|
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": |
|
|
|
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 |
|
|
|
|
|
Configure.tempDir = tempfile.TemporaryDirectory() |
|
directory_to_extract_to = Configure.tempDir.name |
|
|
|
|
|
with zipfile.ZipFile(filepath, 'r') as zip_ref: |
|
zip_ref.extractall(directory_to_extract_to) |
|
|
|
|
|
filepath = os.path.join(directory_to_extract_to, "model.ldr") |
|
|
|
|
|
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 |
|
|
|
|
|
lines = FileSystem.readTextFile(filepath) |
|
if lines is None: |
|
printWarningOnce("Could not read file {0}".format(filepath)) |
|
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 |
|
|
|
|
|
self.filename = sections[0][0] |
|
self.lines = sections[0][1] |
|
|
|
|
|
for (sectionFilename, lines) in sections[1:]: |
|
|
|
file = LDrawFile(sectionFilename, False, filepath, lines, False) |
|
assert file is not None |
|
|
|
|
|
CachedFiles.addToCache(sectionFilename, file) |
|
|
|
return True |
|
|
|
def __isStud(filename): |
|
"""Is this file a stud?""" |
|
|
|
if LDrawFile.__isStudLogo(filename): |
|
return True |
|
|
|
|
|
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", |
|
"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" |
|
) |
|
|
|
def __isStudLogo(filename): |
|
"""Is this file a stud logo?""" |
|
|
|
|
|
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: |
|
|
|
if not self.__loadLegoFile(self.filename, isFullFilepath, parentFilepath): |
|
return |
|
else: |
|
|
|
self.fullFilepath = parentFilepath |
|
|
|
|
|
|
|
bfcLocalCull = True |
|
bfcWindingCCW = True |
|
bfcInvertNext = False |
|
processingLSynthParts = False |
|
camera = LDrawCamera() |
|
|
|
currentGroupNames = [] |
|
|
|
|
|
|
|
for line in self.lines: |
|
parameters = line.strip().split() |
|
|
|
|
|
if len(parameters) == 0: |
|
continue |
|
|
|
|
|
while len(parameters) < 9: |
|
parameters.append("") |
|
|
|
|
|
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 parameters[1] == "BFC": |
|
|
|
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() |
|
|
|
|
|
parameters = [] |
|
else: |
|
parameters = parameters[1:] |
|
|
|
|
|
else: |
|
if self.bfcCertified is None: |
|
self.bfcCertified = False |
|
|
|
self.isModel = (not self.isPart) and (not self.isSubPart) |
|
|
|
|
|
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)) |
|
|
|
|
|
elif parameters[0] == "2": |
|
self.geometry.parseEdge(parameters) |
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
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.""" |
|
|
|
|
|
if bpy.data.materials.get(blenderName) is None: |
|
material = bpy.data.materials.new(blenderName) |
|
else: |
|
material = bpy.data.materials[blenderName] |
|
|
|
|
|
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: |
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
|
|
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: |
|
|
|
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""" |
|
|
|
|
|
if BlenderMaterials.__is_int(colourName): |
|
colourInt = int(colourName) |
|
if colourInt in LegoColours.colours: |
|
return LegoColours.colours[colourInt] |
|
|
|
|
|
|
|
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 (colourName in BlenderMaterials.__material_list): |
|
result = BlenderMaterials.__material_list[colourName] |
|
return result |
|
|
|
|
|
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 Options.overwriteExistingMaterials is False: |
|
if blenderName in bpy.data.materials: |
|
return bpy.data.materials[blenderName] |
|
|
|
|
|
col = BlenderMaterials.__getColourData(pureColourName) |
|
material = BlenderMaterials.__createNodeBasedMaterial(blenderName, col, isSlopeMaterial) |
|
|
|
if material is None: |
|
printWarningOnce("Could not create material for blenderName {0}".format(blenderName)) |
|
|
|
|
|
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 |
|
|
|
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') |
|
|
|
|
|
node_input = group.nodes.new('NodeGroupInput') |
|
node_input.location = (x1,y1) |
|
|
|
|
|
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") |
|
|
|
group, node_input, node_output = BlenderMaterials.__createGroup('Distance-To-Center', -930, 0, 240, 0, False) |
|
BlenderMaterials.addOutputSocket(group, 'NodeSocketVectorDirection', 'Vector') |
|
|
|
|
|
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) |
|
|
|
|
|
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") |
|
|
|
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') |
|
|
|
|
|
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 |
|
|
|
|
|
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") |
|
|
|
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') |
|
|
|
|
|
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 |
|
|
|
|
|
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") |
|
|
|
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') |
|
|
|
|
|
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 |
|
|
|
|
|
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") |
|
|
|
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') |
|
|
|
|
|
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 |
|
|
|
|
|
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") |
|
|
|
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') |
|
|
|
|
|
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_geom = group.nodes.new('ShaderNodeNewGeometry') |
|
node_geom.location = (-320,-360) |
|
|
|
|
|
|
|
group.links.new(node_input.outputs['Roughness'], node_mix.inputs['Fac']) |
|
group.links.new(node_input.outputs['IOR'], node_fres.inputs['IOR']) |
|
group.links.new(node_input.outputs['Normal'], node_bump.inputs['Normal']) |
|
group.links.new(node_bump.outputs['Normal'], node_mix.inputs['Color1']) |
|
group.links.new(node_geom.outputs['Incoming'], node_mix.inputs['Color2']) |
|
group.links.new(node_mix.outputs['Color'], node_fres.inputs['Normal']) |
|
group.links.new(node_fres.outputs['Fac'], node_output.inputs['Fresnel Factor']) |
|
|
|
|
|
def __createBlenderReflectionNodeGroup(): |
|
if bpy.data.node_groups.get('PBR-Reflection') is None: |
|
debugPrint("createBlenderReflectionNodeGroup #create") |
|
|
|
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) |
|
|
|
|
|
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") |
|
|
|
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) |
|
|
|
|
|
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: |
|
|
|
return node.inputs['Subsurface Color'] |
|
|
|
|
|
return node.inputs['Base Color'] |
|
|
|
|
|
def __createBlenderLegoStandardNodeGroup(): |
|
groupName = BlenderMaterials.__getGroupName('Lego Standard') |
|
if bpy.data.node_groups.get(groupName) is None: |
|
debugPrint("createBlenderLegoStandardNodeGroup #create") |
|
|
|
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' |
|
|
|
|
|
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") |
|
|
|
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) |
|
|
|
|
|
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) |
|
|
|
|
|
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") |
|
|
|
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 |
|
|
|
|
|
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) |
|
|
|
|
|
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") |
|
|
|
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) |
|
|
|
|
|
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") |
|
|
|
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) |
|
|
|
|
|
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") |
|
|
|
|
|
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' |
|
|
|
|
|
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") |
|
|
|
|
|
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) |
|
|
|
|
|
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) |
|
|
|
|
|
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") |
|
|
|
|
|
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) |
|
|
|
|
|
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) |
|
|
|
|
|
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") |
|
|
|
|
|
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) |
|
|
|
|
|
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") |
|
|
|
|
|
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) |
|
|
|
|
|
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") |
|
|
|
|
|
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) |
|
|
|
|
|
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") |
|
|
|
|
|
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) |
|
|
|
|
|
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() |
|
|
|
|
|
|
|
|
|
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() |
|
|
|
|
|
kd = mathutils.kdtree.KDTree(len(bm.verts)) |
|
for i, v in enumerate(bm.verts): |
|
kd.insert(v.co, i) |
|
kd.balance() |
|
|
|
|
|
edgeIndices = {} |
|
for ind, geomEdge in enumerate(geometry.edges): |
|
|
|
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)] |
|
|
|
|
|
|
|
|
|
|
|
|
|
for e0 in edges0: |
|
for e1 in edges1: |
|
edgeIndices[(e0, e1)] = True |
|
edgeIndices[(e1, e0)] = True |
|
|
|
|
|
for meshEdge in bm.edges: |
|
v0 = meshEdge.verts[0].index |
|
v1 = meshEdge.verts[1].index |
|
if (v0, v1) in edgeIndices: |
|
|
|
meshEdge.smooth = False |
|
|
|
|
|
if bpy.app.version < (4, 0, 0): |
|
|
|
|
|
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: |
|
|
|
if bwLayer is not None: |
|
meshEdge[bwLayer] = 1.0 |
|
|
|
return edgeIndices |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
if meshExists and not Options.overwriteExistingMeshes: |
|
mesh = bpy.data.meshes[meshName] |
|
|
|
|
|
|
|
|
|
|
|
if mesh.users == 0 and (len(mesh.polygons) != len(geometry.faces)): |
|
|
|
return False |
|
|
|
|
|
if 'customMeshOptions' in mesh.keys(): |
|
|
|
|
|
|
|
if mesh['customMeshOptions'] == Options.meshOptionsString(): |
|
|
|
return True |
|
|
|
return False |
|
|
|
|
|
def addNodeToParentWithGroups(parentObject, groupNames, newObject): |
|
|
|
if not Options.flattenGroups: |
|
|
|
for groupName in groupNames: |
|
|
|
while len(groupName.encode("utf8")) > 63: |
|
groupName = groupName[:-1] |
|
|
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
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() |
|
|
|
line_number = 0 |
|
for line in lines: |
|
line_number += 1 |
|
line = line.strip() |
|
line = line.split("#")[0] |
|
if line: |
|
line = line.strip() |
|
original_line = line |
|
if line.startswith("Group "): |
|
|
|
finishParent() |
|
macro_name = line[6:].strip().strip(":") |
|
|
|
continue |
|
if line.startswith("Parent "): |
|
|
|
finishParent() |
|
parent = line[7:].strip().strip(":") |
|
|
|
continue |
|
if line in macros: |
|
|
|
|
|
children += macros[line] |
|
continue |
|
|
|
|
|
number1, line = number_split(line) |
|
if number1: |
|
number3 = None |
|
number2, line = number_split(line) |
|
if number2: |
|
number3, line = number_split(line) |
|
if number3: |
|
|
|
try: |
|
attachPoint = (float(number1), float(number2), float(number3)) |
|
except: |
|
attachPoint = None |
|
if attachPoint: |
|
|
|
attach_points.append(attachPoint) |
|
continue |
|
else: |
|
debugPrint("ERROR: Bad attach point found on line %d" % (line_number,)) |
|
partsHierarchy = None |
|
return |
|
|
|
|
|
children.append(original_line) |
|
|
|
finishParent() |
|
|
|
|
|
|
|
return |
|
|
|
|
|
|
|
def setupImplicitParents(): |
|
global globalScaleFactor |
|
|
|
if not Options.minifigHierarchy: |
|
return |
|
|
|
parseParentsFile(Options.scriptDirectory + '/parents.txt') |
|
|
|
if not partsHierarchy: |
|
return |
|
|
|
bpy.context.view_layer.update() |
|
|
|
|
|
parentParts = set() |
|
childParts = set() |
|
for parent, childrenData in partsHierarchy.items(): |
|
parentParts.add(parent) |
|
childParts.update(childrenData[1]) |
|
|
|
|
|
interestingParts = set() |
|
interestingParts.update(parentParts) |
|
interestingParts.update(childParts) |
|
|
|
|
|
|
|
|
|
|
|
tolerance = globalScaleFactor * 5 |
|
squaredTolerance = tolerance * tolerance |
|
|
|
|
|
|
|
parentMeshParts = {} |
|
childMeshParts = {} |
|
parentableMeshes = {} |
|
lego_part_pattern = "([A-Za-z]?\d+)($|\D)" |
|
|
|
|
|
for obj in bpy.data.objects: |
|
if obj.type != 'MESH': |
|
continue |
|
|
|
name = obj.data.name |
|
if not name.startswith('Mesh_'): |
|
continue |
|
|
|
|
|
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) |
|
|
|
|
|
if partName in parentParts: |
|
|
|
parentMeshParts[name] = partName |
|
|
|
|
|
children = partsHierarchy.get(partName) |
|
if children: |
|
parentableMeshes[name] = children |
|
|
|
|
|
if partName in childParts: |
|
|
|
childMeshParts[name] = partName |
|
|
|
|
|
parentObjects = [] |
|
childObjects = [] |
|
for obj in bpy.data.objects: |
|
if obj.type != 'MESH': |
|
continue |
|
meshName = obj.data.name |
|
if meshName in parentMeshParts: |
|
parentObjects.append(obj) |
|
|
|
if meshName in childMeshParts: |
|
childObjects.append(obj) |
|
|
|
|
|
for obj in parentObjects: |
|
meshName = obj.data.name |
|
childrenData = parentableMeshes.get(meshName) |
|
if not childrenData: |
|
continue |
|
|
|
|
|
|
|
|
|
slotLocations = [] |
|
for slot in childrenData[0]: |
|
loc = obj.matrix_world @ (mathutils.Vector(slot) * globalScaleFactor) |
|
slotLocations.append(loc) |
|
|
|
|
|
|
|
for childObj in childObjects: |
|
childMeshName = childObj.data.name |
|
childPartName = childMeshParts[childMeshName] |
|
if childPartName not in childrenData[1]: |
|
continue |
|
childLocation = childObj.matrix_world.to_translation() |
|
|
|
for slotLocation in slotLocations: |
|
|
|
diff = slotLocation - childLocation |
|
squaredDistance = diff.length_squared |
|
|
|
if squaredDistance <= squaredTolerance: |
|
temp = childObj.matrix_world |
|
childObj.parent = obj |
|
|
|
childObj.matrix_world = temp |
|
|
|
|
|
|
|
def slopeAnglesForPart(partName): |
|
""" |
|
Gets the allowable slope angles for a given part. |
|
""" |
|
global globalSlopeAngles |
|
|
|
|
|
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. |
|
""" |
|
|
|
|
|
if not isGrainySlopeAllowed: |
|
return False |
|
|
|
|
|
faceNormal = (faceVertices[1] - faceVertices[0]).cross(faceVertices[2]-faceVertices[0]) |
|
faceNormal.normalize() |
|
|
|
|
|
cosine = min(max(faceNormal.y, -1.0), 1.0) |
|
|
|
|
|
angleToGroundDegrees = math.degrees(math.acos(cosine)) - 90 |
|
|
|
|
|
|
|
|
|
return any(c[0] <= angleToGroundDegrees <= c[1] for c in slopeAngles) |
|
|
|
|
|
def createMesh(name, meshName, geometry): |
|
|
|
if not geometry.points: |
|
return (None, False) |
|
|
|
newMeshCreated = False |
|
|
|
|
|
if Options.createInstances and hasattr(geometry, 'mesh'): |
|
mesh = geometry.mesh |
|
else: |
|
|
|
if meshIsReusable(meshName, geometry): |
|
mesh = bpy.data.meshes[meshName] |
|
else: |
|
|
|
|
|
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() |
|
|
|
|
|
|
|
mesh['customMeshOptions'] = Options.meshOptionsString() |
|
|
|
newMeshCreated = True |
|
|
|
|
|
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 |
|
|
|
|
|
|
|
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)) |
|
|
|
|
|
if newMeshCreated: |
|
geometry.mesh = mesh |
|
|
|
return (mesh, newMeshCreated) |
|
|
|
|
|
def addModifiers(ob): |
|
global globalScaleFactor |
|
|
|
|
|
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 |
|
|
|
|
|
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): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
linkToScene(ob) |
|
|
|
|
|
selectObject(ob) |
|
|
|
|
|
if Options.smoothShading: |
|
|
|
bpy.ops.object.shade_smooth() |
|
|
|
if Options.instructionsLook: |
|
|
|
me = bpy.context.object.data |
|
for e in me.edges: |
|
e.use_freestyle_mark = e.use_edge_sharp |
|
|
|
|
|
deselectObject(ob) |
|
|
|
|
|
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) |
|
|
|
|
|
if Options.numberNodes: |
|
blenderName = str(globalBrickCount).zfill(5) + "_" + name |
|
else: |
|
blenderName = name |
|
globalBrickCount = globalBrickCount + 1 |
|
|
|
|
|
ob = bpy.data.objects.new(blenderName, mesh) |
|
ob.matrix_local = blenderParentTransform @ localMatrix |
|
|
|
if newMeshCreated: |
|
|
|
|
|
|
|
|
|
|
|
if hasattr(ob.data, "use_customdata_edge_bevel"): |
|
ob.data.use_customdata_edge_bevel = True |
|
else: |
|
if bpy.app.version < (4, 0, 0): |
|
|
|
linkToScene(ob) |
|
|
|
|
|
|
|
area_type = 'VIEW_3D' |
|
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) |
|
|
|
|
|
ob.empty_display_size = 250.0 * globalScaleFactor |
|
|
|
|
|
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 |
|
|
|
|
|
|
|
addNodeToParentWithGroups(blenderNodeParent, node.groupNames, ob) |
|
|
|
|
|
blenderNodeParent = ob |
|
blenderParentTransform = Math.identityMatrix |
|
|
|
|
|
|
|
|
|
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: |
|
|
|
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() |
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
if recalculateNormals: |
|
bmesh.ops.recalc_face_normals(bm, faces=bm.faces[:]) |
|
|
|
|
|
edgeIndices = addSharpEdges(bm, geometry, name) |
|
|
|
bm.to_mesh(ob.data) |
|
|
|
|
|
if (bpy.app.version >= (4, 0, 0)) and edgeIndices: |
|
|
|
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() |
|
|
|
|
|
for area in bpy.context.screen.areas: |
|
if area.type == 'VIEW_3D': |
|
for space in area.spaces: |
|
if space.type == 'VIEW_3D': |
|
space.overlay.show_edge_sharp = True |
|
|
|
|
|
if Options.gaps and node.file.isPart: |
|
|
|
|
|
|
|
gapHeight = 0.33 * Options.realGapWidth |
|
objScale = ob.scale |
|
dim = ob.dimensions |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
|
|
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) |
|
|
|
|
|
|
|
if Options.positionObjectOnGroundAtOrigin or Options.positionCamera: |
|
if mesh and mesh.vertices: |
|
localTransform = localToWorldSpaceMatrix @ localMatrix |
|
points = [localTransform @ p.co for p in mesh.vertices] |
|
|
|
|
|
globalPoints.extend(points) |
|
|
|
|
|
if node.file.isStud: |
|
ob.hide_select = True |
|
|
|
|
|
if mesh: |
|
addModifiers(ob) |
|
|
|
else: |
|
blenderParentTransform = blenderParentTransform @ localMatrix |
|
|
|
|
|
for childNode in node.file.childNodes: |
|
|
|
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)] |
|
|
|
|
|
lineset.linestyle.color = (0.0, 0.0, 0.0) |
|
|
|
|
|
if 'LegoMaterial' not in lineset.linestyle.color_modifiers: |
|
lineset.linestyle.color_modifiers.new('LegoMaterial', 'MATERIAL') |
|
|
|
|
|
lineset.linestyle.caps = 'SQUARE' |
|
|
|
|
|
lineset.linestyle.thickness_position = 'INSIDE' |
|
|
|
|
|
lineset.linestyle.thickness = thickness |
|
|
|
|
|
def setupRealisticLook(): |
|
scene = bpy.context.scene |
|
render = scene.render |
|
|
|
|
|
scene.render.engine = 'CYCLES' |
|
|
|
|
|
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 |
|
|
|
|
|
layerNames = getLayerNames(scene) |
|
if ("SolidBricks" in layerNames) or ("TransparentBricks" in layerNames): |
|
render.use_freestyle = False |
|
|
|
|
|
if scene.camera is not None: |
|
scene.camera.data.type = 'PERSP' |
|
|
|
|
|
if hasattr(render, "alpha_mode"): |
|
render.alpha_mode = 'SKY' |
|
|
|
|
|
scene.cycles.film_transparent = False |
|
|
|
|
|
layers = getLayers(scene) |
|
|
|
|
|
for i in range(len(layers)): |
|
layers[i].use = True |
|
|
|
|
|
if "SolidBricks" in layerNames: |
|
layers.remove(layers["SolidBricks"]) |
|
|
|
if "TransparentBricks" in layerNames: |
|
layers.remove(layers["TransparentBricks"]) |
|
|
|
|
|
for i in range(len(layers)): |
|
layers[i].use = True |
|
|
|
|
|
scene.use_nodes = True |
|
|
|
|
|
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"]) |
|
|
|
|
|
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: |
|
|
|
bpy.data.collections.new(name) |
|
|
|
scene.collection.children.link(bpy.data.collections[name]) |
|
|
|
|
|
def setupInstructionsLook(): |
|
scene = bpy.context.scene |
|
render = scene.render |
|
render.use_freestyle = True |
|
|
|
|
|
try: |
|
render.engine = 'BLENDER_EEVEE' |
|
except: |
|
render.engine = 'BLENDER_EEVEE_NEXT' |
|
|
|
|
|
if scene.camera is not None: |
|
scene.camera.data.type = 'ORTHO' |
|
|
|
|
|
if hasattr(render, "alpha_mode"): |
|
render.alpha_mode = 'TRANSPARENT' |
|
|
|
|
|
scene.cycles.film_transparent = True |
|
|
|
|
|
|
|
if scene.cycles.transparent_max_bounces < 80: |
|
scene.cycles.transparent_max_bounces = 80 |
|
|
|
|
|
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') |
|
|
|
|
|
layers = getLayers(scene) |
|
|
|
|
|
current_view_layer = bpy.context.view_layer |
|
|
|
|
|
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") |
|
|
|
|
|
bpy.context.window.view_layer = current_view_layer |
|
|
|
|
|
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 |
|
|
|
|
|
for i in range(len(layers)): |
|
if i not in [solidLayer, transLayer]: |
|
layers[i].use = False |
|
|
|
layers[solidLayer].use = True |
|
layers[transLayer].use = True |
|
|
|
|
|
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' |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for object in scene.objects: |
|
isTransparent = False |
|
if "Lego.isTransparent" in object: |
|
isTransparent = object["Lego.isTransparent"] |
|
|
|
|
|
if isTransparent: |
|
linkToCollection('Transparent Bricks Collection', object) |
|
else: |
|
linkToCollection('Solid Bricks Collection', object) |
|
|
|
|
|
if object.data != None: |
|
colour = object.data.materials[0].diffuse_color |
|
|
|
|
|
if LegoColours.isDark(colour): |
|
linkToCollection('White Edged Bricks Collection', object) |
|
else: |
|
linkToCollection('Black Edged Bricks Collection', object) |
|
|
|
|
|
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') |
|
|
|
|
|
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]) |
|
|
|
|
|
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 |
|
|
|
|
|
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' |
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
d = (vcentre3d - camera.location).length |
|
|
|
|
|
largestSpan = max(maxX-minX, maxY-minY) |
|
|
|
|
|
if Options.cameraBorderPercent > 0.99999: |
|
Options.cameraBorderPercent = 0.99999 |
|
|
|
|
|
|
|
scale = largestSpan/(2 - 2 * Options.cameraBorderPercent) |
|
desiredMinDistToCamera = scale * minDistToCamera |
|
|
|
|
|
offsetD = minDistToCamera - desiredMinDistToCamera |
|
|
|
|
|
centre2d = mathutils.Vector(((minX + maxX)*0.5, (minY+maxY)*0.5)) |
|
|
|
|
|
tempMatrix = camera.matrix_world.copy() |
|
tempMatrix.invert() |
|
forwards4d = -tempMatrix[2] |
|
forwards3d = mathutils.Vector((forwards4d.x, forwards4d.y, forwards4d.z)) |
|
|
|
|
|
if isOrtho: |
|
centre3d = mpinv_matrix @ mathutils.Vector((centre2d.x, centre2d.y, 0, 1)) |
|
centre3d = mathutils.Vector((centre3d.x, centre3d.y, centre3d.z)) |
|
|
|
|
|
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)) |
|
|
|
|
|
forwards = centre3d - camera.location |
|
forwards.normalize() |
|
centre3d = camera.location + d * forwards |
|
|
|
|
|
|
|
origin3d = camera.location + d * forwards3d |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if moveCamera: |
|
if isOrtho: |
|
offset3d = (centre3d - origin3d) |
|
|
|
camera.data.ortho_scale *= scale |
|
else: |
|
|
|
|
|
|
|
|
|
|
|
offset3d = 0.93 * (centre3d - origin3d) + offsetD * forwards3d |
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
globalScaleFactor = 0.0004 * Options.realScale |
|
globalWeldDistance = 0.01 * globalScaleFactor |
|
|
|
globalCamerasToAdd = [] |
|
globalContext = context |
|
|
|
|
|
|
|
Configure() |
|
LegoColours() |
|
Math() |
|
|
|
if Configure.ldrawInstallDirectory == "": |
|
printError("Could not find LDraw Part Library") |
|
return None |
|
|
|
|
|
CachedDirectoryFilenames.clearCache() |
|
CachedFiles.clearCache() |
|
CachedGeometry.clearCache() |
|
BlenderMaterials.clearCache() |
|
Configure.warningSuppression = {} |
|
|
|
if Options.useLogoStuds: |
|
debugPrint("Loading stud files") |
|
|
|
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") |
|
|
|
|
|
filename = os.path.expanduser(filename) |
|
|
|
debugPrint("Loading files") |
|
node = LDrawNode(filename, isFullFilepath, os.path.dirname(filename)) |
|
node.load() |
|
|
|
|
|
if node.file.isModel: |
|
|
|
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 |
|
|
|
|
|
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() |
|
|
|
|
|
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))) |
|
|
|
|
|
|
|
getConvexHull() |
|
debugPrint("Number of convex hull vertices: " + str(len(globalPoints))) |
|
|
|
|
|
if scene.camera is not None: |
|
if Options.instructionsLook: |
|
scene.camera.data.type = 'ORTHO' |
|
else: |
|
scene.camera.data.type = 'PERSP' |
|
|
|
|
|
if node.file.isModel and globalPoints: |
|
|
|
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) |
|
|
|
|
|
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 |
|
|
|
|
|
boundingBoxMin += offsetToCentreModel |
|
boundingBoxMax += offsetToCentreModel |
|
boundingBoxCentre += offsetToCentreModel |
|
|
|
|
|
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 |
|
camera.data.clip_end = 250000 * globalScaleFactor |
|
|
|
|
|
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') |
|
|
|
|
|
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 |
|
|
|
|
|
|
|
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 |
|
view3d.region_3d.view_distance = boundingBoxDistance |
|
|
|
|
|
sceneObjectNames = [x.name for x in scene.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) |
|
|
|
|
|
debugPrint("Adding {0} objects to scene".format(len(globalObjectsToAdd))) |
|
for ob in globalObjectsToAdd: |
|
linkToScene(ob) |
|
|
|
|
|
|
|
setupImplicitParents() |
|
|
|
|
|
for ob in globalCamerasToAdd: |
|
cam = ob.createCameraNode() |
|
cam.parent = rootOb |
|
|
|
globalObjectsToAdd = [] |
|
globalCamerasToAdd = [] |
|
|
|
|
|
selectObject(rootOb) |
|
|
|
|
|
sceneObjectNames = [x.name for x in scene.objects] |
|
|
|
|
|
if Options.addGroundPlane and not Options.instructionsLook: |
|
if "LegoGroundPlane" not in sceneObjectNames: |
|
addPlane((0,0,0), 100000 * globalScaleFactor) |
|
|
|
blenderName = "Mat_LegoGroundPlane" |
|
|
|
if bpy.data.materials.get(blenderName) is None: |
|
material = bpy.data.materials.new(blenderName) |
|
else: |
|
material = bpy.data.materials[blenderName] |
|
|
|
|
|
material.use_nodes = True |
|
|
|
nodes = material.node_tree.nodes |
|
links = material.node_tree.links |
|
|
|
|
|
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) |
|
|
|
|
|
if Options.setRenderSettings: |
|
scene.render.resolution_percentage = 100 |
|
|
|
|
|
if Options.instructionsLook: |
|
setupInstructionsLook() |
|
else: |
|
setupRealisticLook() |
|
|
|
|
|
if Configure.tempDir: |
|
Configure.tempDir.cleanup() |
|
|
|
debugPrint("Load Done") |
|
return rootOb |