using System; using Unity.Collections.LowLevel.Unsafe; using Unity.Mathematics; using Unity.Jobs.LowLevel.Unsafe; using UnityEngine; using System.Collections.Generic; using Unity.Burst; using UnityEngine.Profiling; using Unity.Collections; using Unity.Jobs; using UnityEngine.Rendering; using System.Runtime.InteropServices; namespace Pathfinding.Drawing { using static DrawingData; using BitPackedMeta = DrawingData.BuilderData.BitPackedMeta; using Pathfinding.Drawing.Text; using Unity.Profiling; /// /// Specifies text alignment relative to an anchor point. /// /// /// Draw.Label2D(transform.position, "Hello World", 14, LabelAlignment.TopCenter); /// /// /// // Draw the label 20 pixels below the object /// Draw.Label2D(transform.position, "Hello World", 14, LabelAlignment.TopCenter.withPixelOffset(0, -20)); /// /// /// See: /// See: /// public struct LabelAlignment { /// /// Where on the text's bounding box to anchor the text. /// /// The pivot is specified in relative coordinates, where (0,0) is the bottom left corner and (1,1) is the top right corner. /// public float2 relativePivot; /// How much to move the text in screen-space public float2 pixelOffset; public static readonly LabelAlignment TopLeft = new LabelAlignment { relativePivot = new float2(0.0f, 1.0f), pixelOffset = new float2(0, 0) }; public static readonly LabelAlignment MiddleLeft = new LabelAlignment { relativePivot = new float2(0.0f, 0.5f), pixelOffset = new float2(0, 0) }; public static readonly LabelAlignment BottomLeft = new LabelAlignment { relativePivot = new float2(0.0f, 0.0f), pixelOffset = new float2(0, 0) }; public static readonly LabelAlignment BottomCenter = new LabelAlignment { relativePivot = new float2(0.5f, 0.0f), pixelOffset = new float2(0, 0) }; public static readonly LabelAlignment BottomRight = new LabelAlignment { relativePivot = new float2(1.0f, 0.0f), pixelOffset = new float2(0, 0) }; public static readonly LabelAlignment MiddleRight = new LabelAlignment { relativePivot = new float2(1.0f, 0.5f), pixelOffset = new float2(0, 0) }; public static readonly LabelAlignment TopRight = new LabelAlignment { relativePivot = new float2(1.0f, 1.0f), pixelOffset = new float2(0, 0) }; public static readonly LabelAlignment TopCenter = new LabelAlignment { relativePivot = new float2(0.5f, 1.0f), pixelOffset = new float2(0, 0) }; public static readonly LabelAlignment Center = new LabelAlignment { relativePivot = new float2(0.5f, 0.5f), pixelOffset = new float2(0, 0) }; /// /// Moves the text by the specified amount of pixels in screen-space. /// /// /// // Draw the label 20 pixels below the object /// Draw.Label2D(transform.position, "Hello World", 14, LabelAlignment.TopCenter.withPixelOffset(0, -20)); /// /// public LabelAlignment withPixelOffset (float x, float y) { return new LabelAlignment { relativePivot = this.relativePivot, pixelOffset = new float2(x, y), }; } } /// Maximum allowed delay for a job that is drawing to a command buffer public enum AllowedDelay { /// /// If the job is not complete at the end of the frame, drawing will block until it is completed. /// This is recommended for most jobs that are expected to complete within a single frame. /// EndOfFrame, /// /// Wait indefinitely for the job to complete, and only submit the results for rendering once it is done. /// This is recommended for long running jobs that may take many frames to complete. /// Infinite, } /// Some static fields that need to be in a separate class because Burst doesn't support them static class CommandBuilderSamplers { internal static readonly ProfilerMarker MarkerConvert = new ProfilerMarker("Convert"); internal static readonly ProfilerMarker MarkerSetLayout = new ProfilerMarker("SetLayout"); internal static readonly ProfilerMarker MarkerUpdateVertices = new ProfilerMarker("UpdateVertices"); internal static readonly ProfilerMarker MarkerUpdateIndices = new ProfilerMarker("UpdateIndices"); internal static readonly ProfilerMarker MarkerSubmesh = new ProfilerMarker("Submesh"); internal static readonly ProfilerMarker MarkerUpdateBuffer = new ProfilerMarker("UpdateComputeBuffer"); internal static readonly ProfilerMarker MarkerProcessCommands = new ProfilerMarker("Commands"); internal static readonly ProfilerMarker MarkerCreateTriangles = new ProfilerMarker("CreateTriangles"); } /// /// Builder for drawing commands. /// You can use this to queue many drawing commands. The commands will be queued for rendering when you call the Dispose method. /// It is recommended that you use the using statement which automatically calls the Dispose method. /// /// /// // Create a new CommandBuilder /// using (var draw = DrawingManager.GetBuilder()) { /// // Use the exact same API as the global Draw class /// draw.WireBox(Vector3.zero, Vector3.one); /// } /// /// /// Warning: You must call either or when you are done with this object to avoid memory leaks. /// [StructLayout(LayoutKind.Sequential)] [BurstCompile] public partial struct CommandBuilder : IDisposable { // Note: Many fields/methods are explicitly marked as private. This is because doxygen otherwise thinks they are public by default (like struct members are in c++) [NativeDisableUnsafePtrRestriction] internal unsafe UnsafeAppendBuffer* buffer; private GCHandle gizmos; [NativeSetThreadIndex] private int threadIndex; private DrawingData.BuilderData.BitPackedMeta uniqueID; internal unsafe CommandBuilder(UnsafeAppendBuffer* buffer, GCHandle gizmos, int threadIndex, DrawingData.BuilderData.BitPackedMeta uniqueID) { this.buffer = buffer; this.gizmos = gizmos; this.threadIndex = threadIndex; this.uniqueID = uniqueID; } internal CommandBuilder(DrawingData gizmos, Hasher hasher, RedrawScope frameRedrawScope, RedrawScope customRedrawScope, bool isGizmos, bool isBuiltInCommandBuilder, int sceneModeVersion) { // We need to use a GCHandle instead of a normal reference to be able to pass this object to burst compiled function pointers. // The NativeSetClassTypeToNullOnSchedule unfortunately only works together with the job system, not with raw functions. this.gizmos = GCHandle.Alloc(gizmos, GCHandleType.Normal); threadIndex = 0; uniqueID = gizmos.data.Reserve(isBuiltInCommandBuilder); gizmos.data.Get(uniqueID).Init(hasher, frameRedrawScope, customRedrawScope, isGizmos, gizmos.GetNextDrawOrderIndex(), sceneModeVersion); unsafe { buffer = gizmos.data.Get(uniqueID).bufferPtr; } } internal unsafe int BufferSize { get { return buffer->Length; } set { buffer->Length = value; } } /// /// Wrapper for drawing in the XY plane. /// /// /// var p1 = new Vector2(0, 1); /// var p2 = new Vector2(5, 7); /// /// // Draw it in the XY plane /// Draw.xy.Line(p1, p2); /// /// // Draw it in the XZ plane /// Draw.xz.Line(p1, p2); /// /// /// See: 2d-drawing (view in online documentation for working links) /// See: /// public CommandBuilder2D xy => new CommandBuilder2D(this, true); /// /// Wrapper for drawing in the XZ plane. /// /// /// var p1 = new Vector2(0, 1); /// var p2 = new Vector2(5, 7); /// /// // Draw it in the XY plane /// Draw.xy.Line(p1, p2); /// /// // Draw it in the XZ plane /// Draw.xz.Line(p1, p2); /// /// /// See: 2d-drawing (view in online documentation for working links) /// See: /// public CommandBuilder2D xz => new CommandBuilder2D(this, false); static readonly float3 DEFAULT_UP = new float3(0, 1, 0); /// /// Can be set to render specifically to these cameras. /// If you set this property to an array of cameras then this command builder will only be rendered /// to the specified cameras. Setting this property bypasses . /// The camera will be rendered to even if it renders to a render texture. /// /// A null value indicates that all valid cameras should be rendered to. This is the default value. /// /// /// var draw = DrawingManager.GetBuilder(true); /// /// draw.cameraTargets = new Camera[] { myCamera }; /// // This sphere will only be rendered to myCamera /// draw.WireSphere(Vector3.zero, 0.5f, Color.black); /// draw.Dispose(); /// /// /// See: advanced (view in online documentation for working links) /// public Camera[] cameraTargets { get { if (gizmos.IsAllocated && gizmos.Target != null) { var target = gizmos.Target as DrawingData; if (target.data.StillExists(uniqueID)) { return target.data.Get(uniqueID).meta.cameraTargets; } } throw new System.Exception("Cannot get cameraTargets because the command builder has already been disposed or does not exist."); } set { if (uniqueID.isBuiltInCommandBuilder) throw new System.Exception("You cannot set the camera targets for a built-in command builder. Create a custom command builder instead."); if (gizmos.IsAllocated && gizmos.Target != null) { var target = gizmos.Target as DrawingData; if (!target.data.StillExists(uniqueID)) { throw new System.Exception("Cannot set cameraTargets because the command builder has already been disposed or does not exist."); } target.data.Get(uniqueID).meta.cameraTargets = value; } } } /// Submits this command builder for rendering public void Dispose () { if (uniqueID.isBuiltInCommandBuilder) throw new System.Exception("You cannot dispose a built-in command builder"); DisposeInternal(); } /// /// Disposes this command builder after the given job has completed. /// /// This is convenient if you are using the entity-component-system/burst in Unity and don't know exactly when the job will complete. /// /// You will not be able to use this command builder on the main thread anymore. /// /// See: job-system (view in online documentation for working links) /// /// The job that must complete before this command builder is disposed. /// Whether to block on this dependency before rendering the current frame or not. /// If the job is expected to complete during a single frame, leave at the default of \reflink{AllowedDelay.EndOfFrame}. /// But if the job is expected to take multiple frames to complete, you can set this to \reflink{AllowedDelay.Infinite}. public void DisposeAfter (JobHandle dependency, AllowedDelay allowedDelay = AllowedDelay.EndOfFrame) { if (!gizmos.IsAllocated) throw new System.Exception("You cannot dispose an invalid command builder. Are you trying to dispose it twice?"); try { if (gizmos.IsAllocated && gizmos.Target != null) { var target = gizmos.Target as DrawingData; if (!target.data.StillExists(uniqueID)) { throw new System.Exception("Cannot dispose the command builder because the drawing manager has been destroyed"); } target.data.Get(uniqueID).SubmitWithDependency(gizmos, dependency, allowedDelay); } } finally { this = default; } } internal void DisposeInternal () { if (!gizmos.IsAllocated) throw new System.Exception("You cannot dispose an invalid command builder. Are you trying to dispose it twice?"); try { if (gizmos.IsAllocated && gizmos.Target != null) { var target = gizmos.Target as DrawingData; if (!target.data.StillExists(uniqueID)) { throw new System.Exception("Cannot dispose the command builder because the drawing manager has been destroyed"); } target.data.Get(uniqueID).Submit(gizmos.Target as DrawingData); } } finally { gizmos.Free(); this = default; } } /// /// Discards the contents of this command builder without rendering anything. /// If you are not going to draw anything (i.e. you do not call the method) then you must call this method to avoid /// memory leaks. /// public void DiscardAndDispose () { if (uniqueID.isBuiltInCommandBuilder) throw new System.Exception("You cannot dispose a built-in command builder"); DiscardAndDisposeInternal(); } internal void DiscardAndDisposeInternal () { try { if (gizmos.IsAllocated && gizmos.Target != null) { var target = gizmos.Target as DrawingData; if (!target.data.StillExists(uniqueID)) { throw new System.Exception("Cannot dispose the command builder because the drawing manager has been destroyed"); } target.data.Release(uniqueID); } } finally { if (gizmos.IsAllocated) gizmos.Free(); this = default; } } /// /// Pre-allocates the internal buffer to an additional size bytes. /// This can give you a minor performance boost if you are drawing a lot of things. /// /// Note: Only resizes the buffer for the current thread. /// public void Preallocate (int size) { Reserve(size); } /// Internal rendering command [System.Flags] internal enum Command { PushColorInline = 1 << 8, PushColor = 0, PopColor, PushMatrix, PushSetMatrix, PopMatrix, Line, Circle, CircleXZ, Disc, DiscXZ, SphereOutline, Box, WirePlane, WireBox, SolidTriangle, PushPersist, PopPersist, Text, Text3D, PushLineWidth, PopLineWidth, CaptureState, } internal struct TriangleData { public float3 a, b, c; } /// Holds rendering data for a line internal struct LineData { public float3 a, b; } internal struct LineDataV3 { public Vector3 a, b; } /// Holds rendering data for a circle internal struct CircleXZData { public float3 center; public float radius, startAngle, endAngle; } /// Holds rendering data for a circle internal struct CircleData { public float3 center; public float3 normal; public float radius; } /// Holds rendering data for a sphere internal struct SphereData { public float3 center; public float radius; } /// Holds rendering data for a box internal struct BoxData { public float3 center; public float3 size; } internal struct PlaneData { public float3 center; public quaternion rotation; public float2 size; } internal struct PersistData { public float endTime; } internal struct LineWidthData { public float pixels; public bool automaticJoins; } internal struct TextData { public float3 center; public LabelAlignment alignment; public float sizeInPixels; public int numCharacters; } internal struct TextData3D { public float3 center; public quaternion rotation; public LabelAlignment alignment; public float size; public int numCharacters; } /// Ensures the buffer has room for at least N more bytes [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] private void Reserve (int additionalSpace) { unsafe { if (Unity.Burst.CompilerServices.Hint.Unlikely(threadIndex >= 0)) { #if ENABLE_UNITY_COLLECTIONS_CHECKS if (threadIndex < 0 || threadIndex >= JobsUtility.MaxJobThreadCount) throw new System.Exception("Thread index outside the expected range"); if (threadIndex > 0 && uniqueID.isBuiltInCommandBuilder) throw new System.Exception("You should use a custom command builder when using the Unity Job System. Take a look at the documentation for more info."); if (buffer == null) throw new System.Exception("CommandBuilder does not have a valid buffer. Is it properly initialized?"); // Exploit the fact that right after this package has drawn gizmos the buffers will be empty // and the next task is that Unity will render its own internal gizmos. // We can therefore easily (and without a high performance cost) // trap accidental Draw.* calls from OnDrawGizmos functions // by doing this check when the first Reserve call is made. AssertNotRendering(); #endif buffer += threadIndex; threadIndex = -1; } var newLength = buffer->Length + additionalSpace; if (newLength > buffer->Capacity) { #if ENABLE_UNITY_COLLECTIONS_CHECKS // This really should run every time we access the buffer... but that would be a bit slow // This code will catch the error eventually. AssertBufferExists(); const int MAX_BUFFER_SIZE = 1024 * 1024 * 256; // 256 MB if (buffer->Length * 2 > MAX_BUFFER_SIZE) { throw new System.Exception("CommandBuilder buffer is very large. Are you trying to draw things in an infinite loop?"); } #endif buffer->SetCapacity(math.max(newLength, buffer->Length * 2)); } } } [BurstDiscard] private void AssertBufferExists () { if (!gizmos.IsAllocated || gizmos.Target == null || !(gizmos.Target as DrawingData).data.StillExists(uniqueID)) { // This command builder is invalid, clear all data on it to prevent it being used again this = default; throw new System.Exception("This command builder no longer exists. Are you trying to draw to a command builder which has already been disposed?"); } } [BurstDiscard] static void AssertNotRendering () { // Some checking to see if drawing is being done from inside OnDrawGizmos // This check is relatively fast (about 0.05 ms), but we still do it only every 128th frame for performance reasons if (!GizmoContext.drawingGizmos && !JobsUtility.IsExecutingJob && (Time.renderedFrameCount & 127) == 0) { // Inspect the stack-trace to be able to provide more helpful error messages var st = StackTraceUtility.ExtractStackTrace(); if (st.Contains("OnDrawGizmos")) { throw new System.Exception("You are trying to use Draw.* functions from within Unity's OnDrawGizmos function. Use this package's gizmo callbacks instead (see the documentation)."); } } } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] internal void Reserve() where A : struct { Reserve(UnsafeUtility.SizeOf() + UnsafeUtility.SizeOf()); } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] internal void Reserve() where A : struct where B : struct { Reserve(UnsafeUtility.SizeOf() * 2 + UnsafeUtility.SizeOf() + UnsafeUtility.SizeOf()); } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] internal void Reserve() where A : struct where B : struct where C : struct { Reserve(UnsafeUtility.SizeOf() * 3 + UnsafeUtility.SizeOf() + UnsafeUtility.SizeOf() + UnsafeUtility.SizeOf()); } /// /// Converts a Color to a Color32. /// This method is faster than Unity's native color conversion, especially when using Burst. /// [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] internal static unsafe uint ConvertColor (Color color) { // If SSE2 is supported (which it is on essentially all X86 CPUs) // then we can use a much faster conversion from Color to Color32. // This will only be possible inside Burst. if (Unity.Burst.Intrinsics.X86.Sse2.IsSse2Supported) { // Convert from 0-1 float range to 0-255 integer range var ci = (int4)(255 * new float4(color.r, color.g, color.b, color.a) + 0.5f); var v32 = new Unity.Burst.Intrinsics.v128(ci.x, ci.y, ci.z, ci.w); // Convert four 32-bit numbers to four 16-bit numbers var v16 = Unity.Burst.Intrinsics.X86.Sse2.packs_epi32(v32, v32); // Convert four 16-bit numbers to four 8-bit numbers var v8 = Unity.Burst.Intrinsics.X86.Sse2.packus_epi16(v16, v16); return v8.UInt0; } else { // If we don't have SSE2 (most likely we are not running inside Burst), // then we will do a manual conversion from Color to Color32. // This is significantly faster than just casting to a Color32. var r = (uint)Mathf.Clamp((int)(color.r*255f + 0.5f), 0, 255); var g = (uint)Mathf.Clamp((int)(color.g*255f + 0.5f), 0, 255); var b = (uint)Mathf.Clamp((int)(color.b*255f + 0.5f), 0, 255); var a = (uint)Mathf.Clamp((int)(color.a*255f + 0.5f), 0, 255); return (a << 24) | (b << 16) | (g << 8) | r; } } internal unsafe void Add(T value) where T : struct { int num = UnsafeUtility.SizeOf(); var buffer = this.buffer; var bufferSize = buffer->Length; // We assume this because the Reserve function has already taken care of that. // This removes a few branches from the assembly when running in burst. Unity.Burst.CompilerServices.Hint.Assume(buffer->Ptr != null); Unity.Burst.CompilerServices.Hint.Assume(buffer->Ptr + bufferSize != null); unsafe { UnsafeUtility.CopyStructureToPtr(ref value, (void*)((byte*)buffer->Ptr + bufferSize)); buffer->Length = bufferSize + num; } } public struct ScopeMatrix : IDisposable { internal CommandBuilder builder; public void Dispose () { #if ENABLE_UNITY_COLLECTIONS_CHECKS if (!builder.gizmos.IsAllocated || !(builder.gizmos.Target is DrawingData data) || !data.data.StillExists(builder.uniqueID)) throw new System.InvalidOperationException("The drawing instance this matrix scope belongs to no longer exists. Matrix scopes cannot survive for longer than a frame unless you have a custom drawing instance. Are you using a matrix scope inside a coroutine?"); #endif unsafe { builder.PopMatrix(); builder.buffer = null; } } } public struct ScopeColor : IDisposable { internal CommandBuilder builder; public void Dispose () { #if ENABLE_UNITY_COLLECTIONS_CHECKS if (!builder.gizmos.IsAllocated || !(builder.gizmos.Target is DrawingData data) || !data.data.StillExists(builder.uniqueID)) throw new System.InvalidOperationException("The drawing instance this color scope belongs to no longer exists. Color scopes cannot survive for longer than a frame unless you have a custom drawing instance. Are you using a color scope inside a coroutine?"); #endif unsafe { builder.PopColor(); builder.buffer = null; } } } public struct ScopePersist : IDisposable { internal CommandBuilder builder; public void Dispose () { #if ENABLE_UNITY_COLLECTIONS_CHECKS if (!builder.gizmos.IsAllocated || !(builder.gizmos.Target is DrawingData data) || !data.data.StillExists(builder.uniqueID)) throw new System.InvalidOperationException("The drawing instance this persist scope belongs to no longer exists. Persist scopes cannot survive for longer than a frame unless you have a custom drawing instance. Are you using a persist scope inside a coroutine?"); #endif unsafe { builder.PopDuration(); builder.buffer = null; } } } /// /// Scope that does nothing. /// Used for optimization in standalone builds. /// public struct ScopeEmpty : IDisposable { public void Dispose () { } } public struct ScopeLineWidth : IDisposable { internal CommandBuilder builder; public void Dispose () { #if ENABLE_UNITY_COLLECTIONS_CHECKS if (!builder.gizmos.IsAllocated || !(builder.gizmos.Target is DrawingData data) || !data.data.StillExists(builder.uniqueID)) throw new System.InvalidOperationException("The drawing instance this line width scope belongs to no longer exists. Line width scopes cannot survive for longer than a frame unless you have a custom drawing instance. Are you using a line width scope inside a coroutine?"); #endif unsafe { builder.PopLineWidth(); builder.buffer = null; } } } /// /// Scope to draw multiple things with an implicit matrix transformation. /// All coordinates for items drawn inside the scope will be multiplied by the matrix. /// If WithMatrix scopes are nested then coordinates are multiplied by all nested matrices in order. /// /// /// using (Draw.InLocalSpace(transform)) { /// // Draw a box at (0,0,0) relative to the current object /// // This means it will show up at the object's position /// Draw.WireBox(Vector3.zero, Vector3.one); /// } /// /// // Equivalent code using the lower level WithMatrix scope /// using (Draw.WithMatrix(transform.localToWorldMatrix)) { /// Draw.WireBox(Vector3.zero, Vector3.one); /// } /// /// /// See: /// [BurstDiscard] public ScopeMatrix WithMatrix (Matrix4x4 matrix) { PushMatrix(matrix); // TODO: Keep track of alive scopes and prevent dispose unless all scopes have been disposed unsafe { return new ScopeMatrix { builder = this }; } } /// /// Scope to draw multiple things with an implicit matrix transformation. /// All coordinates for items drawn inside the scope will be multiplied by the matrix. /// If WithMatrix scopes are nested then coordinates are multiplied by all nested matrices in order. /// /// /// using (Draw.InLocalSpace(transform)) { /// // Draw a box at (0,0,0) relative to the current object /// // This means it will show up at the object's position /// Draw.WireBox(Vector3.zero, Vector3.one); /// } /// /// // Equivalent code using the lower level WithMatrix scope /// using (Draw.WithMatrix(transform.localToWorldMatrix)) { /// Draw.WireBox(Vector3.zero, Vector3.one); /// } /// /// /// See: /// [BurstDiscard] public ScopeMatrix WithMatrix (float3x3 matrix) { PushMatrix(new float4x4(matrix, float3.zero)); // TODO: Keep track of alive scopes and prevent dispose unless all scopes have been disposed unsafe { return new ScopeMatrix { builder = this }; } } /// /// Scope to draw multiple things with the same color. /// /// /// void Update () { /// using (Draw.WithColor(Color.red)) { /// Draw.Line(new Vector3(0, 0, 0), new Vector3(1, 1, 1)); /// Draw.Line(new Vector3(0, 0, 0), new Vector3(0, 1, 2)); /// } /// } /// /// /// Any command that is passed an explicit color parameter will override this color. /// If another color scope is nested inside this one then that scope will override this color. /// [BurstDiscard] public ScopeColor WithColor (Color color) { PushColor(color); unsafe { return new ScopeColor { builder = this }; } } /// /// Scope to draw multiple things for a longer period of time. /// /// Normally drawn items will only be rendered for a single frame. /// Using a persist scope you can make the items be drawn for any amount of time. /// /// /// void Update () { /// using (Draw.WithDuration(1.0f)) { /// var offset = Time.time; /// Draw.Line(new Vector3(offset, 0, 0), new Vector3(offset, 0, 1)); /// } /// } /// /// /// Note: Outside of play mode the duration is measured against Unity's Time.realtimeSinceStartup. /// /// Warning: It is recommended not to use this inside a DrawGizmos callback since DrawGizmos is called every frame anyway. /// /// How long the drawn items should persist in seconds. [BurstDiscard] public ScopePersist WithDuration (float duration) { PushDuration(duration); unsafe { return new ScopePersist { builder = this }; } } /// /// Scope to draw multiple things with a given line width. /// /// Note that the line join algorithm is a quite simple one optimized for speed. It normally looks good on a 2D plane, but if the polylines curve a lot in 3D space then /// it can look odd from some angles. /// /// [Open online documentation to see images] /// /// In the picture the top row has automaticJoins enabled and in the bottom row it is disabled. /// /// Line width in pixels /// If true then sequences of lines that are adjacent will be automatically joined at their vertices. This typically produces nicer polylines without weird gaps. [BurstDiscard] public ScopeLineWidth WithLineWidth (float pixels, bool automaticJoins = true) { PushLineWidth(pixels, automaticJoins); unsafe { return new ScopeLineWidth { builder = this }; } } /// /// Scope to draw multiple things relative to a transform object. /// All coordinates for items drawn inside the scope will be multiplied by the transform's localToWorldMatrix. /// /// /// void Update () { /// using (Draw.InLocalSpace(transform)) { /// // Draw a box at (0,0,0) relative to the current object /// // This means it will show up at the object's position /// // The box is also rotated and scaled with the transform /// Draw.WireBox(Vector3.zero, Vector3.one); /// } /// } /// /// /// [Open online documentation to see videos] /// [BurstDiscard] public ScopeMatrix InLocalSpace (Transform transform) { return WithMatrix(transform.localToWorldMatrix); } /// /// Scope to draw multiple things in screen space of a camera. /// If you draw 2D coordinates (i.e. (x,y,0)) they will be projected onto a plane approximately [2*near clip plane of the camera] world units in front of the camera (but guaranteed to be between the near and far planes). /// /// The lower left corner of the camera is (0,0,0) and the upper right is (camera.pixelWidth, camera.pixelHeight, 0) /// /// Note: As a corollary, the centers of pixels are offset by 0.5. So for example the center of the top left pixel is at (0.5, 0.5, 0). /// Therefore, if you want to draw 1 pixel wide lines in screen space, you may want to offset the coordinates by 0.5 pixels. /// /// See: /// See: /// [BurstDiscard] public ScopeMatrix InScreenSpace (Camera camera) { return WithMatrix(camera.cameraToWorldMatrix * camera.nonJitteredProjectionMatrix.inverse * Matrix4x4.TRS(new Vector3(-1.0f, -1.0f, 0), Quaternion.identity, new Vector3(2.0f/camera.pixelWidth, 2.0f/camera.pixelHeight, 1))); } /// /// Multiply all coordinates until the next with the given matrix. /// /// PushMatrix and PushSetMatrix are slightly different: /// /// - PushMatrix stacks with all previously pushed matrices. The active matrix becomes the product of the given matrix and the previously active one. /// - PushSetMatrix sets the current matrix directly. The active matrix becomes the last pushed matrix. /// public void PushMatrix (Matrix4x4 matrix) { Reserve(); Add(Command.PushMatrix); Add(matrix); } /// /// Multiply all coordinates until the next with the given matrix. /// /// PushMatrix and PushSetMatrix are slightly different: /// /// - PushMatrix stacks with all previously pushed matrices. The active matrix becomes the product of the given matrix and the previously active one. /// - PushSetMatrix sets the current matrix directly. The active matrix becomes the last pushed matrix. /// public void PushMatrix (float4x4 matrix) { Reserve(); Add(Command.PushMatrix); Add(matrix); } /// /// Multiply all coordinates until the next with the given matrix. /// /// PushMatrix and PushSetMatrix are slightly different: /// /// - PushMatrix stacks with all previously pushed matrices. The active matrix becomes the product of the given matrix and the previously active one. /// - PushSetMatrix sets the current matrix directly. The active matrix becomes the last pushed matrix. /// public void PushSetMatrix (Matrix4x4 matrix) { Reserve(); Add(Command.PushSetMatrix); Add((float4x4)matrix); } /// /// Multiply all coordinates until the next PopMatrix with the given matrix. /// /// PushMatrix and PushSetMatrix are slightly different: /// /// - PushMatrix stacks with all previously pushed matrices. The active matrix becomes the product of the given matrix and the previously active one. /// - PushSetMatrix sets the current matrix directly. The active matrix becomes the last pushed matrix. /// public void PushSetMatrix (float4x4 matrix) { Reserve(); Add(Command.PushSetMatrix); Add(matrix); } /// /// Pops a matrix from the stack. /// /// See: /// See: /// public void PopMatrix () { Reserve(4); Add(Command.PopMatrix); } /// /// Draws everything until the next PopColor with the given color. /// Any command that is passed an explicit color parameter will override this color. /// If another color scope is nested inside this one then that scope will override this color. /// public void PushColor (Color color) { Reserve(); Add(Command.PushColor); Add(ConvertColor(color)); } /// Pops a color from the stack public void PopColor () { Reserve(4); Add(Command.PopColor); } /// /// Draws everything until the next PopDuration for a number of seconds. /// Warning: This is not recommended inside a DrawGizmos callback since DrawGizmos is called every frame anyway. /// public void PushDuration (float duration) { Reserve(); Add(Command.PushPersist); // We must use the BurstTime variable which is updated more rarely than Time.time. // This is necessary because this code may be called from a burst job or from a different thread. // Time.time can only be accessed in the main thread. Add(new PersistData { endTime = SharedDrawingData.BurstTime.Data + duration }); } /// Pops a duration scope from the stack public void PopDuration () { Reserve(4); Add(Command.PopPersist); } /// /// Draws everything until the next PopPersist for a number of seconds. /// Warning: This is not recommended inside a DrawGizmos callback since DrawGizmos is called every frame anyway. /// /// Deprecated: Renamed to /// [System.Obsolete("Renamed to PushDuration for consistency")] public void PushPersist (float duration) { PushDuration(duration); } /// /// Pops a persist scope from the stack. /// Deprecated: Renamed to /// [System.Obsolete("Renamed to PopDuration for consistency")] public void PopPersist () { PopDuration(); } /// /// Draws all lines until the next PopLineWidth with a given line width in pixels. /// /// Note that the line join algorithm is a quite simple one optimized for speed. It normally looks good on a 2D plane, but if the polylines curve a lot in 3D space then /// it can look odd from some angles. /// /// [Open online documentation to see images] /// /// In the picture the top row has automaticJoins enabled and in the bottom row it is disabled. /// /// Line width in pixels /// If true then sequences of lines that are adjacent will be automatically joined at their vertices. This typically produces nicer polylines without weird gaps. public void PushLineWidth (float pixels, bool automaticJoins = true) { if (pixels < 0) throw new System.ArgumentOutOfRangeException("pixels", "Line width must be positive"); Reserve(); Add(Command.PushLineWidth); Add(new LineWidthData { pixels = pixels, automaticJoins = automaticJoins }); } /// Pops a line width scope from the stack public void PopLineWidth () { Reserve(4); Add(Command.PopLineWidth); } /// /// Draws a line between two points. /// /// [Open online documentation to see images] /// /// /// void Update () { /// Draw.Line(Vector3.zero, Vector3.up); /// } /// /// public void Line (float3 a, float3 b) { Reserve(); Add(Command.Line); Add(new LineData { a = a, b = b }); } /// /// Draws a line between two points. /// /// [Open online documentation to see images] /// /// /// void Update () { /// Draw.Line(Vector3.zero, Vector3.up); /// } /// /// public void Line (Vector3 a, Vector3 b) { Reserve(); // Add(Command.Line); // Add(new LineDataV3 { a = a, b = b }); // The code below is equivalent to the commented out code above. // But drawing lines is the most common operation so it needs to be really fast. // Having this hardcoded improves line rendering performance by about 8%. var bufferSize = BufferSize; unsafe { var newLen = bufferSize + 4 + 24; #if ENABLE_UNITY_COLLECTIONS_CHECKS UnityEngine.Assertions.Assert.IsTrue(newLen <= buffer->Capacity); #endif var ptr = (byte*)buffer->Ptr + bufferSize; *(Command*)ptr = Command.Line; var lineData = (LineDataV3*)(ptr + 4); lineData->a = a; lineData->b = b; buffer->Length = newLen; } } /// /// Draws a line between two points. /// /// [Open online documentation to see images] /// /// /// void Update () { /// Draw.Line(Vector3.zero, Vector3.up); /// } /// /// public void Line (Vector3 a, Vector3 b, Color color) { Reserve(); // Add(Command.Line | Command.PushColorInline); // Add(ConvertColor(color)); // Add(new LineDataV3 { a = a, b = b }); // The code below is equivalent to the code which is commented out above. // But drawing lines is the most common operation so it needs to be really fast // Having this hardcoded improves line rendering performance by about 8%. var bufferSize = BufferSize; unsafe { var newLen = bufferSize + 4 + 24 + 4; #if ENABLE_UNITY_COLLECTIONS_CHECKS UnityEngine.Assertions.Assert.IsTrue(newLen <= buffer->Capacity); #endif var ptr = (byte*)buffer->Ptr + bufferSize; *(Command*)ptr = Command.Line | Command.PushColorInline; *(uint*)(ptr + 4) = ConvertColor(color); var lineData = (LineDataV3*)(ptr + 8); lineData->a = a; lineData->b = b; buffer->Length = newLen; } } /// /// Draws a ray starting at a point and going in the given direction. /// The ray will end at origin + direction. /// /// [Open online documentation to see images] /// /// /// Draw.Ray(Vector3.zero, Vector3.up); /// /// public void Ray (float3 origin, float3 direction) { Line(origin, origin + direction); } /// /// Draws a ray with a given length. /// /// [Open online documentation to see images] /// /// /// Draw.Ray(Camera.main.ScreenPointToRay(Vector3.zero), 10); /// /// public void Ray (Ray ray, float length) { Line(ray.origin, ray.origin + ray.direction * length); } /// /// Draws an arc between two points. /// /// The rendered arc is the shortest arc between the two points. /// The radius of the arc will be equal to the distance between center and start. /// /// [Open online documentation to see images] /// /// float a1 = Mathf.PI*0.9f; /// float a2 = Mathf.PI*0.1f; /// var arcStart = new float3(Mathf.Cos(a1), 0, Mathf.Sin(a1)); /// var arcEnd = new float3(Mathf.Cos(a2), 0, Mathf.Sin(a2)); /// Draw.Arc(new float3(0, 0, 0), arcStart, arcEnd, color); /// /// /// See: /// /// Center of the imaginary circle that the arc is part of. /// Starting point of the arc. /// End point of the arc. public void Arc (float3 center, float3 start, float3 end) { var d1 = start - center; var d2 = end - center; var normal = math.cross(d2, d1); if (math.any(normal != 0) && math.all(math.isfinite(normal))) { var m = Matrix4x4.TRS(center, Quaternion.LookRotation(d1, normal), Vector3.one); var angle = Vector3.SignedAngle(d1, d2, normal) * Mathf.Deg2Rad; PushMatrix(m); CircleXZInternal(float3.zero, math.length(d1), 90 * Mathf.Deg2Rad, 90 * Mathf.Deg2Rad - angle); PopMatrix(); } } /// /// Draws a circle in the XZ plane. /// /// You can draw an arc by supplying the startAngle and endAngle parameters. /// /// [Open online documentation to see images] /// /// See: /// See: /// See: /// /// Center of the circle or arc. /// Radius of the circle or arc. /// Starting angle in radians. 0 corrsponds to the positive X axis. /// End angle in radians. [System.Obsolete("Use Draw.xz.Circle instead")] public void CircleXZ (float3 center, float radius, float startAngle = 0f, float endAngle = 2 * Mathf.PI) { CircleXZInternal(center, radius, startAngle, endAngle); } internal void CircleXZInternal (float3 center, float radius, float startAngle = 0f, float endAngle = 2 * Mathf.PI) { Reserve(); Add(Command.CircleXZ); Add(new CircleXZData { center = center, radius = radius, startAngle = startAngle, endAngle = endAngle }); } internal void CircleXZInternal (float3 center, float radius, float startAngle, float endAngle, Color color) { Reserve(); Add(Command.CircleXZ | Command.PushColorInline); Add(ConvertColor(color)); Add(new CircleXZData { center = center, radius = radius, startAngle = startAngle, endAngle = endAngle }); } internal static readonly float4x4 XZtoXYPlaneMatrix = float4x4.RotateX(-math.PI*0.5f); internal static readonly float4x4 XZtoYZPlaneMatrix = float4x4.RotateZ(math.PI*0.5f); /// /// Draws a circle in the XY plane. /// /// You can draw an arc by supplying the startAngle and endAngle parameters. /// /// [Open online documentation to see images] /// /// See: /// See: /// /// Center of the circle or arc. /// Radius of the circle or arc. /// Starting angle in radians. 0 corrsponds to the positive X axis. /// End angle in radians. [System.Obsolete("Use Draw.xy.Circle instead")] public void CircleXY (float3 center, float radius, float startAngle = 0f, float endAngle = 2 * Mathf.PI) { PushMatrix(XZtoXYPlaneMatrix); CircleXZ(new float3(center.x, -center.z, center.y), radius, startAngle, endAngle); PopMatrix(); } /// /// Draws a circle. /// /// [Open online documentation to see images] /// /// Note: This overload does not allow you to draw an arc. For that purpose use , or instead. /// public void Circle (float3 center, float3 normal, float radius) { Reserve(); Add(Command.Circle); Add(new CircleData { center = center, normal = normal, radius = radius }); } /// /// Draws a solid arc between two points. /// /// The rendered arc is the shortest arc between the two points. /// The radius of the arc will be equal to the distance between center and start. /// /// [Open online documentation to see images] /// /// float a1 = Mathf.PI*0.9f; /// float a2 = Mathf.PI*0.1f; /// var arcStart = new float3(Mathf.Cos(a1), 0, Mathf.Sin(a1)); /// var arcEnd = new float3(Mathf.Cos(a2), 0, Mathf.Sin(a2)); /// Draw.SolidArc(new float3(0, 0, 0), arcStart, arcEnd, color); /// /// /// See: /// /// Center of the imaginary circle that the arc is part of. /// Starting point of the arc. /// End point of the arc. public void SolidArc (float3 center, float3 start, float3 end) { var d1 = start - center; var d2 = end - center; var normal = math.cross(d2, d1); if (math.any(normal)) { var m = Matrix4x4.TRS(center, Quaternion.LookRotation(d1, normal), Vector3.one); var angle = Vector3.SignedAngle(d1, d2, normal) * Mathf.Deg2Rad; PushMatrix(m); SolidCircleXZInternal(float3.zero, math.length(d1), 90 * Mathf.Deg2Rad, 90 * Mathf.Deg2Rad - angle); PopMatrix(); } } /// /// Draws a disc in the XZ plane. /// /// You can draw an arc by supplying the startAngle and endAngle parameters. /// /// [Open online documentation to see images] /// /// See: /// See: /// See: /// /// Center of the disc or solid arc. /// Radius of the disc or solid arc. /// Starting angle in radians. 0 corrsponds to the positive X axis. /// End angle in radians. [System.Obsolete("Use Draw.xz.SolidCircle instead")] public void SolidCircleXZ (float3 center, float radius, float startAngle = 0f, float endAngle = 2 * Mathf.PI) { SolidCircleXZInternal(center, radius, startAngle, endAngle); } internal void SolidCircleXZInternal (float3 center, float radius, float startAngle = 0f, float endAngle = 2 * Mathf.PI) { Reserve(); Add(Command.DiscXZ); Add(new CircleXZData { center = center, radius = radius, startAngle = startAngle, endAngle = endAngle }); } internal void SolidCircleXZInternal (float3 center, float radius, float startAngle, float endAngle, Color color) { Reserve(); Add(Command.DiscXZ | Command.PushColorInline); Add(ConvertColor(color)); Add(new CircleXZData { center = center, radius = radius, startAngle = startAngle, endAngle = endAngle }); } /// /// Draws a disc in the XY plane. /// /// You can draw an arc by supplying the startAngle and endAngle parameters. /// /// [Open online documentation to see images] /// /// See: /// See: /// See: /// /// Center of the disc or solid arc. /// Radius of the disc or solid arc. /// Starting angle in radians. 0 corrsponds to the positive X axis. /// End angle in radians. [System.Obsolete("Use Draw.xy.SolidCircle instead")] public void SolidCircleXY (float3 center, float radius, float startAngle = 0f, float endAngle = 2 * Mathf.PI) { PushMatrix(XZtoXYPlaneMatrix); SolidCircleXZInternal(new float3(center.x, -center.z, center.y), radius, startAngle, endAngle); PopMatrix(); } /// /// Draws a disc. /// /// [Open online documentation to see images] /// /// Note: This overload does not allow you to draw an arc. For that purpose use or instead. /// public void SolidCircle (float3 center, float3 normal, float radius) { Reserve(); Add(Command.Disc); Add(new CircleData { center = center, normal = normal, radius = radius }); } /// /// Draws a circle outline around a sphere. /// /// Visually, this is a circle that always faces the camera, and is resized automatically to fit the sphere. /// /// [Open online documentation to see images] /// public void SphereOutline (float3 center, float radius) { Reserve(); Add(Command.SphereOutline); Add(new SphereData { center = center, radius = radius }); } /// /// Draws a cylinder. /// The cylinder's bottom circle will be centered at the bottom parameter and similarly for the top circle. /// /// /// // Draw a tilted cylinder between the points (0,0,0) and (1,1,1) with a radius of 0.5 /// Draw.WireCylinder(Vector3.zero, Vector3.one, 0.5f, Color.black); /// /// /// [Open online documentation to see images] /// public void WireCylinder (float3 bottom, float3 top, float radius) { WireCylinder(bottom, top - bottom, math.length(top - bottom), radius); } /// /// Draws a cylinder. /// /// /// // Draw a two meter tall cylinder at the world origin with a radius of 0.5 /// Draw.WireCylinder(Vector3.zero, Vector3.up, 2, 0.5f, Color.black); /// /// /// [Open online documentation to see images] /// /// The center of the cylinder's "bottom" circle. /// The cylinder's main axis. Does not have to be normalized. If zero, nothing will be drawn. /// The length of the cylinder, as measured along it's main axis. /// The radius of the cylinder. public void WireCylinder (float3 position, float3 up, float height, float radius) { up = math.normalizesafe(up); if (math.all(up == 0) || math.any(math.isnan(up)) || math.isnan(height) || math.isnan(radius)) return; OrthonormalBasis(up, out var basis1, out var basis2); PushMatrix(new float4x4( new float4(basis1 * radius, 0), new float4(up * height, 0), new float4(basis2 * radius, 0), new float4(position, 1) )); CircleXZInternal(float3.zero, 1); if (height > 0) { CircleXZInternal(new float3(0, 1, 0), 1); Line(new float3(1, 0, 0), new float3(1, 1, 0)); Line(new float3(-1, 0, 0), new float3(-1, 1, 0)); Line(new float3(0, 0, 1), new float3(0, 1, 1)); Line(new float3(0, 0, -1), new float3(0, 1, -1)); } PopMatrix(); } /// /// Constructs an orthonormal basis from a single normal vector. /// /// This is similar to math.orthonormal_basis, but it tries harder to be continuous in its input. /// In contrast, math.orthonormal_basis has a tendency to jump around even with small changes to the normal. /// /// It's not as fast as math.orthonormal_basis, though. /// static void OrthonormalBasis (float3 normal, out float3 basis1, out float3 basis2) { basis1 = math.cross(normal, new float3(1, 1, 1)); if (math.all(basis1 == 0)) basis1 = math.cross(normal, new float3(-1, 1, 1)); basis1 = math.normalizesafe(basis1); basis2 = math.cross(normal, basis1); } /// /// Draws a capsule with a (start,end) parameterization. /// /// The behavior of this method matches common Unity APIs such as Physics.CheckCapsule. /// /// /// // Draw a tilted capsule between the points (0,0,0) and (1,1,1) with a radius of 0.5 /// Draw.WireCapsule(Vector3.zero, Vector3.one, 0.5f, Color.black); /// /// /// [Open online documentation to see images] /// /// Center of the start hemisphere of the capsule. /// Center of the end hemisphere of the capsule. /// Radius of the capsule. public void WireCapsule (float3 start, float3 end, float radius) { var dir = end - start; var length = math.length(dir); if (length < 0.0001) { // The endpoints are the same, we can't draw a capsule from this because we don't know its orientation. // Draw a sphere as a fallback WireSphere(start, radius); } else { var normalized_dir = dir / length; WireCapsule(start - normalized_dir*radius, normalized_dir, length + 2*radius, radius); } } // TODO: Change to center, up, height parameterization /// /// Draws a capsule with a (position,direction/length) parameterization. /// /// /// // Draw a capsule that touches the y=0 plane, is 2 meters tall and has a radius of 0.5 /// Draw.WireCapsule(Vector3.zero, Vector3.up, 2.0f, 0.5f, Color.black); /// /// /// [Open online documentation to see images] /// /// One endpoint of the capsule. This is at the edge of the capsule, not at the center of one of the hemispheres. /// The main axis of the capsule. Does not have to be normalized. If zero, nothing will be drawn. /// Distance between the two endpoints of the capsule. The length will be clamped to be at least 2*radius. /// The radius of the capsule. public void WireCapsule (float3 position, float3 direction, float length, float radius) { direction = math.normalizesafe(direction); if (math.all(direction == 0) || math.any(math.isnan(direction)) || math.isnan(length) || math.isnan(radius)) return; if (radius <= 0) { Line(position, position + direction * length); } else { length = math.max(length, radius*2); OrthonormalBasis(direction, out var basis1, out var basis2); PushMatrix(new float4x4( new float4(basis1, 0), new float4(direction, 0), new float4(basis2, 0), new float4(position, 1) )); CircleXZInternal(new float3(0, radius, 0), radius); PushMatrix(XZtoXYPlaneMatrix); CircleXZInternal(new float3(0, 0, radius), radius, Mathf.PI, 2 * Mathf.PI); PopMatrix(); PushMatrix(XZtoYZPlaneMatrix); CircleXZInternal(new float3(radius, 0, 0), radius, Mathf.PI*0.5f, Mathf.PI*1.5f); PopMatrix(); if (length > 0) { var upperY = length - radius; var lowerY = radius; CircleXZInternal(new float3(0, upperY, 0), radius); PushMatrix(XZtoXYPlaneMatrix); CircleXZInternal(new float3(0, 0, upperY), radius, 0, Mathf.PI); PopMatrix(); PushMatrix(XZtoYZPlaneMatrix); CircleXZInternal(new float3(upperY, 0, 0), radius, -Mathf.PI*0.5f, Mathf.PI*0.5f); PopMatrix(); Line(new float3(radius, lowerY, 0), new float3(radius, upperY, 0)); Line(new float3(-radius, lowerY, 0), new float3(-radius, upperY, 0)); Line(new float3(0, lowerY, radius), new float3(0, upperY, radius)); Line(new float3(0, lowerY, -radius), new float3(0, upperY, -radius)); } PopMatrix(); } } /// /// Draws a wire sphere. /// /// [Open online documentation to see images] /// /// /// // Draw a wire sphere at the origin with a radius of 0.5 /// Draw.WireSphere(Vector3.zero, 0.5f, Color.black); /// /// /// See: /// public void WireSphere (float3 position, float radius) { SphereOutline(position, radius); Circle(position, new float3(1, 0, 0), radius); Circle(position, new float3(0, 1, 0), radius); Circle(position, new float3(0, 0, 1), radius); } /// /// Draws lines through a sequence of points. /// /// [Open online documentation to see images] /// /// // Draw a square /// Draw.Polyline(new [] { new Vector3(0, 0, 0), new Vector3(1, 0, 0), new Vector3(1, 1, 0), new Vector3(0, 1, 0) }, true); /// /// /// Sequence of points to draw lines through /// If true a line will be drawn from the last point in the sequence back to the first point. [BurstDiscard] public void Polyline (List points, bool cycle = false) { for (int i = 0; i < points.Count - 1; i++) { Line(points[i], points[i+1]); } if (cycle && points.Count > 1) Line(points[points.Count - 1], points[0]); } /// /// Draws lines through a sequence of points. /// /// [Open online documentation to see images] /// /// // Draw a square /// Draw.Polyline(new [] { new Vector3(0, 0, 0), new Vector3(1, 0, 0), new Vector3(1, 1, 0), new Vector3(0, 1, 0) }, true); /// /// /// Sequence of points to draw lines through /// If true a line will be drawn from the last point in the sequence back to the first point. public void Polyline(T points, bool cycle = false) where T : IReadOnlyList { for (int i = 0; i < points.Count - 1; i++) { Line(points[i], points[i+1]); } if (cycle && points.Count > 1) Line(points[points.Count - 1], points[0]); } /// /// Draws lines through a sequence of points. /// /// [Open online documentation to see images] /// /// // Draw a square /// Draw.Polyline(new [] { new Vector3(0, 0, 0), new Vector3(1, 0, 0), new Vector3(1, 1, 0), new Vector3(0, 1, 0) }, true); /// /// /// Sequence of points to draw lines through /// If true a line will be drawn from the last point in the sequence back to the first point. [BurstDiscard] public void Polyline (Vector3[] points, bool cycle = false) { for (int i = 0; i < points.Length - 1; i++) { Line(points[i], points[i+1]); } if (cycle && points.Length > 1) Line(points[points.Length - 1], points[0]); } /// /// Draws lines through a sequence of points. /// /// [Open online documentation to see images] /// /// // Draw a square /// Draw.Polyline(new [] { new Vector3(0, 0, 0), new Vector3(1, 0, 0), new Vector3(1, 1, 0), new Vector3(0, 1, 0) }, true); /// /// /// Sequence of points to draw lines through /// If true a line will be drawn from the last point in the sequence back to the first point. [BurstDiscard] public void Polyline (float3[] points, bool cycle = false) { for (int i = 0; i < points.Length - 1; i++) { Line(points[i], points[i+1]); } if (cycle && points.Length > 1) Line(points[points.Length - 1], points[0]); } /// /// Draws lines through a sequence of points. /// /// [Open online documentation to see images] /// /// // Draw a square /// Draw.Polyline(new [] { new Vector3(0, 0, 0), new Vector3(1, 0, 0), new Vector3(1, 1, 0), new Vector3(0, 1, 0) }, true); /// /// /// Sequence of points to draw lines through /// If true a line will be drawn from the last point in the sequence back to the first point. public void Polyline (NativeArray points, bool cycle = false) { for (int i = 0; i < points.Length - 1; i++) { Line(points[i], points[i+1]); } if (cycle && points.Length > 1) Line(points[points.Length - 1], points[0]); } /// Determines the symbol to use for public enum SymbolDecoration { /// /// No symbol. /// /// Space will still be reserved, but no symbol will be drawn. /// Can be used to draw dashed lines. /// /// [Open online documentation to see images] /// None, /// /// An arrowhead symbol. /// /// [Open online documentation to see images] /// ArrowHead, /// /// A circle symbol. /// /// [Open online documentation to see images] /// Circle, } /// /// Draws a dashed line between two points. /// /// /// Draw.DashedPolyline(points, 0.1f, 0.1f, color); /// /// /// [Open online documentation to see images] /// /// Warning: An individual line segment is drawn for each dash. This means that performance may suffer if you make the dash + gap distance too small. /// But for most use cases the performance is nothing to worry about. /// /// See: /// See: /// public void DashedLine (float3 a, float3 b, float dash, float gap) { var p = new PolylineWithSymbol(SymbolDecoration.None, gap, 0, dash + gap); p.MoveTo(ref this, a); p.MoveTo(ref this, b); } /// /// Draws a dashed line through a sequence of points. /// /// /// Draw.DashedPolyline(points, 0.1f, 0.1f, color); /// /// /// [Open online documentation to see images] /// /// Warning: An individual line segment is drawn for each dash. This means that performance may suffer if you make the dash + gap distance too small. /// But for most use cases the performance is nothing to worry about. /// /// If you have a different collection type, or you do not have the points in a collection at all, then you can use the struct directly. /// /// /// using (Draw.WithColor(color)) { /// var dash = 0.1f; /// var gap = 0.1f; /// var p = new CommandBuilder.PolylineWithSymbol(CommandBuilder.SymbolDecoration.None, gap, 0, dash + gap); /// for (int i = 0; i < points.Count; i++) { /// p.MoveTo(ref Draw.editor, points[i]); /// } /// } /// /// /// See: /// See: /// public void DashedPolyline (List points, float dash, float gap) { var p = new PolylineWithSymbol(SymbolDecoration.None, gap, 0, dash + gap); for (int i = 0; i < points.Count; i++) { p.MoveTo(ref this, points[i]); } } /// /// Helper for drawing a polyline with symbols at regular intervals. /// /// /// var generator = new CommandBuilder.PolylineWithSymbol(CommandBuilder.SymbolDecoration.Circle, 0.2f, 0.0f, 0.47f); /// generator.MoveTo(ref Draw.editor, new float3(-0.5f, 0, -0.5f)); /// generator.MoveTo(ref Draw.editor, new float3(0.5f, 0, 0.5f)); /// /// /// [Open online documentation to see images] /// /// [Open online documentation to see images] /// /// You can also draw a dashed line using this struct, but for common cases you can use the helper function instead. /// /// /// using (Draw.WithColor(color)) { /// var dash = 0.1f; /// var gap = 0.1f; /// var p = new CommandBuilder.PolylineWithSymbol(CommandBuilder.SymbolDecoration.None, gap, 0, dash + gap); /// for (int i = 0; i < points.Count; i++) { /// p.MoveTo(ref Draw.editor, points[i]); /// } /// } /// /// /// [Open online documentation to see images] /// public struct PolylineWithSymbol { float3 prev; float offset; readonly float symbolSize; readonly float symbolSpacing; readonly float symbolPadding; readonly float symbolOffset; readonly SymbolDecoration symbol; readonly bool reverseSymbols; bool odd; /// Create a new polyline with symbol generator. /// The symbol to use /// The size of the symbol. In case of a circle, this is the diameter. /// The padding on both sides of the symbol between the symbol and the line. /// The spacing between symbols. This is the distance between the centers of the symbols. /// If true, the symbols will be reversed. For cicles this has no effect, but arrowhead symbols will be reversed. public PolylineWithSymbol(SymbolDecoration symbol, float symbolSize, float symbolPadding, float symbolSpacing, bool reverseSymbols = false) { if (symbolSpacing <= math.FLT_MIN_NORMAL) throw new System.ArgumentOutOfRangeException(nameof(symbolSpacing), "Symbol spacing must be greater than zero"); if (symbolSize <= math.FLT_MIN_NORMAL) throw new System.ArgumentOutOfRangeException(nameof(symbolSize), "Symbol size must be greater than zero"); if (symbolPadding < 0) throw new System.ArgumentOutOfRangeException(nameof(symbolPadding), "Symbol padding must non-negative"); this.prev = float3.zero; this.symbol = symbol; this.symbolSize = symbolSize; this.symbolPadding = symbolPadding; this.symbolSpacing = math.max(0, symbolSpacing - symbolPadding * 2f - symbolSize); this.reverseSymbols = reverseSymbols; symbolOffset = symbol == SymbolDecoration.ArrowHead ? -0.25f * symbolSize : 0; if (reverseSymbols) { symbolOffset = -symbolOffset; } symbolOffset += 0.5f * symbolSize; offset = -1; odd = false; } /// /// Move to a new point. /// /// This will draw the symbols and line segments between the previous point and the new point. /// /// The command builder to draw to. You can use a built-in builder like \reflink{Draw.editor} or \reflink{Draw.ingame}, or use a custom one. /// The next point in the polyline to move to. public void MoveTo (ref CommandBuilder draw, float3 next) { if (offset == -1) { offset = this.symbolSpacing * 0.5f; prev = next; return; } var len = math.length(next - prev); var invLen = math.rcp(len); var dir = next - prev; float3 up = default; if (symbol != SymbolDecoration.None) { up = math.normalizesafe(math.cross(dir, math.cross(dir, new float3(0, 1, 0)))); if (math.all(up == 0f)) { up = new float3(0, 0, 1); } } if (reverseSymbols) dir = -dir; if (offset > 0 && !odd) { draw.Line(prev, math.lerp(prev, next, math.min(offset * invLen, 1))); } while (offset < len) { if (odd) { var pLast = math.lerp(prev, next, offset * invLen); offset += symbolSpacing; var p = math.lerp(prev, next, math.min(offset * invLen, 1)); draw.Line(pLast, p); offset += symbolPadding; } else { var p = math.lerp(prev, next, (offset + symbolOffset) * invLen); switch (symbol) { case SymbolDecoration.None: break; case SymbolDecoration.ArrowHead: draw.Arrowhead(p, dir, up, symbolSize); break; case SymbolDecoration.Circle: default: draw.Circle(p, up, symbolSize * 0.5f); break; } offset += symbolSize + symbolPadding; } odd = !odd; } offset -= len; prev = next; } } /// /// Draws the outline of a box which is axis-aligned. /// /// [Open online documentation to see images] /// /// Center of the box /// Width of the box along all dimensions public void WireBox (float3 center, float3 size) { Reserve(); Add(Command.WireBox); Add(new BoxData { center = center, size = size }); } /// /// Draws the outline of a box. /// /// [Open online documentation to see images] /// /// Center of the box /// Rotation of the box /// Width of the box along all dimensions public void WireBox (float3 center, quaternion rotation, float3 size) { PushMatrix(float4x4.TRS(center, rotation, size)); WireBox(float3.zero, new float3(1, 1, 1)); PopMatrix(); } /// /// Draws the outline of a box. /// /// [Open online documentation to see images] /// public void WireBox (Bounds bounds) { WireBox(bounds.center, bounds.size); } /// /// Draws a wire mesh. /// Every single edge of the mesh will be drawn using a command. /// /// /// var go = GameObject.CreatePrimitive(PrimitiveType.Sphere); /// go.transform.position = new Vector3(0, 0, 0); /// using (Draw.InLocalSpace(go.transform)) { /// Draw.WireMesh(go.GetComponent().sharedMesh, color); /// } /// /// [Open online documentation to see images] /// /// See: /// /// Version: Supported in Unity 2020.1 or later. /// public void WireMesh (Mesh mesh) { #if UNITY_2020_1_OR_NEWER if (mesh == null) throw new System.ArgumentNullException(); // Use a burst compiled function to draw the lines // This is significantly faster than pure C# (about 5x). var meshDataArray = Mesh.AcquireReadOnlyMeshData(mesh); var meshData = meshDataArray[0]; JobWireMesh.JobWireMeshFunctionPointer(ref meshData, ref this); meshDataArray.Dispose(); #else Debug.LogError("The WireMesh method is only suppored in Unity 2020.1 or later"); #endif } /// /// Draws a wire mesh. /// Every single edge of the mesh will be drawn using a command. /// /// /// var go = GameObject.CreatePrimitive(PrimitiveType.Sphere); /// go.transform.position = new Vector3(0, 0, 0); /// using (Draw.InLocalSpace(go.transform)) { /// Draw.WireMesh(go.GetComponent().sharedMesh, color); /// } /// /// [Open online documentation to see images] /// /// See: /// /// Version: Supported in Unity 2020.1 or later. /// public void WireMesh (NativeArray vertices, NativeArray triangles) { #if UNITY_2020_1_OR_NEWER unsafe { JobWireMesh.WireMesh((float3*)vertices.GetUnsafeReadOnlyPtr(), (int*)triangles.GetUnsafeReadOnlyPtr(), vertices.Length, triangles.Length, ref this); } #else Debug.LogError("The WireMesh method is only suppored in Unity 2020.1 or later"); #endif } #if UNITY_2020_1_OR_NEWER /// Helper job for [BurstCompile] class JobWireMesh { public delegate void JobWireMeshDelegate(ref Mesh.MeshData rawMeshData, ref CommandBuilder draw); public static readonly JobWireMeshDelegate JobWireMeshFunctionPointer = BurstCompiler.CompileFunctionPointer(Execute).Invoke; [BurstCompile] public static unsafe void WireMesh (float3* verts, int* indices, int vertexCount, int indexCount, ref CommandBuilder draw) { if (indexCount % 3 != 0) { throw new System.ArgumentException("Invalid index count. Must be a multiple of 3"); } // Ignore warning about NativeHashMap being obsolete in early versions of the collections package. // It works just fine, and in later versions the NativeHashMap is not obsolete. #pragma warning disable 618 var seenEdges = new NativeHashMap(indexCount, Allocator.Temp); #pragma warning restore 618 for (int i = 0; i < indexCount; i += 3) { var a = indices[i]; var b = indices[i+1]; var c = indices[i+2]; if (a < 0 || b < 0 || c < 0 || a >= vertexCount || b >= vertexCount || c >= vertexCount) { throw new Exception("Invalid vertex index. Index out of bounds"); } int v1, v2; // Draw each edge of the triangle. // Check so that we do not draw an edge twice. v1 = math.min(a, b); v2 = math.max(a, b); if (!seenEdges.ContainsKey(new int2(v1, v2))) { seenEdges.Add(new int2(v1, v2), true); draw.Line(verts[v1], verts[v2]); } v1 = math.min(b, c); v2 = math.max(b, c); if (!seenEdges.ContainsKey(new int2(v1, v2))) { seenEdges.Add(new int2(v1, v2), true); draw.Line(verts[v1], verts[v2]); } v1 = math.min(c, a); v2 = math.max(c, a); if (!seenEdges.ContainsKey(new int2(v1, v2))) { seenEdges.Add(new int2(v1, v2), true); draw.Line(verts[v1], verts[v2]); } } } [BurstCompile] [AOT.MonoPInvokeCallback(typeof(JobWireMeshDelegate))] static void Execute (ref Mesh.MeshData rawMeshData, ref CommandBuilder draw) { int maxIndices = 0; for (int subMeshIndex = 0; subMeshIndex < rawMeshData.subMeshCount; subMeshIndex++) { maxIndices = math.max(maxIndices, rawMeshData.GetSubMesh(subMeshIndex).indexCount); } var tris = new NativeArray(maxIndices, Allocator.Temp, NativeArrayOptions.UninitializedMemory); var verts = new NativeArray(rawMeshData.vertexCount, Allocator.Temp, NativeArrayOptions.UninitializedMemory); rawMeshData.GetVertices(verts); for (int subMeshIndex = 0; subMeshIndex < rawMeshData.subMeshCount; subMeshIndex++) { var submesh = rawMeshData.GetSubMesh(subMeshIndex); rawMeshData.GetIndices(tris, subMeshIndex); unsafe { WireMesh((float3*)verts.GetUnsafeReadOnlyPtr(), (int*)tris.GetUnsafeReadOnlyPtr(), verts.Length, submesh.indexCount, ref draw); } } } } #endif /// /// Draws a solid mesh. /// The mesh will be drawn with a solid color. /// /// /// var go = GameObject.CreatePrimitive(PrimitiveType.Sphere); /// go.transform.position = new Vector3(0, 0, 0); /// using (Draw.InLocalSpace(go.transform)) { /// Draw.SolidMesh(go.GetComponent().sharedMesh, color); /// } /// /// [Open online documentation to see images] /// /// Note: This method is not thread safe and must not be used from the Unity Job System. /// TODO: Are matrices handled? /// /// See: /// public void SolidMesh (Mesh mesh) { SolidMeshInternal(mesh, false); } void SolidMeshInternal (Mesh mesh, bool temporary, Color color) { PushColor(color); SolidMeshInternal(mesh, temporary); PopColor(); } void SolidMeshInternal (Mesh mesh, bool temporary) { var g = gizmos.Target as DrawingData; g.data.Get(uniqueID).meshes.Add(new SubmittedMesh { mesh = mesh, temporary = temporary, }); // Internally we need to make sure to capture the current state // (which includes the current matrix and color) so that it // can be applied to the mesh. Reserve(4); Add(Command.CaptureState); } /// /// Draws a solid mesh with the given vertices. /// /// [Open online documentation to see images] /// /// Note: This method is not thread safe and must not be used from the Unity Job System. /// TODO: Are matrices handled? /// [BurstDiscard] public void SolidMesh (List vertices, List triangles, List colors) { if (vertices.Count != colors.Count) throw new System.ArgumentException("Number of colors must be the same as the number of vertices"); // TODO: Is this mesh getting recycled at all? var g = gizmos.Target as DrawingData; var mesh = g.GetMesh(vertices.Count); // Set all data on the mesh mesh.Clear(); mesh.SetVertices(vertices); mesh.SetTriangles(triangles, 0); mesh.SetColors(colors); // Upload all data mesh.UploadMeshData(false); SolidMeshInternal(mesh, true); } /// /// Draws a solid mesh with the given vertices. /// /// [Open online documentation to see images] /// /// Note: This method is not thread safe and must not be used from the Unity Job System. /// TODO: Are matrices handled? /// [BurstDiscard] public void SolidMesh (Vector3[] vertices, int[] triangles, Color[] colors, int vertexCount, int indexCount) { if (vertices.Length != colors.Length) throw new System.ArgumentException("Number of colors must be the same as the number of vertices"); // TODO: Is this mesh getting recycled at all? var g = gizmos.Target as DrawingData; var mesh = g.GetMesh(vertices.Length); // Set all data on the mesh mesh.Clear(); mesh.SetVertices(vertices, 0, vertexCount); mesh.SetTriangles(triangles, 0, indexCount, 0); mesh.SetColors(colors, 0, vertexCount); // Upload all data mesh.UploadMeshData(false); SolidMeshInternal(mesh, true); } /// /// Draws a 3D cross. /// /// [Open online documentation to see images] /// public void Cross (float3 position, float size = 1) { size *= 0.5f; Line(position - new float3(size, 0, 0), position + new float3(size, 0, 0)); Line(position - new float3(0, size, 0), position + new float3(0, size, 0)); Line(position - new float3(0, 0, size), position + new float3(0, 0, size)); } /// /// Draws a cross in the XZ plane. /// /// [Open online documentation to see images] /// [System.Obsolete("Use Draw.xz.Cross instead")] public void CrossXZ (float3 position, float size = 1) { size *= 0.5f; Line(position - new float3(size, 0, 0), position + new float3(size, 0, 0)); Line(position - new float3(0, 0, size), position + new float3(0, 0, size)); } /// /// Draws a cross in the XY plane. /// /// [Open online documentation to see images] /// [System.Obsolete("Use Draw.xy.Cross instead")] public void CrossXY (float3 position, float size = 1) { size *= 0.5f; Line(position - new float3(size, 0, 0), position + new float3(size, 0, 0)); Line(position - new float3(0, size, 0), position + new float3(0, size, 0)); } /// Returns a point on a cubic bezier curve. t is clamped between 0 and 1 public static float3 EvaluateCubicBezier (float3 p0, float3 p1, float3 p2, float3 p3, float t) { t = math.clamp(t, 0, 1); float tr = 1-t; return tr*tr*tr * p0 + 3 * tr*tr * t * p1 + 3 * tr * t*t * p2 + t*t*t * p3; } /// /// Draws a cubic bezier curve. /// /// [Open online documentation to see images] /// /// [Open online documentation to see images] /// /// TODO: Currently uses a fixed resolution of 20 segments. Resolution should depend on the distance to the camera. /// /// See: https://en.wikipedia.org/wiki/Bezier_curve /// /// Start point /// First control point /// Second control point /// End point public void Bezier (float3 p0, float3 p1, float3 p2, float3 p3) { float3 prev = p0; for (int i = 1; i <= 20; i++) { float t = i/20.0f; float3 p = EvaluateCubicBezier(p0, p1, p2, p3, t); Line(prev, p); prev = p; } } /// /// Draws a smooth curve through a list of points. /// /// A catmull-rom spline is equivalent to a bezier curve with control points determined by an algorithm. /// In fact, this package displays catmull-rom splines by first converting them to bezier curves. /// /// [Open online documentation to see images] /// /// See: https://en.wikipedia.org/wiki/Centripetal_Catmull%E2%80%93Rom_spline /// See: /// /// The curve will smoothly pass through each point in the list in order. public void CatmullRom (List points) { if (points.Count < 2) return; if (points.Count == 2) { Line(points[0], points[1]); } else { // count >= 3 var count = points.Count; // Draw first curve, this is special because the first two control points are the same CatmullRom(points[0], points[0], points[1], points[2]); for (int i = 0; i + 3 < count; i++) { CatmullRom(points[i], points[i+1], points[i+2], points[i+3]); } // Draw last curve CatmullRom(points[count-3], points[count-2], points[count-1], points[count-1]); } } /// /// Draws a centripetal catmull rom spline. /// /// The curve starts at p1 and ends at p2. /// /// [Open online documentation to see images] /// [Open online documentation to see images] /// /// See: /// /// First control point /// Second control point. Start of the curve. /// Third control point. End of the curve. /// Fourth control point. public void CatmullRom (float3 p0, float3 p1, float3 p2, float3 p3) { // References used: // p.266 GemsV1 // // tension is often set to 0.5 but you can use any reasonable value: // http://www.cs.cmu.edu/~462/projects/assn2/assn2/catmullRom.pdf // // bias and tension controls: // http://local.wasp.uwa.edu.au/~pbourke/miscellaneous/interpolation/ // We will convert the catmull rom spline to a bezier curve for simplicity. // The end result of this will be a conversion matrix where we transform catmull rom control points // into the equivalent bezier curve control points. // Conversion matrix // ================= // A centripetal catmull rom spline can be separated into the following terms: // 1 * p1 + // t * (-0.5 * p0 + 0.5*p2) + // t*t * (p0 - 2.5*p1 + 2.0*p2 + 0.5*t2) + // t*t*t * (-0.5*p0 + 1.5*p1 - 1.5*p2 + 0.5*p3) // // Matrix form: // 1 t t^2 t^3 // {0, -1/2, 1, -1/2} // {1, 0, -5/2, 3/2} // {0, 1/2, 2, -3/2} // {0, 0, -1/2, 1/2} // Transposed matrix: // M_1 = {{0, 1, 0, 0}, {-1/2, 0, 1/2, 0}, {1, -5/2, 2, -1/2}, {-1/2, 3/2, -3/2, 1/2}} // A bezier spline can be separated into the following terms: // (-t^3 + 3 t^2 - 3 t + 1) * c0 + // (3t^3 - 6*t^2 + 3t) * c1 + // (3t^2 - 3t^3) * c2 + // t^3 * c3 // // Matrix form: // 1 t t^2 t^3 // {1, -3, 3, -1} // {0, 3, -6, 3} // {0, 0, 3, -3} // {0, 0, 0, 1} // Transposed matrix: // M_2 = {{1, 0, 0, 0}, {-3, 3, 0, 0}, {3, -6, 3, 0}, {-1, 3, -3, 1}} // Thus a bezier curve can be evaluated using the expression // output1 = T * M_1 * c // where T = [1, t, t^2, t^3] and c being the control points c = [c0, c1, c2, c3]^T // // and a catmull rom spline can be evaluated using // // output2 = T * M_2 * p // where T = same as before and p = [p0, p1, p2, p3]^T // // We can solve for c in output1 = output2 // T * M_1 * c = T * M_2 * p // M_1 * c = M_2 * p // c = M_1^(-1) * M_2 * p // Thus a conversion matrix from p to c is M_1^(-1) * M_2 // This can be calculated and the result is the following matrix: // // {0, 1, 0, 0} // {-1/6, 1, 1/6, 0} // {0, 1/6, 1, -1/6} // {0, 0, 1, 0} // ------------------------------------------------------------------ // // Using this we can calculate c = M_1^(-1) * M_2 * p var c0 = p1; var c1 = (-p0 + 6*p1 + 1*p2)*(1/6.0f); var c2 = (p1 + 6*p2 - p3)*(1/6.0f); var c3 = p2; // And finally draw the bezier curve which is equivalent to the desired catmull-rom spline Bezier(c0, c1, c2, c3); } /// /// Draws an arrow between two points. /// /// The size of the head defaults to 20% of the length of the arrow. /// /// [Open online documentation to see images] /// /// See: /// See: /// See: /// /// Base of the arrow. /// Head of the arrow. public void Arrow (float3 from, float3 to) { ArrowRelativeSizeHead(from, to, DEFAULT_UP, 0.2f); } /// /// Draws an arrow between two points. /// /// [Open online documentation to see images] /// /// See: /// See: /// /// Base of the arrow. /// Head of the arrow. /// Up direction of the world, the arrowhead plane will be as perpendicular as possible to this direction. Defaults to Vector3.up. /// The size of the arrowhead in world units. public void Arrow (float3 from, float3 to, float3 up, float headSize) { var length_sq = math.lengthsq(to - from); if (length_sq > 0.000001f) { ArrowRelativeSizeHead(from, to, up, headSize * math.rsqrt(length_sq)); } } /// /// Draws an arrow between two points with a head that varies with the length of the arrow. /// /// [Open online documentation to see images] /// /// See: /// See: /// /// Base of the arrow. /// Head of the arrow. /// Up direction of the world, the arrowhead plane will be as perpendicular as possible to this direction. /// The length of the arrowhead is the distance between from and to multiplied by this fraction. Should be between 0 and 1. public void ArrowRelativeSizeHead (float3 from, float3 to, float3 up, float headFraction) { Line(from, to); var dir = to - from; var normal = math.cross(dir, up); // Pick a different up direction if the direction happened to be colinear with that one. if (math.all(normal == 0)) normal = math.cross(new float3(1, 0, 0), dir); // Pick a different up direction if up=(1,0,0) and thus the above check would have generated a zero vector again if (math.all(normal == 0)) normal = math.cross(new float3(0, 1, 0), dir); normal = math.normalizesafe(normal) * math.length(dir); Line(to, to - (dir + normal) * headFraction); Line(to, to - (dir - normal) * headFraction); } /// /// Draws an arrowhead at a point. /// /// /// Draw.Arrowhead(Vector3.zero, Vector3.forward, 0.75f, color); /// /// [Open online documentation to see images] /// /// See: /// See: /// /// Center of the arrowhead. /// Direction the arrow is pointing. /// Distance from the center to each corner of the arrowhead. public void Arrowhead (float3 center, float3 direction, float radius) { Arrowhead(center, direction, DEFAULT_UP, radius); } /// /// Draws an arrowhead at a point. /// /// /// Draw.Arrowhead(Vector3.zero, Vector3.forward, 0.75f, color); /// /// [Open online documentation to see images] /// /// See: /// See: /// /// Center of the arrowhead. /// Direction the arrow is pointing. /// Up direction of the world, the arrowhead plane will be as perpendicular as possible to this direction. Defaults to Vector3.up. Must be normalized. /// Distance from the center to each corner of the arrowhead. public void Arrowhead (float3 center, float3 direction, float3 up, float radius) { if (math.all(direction == 0)) return; direction = math.normalizesafe(direction); var normal = math.cross(direction, up); const float SinPiOver3 = 0.866025f; const float CosPiOver3 = 0.5f; var circleCenter = center - radius * (1 - CosPiOver3)*0.5f * direction; var p1 = circleCenter + radius * direction; var p2 = circleCenter - radius * CosPiOver3 * direction + radius * SinPiOver3 * normal; var p3 = circleCenter - radius * CosPiOver3 * direction - radius * SinPiOver3 * normal; Line(p1, p2); Line(p2, circleCenter); Line(circleCenter, p3); Line(p3, p1); } /// /// Draws an arrowhead centered around a circle. /// /// This can be used to for example show the direction a character is moving in. /// /// [Open online documentation to see images] /// /// Note: In the image above the arrowhead is the only part that is drawn by this method. The cylinder is only included for context. /// /// See: /// /// Point around which the arc is centered /// Direction the arrow is pointing /// Distance from origin that the arrow starts. /// Width of the arrowhead in degrees (defaults to 60). Should be between 0 and 90. public void ArrowheadArc (float3 origin, float3 direction, float offset, float width = 60) { if (!math.any(direction)) return; if (offset < 0) throw new System.ArgumentOutOfRangeException(nameof(offset)); if (offset == 0) return; var rot = Quaternion.LookRotation(direction, DEFAULT_UP); PushMatrix(Matrix4x4.TRS(origin, rot, Vector3.one)); var a1 = math.PI * 0.5f - width * (0.5f * Mathf.Deg2Rad); var a2 = math.PI * 0.5f + width * (0.5f * Mathf.Deg2Rad); CircleXZInternal(float3.zero, offset, a1, a2); var p1 = new float3(math.cos(a1), 0, math.sin(a1)) * offset; var p2 = new float3(math.cos(a2), 0, math.sin(a2)) * offset; const float sqrt2 = 1.4142f; var p3 = new float3(0, 0, sqrt2 * offset); Line(p1, p3); Line(p3, p2); PopMatrix(); } /// /// Draws a grid of lines. /// /// /// Draw.xz.WireGrid(Vector3.zero, new int2(3, 3), new float2(1, 1), color); /// /// [Open online documentation to see images] /// /// Center of the grid /// Rotation of the grid. The grid will be aligned to the X and Z axes of the rotation. /// Number of cells of the grid. Should be greater than 0. /// Total size of the grid along the X and Z axes. public void WireGrid (float3 center, quaternion rotation, int2 cells, float2 totalSize) { cells = math.max(cells, new int2(1, 1)); PushMatrix(float4x4.TRS(center, rotation, new Vector3(totalSize.x, 0, totalSize.y))); int w = cells.x; int h = cells.y; for (int i = 0; i <= w; i++) Line(new float3(i/(float)w - 0.5f, 0, -0.5f), new float3(i/(float)w - 0.5f, 0, 0.5f)); for (int i = 0; i <= h; i++) Line(new float3(-0.5f, 0, i/(float)h - 0.5f), new float3(0.5f, 0, i/(float)h - 0.5f)); PopMatrix(); } /// /// Draws a triangle outline. /// /// /// Draw.WireTriangle(new Vector3(-0.5f, 0, 0), new Vector3(0, 1, 0), new Vector3(0.5f, 0, 0), Color.black); /// /// [Open online documentation to see images] /// /// See: /// See: /// See: /// /// First corner of the triangle /// Second corner of the triangle /// Third corner of the triangle public void WireTriangle (float3 a, float3 b, float3 c) { Line(a, b); Line(b, c); Line(c, a); } /// /// Draws a rectangle outline. /// The rectangle will be aligned to the X and Z axes. /// /// /// Draw.xz.WireRectangle(new Vector3(0f, 0, 0), new Vector2(1, 1), Color.black); /// /// [Open online documentation to see images] /// /// See: /// [System.Obsolete("Use Draw.xz.WireRectangle instead")] public void WireRectangleXZ (float3 center, float2 size) { WireRectangle(center, quaternion.identity, size); } /// /// Draws a rectangle outline. /// The rectangle will be oriented along the rotation's X and Z axes. /// /// /// Draw.WireRectangle(new Vector3(0f, 0, 0), Quaternion.identity, new Vector2(1, 1), Color.black); /// /// [Open online documentation to see images] /// /// This is identical to , but this name is added for consistency. /// /// See: /// public void WireRectangle (float3 center, quaternion rotation, float2 size) { WirePlane(center, rotation, size); } /// /// Draws a rectangle outline. /// The rectangle corners are assumed to be in XY space. /// This is particularly useful when combined with . /// /// /// using (Draw.InScreenSpace(Camera.main)) { /// Draw.xy.WireRectangle(new Rect(10, 10, 100, 100), Color.black); /// } /// /// [Open online documentation to see images] /// /// See: /// See: /// See: /// [System.Obsolete("Use Draw.xy.WireRectangle instead")] public void WireRectangle (Rect rect) { xy.WireRectangle(rect); } /// /// Draws a triangle outline. /// /// /// Draw.WireTriangle(Vector3.zero, Quaternion.identity, 0.5f, color); /// /// [Open online documentation to see images] /// /// Note: This is a convenience wrapper for /// /// See: /// /// Center of the triangle. /// Rotation of the triangle. The first vertex will be radius units in front of center as seen from the rotation's point of view. /// Distance from the center to each vertex. public void WireTriangle (float3 center, quaternion rotation, float radius) { WirePolygon(center, 3, rotation, radius); } /// /// Draws a pentagon outline. /// /// /// Draw.WirePentagon(Vector3.zero, Quaternion.identity, 0.5f, color); /// /// [Open online documentation to see images] /// /// Note: This is a convenience wrapper for /// /// Center of the polygon. /// Rotation of the polygon. The first vertex will be radius units in front of center as seen from the rotation's point of view. /// Distance from the center to each vertex. public void WirePentagon (float3 center, quaternion rotation, float radius) { WirePolygon(center, 5, rotation, radius); } /// /// Draws a hexagon outline. /// /// /// Draw.WireHexagon(Vector3.zero, Quaternion.identity, 0.5f, color); /// /// [Open online documentation to see images] /// /// Note: This is a convenience wrapper for /// /// Center of the polygon. /// Rotation of the polygon. The first vertex will be radius units in front of center as seen from the rotation's point of view. /// Distance from the center to each vertex. public void WireHexagon (float3 center, quaternion rotation, float radius) { WirePolygon(center, 6, rotation, radius); } /// /// Draws a regular polygon outline. /// /// /// Draw.WirePolygon(new Vector3(-0.5f, 0, +0.5f), 3, Quaternion.identity, 0.4f, color); /// Draw.WirePolygon(new Vector3(+0.5f, 0, +0.5f), 4, Quaternion.identity, 0.4f, color); /// Draw.WirePolygon(new Vector3(-0.5f, 0, -0.5f), 5, Quaternion.identity, 0.4f, color); /// Draw.WirePolygon(new Vector3(+0.5f, 0, -0.5f), 6, Quaternion.identity, 0.4f, color); /// /// [Open online documentation to see images] /// /// See: /// See: /// See: /// /// Center of the polygon. /// Number of corners (and sides) of the polygon. /// Rotation of the polygon. The first vertex will be radius units in front of center as seen from the rotation's point of view. /// Distance from the center to each vertex. public void WirePolygon (float3 center, int vertices, quaternion rotation, float radius) { PushMatrix(float4x4.TRS(center, rotation, new float3(radius, radius, radius))); float3 prev = new float3(0, 0, 1); for (int i = 1; i <= vertices; i++) { float a = 2 * math.PI * (i / (float)vertices); var p = new float3(math.sin(a), 0, math.cos(a)); Line(prev, p); prev = p; } PopMatrix(); } /// /// Draws a solid rectangle. /// The rectangle corners are assumed to be in XY space. /// This is particularly useful when combined with . /// /// Behind the scenes this is implemented using . /// /// /// using (Draw.InScreenSpace(Camera.main)) { /// Draw.xy.SolidRectangle(new Rect(10, 10, 100, 100), Color.black); /// } /// /// [Open online documentation to see images] /// /// See: /// See: /// See: /// [System.Obsolete("Use Draw.xy.SolidRectangle instead")] public void SolidRectangle (Rect rect) { xy.SolidRectangle(rect); } /// /// Draws a solid plane. /// /// /// Draw.SolidPlane(new float3(0, 0, 0), new float3(0, 1, 0), 1.0f, color); /// /// [Open online documentation to see images] /// /// Center of the visualized plane. /// Direction perpendicular to the plane. If this is (0,0,0) then nothing will be rendered. /// Width and height of the visualized plane. public void SolidPlane (float3 center, float3 normal, float2 size) { if (math.any(normal)) { SolidPlane(center, Quaternion.LookRotation(calculateTangent(normal), normal), size); } } /// /// Draws a solid plane. /// /// The plane will lie in the XZ plane with respect to the rotation. /// /// /// Draw.SolidPlane(new float3(0, 0, 0), new float3(0, 1, 0), 1.0f, color); /// /// [Open online documentation to see images] /// /// Center of the visualized plane. /// Width and height of the visualized plane. public void SolidPlane (float3 center, quaternion rotation, float2 size) { PushMatrix(float4x4.TRS(center, rotation, new float3(size.x, 0, size.y))); Reserve(); Add(Command.Box); Add(new BoxData { center = 0, size = 1 }); PopMatrix(); } /// Returns an arbitrary vector which is orthogonal to the given one private static float3 calculateTangent (float3 normal) { var tangent = math.cross(new float3(0, 1, 0), normal); if (math.all(tangent == 0)) tangent = math.cross(new float3(1, 0, 0), normal); return tangent; } /// /// Draws a wire plane. /// /// /// Draw.WirePlane(new float3(0, 0, 0), new float3(0, 1, 0), 1.0f, color); /// /// [Open online documentation to see images] /// /// Center of the visualized plane. /// Direction perpendicular to the plane. If this is (0,0,0) then nothing will be rendered. /// Width and height of the visualized plane. public void WirePlane (float3 center, float3 normal, float2 size) { if (math.any(normal)) { WirePlane(center, Quaternion.LookRotation(calculateTangent(normal), normal), size); } } /// /// Draws a wire plane. /// /// This is identical to , but it is included for consistency. /// /// /// Draw.WirePlane(new float3(0, 0, 0), new float3(0, 1, 0), 1.0f, color); /// /// [Open online documentation to see images] /// /// Center of the visualized plane. /// Rotation of the plane. The plane will lie in the XZ plane with respect to the rotation. /// Width and height of the visualized plane. public void WirePlane (float3 center, quaternion rotation, float2 size) { Reserve(); Add(Command.WirePlane); Add(new PlaneData { center = center, rotation = rotation, size = size }); } /// /// Draws a plane and a visualization of its normal. /// /// /// Draw.PlaneWithNormal(new float3(0, 0, 0), new float3(0, 1, 0), 1.0f, color); /// /// [Open online documentation to see images] /// /// Center of the visualized plane. /// Direction perpendicular to the plane. If this is (0,0,0) then nothing will be rendered. /// Width and height of the visualized plane. public void PlaneWithNormal (float3 center, float3 normal, float2 size) { if (math.any(normal)) { PlaneWithNormal(center, Quaternion.LookRotation(calculateTangent(normal), normal), size); } } /// /// Draws a plane and a visualization of its normal. /// /// /// Draw.PlaneWithNormal(new float3(0, 0, 0), new float3(0, 1, 0), 1.0f, color); /// /// [Open online documentation to see images] /// /// Center of the visualized plane. /// Rotation of the plane. The plane will lie in the XZ plane with respect to the rotation. /// Width and height of the visualized plane. public void PlaneWithNormal (float3 center, quaternion rotation, float2 size) { SolidPlane(center, rotation, size); WirePlane(center, rotation, size); ArrowRelativeSizeHead(center, center + math.mul(rotation, new float3(0, 1, 0)) * 0.5f, math.mul(rotation, new float3(0, 0, 1)), 0.2f); } /// /// Draws a solid triangle. /// /// /// Draw.xy.SolidTriangle(new float2(-0.43f, -0.25f), new float2(0, 0.5f), new float2(0.43f, -0.25f), color); /// /// [Open online documentation to see images] /// /// Note: If you are going to be drawing lots of triangles it's better to use instead as it will be more efficient. /// /// See: /// See: /// /// First corner of the triangle. /// Second corner of the triangle. /// Third corner of the triangle. public void SolidTriangle (float3 a, float3 b, float3 c) { Reserve(); Add(Command.SolidTriangle); Add(new TriangleData { a = a, b = b, c = c }); } /// /// Draws a solid box. /// /// /// Draw.SolidBox(new float3(0, 0, 0), new float3(1, 1, 1), color); /// /// [Open online documentation to see images] /// /// Center of the box /// Width of the box along all dimensions public void SolidBox (float3 center, float3 size) { Reserve(); Add(Command.Box); Add(new BoxData { center = center, size = size }); } /// /// Draws a solid box. /// /// /// Draw.SolidBox(new float3(0, 0, 0), new float3(1, 1, 1), color); /// /// [Open online documentation to see images] /// /// Bounding box of the box public void SolidBox (Bounds bounds) { SolidBox(bounds.center, bounds.size); } /// /// Draws a solid box. /// /// /// Draw.SolidBox(new float3(0, 0, 0), new float3(1, 1, 1), color); /// /// [Open online documentation to see images] /// /// Center of the box /// Rotation of the box /// Width of the box along all dimensions public void SolidBox (float3 center, quaternion rotation, float3 size) { PushMatrix(float4x4.TRS(center, rotation, size)); SolidBox(float3.zero, Vector3.one); PopMatrix(); } /// /// Draws a label in 3D space. /// /// The default alignment is . /// /// /// Draw.Label3D(new float3(0.2f, -1f, 0.2f), Quaternion.Euler(45, -110, -90), "Label", 1, LabelAlignment.Center, color); /// /// [Open online documentation to see images] /// /// See: Label3D(float3,quaternion,string,float,LabelAlignment) /// /// Note: Only ASCII is supported since the built-in font texture only includes ASCII. Other characters will be rendered as question marks (?). /// /// Position in 3D space. /// Rotation in 3D space. /// Text to display. /// World size of the text. For large sizes an SDF (signed distance field) font is used and for small sizes a normal font texture is used. public void Label3D (float3 position, quaternion rotation, string text, float size) { Label3D(position, rotation, text, size, LabelAlignment.MiddleLeft); } /// /// Draws a label in 3D space. /// /// /// Draw.Label3D(new float3(0.2f, -1f, 0.2f), Quaternion.Euler(45, -110, -90), "Label", 1, LabelAlignment.Center, color); /// /// [Open online documentation to see images] /// /// See: Label3D(float3,quaternion,string,float) /// /// Note: Only ASCII is supported since the built-in font texture only includes ASCII. Other characters will be rendered as question marks (?). /// /// Note: This method cannot be used in burst since managed strings are not suppported in burst. However, you can use the separate Label3D overload which takes a FixedString. /// /// Position in 3D space. /// Rotation in 3D space. /// Text to display. /// World size of the text. For large sizes an SDF (signed distance field) font is used and for small sizes a normal font texture is used. /// How to align the text relative to the given position. public void Label3D (float3 position, quaternion rotation, string text, float size, LabelAlignment alignment) { AssertBufferExists(); Reserve(); Add(Command.Text3D); Add(new TextData3D { center = position, rotation = rotation, numCharacters = text.Length, size = size, alignment = alignment }); AddText(text); } /// /// Draws a label in 3D space aligned with the camera. /// /// The default alignment is . /// /// /// Draw.Label2D(Vector3.zero, "Label", 48, LabelAlignment.Center, color); /// /// [Open online documentation to see images] /// /// See: Label2D(float3,string,float,LabelAlignment) /// /// Note: Only ASCII is supported since the built-in font texture only includes ASCII. Other characters will be rendered as question marks (?). /// /// Position in 3D space. /// Text to display. /// Size of the text in screen pixels. For large sizes an SDF (signed distance field) font is used and for small sizes a normal font texture is used. public void Label2D (float3 position, string text, float sizeInPixels = 14) { Label2D(position, text, sizeInPixels, LabelAlignment.MiddleLeft); } /// /// Draws a label in 3D space aligned with the camera. /// /// /// Draw.Label2D(Vector3.zero, "Label", 48, LabelAlignment.Center, color); /// /// [Open online documentation to see images] /// /// See: Label2D(float3,string,float) /// /// Note: Only ASCII is supported since the built-in font texture only includes ASCII. Other characters will be rendered as question marks (?). /// /// Note: This method cannot be used in burst since managed strings are not suppported in burst. However, you can use the separate Label2D overload which takes a FixedString. /// /// Position in 3D space. /// Text to display. /// Size of the text in screen pixels. For large sizes an SDF (signed distance field) font is used and for small sizes a normal font texture is used. /// How to align the text relative to the given position. public void Label2D (float3 position, string text, float sizeInPixels, LabelAlignment alignment) { AssertBufferExists(); Reserve(); Add(Command.Text); Add(new TextData { center = position, numCharacters = text.Length, sizeInPixels = sizeInPixels, alignment = alignment }); AddText(text); } void AddText (string text) { var g = gizmos.Target as DrawingData; Reserve(UnsafeUtility.SizeOf() * text.Length); for (int i = 0; i < text.Length; i++) { char c = text[i]; System.UInt16 index = (System.UInt16)g.fontData.GetIndex(c); Add(index); } } #region Label2DFixedString /// /// Draws a label in 3D space aligned with the camera. /// /// /// // This part can be inside a burst job /// for (int i = 0; i < 10; i++) { /// Unity.Collections.FixedString32Bytes text = $"X = {i}"; /// builder.Label2D(new float3(i, 0, 0), ref text, 12, LabelAlignment.Center); /// } /// /// [Open online documentation to see images] /// /// See: Label2D(float3,string,float) /// /// Note: Only ASCII is supported since the built-in font texture only includes ASCII. Other characters will be rendered as question marks (?). /// /// Note: This method requires the Unity.Collections package version 0.8 or later. /// /// Position in 3D space. /// Text to display. /// Size of the text in screen pixels. For large sizes an SDF (signed distance field) font is used and for small sizes a normal font texture is used. public void Label2D (float3 position, ref FixedString32Bytes text, float sizeInPixels = 14) { Label2D(position, ref text, sizeInPixels, LabelAlignment.MiddleLeft); } /// \copydocref{Label2D(float3,FixedString32Bytes,float)} public void Label2D (float3 position, ref FixedString64Bytes text, float sizeInPixels = 14) { Label2D(position, ref text, sizeInPixels, LabelAlignment.MiddleLeft); } /// \copydocref{Label2D(float3,FixedString32Bytes,float)} public void Label2D (float3 position, ref FixedString128Bytes text, float sizeInPixels = 14) { Label2D(position, ref text, sizeInPixels, LabelAlignment.MiddleLeft); } /// \copydocref{Label2D(float3,FixedString32Bytes,float)} public void Label2D (float3 position, ref FixedString512Bytes text, float sizeInPixels = 14) { Label2D(position, ref text, sizeInPixels, LabelAlignment.MiddleLeft); } /// /// Draws a label in 3D space aligned with the camera. /// /// /// // This part can be inside a burst job /// for (int i = 0; i < 10; i++) { /// Unity.Collections.FixedString32Bytes text = $"X = {i}"; /// builder.Label2D(new float3(i, 0, 0), ref text, 12, LabelAlignment.Center); /// } /// /// [Open online documentation to see images] /// /// See: Label2D(float3,string,float) /// /// Note: Only ASCII is supported since the built-in font texture only includes ASCII. Other characters will be rendered as question marks (?). /// /// Note: This method requires the Unity.Collections package version 0.8 or later. /// /// Position in 3D space. /// Text to display. /// Size of the text in screen pixels. For large sizes an SDF (signed distance field) font is used and for small sizes a normal font texture is used. /// How to align the text relative to the given position. public void Label2D (float3 position, ref FixedString32Bytes text, float sizeInPixels, LabelAlignment alignment) { #if MODULE_COLLECTIONS_0_12_0_OR_NEWER unsafe { Label2D(position, text.GetUnsafePtr(), text.Length, sizeInPixels, alignment); } #else Debug.LogError("The Label2D method which takes FixedStrings requires the Unity.Collections package version 0.12 or newer"); #endif } /// \copydocref{Label2D(float3,FixedString32Bytes,float,LabelAlignment)} public void Label2D (float3 position, ref FixedString64Bytes text, float sizeInPixels, LabelAlignment alignment) { #if MODULE_COLLECTIONS_0_12_0_OR_NEWER unsafe { Label2D(position, text.GetUnsafePtr(), text.Length, sizeInPixels, alignment); } #else Debug.LogError("The Label2D method which takes FixedStrings requires the Unity.Collections package version 0.12 or newer"); #endif } /// \copydocref{Label2D(float3,FixedString32Bytes,float,LabelAlignment)} public void Label2D (float3 position, ref FixedString128Bytes text, float sizeInPixels, LabelAlignment alignment) { #if MODULE_COLLECTIONS_0_12_0_OR_NEWER unsafe { Label2D(position, text.GetUnsafePtr(), text.Length, sizeInPixels, alignment); } #else Debug.LogError("The Label2D method which takes FixedStrings requires the Unity.Collections package version 0.12 or newer"); #endif } /// \copydocref{Label2D(float3,FixedString32Bytes,float,LabelAlignment)} public void Label2D (float3 position, ref FixedString512Bytes text, float sizeInPixels, LabelAlignment alignment) { #if MODULE_COLLECTIONS_0_12_0_OR_NEWER unsafe { Label2D(position, text.GetUnsafePtr(), text.Length, sizeInPixels, alignment); } #else Debug.LogError("The Label2D method which takes FixedStrings requires the Unity.Collections package version 0.12 or newer"); #endif } /// \copydocref{Label2D(float3,FixedString32Bytes,float,LabelAlignment)} internal unsafe void Label2D (float3 position, byte* text, int byteCount, float sizeInPixels, LabelAlignment alignment) { #if MODULE_COLLECTIONS_0_12_0_OR_NEWER AssertBufferExists(); Reserve(); Add(Command.Text); Add(new TextData { center = position, numCharacters = byteCount, sizeInPixels = sizeInPixels, alignment = alignment }); Reserve(UnsafeUtility.SizeOf() * byteCount); for (int i = 0; i < byteCount; i++) { // The first 128 elements in the font data are guaranteed to be laid out as ascii. // We use this since we cannot use the dynamic font lookup. System.UInt16 c = *(text + i); if (c >= 128) c = (System.UInt16) '?'; if (c == (byte)'\n') c = SDFLookupData.Newline; // Ignore carriage return instead of printing them as '?'. Windows encodes newlines as \r\n. if (c == (byte)'\r') continue; Add(c); } #endif } #endregion #region Label3DFixedString /// /// Draws a label in 3D space. /// /// /// // This part can be inside a burst job /// for (int i = 0; i < 10; i++) { /// Unity.Collections.FixedString32Bytes text = $"X = {i}"; /// builder.Label3D(new float3(i, 0, 0), quaternion.identity, ref text, 1, LabelAlignment.Center); /// } /// /// [Open online documentation to see images] /// /// See: Label3D(float3,quaternion,string,float) /// /// Note: Only ASCII is supported since the built-in font texture only includes ASCII. Other characters will be rendered as question marks (?). /// /// Note: This method requires the Unity.Collections package version 0.8 or later. /// /// Position in 3D space. /// Rotation in 3D space. /// Text to display. /// World size of the text. For large sizes an SDF (signed distance field) font is used and for small sizes a normal font texture is used. public void Label3D (float3 position, quaternion rotation, ref FixedString32Bytes text, float size) { Label3D(position, rotation, ref text, size, LabelAlignment.MiddleLeft); } /// \copydocref{Label3D(float3,quaternion,FixedString32Bytes,float)} public void Label3D (float3 position, quaternion rotation, ref FixedString64Bytes text, float size) { Label3D(position, rotation, ref text, size, LabelAlignment.MiddleLeft); } /// \copydocref{Label3D(float3,quaternion,FixedString32Bytes,float)} public void Label3D (float3 position, quaternion rotation, ref FixedString128Bytes text, float size) { Label3D(position, rotation, ref text, size, LabelAlignment.MiddleLeft); } /// \copydocref{Label3D(float3,quaternion,FixedString32Bytes,float)} public void Label3D (float3 position, quaternion rotation, ref FixedString512Bytes text, float size) { Label3D(position, rotation, ref text, size, LabelAlignment.MiddleLeft); } /// /// Draws a label in 3D space. /// /// /// // This part can be inside a burst job /// for (int i = 0; i < 10; i++) { /// Unity.Collections.FixedString32Bytes text = $"X = {i}"; /// builder.Label3D(new float3(i, 0, 0), quaternion.identity, ref text, 1, LabelAlignment.Center); /// } /// /// [Open online documentation to see images] /// /// See: Label3D(float3,quaternion,string,float) /// /// Note: Only ASCII is supported since the built-in font texture only includes ASCII. Other characters will be rendered as question marks (?). /// /// Note: This method requires the Unity.Collections package version 0.8 or later. /// /// Position in 3D space. /// Rotation in 3D space. /// Text to display. /// World size of the text. For large sizes an SDF (signed distance field) font is used and for small sizes a normal font texture is used. /// How to align the text relative to the given position. public void Label3D (float3 position, quaternion rotation, ref FixedString32Bytes text, float size, LabelAlignment alignment) { #if MODULE_COLLECTIONS_0_12_0_OR_NEWER unsafe { Label3D(position, rotation, text.GetUnsafePtr(), text.Length, size, alignment); } #else Debug.LogError("The Label3D method which takes FixedStrings requires the Unity.Collections package version 0.12 or newer"); #endif } /// \copydocref{Label3D(float3,quaternion,FixedString32Bytes,float,LabelAlignment)} public void Label3D (float3 position, quaternion rotation, ref FixedString64Bytes text, float size, LabelAlignment alignment) { #if MODULE_COLLECTIONS_0_12_0_OR_NEWER unsafe { Label3D(position, rotation, text.GetUnsafePtr(), text.Length, size, alignment); } #else Debug.LogError("The Label3D method which takes FixedStrings requires the Unity.Collections package version 0.12 or newer"); #endif } /// \copydocref{Label3D(float3,quaternion,FixedString32Bytes,float,LabelAlignment)} public void Label3D (float3 position, quaternion rotation, ref FixedString128Bytes text, float size, LabelAlignment alignment) { #if MODULE_COLLECTIONS_0_12_0_OR_NEWER unsafe { Label3D(position, rotation, text.GetUnsafePtr(), text.Length, size, alignment); } #else Debug.LogError("The Label3D method which takes FixedStrings requires the Unity.Collections package version 0.12 or newer"); #endif } /// \copydocref{Label3D(float3,quaternion,FixedString32Bytes,float,LabelAlignment)} public void Label3D (float3 position, quaternion rotation, ref FixedString512Bytes text, float size, LabelAlignment alignment) { #if MODULE_COLLECTIONS_0_12_0_OR_NEWER unsafe { Label3D(position, rotation, text.GetUnsafePtr(), text.Length, size, alignment); } #else Debug.LogError("The Label3D method which takes FixedStrings requires the Unity.Collections package version 0.12 or newer"); #endif } /// \copydocref{Label3D(float3,quaternion,FixedString32Bytes,float,LabelAlignment)} internal unsafe void Label3D (float3 position, quaternion rotation, byte* text, int byteCount, float size, LabelAlignment alignment) { #if MODULE_COLLECTIONS_0_12_0_OR_NEWER AssertBufferExists(); Reserve(); Add(Command.Text3D); Add(new TextData3D { center = position, rotation = rotation, numCharacters = byteCount, size = size, alignment = alignment }); Reserve(UnsafeUtility.SizeOf() * byteCount); for (int i = 0; i < byteCount; i++) { // The first 128 elements in the font data are guaranteed to be laid out as ascii. // We use this since we cannot use the dynamic font lookup. System.UInt16 c = *(text + i); if (c >= 128) c = (System.UInt16) '?'; if (c == (byte)'\n') c = SDFLookupData.Newline; Add(c); } #endif } #endregion } }