In Godot 2D you can use a Draw2D function to draw lines and other shapes in the scene.
But in 3D we don’t have a corresponding method, Draw3D just doesn’t exist.
This guide will focus on adding this missing function to your Godot project.
This page was largely inspired by
Ryan Mirch's Draw3D script.
[GD script youtube tutorial]
Start by creating a C# script anywhere in your project.
Name the script “Draw3D.cs” and paste the following code into the script:
using Godot;
public partial class Draw3D : Node3D
{
public static MeshInstance3D Line(Vector3 from, Vector3 to, Color? color = null)
{
var meshInstance = new MeshInstance3D();
var immediateMesh = new ImmediateMesh();
var material = new StandardMaterial3D();
meshInstance.Mesh = immediateMesh;
meshInstance.CastShadow = GeometryInstance3D.ShadowCastingSetting.Off;
immediateMesh.SurfaceBegin(Mesh.PrimitiveType.Lines, material);
immediateMesh.SurfaceAddVertex(from);
immediateMesh.SurfaceAddVertex(to);
immediateMesh.SurfaceEnd();
material.ShadingMode = StandardMaterial3D.ShadingModeEnum.Unshaded;
material.AlbedoColor = color ?? Colors.WhiteSmoke;
(Engine.GetMainLoop() as SceneTree).Root.CallDeferred("add_child", meshInstance);
return meshInstance;
}
public static MeshInstance3D Point(Vector3 position, float radius = 0.05f, Color? color = null)
{
var meshInstance = new MeshInstance3D();
var sphereMesh = new SphereMesh();
var material = new StandardMaterial3D();
meshInstance.Mesh = sphereMesh;
meshInstance.CastShadow = GeometryInstance3D.ShadowCastingSetting.Off;
meshInstance.Position = position;
sphereMesh.Radius = radius;
sphereMesh.Height = radius * 2f;
sphereMesh.Material = material;
material.ShadingMode = StandardMaterial3D.ShadingModeEnum.Unshaded;
material.AlbedoColor = color ?? Colors.WhiteSmoke;
(Engine.GetMainLoop() as SceneTree).Root.CallDeferred("add_child", meshInstance);
return meshInstance;
}
}
(This script doesn’t need to be appended to any nodes in the scene)
Now you should have two new methods to work with:
Draw3D.Line();
Draw3D.Point();
To draw a line you’ll need to specify where it starts and where it ends:
public override void _Ready()
{
Vector3 from = new Vector3(-2,1,2);
Vector3 to = new Vector3(2,1,-2);
Draw3D.Line(from,to);
}
If you run this code, you should see a white line appear.
You can also change its color like this:
Vector3 from = new Vector3(-2,1,2);
Vector3 to = new Vector3(2,1,-2);
Draw3D.Line(from,to,Colors.Red);
You'll need to specify a position, and optionally a size and color.
Vector3 position = new Vector3(0,1,0);
Draw3D.Point(position,0.1f,Colors.Orange);
This section will explain what the Draw3D.cs script does and how to change it to suit your needs.
First we declare our types:
meshInstance – a representation of an object in our scene.
immediateMesh – a representation of the mesh’s shape/vertex data.
[More on how to use ImmediateMesh]
material – a representation of the mesh’s texture.
var meshInstance = new MeshInstance3D();
var immediateMesh = new ImmediateMesh();
var material = new StandardMaterial3D();
Apply the immediateMesh (shape) to our meshInstance (object).
meshInstance.Mesh = immediateMesh;
Disable the mesh’s shadows.
meshInstance.CastShadow = GeometryInstance3D.ShadowCastingSetting.Off;
To begin generating geometry call SurfaceBegin(), it takes a PrimitiveType argument.
immediateMesh.SurfaceBegin(Mesh.PrimitiveType.Lines, material);
Add vertices to our immediateMesh.
immediateMesh.SurfaceAddVertex(from);
immediateMesh.SurfaceAddVertex(to);
To stop generating geometry call SurfaceEnd().
immediateMesh.SurfaceEnd();
ShadingMode – Remove self shadow, so our shape’s color will be uniform.
AlbedoColor – Set the default color to be Colors.WhiteSmoke.
material.ShadingMode = StandardMaterial3D.ShadingModeEnum.Unshaded;
material.AlbedoColor = color ?? Colors.WhiteSmoke;
Adds the meshInstance to our scene’s root node, therefor drawing a line.
(Engine.GetMainLoop() as SceneTree).Root.CallDeferred("add_child", meshInstance);
Instead of immediateMesh we can use a SphereMesh.
var sphereMesh = new SphereMesh();
Set the meshInstance’s shape to sphereMesh, and set its position.
meshInstance.Mesh = sphereMesh;
meshInstance.Position = position;
Set the sphere’s radius (width), height and material.
sphereMesh.Radius = radius;
sphereMesh.Height = radius * 2f;
sphereMesh.Material = material;
Use example:
Draw3D.Box(position,new Vector3(2,0.5f,1)); // (position,size)
Draw3D.cs code:
public static MeshInstance3D Box(Vector3 position, Vector3? size = null, Color? color = null)
{
var meshInstance = new MeshInstance3D();
var boxMesh = new BoxMesh();
var material = new StandardMaterial3D();
meshInstance.Mesh = boxMesh;
meshInstance.CastShadow = GeometryInstance3D.ShadowCastingSetting.Off;
meshInstance.Position = position;
boxMesh.Size = size ?? new Vector3(0.5f,0.5f,0.5f);
boxMesh.Material = material;
material.ShadingMode = StandardMaterial3D.ShadingModeEnum.Unshaded;
material.Transparency = BaseMaterial3D.TransparencyEnum.Alpha;
material.AlbedoColor = color ?? Color.Color8(100,100,100,100);
(Engine.GetMainLoop() as SceneTree).Root.CallDeferred("add_child", meshInstance);
return meshInstance;
}
In the material section to make a shape transparent we can set its alpha mode.
We can also change its color with Color8:
material.Transparency = BaseMaterial3D.TransparencyEnum.Alpha;
material.AlbedoColor = color ?? Color.Color8(100,100,100,100);
Use example:
Draw3D.Axis(position,1f); // (position,axis' arrows length)
Draw3D.cs code:
public static Node3D Axis(Vector3 position, float length = 0.5f)
{
Node3D helper_node = new Node3D();
var meshInstance_AxisUp = Line(position,position + Vector3.Up * length, Colors.WebGreen);
var meshInstance_AxisLeft = Line(position,position + Vector3.Left * length, Colors.Red);
var meshInstance_AxisForward = Line(position,position + Vector3.Forward * length, Colors.NavyBlue);
meshInstance_AxisUp.CallDeferred("reparent",helper_node);
meshInstance_AxisLeft.CallDeferred("reparent",helper_node);
meshInstance_AxisForward.CallDeferred("reparent",helper_node);
(Engine.GetMainLoop() as SceneTree).Root.CallDeferred("add_child", helper_node);
return helper_node;
}
In this example we create a ‘helper_node’ which contains all of our geometry data.
Use example:
Draw3D.Beam(from,to,0.05f,Colors.Aquamarine,32,4); // (start pos, end pos, width, color, segments, rings)
Draw3D.cs code:
public static Node3D Beam(Vector3 from, Vector3 to, float thickness = 0.05f, Color? color = null, int segments = 32, int rings = 4)
{
if (to == from)
{
GD.PrintErr("Draw3D: The 'from' and 'to' vectors cannot be the same when drawing a beam!");
return null;
}
Node3D helper_node = new Node3D();
var meshInstance = new MeshInstance3D();
var capsuleMesh = new CapsuleMesh();
var material = new StandardMaterial3D();
meshInstance.Mesh = capsuleMesh;
meshInstance.CastShadow = GeometryInstance3D.ShadowCastingSetting.Off;
meshInstance.Position = Vector3.Zero;
meshInstance.RotationDegrees = new Vector3(90,0,0);
Vector3 distance = new Vector3(
Mathf.Pow(to.X - from.X,2),
Mathf.Pow(to.Y - from.Y,2),
Mathf.Pow(to.Z - from.Z,2)
);
capsuleMesh.Height = Mathf.Sqrt(distance.X + distance.Y +distance.Z);
capsuleMesh.Radius = thickness;
capsuleMesh.RadialSegments = segments;
capsuleMesh.Rings = rings;
capsuleMesh.Material = material;
material.ShadingMode = StandardMaterial3D.ShadingModeEnum.Unshaded;
material.Transparency = BaseMaterial3D.TransparencyEnum.Alpha;
material.AlbedoColor = color ?? Color.Color8(100,100,100,150);
(Engine.GetMainLoop() as SceneTree).Root.CallDeferred("add_child", helper_node);
helper_node.CallDeferred("add_child", meshInstance);
// If the vector points are above each other, don't use the LookAt function, it causes errors.
if (from.X == to.X && from.Z == to.Z)
{
helper_node.Position = (from + to) /2;
helper_node.RotationDegrees = new Vector3(-90,0,0);
}
else
{
helper_node.LookAtFromPosition((from + to) /2,to);
}
return helper_node;
}
To draw a thick line, this script generates a capsule mesh, with the appropriate length(distance of the two points), width, geometrical segments and geometrical rings.
The capsule(meshInstance) is then rotated 90° degrees on the X axis so that its head would point forward.
The ‘helper_node’ is rotated to look at the end position and its moved to be in the middle of the two points.
In the beginning the script checks if the two points aren’t in the same location and if they are it throws an error and returns a null value.
Later it also checks if the two points are above each other, if so don’t use the LookAtFromPosition() function since it thrown an exception in this instance.
Node3D line;
Vector3 from = new Vector3(1,1,2);
Vector3 to = new Vector3(3,1,-2);
public override void _PhysicsProcess(double delta)
{
if (line != null)
line.QueueFree();
line = Draw3D.Line(from,to,Colors.Red);
}