init commit after fatal local git file corruption

This commit is contained in:
ethan merchant 2024-11-03 16:30:32 -05:00
parent 08406de6fd
commit 14d9add9e6
36 changed files with 1639 additions and 0 deletions

166
Arts.cs Normal file
View file

@ -0,0 +1,166 @@
using System.Collections.Generic;
using StereoKit;
namespace snake;
static class Arts
{
static Model assets_model = Model.FromFile("meshes/assets.glb", Shader.Unlit);
static Dictionary<string, Mesh> meshes = new();
static Material mat_mono = new Material("mono.hlsl");
static Material mat_unlit = new Material("unlit.hlsl");
static Material mat_backbox = new Material("backbox.hlsl");
static Quat food_ori = Quat.Identity;
public static void Init()
{
foreach (ModelNode node in assets_model.Nodes)
{
if (node.Mesh != null)
{
meshes.Add(node.Name, node.Mesh);
}
}
mat_backbox.Transparency = Transparency.Add;
mat_backbox.FaceCull = Cull.Front;
}
public static void Frame()
{
// background
if (Device.DisplayBlend == DisplayBlend.Opaque)
{
}
// fullstick
Mesh.Sphere.Draw(
mat_unlit,
Matrix.TS(
Mono.r_con_stick.position,
5 * U.mm
),
Color.White
);
Lines.Add(
Mono.r_con_stick.position + V.XYZ(0, 0, 0),
Mono.r_con_stick.position + Mono.fullstick * U.cm,
Color.White,
2 * U.mm
);
// box
Hierarchy.Push(Mono.box_pose.ToMatrix(Mono.box_scale));
meshes["InsideOut"].Draw(mat_unlit, Matrix.Identity);
meshes["Corrugation"].Draw(mat_unlit, Matrix.Identity);
meshes["Corrugation"].Draw(mat_backbox, Matrix.Identity);
// snake
float fall_t = 0.0f;
if (Mono.snake_fall.state)
{
// if (Mono.snake_fall.delta == +1) // start falling
fall_t = 1.0f - (float)Mono.step_t;
}
Vec3 fall = V.XYZ(0, fall_t, 0);
bool food_next = (Mono.snake[0] + Mono.snake_dir) == Mono.food;
if (!food_next)
{
meshes["Tongue"].Draw(
mat_mono,
Matrix.TRS(
Mono.snake[0].ToVec3 + fall,
Quat.LookDir(Mono.fullstick),
V.XYZ(1, 1, 0.666f + Maths.smooth_stop((float)Mono.step_t) * 0.333f)
)
);
}
string face = food_next ? "Face1Eat" : "Face0Default";
meshes[face].Draw(
mat_mono,
Matrix.TRS(
Mono.snake[0].ToVec3 + fall,
Quat.LookDir(Mono.snake_dir.ToVec3),
V.XYZ(1, 1, 1)// * (float)(Mono.step_t))
)
);
for (int i = 1; i < Mono.snake_len; i++)
{
float scale = 1.0f;
if ((int)((Time.Total - Mono.eat_timestamp) * Mono.snake_len / Mono.step_step) == i)
{
scale = 1.1f;
}
meshes["Segment"].Draw(
mat_mono,
Matrix.TRS(
Mono.snake[i].ToVec3 + fall,
Quat.LookAt(Mono.snake[i].ToVec3, Mono.snake[i - 1].ToVec3),
scale
)
);
}
// holes
foreach (KeyValuePair<XYZi, XYZi> hole in Mono.holes)
{
meshes["Hole"].Draw(
mat_unlit,
Matrix.TRS(
hole.Key.ToVec3,
Quat.LookDir(hole.Value.ToVec3),
1
)
);
}
// food
float food_fall_t = 0.0f;
if (Mono.food_fall.state)
{
// if (Mono.food_fall.delta == +1) // start falling
food_fall_t = 1.0f - (float)Mono.step_t;
}
if (!food_next)
{
food_ori *= Quat.FromAngles(
90 * Time.Stepf,
30 * Time.Stepf,
10 * Time.Stepf
);
}
meshes["Food"].Draw(
mat_mono,
Matrix.TR(
Mono.food.ToVec3 + V.XYZ(0, food_fall_t, 0),
food_ori
)
);
Hierarchy.Pop();
// for (int sx = -Mono.head_fill.Xslen; Mono.head_fill.InX(sx); sx++)
// {
// for (int sy = -Mono.head_fill.Yslen; Mono.head_fill.InY(sy); sy++)
// {
// for (int sz = -Mono.head_fill.Zslen; Mono.head_fill.InZ(sz); sz++)
// {
// Vec3 pos = Mono.box_pose.ToMatrix(U.cm) * V.XYZ(sx, sy, sz);
// Text.Add(
// (Mono.head_fill[new XYZi(sx, sy, sz)] + Mono.tail_fill[new XYZi(sx, sy, sz)]).ToString(),
// Matrix.TRS(
// pos,
// Quat.LookAt(pos, Input.Head.position),
// 0.1f
// )
// );
// }
// }
// }
}
}

43
Assets/backbox.hlsl Normal file
View file

@ -0,0 +1,43 @@
#include "stereokit.hlsli"
//--name = dofdev/backbox
//--color:color = 1, 1, 1, 1
float4 color;
struct vsIn {
float4 pos : SV_Position;
float3 norm : NORMAL0;
float4 col : COLOR0;
};
struct psIn {
float4 pos : SV_POSITION;
float4 color : COLOR0;
uint view_id : SV_RenderTargetArrayIndex;
};
psIn vs(vsIn input, uint id : SV_InstanceID) {
psIn o;
o.view_id = id % sk_view_count;
id = id / sk_view_count;
float3 world = mul(float4(input.pos.xyz, 1), sk_inst[id].world).xyz;
o.pos = mul(float4(world, 1), sk_viewproj[o.view_id]);
float3 normal = normalize(mul(input.norm, (float3x3)sk_inst[id].world));
float3 norm_shade = float3(0.5) + (normal * 0.5);
// float3 norm_shade = float3(0.5) + (norm_color * 0.5);
float3 view_dir = normalize(world - sk_camera_pos[o.view_id]);
// o.color = norm_shaed * clamp(dot(o.normal, -view_dir), 0, 0.2);
// rim lighting
float rim = 1.0 - saturate(dot(normalize(view_dir), normal));
o.color = float4(norm_shade * rim * rim * 0.2, 1);
return o;
}
float4 ps(psIn input) : SV_TARGET {
return input.color;
}

53
Assets/floor.hlsl Normal file
View file

@ -0,0 +1,53 @@
#include <stereokit.hlsli>
//--name = app/floor
//--color:color = 0,0,0,1
float4 color;
//--radius = 5,10,0,0
float4 radius;
struct vsIn {
float4 pos : SV_POSITION;
};
struct psIn {
float4 pos : SV_POSITION;
float4 world : TEXCOORD0;
uint view_id : SV_RenderTargetArrayIndex;
};
psIn vs(vsIn input, uint id : SV_InstanceID) {
psIn o;
o.view_id = id % sk_view_count;
id = id / sk_view_count;
o.world = mul(input.pos, sk_inst[id].world);
o.pos = mul(o.world, sk_viewproj[o.view_id]);
return o;
}
float4 ps(psIn input) : SV_TARGET{
// This line algorithm is inspired by :
// http://madebyevan.com/shaders/grid/
// Minor grid lines
float2 step = 1 / fwidth(input.world.xz);
float2 grid = abs(frac(input.world.xz - 0.5) - 0.5) * step; // minor grid lines
float dist = sqrt(dot(input.world.xz, input.world.xz)); // transparency falloff
float size = min(grid.x, grid.y);
float val = 1.0 - min(size, 1.0);
val *= saturate(1 - ((dist - radius.x)/radius.y));
// Major axis lines
const float extraThickness = 0.5;
float2 axes = (abs(input.world.xz)) * step - extraThickness;
size = min(axes.x, axes.y);
float axisVal = 1.0 - min(size, 1.0);
axisVal *= saturate(1 - ((dist - radius.x*1.5)/(radius.y*1.5)));
// combine, and drop transparent pixels
val = max(val*0.6, axisVal);
if (val <= 0) discard;
return float4(color.rgb, val);
}

BIN
Assets/meshes/assets.glb (Stored with Git LFS) Normal file

Binary file not shown.

48
Assets/mono.hlsl Normal file
View file

@ -0,0 +1,48 @@
#include "stereokit.hlsli"
//--name = dofdev/mono
//--color:color = 1, 1, 1, 1
//--tex_scale = 1
//--diffuse = white
float4 color;
float tex_scale;
Texture2D diffuse : register(t0);
SamplerState diffuse_s : register(s0);
struct vsIn {
float4 pos : SV_Position;
float3 norm : NORMAL0;
float2 uv : TEXCOORD0;
float4 col : COLOR0;
};
struct psIn {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float4 color : COLOR0;
uint view_id : SV_RenderTargetArrayIndex;
};
psIn vs(vsIn input, uint id : SV_InstanceID) {
psIn o;
o.view_id = id % sk_view_count;
id = id / sk_view_count;
float3 world = mul(float4(input.pos.xyz, 1), sk_inst[id].world).xyz;
o.pos = mul(float4(world, 1), sk_viewproj[o.view_id]);
float3 normal = normalize(mul(input.norm, (float3x3)sk_inst[id].world));
o.uv = input.uv * tex_scale;
float3 norm_color = float3(0.5) + (normal * 0.5);
float3 norm_shade = float3(0.5) + (norm_color * 0.5);
o.color = float4(norm_shade, 1) * input.col; // input.col * color * sk_inst[id].color;
return o;
}
float4 ps(psIn input) : SV_TARGET {
float4 col = diffuse.Sample(diffuse_s, input.uv);
col = col * input.color;
return col;
}

BIN
Assets/sfx/crisp_nom.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Assets/sfx/punch_through.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

46
Assets/unlit.hlsl Normal file
View file

@ -0,0 +1,46 @@
#include "stereokit.hlsli"
//--name = dofdev/unlit
//--color:color = 1, 1, 1, 1
//--tex_scale = 1
//--diffuse = white
float4 color;
float tex_scale;
Texture2D diffuse : register(t0);
SamplerState diffuse_s : register(s0);
struct vsIn {
float4 pos : SV_Position;
float3 norm : NORMAL0;
float2 uv : TEXCOORD0;
float4 col : COLOR0;
};
struct psIn {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float4 color : COLOR0;
uint view_id : SV_RenderTargetArrayIndex;
};
psIn vs(vsIn input, uint id : SV_InstanceID) {
psIn o;
o.view_id = id % sk_view_count;
id = id / sk_view_count;
float3 world = mul(float4(input.pos.xyz, 1), sk_inst[id].world).xyz;
o.pos = mul(float4(world, 1), sk_viewproj[o.view_id]);
float3 normal = normalize(mul(input.norm, (float3x3)sk_inst[id].world));
o.uv = input.uv * tex_scale;
o.color = input.col * color * sk_inst[id].color;
return o;
}
float4 ps(psIn input) : SV_TARGET {
float4 col = diffuse.Sample(diffuse_s, input.uv);
col = col * input.color;
return col;
}

193
Maths.cs Normal file
View file

@ -0,0 +1,193 @@
using System;
namespace snake;
public static class Maths
{
public static int u_length(int s_len) => (1 + s_len) * 2;
public static int s_length(int u_len) => (u_len - 1) / 2;
public static int u_index(int s_index, int s_len) => s_len + s_index;
public static int s_index(int u_index, int u_len) => u_index - u_len;
public static float s_scalar(float u_scalar) => (u_scalar * 2.0f) - 1.0f;
public static float u_scalar(float s_scalar) => (1.0f + s_scalar) * 0.5f;
public static int min(int a, int b) => Math.Min(a, b);
public static float min(float a, float b) => Math.Min(a, b);
public static double min(double a, double b) => Math.Min(a, b);
public static int max(int a, int b) => Math.Max(a, b);
public static float max(float a, float b) => Math.Max(a, b);
public static double max(double a, double b) => Math.Max(a, b);
public static int abs(int x) => Math.Abs(x);
public static float abs(float x) => Math.Abs(x);
public static double abs(double x) => Math.Abs(x);
public static int sign(float x) => x < 0 ? -1 : +1;
public static float round(float x) => MathF.Round(x);
// public static int roundi(float value) => MathF.Round(value)
public static float precision(float x, float p)
=> round(x * p) / p;
public static float smooth_start(float t) => (t * t * t);
public static float smooth_stop(float t)
{
float s = sign(t);
return (s - ((s - t) * (s - t) * (s - t)));
}
public static float lerp(float a, float b, float t) => a + (b - a) * t;
}
public class DeltaBool
{
public int delta = 0;
public bool state = false;
public DeltaBool(bool state)
{
this.state = state;
}
public void Step(bool next_state)
{
delta = 0;
if (next_state != state)
{
delta = next_state ? +1 : -1;
state = next_state;
}
}
}
public struct XYZi
{
public int x { get; set; }
public int y { get; set; }
public int z { get; set; }
public XYZi(int x, int y, int z)
{
this.x = x;
this.y = y;
this.z = z;
}
public static XYZi operator -(XYZi a, XYZi b)
=> new(a.x - b.x, a.y - b.y, a.z - b.z);
public static XYZi operator +(XYZi a, XYZi b)
=> new(a.x + b.x, a.y + b.y, a.z + b.z);
public static bool operator ==(XYZi a, XYZi b)
=> (a.x == b.x && a.y == b.y && a.z == b.z);
public static bool operator !=(XYZi a, XYZi b)
=> (a.x != b.x || a.y != b.y || a.z != b.z);
public override bool Equals(object obj)
{
if (obj is XYZi other)
{
return this == other;
}
return false;
}
public override int GetHashCode()
=> HashCode.Combine(x, y, z);
public StereoKit.Vec3 ToVec3
=> StereoKit.V.XYZ(x, y, z);
public override string ToString()
=> string.Format("[{0:0.##}, {1:0.##}, {2:0.##}]", x, y, z);
}
// public class SignedArray<T>
public class SpatialArray<T>
{
private T[,,] u_array;
private T default_value;
private int x_slen; // length past signed zero
private int y_slen;
private int z_slen;
public int Xslen => x_slen;
public int Yslen => y_slen;
public int Zslen => z_slen;
public SpatialArray(
int x_slen,
int y_slen,
int z_slen,
T default_value = default(T)
)
{
this.u_array = new T[
Maths.u_length(x_slen),
Maths.u_length(y_slen),
Maths.u_length(z_slen)
];
this.x_slen = x_slen;
this.y_slen = y_slen;
this.z_slen = z_slen;
this.default_value = default_value;
// initialize the arrays with the default value
Clear(default_value);
}
public T this[XYZi sv]
{
get
{
if (InRange(sv))
{
return u_array[
Maths.u_index(sv.x, x_slen),
Maths.u_index(sv.y, y_slen),
Maths.u_index(sv.z, z_slen)
];
}
else
{
return default_value;
}
}
set
{
if (InRange(sv))
{
u_array[
Maths.u_index(sv.x, x_slen),
Maths.u_index(sv.y, y_slen),
Maths.u_index(sv.z, z_slen)
] = value;
}
else
{
// throw new IndexOutOfRangeException("set index is out of range.");
}
}
}
public bool InX(int sx) => !(Math.Abs(sx) > x_slen);
public bool InY(int sy) => !(Math.Abs(sy) > y_slen);
public bool InZ(int sz) => !(Math.Abs(sz) > z_slen);
public bool InRange(int sx, int sy, int sz) => InX(sx) && InY(sy) && InZ(sz);
public bool InRange(XYZi sv) => InRange(sv.x, sv.y, sv.z);
public void Clear(T value)
{
for (int ux = 0; ux < u_array.GetLength(0); ux++)
{
for (int uy = 0; uy < u_array.GetLength(1); uy++)
{
for (int uz = 0; uz < u_array.GetLength(2); uz++)
{
u_array[ux, uy, uz] = value;
}
}
}
}
}

280
Mono.cs Normal file
View file

@ -0,0 +1,280 @@
using System.Collections.Generic;
using StereoKit;
namespace snake;
static class Mono
{
public static double step_step = 60.0 / 80; // 80|100|120 bpm
public static double step_time = 0.0;
public static double step_t = 0.0;
public static Pose box_pose = new(0, 0, -10 * U.cm);
public static float box_scale = 1.333f * U.cm;
public const int SD_X = 2, SD_Y = 1, SD_Z = 2;
public static SpatialArray<int>
s_array = new(SD_X, SD_Y, SD_Z, -1),
head_fill = new(SD_X, SD_Y, SD_Z, -1),
tail_fill = new(SD_X, SD_Y, SD_Z, -1);
public static XYZi[] snake = new XYZi[
Maths.u_length(SD_X) * Maths.u_length(SD_Y) * Maths.u_length(SD_Z)
];
public static int snake_len = 4;
public static int grow_buffer = 0;
public static Vec3 fullstick = Vec3.Up;
public static Pose r_con_stick = Pose.Identity;
public static XYZi snake_dir = new(0, 0, 1), last_snake_dir = new(0, 0, 1);
public static DeltaBool in_box = new(true);
public static DeltaBool snake_fall = new(false);
public static Dictionary<XYZi, XYZi> holes = new();
public static XYZi food = new(2, 0, 0);
public static DeltaBool food_fall = new(false);
public static double eat_timestamp = 0.0;
static void PlayBox(this Sound sound, XYZi pos)
{
sound.Play(box_pose.ToMatrix(box_scale) * pos.ToVec3);
}
public static Sound sfx_crisp_nom = Sound.FromFile("sfx/crisp_nom.mp3");
public static Sound sfx_punch_through = Sound.FromFile("sfx/punch_through.mp3");
public static void Init()
{
for (int i = 0; i < snake.Length; i++)
{
snake[i] = new XYZi(0, 1, 0);
}
}
public static void Frame()
{
// flatscreen dev controls
if (Device.Name == "Simulator")
{
if (Input.Key(Key.MouseLeft).IsActive())
{
float sx = Maths.s_scalar(Input.Mouse.pos.x / 640);
float ssx = Maths.smooth_start(sx);
box_pose.orientation *= Quat.FromAngles(0, sx * 180.0f * Time.Stepf, 0);
}
if (Input.Key(Key.A).IsJustActive()) snake_dir = new(-1, 0, 0);
if (Input.Key(Key.S).IsJustActive()) snake_dir = new(+1, 0, 0);
if (Input.Key(Key.W).IsJustActive()) snake_dir = new(0, 0, -1);
if (Input.Key(Key.R).IsJustActive()) snake_dir = new(0, 0, +1);
if (Input.Key(Key.Shift).IsJustActive()) snake_dir = new(0, -1, 0);
if (Input.Key(Key.Space).IsJustActive()) snake_dir = new(0, +1, 0);
fullstick = snake_dir.ToVec3;
}
else
{
Pose head = Input.Head;
box_pose.position = head.position + head.orientation * V.XYZ(0, -(SD_Y + 0.5f) * box_scale, -32 * U.cm);
Hand r_hand = Input.Hand(Handed.Right);
Controller r_con = Input.Controller(Handed.Right);
bool con_tracked = r_con.trackedPos > TrackState.Lost;
Input.HandVisible(Handed.Max, !con_tracked);
if (con_tracked)
{
Vec2 stick = r_con.stick;
Quat stick_rot = Quat.FromAngles(stick.y * -90, 0, stick.x * +90);
float stick_sign = r_con.IsStickClicked ? -1 : +1;
r_con_stick = r_con.pose;
r_con_stick.position += r_con_stick.orientation * V.XYZ(0.0065f, -0.012f, -0.05f);
r_con_stick.orientation *= Quat.FromAngles(-50, 0, 0);
fullstick = r_con_stick.orientation * stick_rot * Vec3.Up * stick_sign;
// Vec3 fullstick = r_hand.palm.orientation * Vec3.Up;
float ax = Maths.abs(fullstick.x);
float ay = Maths.abs(fullstick.y);
float az = Maths.abs(fullstick.z);
if (ax > ay && ax > az) snake_dir = new(Maths.sign(fullstick.x), 0, 0);
if (ay > ax && ay > az) snake_dir = new(0, Maths.sign(fullstick.y), 0);
if (az > ax && az > ay) snake_dir = new(0, 0, Maths.sign(fullstick.z));
}
}
// catch invalid direction and revert to last_dir
if (snake[0] + snake_dir == snake[1])
{
snake_dir = last_snake_dir;
}
last_snake_dir = snake_dir;
}
public static void Step()
{
if (s_array[snake[0] + snake_dir] > -1)
{
// lose condition
bool stuck = true;
for (int i = 0; i < directions.Length; i++)
{
if (s_array[snake[0] + directions[i]] == -1)
{
stuck = false;
}
}
if (stuck)
{
Log.Info("your stuck");
}
return;
}
if (snake_len == snake.Length)
{
// win condition
Log.Info("full snake");
return;
}
else
{
if (grow_buffer > 0)
{
snake_len++;
grow_buffer--;
}
}
// slither
for (int i = snake.Length - 1; i > 0; i--)
{
snake[i] = snake[i - 1];
}
snake[0] += snake_dir;
// gravity
bool grounded = false;
for (int i = 0; i < snake_len; i++)
{
if (snake[i].y == -SD_Y)
{
grounded = true;
break;
}
}
snake_fall.Step(!grounded);
if (snake_fall.state)
{
for (int i = 0; i < snake_len; i++)
{
snake[i] -= new XYZi(0, 1, 0);
}
}
bool food_grounded = food.y == -SD_Y;
XYZi below_food = new XYZi(food.x, food.y - 1, food.z);
bool on_snake = s_array.InRange(below_food) && s_array[below_food] > -1;
food_fall.Step(!food_grounded && !on_snake);
if (food_fall.state)
{
food -= new XYZi(0, 1, 0);
}
in_box.Step(s_array.InRange(snake[0]));
if (in_box.delta != 0) // 1 just in -1 just out
{
holes.Add(snake[0], snake_dir);
sfx_punch_through.PlayBox(snake[0]);
}
if (holes.ContainsKey(snake[snake_len - 1]))
{
holes.Remove(snake[snake_len - 1]);
}
s_array.Clear(-1);
for (int i = 0; i < snake_len; i++)
{
s_array[snake[i]] = i;
}
// eat
if (food == snake[0])
{
eat_timestamp = Time.Total;
grow_buffer += 3;
Feed();
sfx_crisp_nom.PlayBox(snake[0]);
}
}
static void Feed()
{
head_fill.Clear(-1);
Gas(head_fill, snake[0]);
// handle out of the box exception on tail
tail_fill.Clear(-1);
Gas(tail_fill, snake[snake_len - 1]);
XYZi farthest_cell = new XYZi(0, 0, 0);
int farthest_dist = 0;
// [*] design configurable (we basically want it to be as far away as it can reasonably be)
const int max_dist = 6;
// [!] given how it loops over the space it directionally biases the results
for (int sx = -s_array.Xslen; s_array.InX(sx); sx++)
{
for (int sy = -s_array.Yslen; s_array.InY(sy); sy++)
{
for (int sz = -s_array.Zslen; s_array.InZ(sz); sz++)
{
XYZi sv = new XYZi(sx, sy, sz);
int dist = head_fill[sv];
bool good_dist = dist > farthest_dist && dist < max_dist;
bool tail_access = tail_fill[sv] > 0; // help ensure completability
bool snake_free = s_array[sv] == -1;
if (good_dist && tail_access && snake_free)
{
farthest_dist = dist;
farthest_cell = sv;
}
}
}
}
food = farthest_cell;
}
// space fill algorithm
static void Gas(SpatialArray<int> fill_array, XYZi sv)
{
Queue<XYZi> queue = new Queue<XYZi>();
queue.Enqueue(sv);
fill_array[sv] = 0;
while (queue.Count > 0)
{
XYZi _sv = queue.Dequeue();
int currentDistance = fill_array[_sv];
// check all 4 directions
foreach (XYZi dir in directions)
{
XYZi newV = _sv + dir;
if (fill_array.InRange(newV) && fill_array[newV] == -1 && s_array[newV] == -1)
{
fill_array[newV] = currentDistance + 1;
queue.Enqueue(newV);
}
}
}
}
// directions for moving in the grid
static readonly XYZi[] directions = new XYZi[]
{
new XYZi(-1, 0, 0), // lft
new XYZi(+1, 0, 0), // rht
new XYZi(0, -1, 0), // dwn
new XYZi(0, +1, 0), // up
new XYZi(0, 0, -1), // fwd
new XYZi(0, 0, +1), // back
};
}

315
PassthroughFBExt.cs Normal file
View file

@ -0,0 +1,315 @@
// SPDX-License-Identifier: MIT
// The authors below grant copyright rights under the MIT license:
// Copyright (c) 2024 Nick Klingensmith
// Copyright (c) 2024 Qualcomm Technologies, Inc.
// This requires an addition to the Android Manifest to work on quest:
// <uses-feature android:name="com.oculus.feature.PASSTHROUGH" android:required="true" />
// And adding this to the application section can also improve the passthrough
// experience:
// <meta-data android:name="com.oculus.ossplash.background" android:value="passthrough-contextual"/>
//
// To work on Quest+Link, you may need to enable beta features in the Oculus
// app's settings.
using System;
using System.Runtime.InteropServices;
namespace StereoKit.Framework
{
class PassthroughFBExt : IStepper
{
bool extAvailable;
bool enabled;
bool enableOnInitialize;
XrPassthroughFB activePassthrough = new XrPassthroughFB();
XrPassthroughLayerFB activeLayer = new XrPassthroughLayerFB();
Color oldColor;
bool oldSky;
public bool Available => extAvailable;
public bool Enabled { get => enabled; set {
if (extAvailable == false || enabled == value) return;
if (value)
{
enabled = StartPassthrough();
}
else
{
PausePassthrough();
enabled = false;
}
} }
public PassthroughFBExt() : this(true) { }
public PassthroughFBExt(bool enabled = true)
{
if (SK.IsInitialized)
Log.Err("PassthroughFBExt must be constructed before StereoKit is initialized!");
Backend.OpenXR.RequestExt("XR_FB_passthrough");
enableOnInitialize = enabled;
}
public bool Initialize()
{
extAvailable =
Backend.XRType == BackendXRType.OpenXR &&
Backend.OpenXR.ExtEnabled("XR_FB_passthrough") &&
LoadBindings() &&
InitPassthrough();
return true;
}
public void Step()
{
if (Enabled == false) return;
XrCompositionLayerPassthroughFB layer = new XrCompositionLayerPassthroughFB(
XrCompositionLayerFlags.BLEND_TEXTURE_SOURCE_ALPHA_BIT, activeLayer);
Backend.OpenXR.AddCompositionLayer(layer, -1);
}
public void Shutdown()
{
if (!Enabled) return;
Enabled = false;
DestroyPassthrough();
}
bool InitPassthrough()
{
XrPassthroughFlagsFB flags = enableOnInitialize
? XrPassthroughFlagsFB.IS_RUNNING_AT_CREATION_BIT_FB
: XrPassthroughFlagsFB.None;
XrResult result = xrCreatePassthroughFB(
Backend.OpenXR.Session,
new XrPassthroughCreateInfoFB(flags),
out activePassthrough);
if (result != XrResult.Success)
{
Log.Err($"xrCreatePassthroughFB failed: {result}");
return false;
}
result = xrCreatePassthroughLayerFB(
Backend.OpenXR.Session,
new XrPassthroughLayerCreateInfoFB(activePassthrough, flags, XrPassthroughLayerPurposeFB.RECONSTRUCTION_FB),
out activeLayer);
if (result != XrResult.Success)
{
Log.Err($"xrCreatePassthroughLayerFB failed: {result}");
return false;
}
enabled = enableOnInitialize;
StartSky();
return true;
}
void DestroyPassthrough()
{
xrDestroyPassthroughLayerFB(activeLayer);
xrDestroyPassthroughFB(activePassthrough);
}
bool StartPassthrough()
{
XrResult result = xrPassthroughStartFB(activePassthrough);
if (result != XrResult.Success)
{
Log.Err($"xrPassthroughStartFB failed: {result}");
return false;
}
result = xrPassthroughLayerResumeFB(activeLayer);
if (result != XrResult.Success)
{
Log.Err($"xrPassthroughLayerResumeFB failed: {result}");
return false;
}
StartSky();
return true;
}
void StartSky()
{
oldColor = Renderer.ClearColor;
oldSky = Renderer.EnableSky;
Renderer.ClearColor = Color.BlackTransparent;
Renderer.EnableSky = false;
}
void PausePassthrough()
{
XrResult result = xrPassthroughLayerPauseFB(activeLayer);
if (result != XrResult.Success)
{
Log.Err($"xrPassthroughLayerPauseFB failed: {result}");
return;
}
result = xrPassthroughPauseFB(activePassthrough);
if (result != XrResult.Success)
{
Log.Err($"xrPassthroughPauseFB failed: {result}");
return;
}
Renderer.ClearColor = oldColor;
Renderer.EnableSky = oldSky;
}
#region OpenXR native bindings and types
enum XrStructureType : UInt64
{
XR_TYPE_PASSTHROUGH_CREATE_INFO_FB = 1000118001,
XR_TYPE_PASSTHROUGH_LAYER_CREATE_INFO_FB = 1000118002,
XR_TYPE_PASSTHROUGH_STYLE_FB = 1000118020,
XR_TYPE_COMPOSITION_LAYER_PASSTHROUGH_FB = 1000118003,
}
enum XrPassthroughFlagsFB : UInt64
{
None = 0,
IS_RUNNING_AT_CREATION_BIT_FB = 0x00000001,
LAYER_DEPTH_BIT_FB = 0x00000002
}
enum XrCompositionLayerFlags : UInt64
{
None = 0,
CORRECT_CHROMATIC_ABERRATION_BIT = 0x00000001,
BLEND_TEXTURE_SOURCE_ALPHA_BIT = 0x00000002,
UNPREMULTIPLIED_ALPHA_BIT = 0x00000004,
}
enum XrPassthroughLayerPurposeFB : UInt32
{
RECONSTRUCTION_FB = 0,
PROJECTED_FB = 1,
TRACKED_KEYBOARD_HANDS_FB = 1000203001,
MAX_ENUM_FB = 0x7FFFFFFF,
}
enum XrResult : Int32
{
Success = 0,
}
#pragma warning disable 0169 // handle is not "used", but required for interop
[StructLayout(LayoutKind.Sequential)] struct XrPassthroughFB { ulong handle; }
[StructLayout(LayoutKind.Sequential)] struct XrPassthroughLayerFB { ulong handle; }
#pragma warning restore 0169
[StructLayout(LayoutKind.Sequential)]
struct XrPassthroughCreateInfoFB
{
private XrStructureType type;
public IntPtr next;
public XrPassthroughFlagsFB flags;
public XrPassthroughCreateInfoFB(XrPassthroughFlagsFB passthroughFlags)
{
type = XrStructureType.XR_TYPE_PASSTHROUGH_CREATE_INFO_FB;
next = IntPtr.Zero;
flags = passthroughFlags;
}
}
[StructLayout(LayoutKind.Sequential)]
struct XrPassthroughLayerCreateInfoFB
{
private XrStructureType type;
public IntPtr next;
public XrPassthroughFB passthrough;
public XrPassthroughFlagsFB flags;
public XrPassthroughLayerPurposeFB purpose;
public XrPassthroughLayerCreateInfoFB(XrPassthroughFB passthrough, XrPassthroughFlagsFB flags, XrPassthroughLayerPurposeFB purpose)
{
type = XrStructureType.XR_TYPE_PASSTHROUGH_LAYER_CREATE_INFO_FB;
next = IntPtr.Zero;
this.passthrough = passthrough;
this.flags = flags;
this.purpose = purpose;
}
}
[StructLayout(LayoutKind.Sequential)]
struct XrPassthroughStyleFB
{
public XrStructureType type;
public IntPtr next;
public float textureOpacityFactor;
public Color edgeColor;
public XrPassthroughStyleFB(float textureOpacityFactor, Color edgeColor)
{
type = XrStructureType.XR_TYPE_PASSTHROUGH_STYLE_FB;
next = IntPtr.Zero;
this.textureOpacityFactor = textureOpacityFactor;
this.edgeColor = edgeColor;
}
}
[StructLayout(LayoutKind.Sequential)]
struct XrCompositionLayerPassthroughFB
{
public XrStructureType type;
public IntPtr next;
public XrCompositionLayerFlags flags;
public ulong space;
public XrPassthroughLayerFB layerHandle;
public XrCompositionLayerPassthroughFB(XrCompositionLayerFlags flags, XrPassthroughLayerFB layerHandle)
{
type = XrStructureType.XR_TYPE_COMPOSITION_LAYER_PASSTHROUGH_FB;
next = IntPtr.Zero;
space = 0;
this.flags = flags;
this.layerHandle = layerHandle;
}
}
delegate XrResult del_xrCreatePassthroughFB (ulong session, [In] XrPassthroughCreateInfoFB createInfo, out XrPassthroughFB outPassthrough);
delegate XrResult del_xrDestroyPassthroughFB (XrPassthroughFB passthrough);
delegate XrResult del_xrPassthroughStartFB (XrPassthroughFB passthrough);
delegate XrResult del_xrPassthroughPauseFB (XrPassthroughFB passthrough);
delegate XrResult del_xrCreatePassthroughLayerFB (ulong session, [In] XrPassthroughLayerCreateInfoFB createInfo, out XrPassthroughLayerFB outLayer);
delegate XrResult del_xrDestroyPassthroughLayerFB (XrPassthroughLayerFB layer);
delegate XrResult del_xrPassthroughLayerPauseFB (XrPassthroughLayerFB layer);
delegate XrResult del_xrPassthroughLayerResumeFB (XrPassthroughLayerFB layer);
delegate XrResult del_xrPassthroughLayerSetStyleFB(XrPassthroughLayerFB layer, [In] XrPassthroughStyleFB style);
del_xrCreatePassthroughFB xrCreatePassthroughFB;
del_xrDestroyPassthroughFB xrDestroyPassthroughFB;
del_xrPassthroughStartFB xrPassthroughStartFB;
del_xrPassthroughPauseFB xrPassthroughPauseFB;
del_xrCreatePassthroughLayerFB xrCreatePassthroughLayerFB;
del_xrDestroyPassthroughLayerFB xrDestroyPassthroughLayerFB;
del_xrPassthroughLayerPauseFB xrPassthroughLayerPauseFB;
del_xrPassthroughLayerResumeFB xrPassthroughLayerResumeFB;
del_xrPassthroughLayerSetStyleFB xrPassthroughLayerSetStyleFB;
bool LoadBindings()
{
xrCreatePassthroughFB = Backend.OpenXR.GetFunction<del_xrCreatePassthroughFB> ("xrCreatePassthroughFB");
xrDestroyPassthroughFB = Backend.OpenXR.GetFunction<del_xrDestroyPassthroughFB> ("xrDestroyPassthroughFB");
xrPassthroughStartFB = Backend.OpenXR.GetFunction<del_xrPassthroughStartFB> ("xrPassthroughStartFB");
xrPassthroughPauseFB = Backend.OpenXR.GetFunction<del_xrPassthroughPauseFB> ("xrPassthroughPauseFB");
xrCreatePassthroughLayerFB = Backend.OpenXR.GetFunction<del_xrCreatePassthroughLayerFB> ("xrCreatePassthroughLayerFB");
xrDestroyPassthroughLayerFB = Backend.OpenXR.GetFunction<del_xrDestroyPassthroughLayerFB> ("xrDestroyPassthroughLayerFB");
xrPassthroughLayerPauseFB = Backend.OpenXR.GetFunction<del_xrPassthroughLayerPauseFB> ("xrPassthroughLayerPauseFB");
xrPassthroughLayerResumeFB = Backend.OpenXR.GetFunction<del_xrPassthroughLayerResumeFB> ("xrPassthroughLayerResumeFB");
xrPassthroughLayerSetStyleFB = Backend.OpenXR.GetFunction<del_xrPassthroughLayerSetStyleFB>("xrPassthroughLayerSetStyleFB");
return
xrCreatePassthroughFB != null &&
xrDestroyPassthroughFB != null &&
xrPassthroughStartFB != null &&
xrPassthroughPauseFB != null &&
xrCreatePassthroughLayerFB != null &&
xrDestroyPassthroughLayerFB != null &&
xrPassthroughLayerPauseFB != null &&
xrPassthroughLayerResumeFB != null &&
xrPassthroughLayerSetStyleFB != null;
}
#endregion
}
}

View file

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-sdk android:minSdkVersion="29" android:targetSdkVersion="32" />
<application android:allowBackup="true" android:icon="@mipmap/appicon" android:label="@string/app_name" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true">
<!-- Oculus -->
<meta-data android:name="com.oculus.supportedDevices" android:value="quest|quest2|quest3|questpro"/>
<meta-data android:name="com.oculus.handtracking.version" android:value="V2.0"/>
<meta-data android:name="com.oculus.ossplash.background" android:value="passthrough-contextual"/>
<uses-native-library android:name="libopenxr_forwardloader.oculus.so" android:required="false"/>
<!-- Pico -->
<meta-data android:name="pvr.app.type" android:value="vr" />
<meta-data android:name="handtracking" android:value="1" />
<!-- Snapdragon -->
<meta-data android:name="spaces.version" android:value="0.15.0"/>
</application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAPTURE_AUDIO_OUTPUT" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<!-- Generic OpenXR items -->
<uses-feature android:name="android.hardware.vr.headtracking" android:required="false" android:version="1" />
<uses-permission android:name="org.khronos.openxr.permission.OPENXR_SYSTEM"/>
<uses-permission android:name="org.khronos.openxr.permission.OPENXR" />
<queries>
<provider android:authorities="org.khronos.openxr.runtime_broker;org.khronos.openxr.system_runtime_broker" />
<intent> <action android:name="org.khronos.openxr.OpenXRRuntimeService" /> </intent>
<intent> <action android:name="org.khronos.openxr.OpenXRApiLayerService" /> </intent>
</queries>
<!-- Vive specific items -->
<uses-feature android:name="wave.feature.handtracking" android:required="false"/>
<uses-feature android:name="wave.feature.tracker" android:required="false"/>
<!-- Snapdragon specific items -->
<uses-permission android:name="com.qualcomm.qti.qxr.QXRServiceClientPermission" android:required="false"/>
<queries>
<package android:name="com.qualcomm.qti.spaces.services" />
<package android:name="com.qualcomm.qti.openxrruntime" />
<intent> <action android:name="com.qualcomm.qti.openxr.spaces.intent.action.BIND" /> </intent>
</queries>
<!-- Oculus specific items -->
<uses-permission android:name="com.oculus.permission.HAND_TRACKING"/>
<uses-permission android:name="com.oculus.permission.BODY_TRACKING"/>
<uses-permission android:name="com.oculus.permission.FACE_TRACKING"/>
<uses-permission android:name="com.oculus.permission.EYE_TRACKING"/>
<uses-feature android:name="com.oculus.feature.PASSTHROUGH" android:required="true"/>
<uses-feature android:name="oculus.software.handtracking" android:required="false"/>
<uses-feature android:name="com.oculus.software.body_tracking" android:required="false"/>
<uses-feature android:name="oculus.software.face_tracking" android:required="false"/>
<uses-feature android:name="oculus.software.eye_tracking" android:required="false"/>
</manifest>

View file

@ -0,0 +1,87 @@
using Android.App;
using Android.Content;
using Android.Graphics;
using Android.OS;
using Android.Runtime;
using Android.Views;
using StereoKit;
using System;
using System.Reflection;
using System.Threading;
namespace snake;
[Activity(Label = "@string/app_name", MainLauncher = true, Exported = true)]
[IntentFilter(new[] { Intent.ActionMain }, Categories = new[] { "org.khronos.openxr.intent.category.IMMERSIVE_HMD", "com.oculus.intent.category.VR", Intent.CategoryLauncher })]
public class MainActivity : Activity, ISurfaceHolderCallback2
{
View surface;
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
Run();
SetContentView(Resource.Layout.activity_main);
}
protected override void OnDestroy()
{
SK.Quit();
base.OnDestroy();
}
static bool running = false;
void Run()
{
if (running) return;
running = true;
// Before anything else, give StereoKit the Activity. This should
// be set before any other SK calls, otherwise native library
// loading may fail.
SK.AndroidActivity = this;
// Set up a surface for StereoKit to draw on, this is only really
// important for flatscreen experiences.
Window.TakeSurface(this);
Window.SetFormat(Format.Unknown);
surface = new View(this);
SetContentView(surface);
surface.RequestFocus();
// Task.Run will eat exceptions, but Thread.Start doesn't seem to.
new Thread(InvokeStereoKit).Start();
}
static void InvokeStereoKit()
{
Type entryClass = typeof(Program);
MethodInfo entryPoint = entryClass?.GetMethod("Main", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
// There are a number of potential method signatures for Main, so
// we need to check each one, and give it the correct values.
//
// Converting MethodInfo into an Action instead of calling Invoke on
// it allows for exceptions to properly bubble up to the IDE.
ParameterInfo[] entryParams = entryPoint?.GetParameters();
if (entryParams == null || entryParams.Length == 0)
{
Action Program_Main = (Action)Delegate.CreateDelegate(typeof(Action), entryPoint);
Program_Main();
}
else if (entryParams?.Length == 1 && entryParams[0].ParameterType == typeof(string[]))
{
Action<string[]> Program_Main = (Action<string[]>)Delegate.CreateDelegate(typeof(Action<string[]>), entryPoint);
Program_Main(new string[] { });
}
else throw new Exception("Couldn't invoke Program.Main!");
Process.KillProcess(Process.MyPid());
}
// Events related to surface state changes
public void SurfaceChanged (ISurfaceHolder holder, [GeneratedEnum] Format format, int width, int height) => SK.SetWindow(holder.Surface.Handle);
public void SurfaceCreated (ISurfaceHolder holder) => SK.SetWindow(holder.Surface.Handle);
public void SurfaceDestroyed (ISurfaceHolder holder) => SK.SetWindow(IntPtr.Zero);
public void SurfaceRedrawNeeded(ISurfaceHolder holder) { }
}

View file

@ -0,0 +1,47 @@
Images, layout descriptions, binary blobs and string dictionaries can be included
in your application as resource files. Various Android APIs are designed to
operate on the resource IDs instead of dealing with images, strings or binary blobs
directly.
For example, a sample Android app that contains a user interface layout (main.xml),
an internationalization string table (strings.xml) and some icons (drawable-XXX/icon.png)
would keep its resources in the "Resources" directory of the application:
Resources/
drawable/
icon.png
layout/
main.xml
values/
strings.xml
For icons, a tool like https://icon.kitchen can be helpful to accelerate the
process of getting all the correct size variants!
In order to get the build system to recognize Android resources, set the build action to
"AndroidResource". The native Android APIs do not operate directly with filenames, but
instead operate on resource IDs. When you compile an Android application that uses resources,
the build system will package the resources for distribution and generate a class called "Resource"
(this is an Android convention) that contains the tokens for each one of the resources
included. For example, for the above Resources layout, this is what the Resource class would expose:
public class Resource {
public class Drawable {
public const int icon = 0x123;
}
public class Layout {
public const int main = 0x456;
}
public class Strings {
public const int first_string = 0xabc;
public const int second_string = 0xbcd;
}
}
You would then use Resource.Drawable.icon to reference the drawable/icon.png file, or
Resource.Layout.main to reference the layout/main.xml file, or Resource.Strings.first_string
to reference the first string in the dictionary file values/strings.xml.

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="@string/app_text"
/>
</RelativeLayout>

View file

@ -0,0 +1,4 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/appicon_background" />
<foreground android:drawable="@mipmap/appicon_foreground" />
</adaptive-icon>

View file

@ -0,0 +1,4 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/appicon_background" />
<foreground android:drawable="@mipmap/appicon_foreground" />
</adaptive-icon>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#2C3E50</color>
</resources>

View file

@ -0,0 +1,4 @@
<resources>
<string name="app_name">snake</string>
<string name="app_text">snake</string>
</resources>

BIN
Platforms/Net/App.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

BIN
Platforms/Net/SKApp.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

58
Program.cs Normal file
View file

@ -0,0 +1,58 @@
using StereoKit;
using StereoKit.Framework;
namespace snake;
class Program
{
static void Main(string[] args)
{
SK.AddStepper<PassthroughFBExt>();
// Initialize StereoKit
SKSettings settings = new SKSettings
{
appName = "snake",
assetsFolder = "Assets",
blendPreference = DisplayBlend.Blend,
// overlayApp = true,
// overlayPriority = 1,
depthMode = DepthMode.D32,
disableFlatscreenMRSim = true,
renderScaling = 2,
renderMultisample = 0,
};
if (!SK.Initialize(settings))
return;
Renderer.Scaling = 2;
// World.OcclusionEnabled = true;
// Device.DisplayBlend = DisplayBlend.Blend;
Mono.Init();
Arts.Init();
// Core application loop
SK.Run(() =>
{
Mono.Frame();
// stepper
if (Time.Total > 3.0)
{
Mono.step_time += Time.Step;
Mono.step_t = Maths.min(Mono.step_time, Mono.step_step) / Mono.step_step;
}
if (Mono.step_time > Mono.step_step)
{
Mono.step_time -= Mono.step_step;
Mono.Step();
}
Arts.Frame();
});
}
}

View file

@ -0,0 +1,50 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0-android</TargetFramework>
<RuntimeIdentifiers>android-arm64</RuntimeIdentifiers>
<SupportedOSPlatformVersion>29</SupportedOSPlatformVersion>
<OutputType>Exe</OutputType>
<ApplicationId>com.dofdev.snake</ApplicationId>
<ApplicationVersion>1</ApplicationVersion>
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
<RootNamespace>snake</RootNamespace>
<ReadOnlyProject>true</ReadOnlyProject>
<SKOpenXRLoader>Standard</SKOpenXRLoader>
<SKAssetFolder>..\..\Assets</SKAssetFolder>
<SKAssetDestination>Assets</SKAssetDestination>
<!--Suppress targetSdkVersion 29 warnings. XR devices use old versions
of Android.-->
<NoWarn>XA4211;XA1006;XA4301</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="StereoKit" Version="0.3.9" />
</ItemGroup>
<!--Point Android to the right folders, otherwise these default to the root
directory.-->
<PropertyGroup>
<AndroidManifest >..\..\Platforms\Android\AndroidManifest.xml</AndroidManifest>
<MonoAndroidResourcePrefix>..\..\Platforms\Android\Resources</MonoAndroidResourcePrefix>
<MonoAndroidAssetsPrefix >..\..\Platforms\Android\Assets</MonoAndroidAssetsPrefix>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\..\**\*.cs" Exclude="..\..\obj\**;..\..\bin\**;..\..\Projects\**" />
<!--Don't compile .NET Core code in this project.-->
<Compile Remove="..\..\**\*.Net.cs" />
<Compile Remove="..\..\Platforms\Net\**\*.cs" />
<!--Hide everything in this project, otherwise it's a lot of
duplication that creates noise.-->
<Compile Update="..\..\**" Visible="false" />
<None Update="..\..\**" Visible="false" />
<Content Update="..\..\**" Visible="false" />
<Compile Update="..\..\**\*.Android.cs" Visible="true" />
<Compile Update="..\..\Platforms\Android\**\*.cs" Visible="true" />
</ItemGroup>
</Project>

56
android_icons.sh Executable file
View file

@ -0,0 +1,56 @@
#!/bin/bash
# Check if input file is provided
if [ $# -eq 0 ]; then
echo "Usage: $0 <input.svg>"
exit 1
fi
input_file="$1"
# filename=$(basename "$input_file" .svg)
filename="appicon_foreground"
output_dir="Platforms/Android/Resources"
# DPI configurations
# Format: [directory_name, scale_percentage]
declare -A dpi_configs=(
["mipmap-mdpi"]="100"
["mipmap-hdpi"]="150"
["mipmap-xhdpi"]="200"
["mipmap-xxhdpi"]="300"
["mipmap-xxxhdpi"]="400"
)
# Check if required tools are installed
if ! command -v inkscape &> /dev/null; then
echo "Error: Inkscape is required but not installed. Install using:"
echo "sudo pacman -S inkscape"
exit 1
fi
# Create output directory structure
mkdir -p "$output_dir"
for dpi in "${!dpi_configs[@]}"; do
mkdir -p "$output_dir/$dpi"
done
# Get base size - let's use 48px as default mdpi size for app icons
# You can adjust this base size as needed
BASE_SIZE=48
# Export for each DPI
for dpi in "${!dpi_configs[@]}"; do
scale=${dpi_configs[$dpi]}
# Calculate new size using pure bash arithmetic
new_size=$(( BASE_SIZE * scale / 100 ))
echo "Converting for $dpi (scale: ${scale}%)"
inkscape --export-type="png" \
--export-filename="$output_dir/$dpi/${filename}.png" \
--export-width="$new_size" \
--export-height="$new_size" \
"$input_file"
done
echo "Conversion complete! Files exported to $output_dir/"

49
readme.md Normal file
View file

@ -0,0 +1,49 @@
[lynx r1 resource](https://github.com/technobaboo/stereokit_lynx_dotnet_template)
[fb passthrough resource](https://www.aesiio.com/blog/passthrough-dot-net-core)
[fb passthrough tool source](https://github.com/StereoKit/StereoKit/blob/master/Examples/StereoKitTest/Tools/PassthroughFBExt.cs)
```shell
# track lfs
git lfs track "*.mp3"
git lfs track "*.glb"
git lfs track "*.png"
ffmpeg -i input.ogg output.wav
# update icon from svg
magick -background none -density 256x256 Raw/icon.svg -define icon:auto-resize=256,128,64,48,32,16 Platforms/Net/App.ico
./android_icons.sh Raw/icon.svg
adb devices
adb tcpip 5555
adb shell ip -f inet addr show wlan0
adb connect 192.168.1.219
# publish build
dotnet publish -c Release Projects/Android/snake.Android.csproj
# stream install
adb install Projects/Android/bin/Release/net7.0-android/com.dofdev.snake-Signed.apk
# debug shell
adb shell monkey -p com.dofdev.snake 1
# install a specific android platform
sdkmanager "platforms;android-33" "build-tools;33.0.0"
```
```
todo
track steps out of box
if > x then snake get's pulled/slips out of the box
check to see how far out of box (with some warning)
if (too far out || more segments out of box than in)than slip out of box from gravity
if going up slipping out due to gravity doesn't make sense so
just prevent the snake from going too far up instead (soft pause)?
this means gravity could affect things even when in box
and breaking through the box and coiling up on yourself allows you to fill the space strategically
the food could even be affected by gravity so everything kind of starts of 2d
and 3d feels like an awesome treat/clever trick the user can play rather than an overwhelming paradigm shift
```

25
snake.csproj Normal file
View file

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<OutputType>Exe</OutputType>
<ApplicationIcon>Platforms\Net\App.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<!--Don't compile Android code in this project, but do show it in the
explorer window.-->
<Compile Remove="**\*.Android.cs" />
<Compile Remove="Platforms\Android\**" />
<None Include="**\*.Android.cs" />
<None Include="Platforms\Android\**" />
<Compile Remove="Projects\**" />
<EmbeddedResource Remove="Projects\**" />
<None Remove="Projects\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="StereoKit" Version="0.3.9" />
</ItemGroup>
</Project>