Monday, 27 January 2020

Week 3 - Json Files Part 1

As per Mike Pickton's suggestion I had a look into using .json files for creating a variety of modular piece sets to use for the building generator. That inevitably meant digging out my very limited and dusty Python knowledge from 4 years ago, plus learning a bunch of new stuff.

I started by testing how to read from and write to .json files in Houdini, but ultimately decided, that we needed an external tool for creating those files, which also adds the advantage of making it reusable in the future ( with some changes ).

The idea is, that in this tool you can create either a list of the entire modular kit, you want to import to Houdini or files for specific sets, e.g. ground floor, roof, etc. and also stores certain attributes of each piece, based on a list of keywords, so that in Houdini I can filter out corner, window, etc. pieces, instead of having to create separate files for all of those.

I plan to add an Editor option to the tool, so you can easily edit those .json files, for example appending or replacing pieces, maybe also based on piece type. Furthermore I need to make an executable file out of it.

"""
.json file generator for tile kits for the FMP procedural building tool
@author: Sophie Pette
"""
import tkinter.messagebox
import tkinter.filedialog
import tkinter.ttk
import tkinter as tk
from CreatorFrame import Creator
from EditorFrame import Editor
class JSONCreatorEditor(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
self.keywordlistType = ['wall','corner_90','corner_45','ww','pillar','unique','sw','window']
self.keyworklistStyle = ['skirting','rd','sq','plain','artdeco']
#GUI
screen_width = self.winfo_screenmmwidth()
screen_height = self.winfo_screenheight()
window_width = 250
window_height = 350
#Center window
x = screen_width/2 - window_width/2
y = screen_height/2 - window_height/2
self.geometry("%dx%d+%d+%d" % (window_width, window_height, x, y))
self.title('Building Generator .json file Editor')
container = tk.Frame(self)
container.pack(side = 'top', fill = 'both', expand = True)
container.grid_rowconfigure(0, weight = 1)
container.grid_columnconfigure(0, weight = 1)
menu = tk.Menu(self)
self.config(menu = menu)
self.frames = {}
for F in (Creator, Editor):
frame = F(container, self, self.keywordlistType, self.keyworklistStyle)
self.frames[F] = frame
frame.grid(row = 0, column = 0, sticky = 'nsew')
self.show_frame(Creator)
#Menu
menu.add_command(label = 'Creator', command = lambda: self.show_frame(Creator))
menu.add_command(label = 'Editor', command = lambda: self.show_frame(Editor))
#display page
def show_frame(self, cont):
frame = self.frames[cont]
frame.tkraise()
if __name__ == '__main__':
app = JSONCreatorEditor()
app.mainloop()

import tkinter as tk
import tkinter.messagebox
import tkinter.filedialog
import os, json
#Creator Window
class Creator(tk.Frame):
def __init__(self, parent, controller, kwListType, kwListStyle):
tk.Frame.__init__(self, parent)
self.keywordsType = kwListType
self.keywordsStyle = kwListStyle
self.rbState = 'disabled'
self.pieceList = []
self.data = []
#File Use
lblUse = tk.LabelFrame(self, text = 'File Use')
lblUse.pack(fill = 'both', expand = 'yes', padx = 2, pady = 2)
self.use = tk.StringVar()
self.use.set('importList')
rbImportList = tk.Radiobutton(lblUse, text = 'Import List', variable = self.use, value = 'importList', command = lambda: self.setUse('disabled'))
rbImportList.pack(anchor = 'w')
rbModuleKit = tk.Radiobutton(lblUse, text = 'Module Kit', variable = self.use, value = 'moduleKit', command = lambda: self.setUse('normal'))
rbModuleKit.pack(anchor = 'w')
#Kit type
lblType = tk.LabelFrame(self, text = 'Kit Type')
lblType.pack(fill = 'both', expand = 'yes', padx = 2, pady = 2)
self.kitType = tk.StringVar()
self.kitType.set('gfloor')
self.rbGFloor = tk.Radiobutton(lblType, text = 'Ground Floor', variable = self.kitType, value = 'gfloor', state = self.rbState, command = self.prntState)
self.rbGFloor.pack(anchor = 'w')
self.rbMFloor = tk.Radiobutton(lblType, text = 'Mid Floors', variable = self.kitType, value = 'mfloor', state = self.rbState, command = self.prntState)
self.rbMFloor.pack(anchor = 'w')
self.rbRoof = tk.Radiobutton(lblType, text = 'Roof', variable = self.kitType, value = 'roof', state = self.rbState, command = self.prntState)
self.rbRoof.pack(anchor = 'w')
self.rbFSep = tk.Radiobutton(lblType, text = 'Floor Seperators', variable = self.kitType, value = 'fseparation', state = self.rbState, command = self.prntState)
self.rbFSep.pack(anchor = 'w')
self.rbRSep = tk.Radiobutton(lblType, text = 'Roof Seperators', variable = self.kitType, value = 'rseparation', state = self.rbState, command = self.prntState)
self.rbRSep.pack(anchor = 'w')
#Buttons
lblButtons = tk.Label(self)
lblButtons.pack(fill = 'both', expand = 'yes')
btnCreateJSON = tk.Button(lblButtons, text = 'Create .json', command = self.selectFiles)
btnCreateJSON.pack(padx = 10, pady = 10)
#disable kitType buttons, if ImportList selected
def setUse(self, state):
for rb in (self.rbGFloor, self.rbMFloor, self.rbRoof, self.rbFSep, self.rbRSep): #go through all kit type ratiobuttons
rb.configure(state = str(state))
self.rbState = state
print(self.rbState)
def prntState(self):
print(self.kitType.get())
#select .FBX files
def selectFiles(self):
try:
self.data = []
self.piece_list = tk.filedialog.askopenfilenames(title = 'Select files', filetypes = [("FBX Files", "*.fbx")])
print(self.use.get())
if (self.use.get() == 'moduleKit'):
for piece in self.piece_list:
name = os.path.basename(piece)
name = name[:-len(".FBX")]
t = self.kitType.get()
if (t in name.lower()):
path = piece
pieceType = self.checkKW(name, self.keywordsType, self.keywordsStyle)[0]
pieceStyle = self.checkKW(name, self.keywordsType, self.keywordsStyle)[1]
d = {'name': name, 'path': path, 'type': pieceType, 'style': pieceStyle}
print(d)
self.data.append(d.copy())
else:
for piece in self.piece_list:
name = os.path.basename(piece)
name = name[:-len(".FBX")]
path = piece
d = {'name': name, 'path': path}
print(d)
self.data.append(d.copy())
self.writeJSON(self.data)
except:
tk.messagebox.showerror('Error', 'File Creation interrupted')
#setting piece type and style
def checkKW(self, name, pTypeList, pStyleList):
t = ''
s = ''
for kwT in pTypeList:
if (kwT in name.lower()):
t = kwT
break
for kwS in pStyleList:
if (kwS in name.lower()):
s = kwS
return [t, s]
#writing data to .json file
def writeJSON(self, data):
#select save location
loc = tk.filedialog.asksaveasfilename(title = 'Save file location', filetypes = [(".json", "*.json")], defaultextension='.json')
json_name = loc
with open((json_name ), 'w') as outfile:
json.dump(data, outfile)
tk.messagebox.showinfo("Info", json_name.rstrip() + " created")
json_name = ''
view raw CreatorFrame.py hosted with ❤ by GitHub

import tkinter as tk
class Editor(tk.Frame):
def __init__(self, parent, controller, kwListType, kwListStyle):
tk.Frame.__init__(self, parent)
self.keywords = kwListType
label = tk.Label(self, text = 'Editor')
label.pack(anchor = 'w', padx = 10, pady = 10)
view raw EditorFrame.py hosted with ❤ by GitHub


Here is how it works in its current state:

Current User Interface



As for future plans I will now move on to properly implementing the use of those files in Houdini and also do some testing with how it translates to UE4. Whilst doing that I am sure I will have to adapt how exactly the tool works, improve and expand it.

Monday, 20 January 2020

Week 1 and 2 - Planning and loads of Houdini

After the final submission of the 3-week projects, my Final Major Project partner Litha ( https://www.artstation.com/lithabacchi ) and I sat down together and started planning and gathering reference for our project.
We had previously decided to recreate a concept she found on Artstation, Atlas Square by Eddie Mendoza.

Atlas Square - Eddie Mendoza (https://www.artstation.com/artwork/ldk5V)
Whilst Litha will be responsible for creating a modular kit, other assets, textures, etc. my first main task will be coming up with a procedural building generator in Houdini, which will probably be quite the challenge, since I have only used Houdini once before, during the Destruction VFX Brief.

1 - Houdini Building Generator

I am currently still in the testing phase and so far have only been working on one floor. Lots of the VEX code needs to be optimised, e.g. putting some code into functions, etc. In the following weeks I will expand the system to work with more different pieces.

1.1 - Building Footprint
After some fiddling around in Houdini I decided to use a Curve to let the user define the corner points of the building. To make sure, that the points of the curve really define corners and are not just in the middle of a straight line, I put the curve through an Attribute Wrangle to remove those points. Essentially I am checking the angle between the edges (defined by the vector between the points on each side of an edge) on each side of a point and remove the ones, where the angle is below a certain threshold.

Before - After

Remove Points Vex

1.2 - Placing corners
After having the optimised footprint I moved on to placing the corner pieces. Since I am using instances for all the pieces I assign an attribute to all the points, called "Instance". Currently I only have one degree simple 45 and 90 degree corner each, but later I will assign those to the points from an array containing all the paths to the geometry files. Right now I am just using the file path to these two files.




To place and rotate those pieces correctly. I use the direction vector of the edge leading up to a point and assigning this vector as the point normal. However, I use atan2 to check, whether the angle is positive or negative and depending on that rotate the normal.
Corner Piece Placement

1.3 - Unique Pieces
Aside from the standard wall and corner pieces we also want the option to add unique pieces, for example storefronts, etc. so I added a multi parameter for those, so the user can add them.
Per piece you can currently chose the piece index ( they are still just basic boxes connected to a switch. Later they will work as instances. ), the building side to place it on, as well as the offset.


Unique Piece Placement
I place those pieces by getting the vector between the edge points, multiplying by offset and adding it to the start point of the edge. In the case of the unique as well as wall piece placement, I set the normal of those points to be perpendicular to the edge direction vector.

Unique Piece Placement VEX

1.4 - Wall Placement
The wall placement proofed to be a bit tricky, since at this point the points, that make up the edges to place the walls on, are not in the right order anymore. Houdini still has them stored as 1,2,3,4....13... instead of 1,2,13,3,4..., if a unique piece (point number 13 ) has been added to an edge.
So I had to figure out how to sort the point indices into the correct order. Since I had already given the unique asset points an attribute storing the edge number, I could use this to do so. Essentially I could sort the points belonging to each edge, excluding the end point, since it is also the start point for the next edge and I do not want to store it twice, based on the distance to the start point. At the end of the "Sorted Indices" array I also added the start point index again, to make any further work easier, instead of having to add a special case for the last edge.
It took me some time to get it right and I definitely have to go back and add a few more comments to the code.

Sorted Indices



Based on these sorted indices I can calculate building side lengths and how many walls I need to place on it. I also added a scale attribute to the points to rescale those wall pieces. I still need to make sure, that if the new scale is below a certain threshold, only plain walls and no window pieces get placed, otherwise we would get unrealistically scaled windows.
To calculate the edge length I also subtract the space taken by corner or unique pieces, using the bounding boxes to do so.

Place wall pieces
This is basically how far I got with the building generator in the 1 1/2 weeks since starting FMP. It involved a lot of research and figuring out how to approach certain problems.
Having sorted those I feel like it will be easier implementing the other planned features.
Here is how the tool looks in action right now:

Walls and unique assets
Building Generator -  Work in progress

2 - Maxscripts

On the side I also have been working on multiple Maxscript tools for Litha to work with.

2.1 - Unwrap Tool
The first tool is for creating quick unwraps, flatten mapping the geometry. You can choose to either have the selected meshes share the UV Space or to unwrap them individually. The UV channel is also selectable.




try (destroyDialog multitool) catch()
rollout unwrapTool "Unwrap Tool" width:264 height:184
(
GroupBox 'grp_unwrap' "Unwrap" pos:[3,12] width:255 height:163 align:#left
spinner 'spn_uv_channel' "UV Channel " pos:[10,89] width:132 height:16 range:[1,100,0] type:#integer align:#left
button 'btn_Unwrap' "Unwrap" pos:[10,135] width:150 height:30 align:#left
checkbox 'chk_share_uv' "Share UV Space" pos:[10,109] width:240 height:16 align:#left
label 'lbl_unwrap_description' "Flatten maps selected meshes with option to unwrap to a specific channel or to the same uv space." pos:[10,41] width:240 height:43 align:#left
on btn_Unwrap pressed do
(
selectedObj = getcurrentselection()
print (selectedObj.count)
if selectedObj.count != 0 then (
max modify mode
convertto selectedObj editable_poly
subObjectLevel = 4
if chk_share_uv.state == true then (
select selectedObj
UnwrapChannel = spn_uv_channel.value
uvShared = unwrap_uvw()
modpanel.addmodtoselection (uvShared)
uvShared.setTVSubObjectMode 3
max select all
uvShared.setMapChannel spn_uv_channel.value
uvShared.flattenMap 45.0 #([1,0,0], [-1,0,0], [0,1,0], [0,-1,0], [0,0,1], [0,0,-1]) 0.01 true 0 true true
uvShared.packnoparams()
uvShared.pack 2 0.001 true true true
)
else (
for obj in selectedObj do (
modPanel.setCurrentObject obj
UnwrapModifier = Unwrap_UVW name:"FlattenUVUnwrap"
addModifier obj UnwrapModifier
UnwrapChannel = spn_uv_channel.value
uv = obj.modifiers["FlattenUVUnwrap"]
uv.setMapChannel UnwrapChannel
uv.setTVSubObjectMode 3
max select all
uv.flattenMap 45.0 #([1,0,0], [-1,0,0], [0,1,0], [0,-1,0], [0,0,1], [0,0,-1]) 0.01 true 0 true true
uv.packnoparams()
uv.pack 2 0.001 true true true
)
)
select selectedObj
convertto selectedObj editable_poly
)
else (
messagebox ("No mesh selected")
)
)
)
createdialog unwrapTool

2.2 - Position and Offset Tool
The next tool was for moving vertices, or vertices of selected faces or edges, to a specified location, or offsetting them. Litha suggested that one to me to help with the creation of the modular pieces. It also has the option to save the current vertex positions and reset back to them, since undoing Maxscript stuff sometimes doesn't work.



--MOVE TOOL
--get rid of already opened instances
try (destroyDialog moveTool) catch()
--Variables
oldpos = #();
--Rollout
rollout moveTool "Position and Offset Tool" width:295 height:267
(
button 'btn_moveToX' "X" pos:[8,66] width:25 height:25 align:#left
button 'btn_moveToY' "Y" pos:[36,66] width:25 height:25 align:#left
button 'btn_moveToZ' "Z" pos:[64,66] width:25 height:25 align:#left
label 'lbl_moveToPos' "Position" pos:[112,66] width:48 height:16 align:#left
spinner 'spn_moveToPos' "" pos:[156,66] width:112 height:16 range:[-100000,100000,0] align:#left
label 'lbl_description_mv' "Moves vertices or vertices of selected Edges/ Faces to the defined Position on the X, Y or Z Axis." pos:[11,31] width:280 height:32 align:#left
button 'btn_reset' "Reset Positions" pos:[147,224] width:135 height:25 align:#left
button 'btn_saveVertPos' "Save Vertex Position " pos:[6,224] width:135 height:25 align:#left
GroupBox 'grp_moveToPos' "Move to Position" pos:[6,5] width:284 height:93 align:#left
GroupBox 'grp_offset' "Offset" pos:[6,104] width:283 height:94 align:#left
button 'btn_offsetX' "X" pos:[8,169] width:25 height:25 align:#left
button 'btn_offsetY' "Y" pos:[36,169] width:25 height:25 align:#left
button 'btn_offsetZ' "Z" pos:[64,169] width:25 height:25 align:#left
label 'lbl_offsetPos' "Offset" pos:[112,169] width:48 height:16 align:#left
spinner 'spn_offset' "" pos:[156,169] width:112 height:16 range:[-100000,100000,0] align:#left
label 'lb_description_offs' "Moves vertices or vertices of selected Edges/ Faces by the defined Offset on the X, Y or Z Axis." pos:[11,128] width:273 height:32 align:#left
--FUNCTIONS
--convert selection to Vertices
fn convertSelection obj = (
max modify mode;
vertSelection = undefined;
--select verts based on subobjectlevel
--Vertex
if ( subobjectlevel == 1 ) then (
vertSelection = polyop.getvertselection obj as array;
)
--Edge
if ( subobjectlevel == 2 ) then (
edgesSelection = polyop.getedgeselection obj as array;
obj.EditablePoly.Convertselection #Edge #Vertex
vertSelection = polyop.getvertselection obj as array;
)
--Border
if ( subobjectlevel == 3 ) then (
subobjectlevel = 2;
edgesSelection = polyop.getedgeselection obj as array;
obj.EditablePoly.Convertselection #Edge #Vertex
vertSelection = polyop.getvertselection obj as array;
)
--Face
if ( subobjectlevel == 4 ) then (
facesSelection == polyop.getfaceselection obj as array;
obj.EditablePoly.Convertselection #Face #Vertex
vertSelection = polyop.getvertselection obj as array;
)
--Element
if ( subobjectlevel == 5) then (
subobjectlevel = 4;
facesSelection = polyop.getfaceselection obj as array;
obj.EditablePoly.Convertselection #Face #Vertex
vertSelection = polyop.getvertselection obj as array;
)
if ( subobjectlevel == 0) then (
messagebox ("Nothing selected");
)
subobjectlevel = 1;
print (vertSelection as string);
return vertSelection;
)
-- MOVE TO POSITIONS
-- move to X
on btn_moveToX pressed do
(
currentObj = $;
newPos = spn_moveToPos.value;
if selection.count != 0 then (
vertSelection = convertSelection(currentObj)
--set new Position
if vertSelection.count != 0 then (
for i in vertSelection do (
currentObj.verts[i].pos.x = newPos;
)
)
else messagebox("Nothing selected");
)
else messagebox ("Nothing selected");
)
--move to Y
on btn_moveToY pressed do
(
currentObj = $;
newPos = spn_moveToPos.value;
if selection.count != 0 then (
vertSelection = convertSelection(currentObj)
--set new Position
if vertSelection.count != 0 then (
for i in vertSelection do (
currentObj.verts[i].pos.y = newPos;
)
)
else messagebox("Nothing selected");
)
else messagebox ("Nothing selected");
)
--move to Z
on btn_moveToZ pressed do
(
currentObj = $;
newPos = spn_moveToPos.value;
if selection.count != 0 then (
vertSelection = convertSelection(currentObj)
--set new Position
if vertSelection.count != 0 then (
for i in vertSelection do (
currentObj.verts[i].pos.z = newPos;
)
)
else messagebox("Nothing selected");
)
else messagebox ("Nothing selected");
)
--VERTEX POSITIONS
--reset to saved vertex Position
on btn_reset pressed do
(
if oldPos.count != 0 then (
for i=1 to oldPos.count do (
$.verts[i].pos = oldPos[i];
)
)
else messagebox ("No positions saved");
)
--save current Vertex Positions
on btn_saveVertPos pressed do
(
for i=1 to $.GetNumVertices() do (
temp = $.verts[i].pos;
append oldPos temp;
)
)
--OFFSET
--offset X
on btn_offsetX pressed do
(
currentObj = $;
offset = spn_offset.value;
if selection.count != 0 then (
vertSelection = convertSelection(currentObj)
--set new Position
if vertSelection.count != 0 then (
for i in vertSelection do (
currentObj.verts[i].pos.x += offset;
)
)
else messagebox("Nothing selected");
)
else messagebox ("Nothing selected");
)
--offset Y
on btn_offsetY pressed do
(
currentObj = $;
offset = spn_offset.value;
if selection.count != 0 then (
vertSelection = convertSelection(currentObj)
--set new Position
if vertSelection.count != 0 then (
for i in vertSelection do (
currentObj.verts[i].pos.y += offset;
)
)
else messagebox("Nothing selected");
)
else messagebox ("Nothing selected");
)
--offsetZ
on btn_offsetZ pressed do
(
currentObj = $;
offset = spn_offset.value;
if selection.count != 0 then (
vertSelection = convertSelection(currentObj)
--set new Position
if vertSelection.count != 0 then (
for i in vertSelection do (
currentObj.verts[i].pos.z += offset;
)
)
else messagebox("Nothing selected");
)
else messagebox ("Nothing selected");
)
)
createdialog moveTool
2.3 -Export Tool
This one is pretty self-explanatory. You can copy paste a file location and choose a unit for export. The tool checks, if the location is valid, or asks if you want to create the folder, if it does not exist yet. You can also choose to export meters and centimetres. I will expand this tool in the future.



--get rid of already opened instances
try (destroyDialog export_tool) catch()
rollout export_tool "Export Tool" width:232 height:267
(
dropdownList 'ddl_units' "" pos:[105,26] width:120 height:21 items:#("cm", "m") align:#left
label 'lbl_unit' "Export Unit" pos:[10,26] width:61 height:17 align:#left
checkbox 'chk_exportMCM' "Export object both in cm and m" pos:[10,53] width:221 height:18 align:#left
GroupBox 'grp_units' "Unit Setup" pos:[3,7] width:227 height:71 align:#left
edittext 'edt_exportPath' "" pos:[105,156] width:120 height:20 align:#left
label 'lbl_exportPath' "Export Path" pos:[10,156] width:90 height:20 align:#left
label 'lbl_exportPathDesc' "The tool will create the folders 'cm' and 'm' in the specified location, depending on unit setup" pos:[10,108] width:217 height:44 align:#left
button 'btn_export' "Export FBX" pos:[7,230] width:214 height:30 align:#left
button 'btn_resetXForm' "Reset X Form" pos:[7,190] width:214 height:30 align:#left
GroupBox 'grp_exportOpt' "Export Settings" pos:[3,88] width:227 height:94 align:#left
--check if export path is valid
fn isExportPathValid exportPath = (
if edt_exportPath.text != "" then (
doesExist = doesFileExist exportPath;
if doesExist == false then (
createFolder = querybox("Folder '" + exportPath + "' doesn't exist. Do you want to create it?") beep:true;
if createFolder then (
success = makeDir exportPath;
if success then (
return true;
)
else (
messagebox("Folder could not be created");
return false;
)
)
else return false;
)
else return true;
)
else messagebox("Please enter an Export Path");
return false;
)
--EXPORT
on btn_export pressed do
(
selectedObj = getcurrentselection();
-- is at least one object selected?
if(selectedObj.count != 0) then (
--export both m and cm?
if chk_exportMCM.state == false then (
exportPath = edt_exportPath.text;
exportPath = exportPath + "/" + ( ddl_units.selected as string );
FbxExporterSetParam "convertUnit" (ddl_units.selected as string );
print(FbxExporterGetParam "convertUnit");
--is export path valid
if (isExportPathValid(exportPath)) then (
for obj in selectedObj do (
select obj
originalpos = obj.pos;
obj.pos = [0,0,0];
exportPathObj = exportPath + "/" + (obj.name as string);
print(exportPathObj);
exportFile exportPathObj #noPrompt selectedOnly:true using:FBXEXP;
obj.pos = originalpos;
)
)
)--export both m and cm?
else (
for i in ddl_units.items do (
exportPath = edt_exportPath.text;
exportPath = exportPath + "/" + ( i as string );
FbxExporterSetParam "convertUnit" ( i as string );
print(FbxExporterGetParam "convertUnit");
--is export path valid
if (isExportPathValid(exportPath)) then (
for obj in selectedObj do (
select obj
originalpos = obj.pos;
obj.pos = [0,0,0];
exportPathObj = exportPath + "/" + (obj.name as string);
print(exportPathObj);
exportFile exportPathObj #noPrompt selectedOnly:true using:FBXEXP;
obj.pos = originalpos;
)
)
)
)
)--is at least one object selected?
else messagebox("Nothing selected");
)
--RESET XFORM
on btn_resetXForm pressed do
(
selectedObj = getcurrentselection();
if (selectedObj.count != 0) then (
for obj in selectedObj do (
resetxform obj;
converttopoly obj;
print( obj.name + " XForm reset " );
)
)
else messagebox("Nothing selected");
)
)
createdialog export_tool