UnityLink:
Wolfram Language Support for Unity

Alec Shedelbower
Wolfram Research

Overview of the Talk

1. Motivation

2. What’s New

3. Physics Simulations

4. Rendering

5. Navigation (NavMesh)

6. Building

Motivation

What does Unity provide?

Physics Engine (PhysX)

Lighting and Rendering (Shadows, Reflections, ...)

Other Built-In Utilities (Path Finding, Terrain Generation, ...)

What does the Wolfram Language provide?

Scripting Environment

Algorithmic Content Creation (Meshes, Textures, Audio, ...)

Tracking and Analytics

What’s New?

Added Scene Functions

CreateUnityScene,OpenUnityScene,FindUnityScene,MergeUnityScenes,SaveUnityScene,CloseUnityScene

Control the Editor Mode From the WL

UnityPlay,UnityPause,UnityStop,UnityStep

Take Screen Captures From Unity Cameras

UnityScreenCapture

Build Projects From the WL

UnityBuild

Added additional support for Materials and Textures

FindUnityShader

Added additional support for Particle Systems

Reworked many existing functions to be more flexible.

FindUnityComponent,FindUnityGameObject,FindUnityAsset

Physics Simulations

Load the UnityLink package:

Quiet@Needs["UnityLink`"]

Create a new project:

UnityOpen["Physics Simulations"]

Create some asset directories to keep our project organized:

CreateUnityAssetDirectory[{"Scenes","Prefabs","Materials","Textures","Meshes"}]

Wrecking Ball: Setup

Create and save a new scene for our wrecking ball simulation:

CreateUnityScene[File["Scenes/Wrecking Ball"]]

Create texture assets:

cautionTexture=CreateUnityTexture2D[File["Textures/Caution"],]
boxTexture=CreateUnityTexture2D[File["Textures/Box"],]
ballTexture=CreateUnityTexture2D[File["Textures/Ball"],]

Create materials using our texture assets:

cautionMaterial=CreateUnityMaterial[File["Materials/Caution"],cautionTexture]
cautionMaterial[["MainTextureScale"]]={5,5}

boxMaterial=CreateUnityMaterial[File["Materials/Box"],boxTexture]

ballMaterial=CreateUnityMaterial[File["Materials/Ball"],ballTexture]

Wrecking Ball: Creating Geometry

Create a platform and set its material:

platform=CreateUnityPlane["Platform",{0,0,0},20]
platform[["SharedMaterial"]]=cautionMaterial

Create a box and set its material:

box=CreateUnityCube["Box",{0,0.25,0},0.5,Properties-> {"SharedMaterial"-> boxMaterial}]

Attach a rigidbody to our box so it is affected by physics:

CreateUnityRigidbody[box]

Create a prefab from the box game object for convenience:

prefab=CreateUnityPrefab[File["Prefabs/Box"],box]

Delete the original box game object:

DeleteUnityGameObject[box]

Wrecking Ball: Building the Wall

Get some positions for the boxes that will make up our wall:

positions=Flatten[Table[{x,y+0.25,0},{x,-1.5,1.5,0.5},{y,0,4,0.6}],1];
positions//Short

Graphics3D[Table[Cuboid[pos,pos+{0.5,0.5,0.5}],{pos,positions}],Boxed->False]

Create an empty game object to be the parent of all the wall boxes:

wall=CreateUnityGameObject["Wall"]

Instantiate a box at each position and make it a child of the wall object:

Do[CreateUnityGameObject[prefab,Properties->{"Position"->pos,"Parent"-> wall}],{pos,positions}]

Wrecking Ball: Creating the Wrecking Ball

Create a Unity sphere primitive to act as our wrecking ball:

ball=CreateUnitySphere["Wrecking Ball",{0,12,10},1]

Attach a hinge component to the wrecking ball and set its anchor to be above the wall:

hinge=CreateUnityHingeJoint[ball]
hinge[["Anchor"]]={0,0,-5};

Increase the mass of the ball to give it more power:

rb=FindUnityComponent[ball,"Rigidbody"]
rb[["Mass"]]=100

Create a pole from the wrecking ball to its anchor point:

pole=CreateUnityCylinder["Pole",Properties-> {"Parent"-> ball,"LocalPosition"-> {0,0,-2.5},"LocalScale"-> {0.1,2,0.1},"EulerAngles"-> {90,0,0}}]

Assign the material to the ball:

ball[["SharedMaterial"]]=ballMaterial;
pole[["SharedMaterial"]]=ballMaterial;

Wrecking Ball: Demolish the Wall

Start the simulation:

UnityPlay[]

Stop the simulation:

UnityStop[]

Wrecking Ball: More Walls

Move the camera to get a better view of the walls:

camera=FindUnityGameObject["Main Camera"]
camera[["Position"]]={10,3,0};
camera[["EulerAngles"]]={10,-90,0};

Find the wall parent object:

wall=FindUnityGameObject["Wall"]

Duplicate the wall to add more layers:

walls=Table[CreateUnityGameObject[wall,Properties-> {"Position"-> {0,0,z}}],{z,-1.8,1.8,0.6}]

Delete our original wall object:

DeleteUnityGameObject[wall]

Wrecking Ball: Demolish the Wall

Start the simulation and immediately pause it:

UnityPause[]
UnityPlay[]

UnityScreenCapture[ImageSize-> 300]

Step 100 frames to when the wrecking ball first hits the boxes:

UnityStep[100]

UnityScreenCapture[ImageSize-> 300]

Finish the simulation:

UnityPause[]

Stop the simulation:

UnityStop[]

Cloth: Creating Assets

Let’s save the previous wrecking ball scene:

SaveUnityScene[]

Create a new scene:

CreateUnityScene[File["Scenes/Cloth"]]

Create materials for our cloth and rod objects:

clothMaterial=CreateUnityMaterial[File["Materials/Cloth"],Red]
rodMaterial=CreateUnityMaterial[File["Materials/Rod"],GrayLevel[0.2]]

Cloth: Creating Rods

Create 3 rods for our cloth to fall onto

Table[CreateUnityCylinder[Properties-> {"Position"-> {0,0,(i-2)*0.5},"EulerAngles"-> {0,0,90},"LocalScale"-> {0.25,3,0.25},"SharedMaterial"-> rodMaterial}],{i,3}]

Increase the collider radius of the rods to improve collision detection:

SetProperty[#,"Radius"-> 0.7]&/@FindUnityComponent[All,"CapsuleCollider",All];

Move the camera to get a better view:

camera=FindUnityGameObject["Main Camera"]
camera[["Position"]]={0,4,-5.5};
camera[["EulerAngles"]]={22.5,0,0};

Cloth: Create the Cloth Game Object

Discretize a disk region so that it has plenty of vertices:

mesh=DiscretizeRegion[Disk[],MaxCellMeasure->0.003]

Create a Unity mesh asset from the disk mesh:

uMesh=CreateUnityMesh[File["Meshes/Cloth Mesh"],mesh]

Create the cloth game object:

go=CreateUnityGameObject["Cloth",uMesh]
go[["EulerAngles"]]={90,0,0}
go[["LocalScale"]]={2,2,0}
go[["Position"]]={0,1.5,0}

Set the cloth’s material:

go[["SharedMaterial"]]=clothMaterial

Cloth: Add Cloth Behavior

Attach a cloth component to the cloth game object:

cloth=CreateUnityCloth[go]

Tell the cloth to collide with the colliders of the rods:

cloth[["CapsuleColliders"]]=FindUnityComponent[All,"CapsuleCollider",All]

Save the scene:

SaveUnityScene[]

Cloth: Run the Simulation

Run the simulation:

UnityPlay[]

Stop the simulation:

UnityStop[]

Rendering: Setup

Let’s close the Physics Simulation project:

CloseUnity[]

Create a new project:

UnityOpen["Rendering"]

Create a new scene:

CreateUnityScene[File["Render Scene"]]

Rendering: Building the Room

Let’s start by making a room out of 6 planes.

Get the positions and rotations of the wall planes:

size=10;
positions={{0,0,0},{0,size,0},{-size/2,size/2,0},{size/2,size/2,0},{0,size/2,size/2},{0,size/2,-size/2}};
rotations={{0,0,0},{180,0,0},{0,0,-90},{0,0,90},{-90,0,0},{90,0,0}};

Create the planes and assign them to their corresponding positions and rotations:

planes=Table[CreateUnityPlane["Plane "<>ToString[i],Properties-> {"Position"-> positions[[i]],"EulerAngles"-> rotations[[i]]}],{i,Length[positions]}]

Rendering: Adding Objects

Let’s add some objects to the room.

Move the camera to get a better view:

camera=FindUnityGameObject["Main Camera"]
camera[["Position"]]={0,5,-6}
camera[["EulerAngles"]]={30,0,0}

Add teapot:

teapotMesh=;
teapot=CreateUnityGameObject["Teapot",teapotMesh]
teapot[["EulerAngles"]]={0,-37,0};
teapot[["Position"]]={-4.3,-0.57,2.75};
teapot[["LocalScale"]]={2,2,2};

Add a guitar:

guitarMesh=;
guitar=CreateUnityGameObject["Guitar",guitarMesh]
guitar[["EulerAngles"]]={0,115,-60};
guitar[["Position"]]={3.52,1.817,4.188};
guitar[["LocalScale"]]={0.1,0.1,0.1};

Add a spikey:

spikey=CreateUnityGameObject["Spikey",PolyhedronData["Spikey"]]
spikey[["LocalScale"]]={0.5,0.5,0.5};
spikey[["Position"]]={2.5,0.625`,-1.8};

Add some Unity primitives:

sphere=CreateUnitySphere["Sphere",{0,1.25,0},1.25]
cube=CreateUnityCube["Cube",{-2.5,0.5,-1.8}]

Rendering: Adding Materials

Create materials for the room planes:

planeMat=CreateUnityMaterial[GrayLevel[0.2]]
Do[plane[["SharedMaterial"]]=planeMat,{plane,planes}]
plane1=FindUnityGameObject["Plane 1"];
plane1[["SharedMaterial"]]=CreateUnityMaterial[GrayLevel[0.7]];

Create materials for the room objects:

teapot[["SharedMaterial"]]=CreateUnityMaterial[RGBColor[0, 0.5, 1]];
guitar[["SharedMaterial"]]=CreateUnityMaterial[RGBColor[1, 0.22, 0]];
spikey[["SharedMaterial"]]=CreateUnityMaterial[Red];
sphere[["SharedMaterial"]]=CreateUnityMaterial[White,Properties-> {"_Glossiness"-> 1.0,"_Metallic"->1.0 }];
cube[["SharedMaterial"]]=CreateUnityMaterial[RGBColor[0, 0.75, 0]];

Attach a reflection probe to the sphere to create a mirror effect:

CreateUnityReflectionProbe[sphere]

Set all game objects to static to start the baking process:

Do[go[["IsStatic"]]=True,{go,FindUnityGameObject[All]}]

Rendering: New Perspectives

OpenUnityScene["Pre-Rendered"]

Create a camera to see the room from the back:

cameraBack=CreateUnityCamera["Camera Back"]
cameraBack[["Transform","Position"]]={0,5,6};
cameraBack[["Transform","EulerAngles"]]={30,180,0};

Create a camera to see the room from the top:

cameraTop=CreateUnityCamera["Camera Top"]
cameraTop[["Transform","Position"]]={0,10,0}
cameraTop[["Transform","EulerAngles"]]={90,0,0}

Get a reference to our main camera at the front:

cameraFront=FindUnityComponent["Main Camera","Camera"]

Rendering: Viewing the Scene

Get the front camera view:

UnityScreenCapture[cameraFront]

Specify a specific resolution:

UnityScreenCapture[cameraFront,ImageResolution-> {2048,1024}]

Get the view from all 3 camera:

UnityScreenCapture[{cameraFront,cameraBack,cameraTop},ImageSize-> 400]

Navigation: Setup

Save and close the previous project:

SaveUnityScene[];
CloseUnity[];

Create a new project:

UnityOpen["Navigation"]

Create the asset directories:

CreateUnityAssetDirectory[{"Scenes","Materials"}]

Create a new scene:

CreateUnityScene[File["Scenes/Maze"]]

Create material assets:

playerColor=Red;
wallColor=Hue[0.59, 1, 1];
platformColor=GrayLevel[0.1];
obstacleColor=Hue[0.125];
textColor=GrayLevel[0.5];
playerMat=CreateUnityMaterial[File["Materials/Player"],playerColor]
wallsMat=CreateUnityMaterial[File["Materials/Walls"],wallColor]
platformMat=CreateUnityMaterial[File["Materials/Platform"],platformColor]
obstaclesMat=CreateUnityMaterial[File["Materials/Obstacles"],obstacleColor]

Navigation: Creating Scene Geometry

Create the platform:

platform=CreateUnityPlane["Platform",{0,0,0},20,Properties-> {"IsStatic"-> True,"Material"->platformMat }]

Create the camera:

camera=FindUnityComponent["Main Camera","Camera"]
camera[["Transform","Position"]]={0,25,-12};
camera[["Transform","EulerAngles"]]={67,0,0};
camera[["ClearFlags"]]="Color";
camera[["BackgroundColor"]]=RGBColor[{0, 0., 0.37}];

Create a player with a camera:

player=CreateUnityCapsule["Player",{-5,1,0},Properties-> {"Material"->playerMat }]
playerCamera=CreateUnityCamera[player]
playerCamera[["ClearFlags"]]="Color";
playerCamera[["BackgroundColor"]]=RGBColor[{0, 0., 0.37}];
playerCamera[["Enabled"]]=False;

Create the Walls

walls=CreateUnityGameObject["Walls"]

l=10;
d=2;
h=3;

(* Outer Walls *)
walls[["EulerAngles"]]={0,0,0};
CreateUnityCube["Wall (1)",{0,h/2,-l-d/2},Properties-> {"Parent"-> walls,"LocalScale"-> {2l+2d,h,d}}]
walls[["EulerAngles"]]={0,90,0}
CreateUnityCube["Wall (2)",{0,h/2,-l-d/2},Properties-> {"Parent"-> walls,"LocalScale"-> {2l,h,d}}]
walls[["EulerAngles"]]={0,180,0};
CreateUnityCube["Wall (3)",{0,h/2,-l-d/2},Properties-> {"Parent"-> walls,"LocalScale"-> {2l+2d,h,d}}]
walls[["EulerAngles"]]={0,270,0}
CreateUnityCube["Wall (4)",{0,h/2,-l-d/2},Properties-> {"Parent"-> walls,"LocalScale"-> {2l,h,d}}]
walls[["EulerAngles"]]={0,0,0}

(* Ramps *)
b=l*0.5;
a=h;
c=Sqrt[a^2+b^2];
CreateUnityCube["Ramp (1)",{l-b/2,(a/2),-l+d/2},Properties-> {"Parent"-> walls,"LocalScale"-> {c,0.05,d},"EulerAngles"-> {0,0,ArcCos[b/c]/Degree}}]
CreateUnityCube["Ramp (2)",{l-b/2,(a/2),l-d/2},Properties-> {"Parent"-> walls,"LocalScale"-> {c,0.05,d},"EulerAngles"-> {0,0,ArcCos[b/c]/Degree}}]
CreateUnityCube["Ramp (3)",{-l+b/2,(a/2),-l+d/2},Properties-> {"Parent"-> walls,"LocalScale"-> {c,0.05,d},"EulerAngles"-> {0,0,-ArcCos[b/c]/Degree}}]
CreateUnityCube["Ramp (4)",{-l+b/2,(a/2),l-d/2},Properties-> {"Parent"-> walls,"LocalScale"-> {c,0.05,d},"EulerAngles"-> {0,0,-ArcCos[b/c]/Degree}}]

d=2;
g=4;
t=0.1;
(* Inner Walls *)
CreateUnityCube["Wall (5)",{-d*N[1/3],h/2,-l/2-g/4},Properties-> {"Parent"-> walls,"LocalScale"-> {d*N[1/3],h,l-g/2}}]
CreateUnityCube["Wall (6)",{d*N[1/3],h/2,-l/2-g/4},Properties-> {"Parent"-> walls,"LocalScale"-> {d*N[1/3],h,l-g/2}}]
CreateUnityCube["Wall (7)",{0,h-t/2,-l/2-g/4},Properties-> {"Parent"-> walls,"LocalScale"-> {d,t,l-g/2}}]
CreateUnityCube["Wall (8)",{0,h/2,l/2+g/4},Properties-> {"Parent"-> walls,"LocalScale"-> {d,h,l-g/2}}]

Do[SetProperty[wall,{"Material"-> wallsMat,"IsStatic"-> True}],{wall,walls[["Children"]]}]

Create the Door

(* Door *)
door=CreateUnityCube["Door",{0,(h-t)/2,-l/2-g/4},Properties-> {"LocalScale"-> {d*N[1/3]*0.95,h-t,l-g/2},,"Material"-> obstaclesMat}]
CreateUnityRigidbody[door,Properties-> {"Mass"-> 10,"Constraints"-> {"FreezePositionX","FreezePositionY","FreezeRotationX","FreezeRotationY","FreezeRotationZ"},"CollisionDetectionMode"-> "Continuous"}]
CreateUnityConstantForce[door,Properties-> {"Force"-> {0,0,-1000}}]

Create the Obstacles

obstacles=CreateUnityGameObject["Obstacles"]

createObstacle[i_,pos_,g_,scale_:{1,1,1}]:=
Block[{obstacle,rb},
obstacle=CreateUnityGameObject["Obstacle ("<>ToString[i]<>")",g,Properties-> {"Parent"-> obstacles,"Position"-> pos,"LocalScale"-> scale},VertexNormals-> {"SmoothingAngle",0.0001}];
rb=CreateUnityRigidbody[obstacle];
SetProperty[FindUnityComponent[obstacle,"MeshCollider"],"Convex"-> True];
]

createObstacle[1,{-5,1.25,-5},PolyhedronData["Spikey"]]

createObstacle[2,{5,1.1,-5},BoundaryDiscretizeGraphics[PolyhedronData["Dodecahedron"]]]

createObstacle[3,{5,1.1,5},BoundaryDiscretizeGraphics[]]

createObstacle[4,{-5,1.85,5},BoundaryDiscretizeGraphics[],{0.8,0.8,0.8}]

Do[SetProperty[ob,{"Material"-> obstaclesMat}],{ob,obstacles[["Children"]]}]

Navigation: NavMesh Components

Attach a NavMesh agent component to the player:

CreateUnityNavMeshAgent[player]

Attach NavMesh obstacle components to the door and obstacles:

CreateUnityNavMeshObstacle[door,Properties-> {"Carving"-> True,"CarveOnlyStationary"-> False}]

Table[CreateUnityNavMeshObstacle[obstacle,Properties-> 
{"Carving"-> True,"CarveOnlyStationary"-> True,"Shape"-> "Capsule"}],{obstacle,obstacles[["Children"]]}]

Build the NavMesh:

UnityExecute["UnityEditor.AI.NavMeshBuilder.BuildNavMesh"]

Navigation: Running the Scene

Control Function

createControls[]:=
Block[{res,doorButton,pauseButton,switchCameraButton,graphic},
doorForce=FindUnityComponent["Door","ConstantForce"];
agent=FindUnityComponent["Player","NavMeshAgent"];
playerCamera=FindUnityComponent["Player","Camera"];
worldCamera=FindUnityComponent["Main Camera","Camera"];
doorClosed=Last[doorForce[["Force"]]]>0;
doorButton=Button["Open/Close Door",doorForce[["Force"]]=doorForce[["Force"]]*-1;doorClosed=!doorClosed];
pauseButton=Button["Stop/Start",agent[["IsStopped"]]=!agent[["IsStopped"]]];
switchCameraButton=Button["Switch View",playerCamera[["Enabled"]]=!playerCamera[["Enabled"]];worldCamera[["Enabled"]]=!worldCamera[["Enabled"]]];
graphic=DynamicModule[{destination={-100,100},path={}},EventHandler[Graphics[{wallColor,Rectangle[{-12,-12},{12,12}],platformColor,Rectangle[{-10,-10},{10,10}],obstacleColor,Dynamic@Translate[Rectangle[{-1/3,-10},{1/3,-2}],If[doorClosed,{0,4},{0,0}]],wallColor,Rectangle[{-1,-10},{1,-2}],Rectangle[{-1,10},{1,2}],Lighter[wallColor],Rectangle[{5,-8},{10,-10}],Rectangle[{5,8},{10,10}],Rectangle[{-5,8},{-10,10}],Rectangle[{-5,-8},{-10,-10}],Red,AbsoluteThickness[2],Dynamic@Circle[destination,0.6],Dynamic[If[Length[path]≤ 2,path=Join[{agent[["Transform"]][["Position"]][[{1,3}]]},agent[["Path"]][["Corners"]][[All,{1,3}]]]];Line[path],Line[path],UpdateInterval->1],EdgeForm[None],playerColor},PlotRange->{{-12,12},{-12,12}},ImageSize->300],"MouseDown":>(destination=MousePosition["Graphics"];agent[["Destination"]]={destination[[1]],0,destination[[2]]};path={})]];
res=Panel@Grid[{{graphic},{Row[{pauseButton,doorButton,switchCameraButton}]}}];
res
]

Run the Simulation

UnityPlay[]

createControls[]

UnityStop[]

Building : Basics

Open our Physics Simulation project:

CloseUnity[]
UnityOpen["Physics Simulations"]

Open the wrecking ball scene:

OpenUnityScene["Scenes/Wrecking Ball"]

Build the project:

UnityBuild[]

Run the built application:

SystemOpen[%]

Building: Options

Build Target

Get the current build target:

$UnityBuildTarget

Get all possible build targets:

$UnityBuildTargets

Specify the build target:

UnityBuild["Platform"->"StandaloneOSX" ]

Name and Directory

Build the application in a specific directory with the given name:

UnityBuild["BuildName"->"Custom Name","BuildDirectory"->"~/Desktop/"]

Icon

Specify the icon to use for the build application:

UnityBuild["Icon"-> ]

SystemOpen[%]

Questions?