Skip to content

Instantly share code, notes, and snippets.

@FreyaHolmer
Last active October 8, 2024 21:25
Show Gist options
  • Save FreyaHolmer/71717be9f3030c1b0990d3ed1ae833e3 to your computer and use it in GitHub Desktop.
Save FreyaHolmer/71717be9f3030c1b0990d3ed1ae833e3 to your computer and use it in GitHub Desktop.
A unity shader .cginc to draw numbers in the fragment shader - see the first comment below for example usage!
///////////////////////////////////////////////////////////////////////////////
// ABOUT: A unity Shader .cginc to draw numbers in the fragment shader
// AUTHOR: Freya Holmér
// LICENSE: Use for whatever, commercial or otherwise!
// Don't hold me liable for issues though
// But pls credit me if it works super well <3
// LIMITATIONS: There's some precision loss beyond 3 decimal places
// CONTRIBUTORS: yes please! if you know a more precise way to get
// decimal digits then pls lemme know!
// GetDecimalSymbolAt() could use some more love/precision
// These are the main drawing functions:
// - returns white text on black background (though trailing zeroes are gray)
// - billboarded to always face the camera
// - you can get pxCoord from the frag shader "SV_POSITION" input
float DrawNumberAtPxPos(float2 pxCoord, float2 pxPos, float number, float fontScale = 2, int decimalCount = 3);
float DrawNumberAtLocalPos(float2 pxCoord, float3 localPos, float number, float scale = 2, int decimalCount = 3);
float DrawNumberAtWorldPos(float2 pxCoord, float3 worldPos, float number, float scale = 2, int decimalCount = 3);
// digit rendering
static uint dBits[5] = {
3959160828,
2828738996,
2881485308,
2853333412,
3958634981
};
static uint po10[] = {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000, 10000000000};
float DrawDigit(int2 px, const int digit)
{
if (px.x < 0 || px.x > 2 || px.y < 0 || px.y > 4)
return 0; // pixel out of bounds
const int xId = (digit == -1) ? 18 : 31 - (3 * digit + px.x);
return (dBits[4 - px.y] & 1 << xId) != 0;
}
// indexed like: XXX.0123
void GetDecimalSymbolAt(const float v, const int i, const int decimalCount, out int symbol, out float opacity)
{
// hide if outside the decimal range
if (i > min(decimalCount - 1, 6))
{
symbol = 0;
opacity = 0;
return;
}
// get the i:th decimal
const float scale = po10[i + 1];
const float scaledF = abs(v) * scale;
symbol = (int)(scaledF) % 10;
// fade trailing zeroes
opacity = (frac(scaledF / 10) != 0) ? 1 : 0.5;
}
// indexed like: 210.XXX
void GetIntSymbolAt(const float v, int i, out int symbol, out float opacity)
{
// don't render more than 9 digits
if (i <= 9)
{
const int scale = po10[i];
const float vAbs = abs(v);
// digits
if (vAbs >= scale)
{
const int it = floor(vAbs);
const int rem = it / scale;
symbol = rem % 10;
opacity = 1;
return;
}
// minus symbol
if ((v < 0) & (vAbs * 10 >= scale))
{
symbol = -1;
opacity = 1;
return;
}
}
// leading zeroes
symbol = 0;
opacity = 0;
}
// Get the digit at the given index of a floating point number
// with -45.78, then with a given dIndex:
// [-3] = - (digit -1)
// [-2] = 4
// [-1] = 5
// [ 0] = . (digit 10)
// [ 1] = 7
// [ 2] = 8
void GetSymbolAtPositionInFloat(float number, int dIndex, int decimalCount, out int symbol, out float opacity)
{
opacity = 1;
if (dIndex == 0)
symbol = 10; // period
else if (dIndex > 0)
GetDecimalSymbolAt(number, dIndex - 1, decimalCount, symbol, opacity);
else
GetIntSymbolAt(number, -dIndex - 1, symbol, opacity);
}
// Given a pixel coordinate pxCoord, draws a number at pxPos
float DrawNumberAtPxPos(float2 pxCoord, float2 pxPos, float number, float fontScale = 2, int decimalCount = 3)
{
int2 p = (int2)(floor((pxCoord - pxPos) / fontScale));
// p.y += 0; // 0 = bottom aligned, 2 = vert. center aligned, 5 = top aligned
// p.x += 0; // 0 = integers are directly to the left, decimal separator and decimals, to the right
if (p.y < 0 || p.y > 4)
return 0; // out of bounds vertically
// shift placement to make it tighter around the decimal separator
float shift = 0;
if (p.x > 1) // decimal digits
p.x += 1;
else if (p.x < 0) // integer digits
{
p.x += -3;
shift = -2;
}
const int SEP = 4; // separation between characters
const int dIndex = floor(p.x / SEP); // the digit index to read
float opacity;
int digit;
GetSymbolAtPositionInFloat(number, dIndex, decimalCount, /*out*/ digit, /*out*/ opacity);
const float2 pos = float2(dIndex * SEP + shift, 0);
return opacity * DrawDigit(p - pos, digit);
}
// btw this might not work on all platforms, it might be Y-flipped or whatever!
float2 ClipToPixel(float4 clip)
{
float2 ndc = float2(clip.x, -clip.y) / clip.w;
ndc = (ndc + 1) / 2;
return ndc * _ScreenParams.xy;
}
float2 LocalToPixel(float3 locPos) { return ClipToPixel(UnityObjectToClipPos(float4(locPos, 1))); }
float2 WorldToPixel(float3 wPos) { return ClipToPixel(UnityWorldToClipPos(float4(wPos, 1))); }
float DrawNumberAtLocalPos(float2 pxCoord, float3 localPos, float number, float scale = 2, int decimalCount = 3)
{
const float2 pxPos = LocalToPixel(localPos);
return DrawNumberAtPxPos(pxCoord, pxPos, number, scale, decimalCount);
}
float DrawNumberAtWorldPos(float2 pxCoord, float3 worldPos, float number, float scale = 2, int decimalCount = 3)
{
const float2 pxPos = WorldToPixel(worldPos);
return DrawNumberAtPxPos(pxCoord, pxPos, number, scale, decimalCount);
}
@FreyaHolmer
Copy link
Author

FreyaHolmer commented Aug 11, 2024

Example usage:

#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "GpuPrinter.cginc" // include the GPU printer

float4 vert(const float4 vertex : POSITION) : SV_POSITION {
	return UnityObjectToClipPos(vertex);
}

float4 frag(const float4 pxPos : SV_POSITION) : SV_Target {
	// note: SV_POSITION is pixel coords in frag(), but clip coords in vert()
	// Let's draw this object's world space Y coordinate, at its origin
	const float exampleNumber = UNITY_MATRIX_M._m13;
	const float numberMask = DrawNumberAtLocalPos(pxPos, float3(0, 0, 0), exampleNumber, 4);
	return float4(numberMask.xxx, 1);
}

It should look something like this when applied to an object!
image

Here are some animations using this!
Local space coordinates
Matrix debugging

@marctem
Copy link

marctem commented Sep 25, 2024

This is really nice! Any chance you'd be willing to use the MIT License or one of the CC ones? Same spirit, and the lawyers won't hassle us.

@FreyaHolmer
Copy link
Author

sure! feel free to treat this code as an MIT license with attribution

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment