diff --git a/Arts.cs b/Arts.cs new file mode 100644 index 0000000..68bc215 --- /dev/null +++ b/Arts.cs @@ -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 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 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 + // ) + // ); + // } + // } + // } + } +} \ No newline at end of file diff --git a/Assets/backbox.hlsl b/Assets/backbox.hlsl new file mode 100644 index 0000000..cf59f51 --- /dev/null +++ b/Assets/backbox.hlsl @@ -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; +} \ No newline at end of file diff --git a/Assets/floor.hlsl b/Assets/floor.hlsl new file mode 100644 index 0000000..8737363 --- /dev/null +++ b/Assets/floor.hlsl @@ -0,0 +1,53 @@ +#include + +//--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); +} \ No newline at end of file diff --git a/Assets/meshes/assets.glb b/Assets/meshes/assets.glb new file mode 100644 index 0000000..3ba69a5 --- /dev/null +++ b/Assets/meshes/assets.glb @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:03e6766296a916fbb3471e3601eb0ed0e2e82bda18b34bf5233116f2e2b80593 +size 405108 diff --git a/Assets/mono.hlsl b/Assets/mono.hlsl new file mode 100644 index 0000000..13eb797 --- /dev/null +++ b/Assets/mono.hlsl @@ -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; +} \ No newline at end of file diff --git a/Assets/sfx/crisp_nom.mp3 b/Assets/sfx/crisp_nom.mp3 new file mode 100644 index 0000000..21544eb --- /dev/null +++ b/Assets/sfx/crisp_nom.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b13783430f48f61c656cdaf1616c58b372bf1c2a3299097dfef91c632bb6326 +size 6518 diff --git a/Assets/sfx/punch_through.mp3 b/Assets/sfx/punch_through.mp3 new file mode 100644 index 0000000..83886cb --- /dev/null +++ b/Assets/sfx/punch_through.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7997e0866066729c4784cc493ea7d3c01ddffee00b196b6607120cc0624bb84a +size 17294 diff --git a/Assets/unlit.hlsl b/Assets/unlit.hlsl new file mode 100644 index 0000000..9f698f2 --- /dev/null +++ b/Assets/unlit.hlsl @@ -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; +} \ No newline at end of file diff --git a/Maths.cs b/Maths.cs new file mode 100644 index 0000000..53e6dad --- /dev/null +++ b/Maths.cs @@ -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 + +public class SpatialArray +{ + 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; + } + } + } + } +} \ No newline at end of file diff --git a/Mono.cs b/Mono.cs new file mode 100644 index 0000000..108fc19 --- /dev/null +++ b/Mono.cs @@ -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 + 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 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 fill_array, XYZi sv) + { + Queue queue = new Queue(); + 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 + }; +} diff --git a/PassthroughFBExt.cs b/PassthroughFBExt.cs new file mode 100644 index 0000000..519b8c9 --- /dev/null +++ b/PassthroughFBExt.cs @@ -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: +// +// And adding this to the application section can also improve the passthrough +// experience: +// +// +// 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 ("xrCreatePassthroughFB"); + xrDestroyPassthroughFB = Backend.OpenXR.GetFunction ("xrDestroyPassthroughFB"); + xrPassthroughStartFB = Backend.OpenXR.GetFunction ("xrPassthroughStartFB"); + xrPassthroughPauseFB = Backend.OpenXR.GetFunction ("xrPassthroughPauseFB"); + xrCreatePassthroughLayerFB = Backend.OpenXR.GetFunction ("xrCreatePassthroughLayerFB"); + xrDestroyPassthroughLayerFB = Backend.OpenXR.GetFunction ("xrDestroyPassthroughLayerFB"); + xrPassthroughLayerPauseFB = Backend.OpenXR.GetFunction ("xrPassthroughLayerPauseFB"); + xrPassthroughLayerResumeFB = Backend.OpenXR.GetFunction ("xrPassthroughLayerResumeFB"); + xrPassthroughLayerSetStyleFB = Backend.OpenXR.GetFunction("xrPassthroughLayerSetStyleFB"); + + return + xrCreatePassthroughFB != null && + xrDestroyPassthroughFB != null && + xrPassthroughStartFB != null && + xrPassthroughPauseFB != null && + xrCreatePassthroughLayerFB != null && + xrDestroyPassthroughLayerFB != null && + xrPassthroughLayerPauseFB != null && + xrPassthroughLayerResumeFB != null && + xrPassthroughLayerSetStyleFB != null; + } + #endregion + } +} \ No newline at end of file diff --git a/Platforms/Android/AndroidManifest.xml b/Platforms/Android/AndroidManifest.xml new file mode 100644 index 0000000..feea22c --- /dev/null +++ b/Platforms/Android/AndroidManifest.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Platforms/Android/MainActivity.cs b/Platforms/Android/MainActivity.cs new file mode 100644 index 0000000..7da7335 --- /dev/null +++ b/Platforms/Android/MainActivity.cs @@ -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 Program_Main = (Action)Delegate.CreateDelegate(typeof(Action), 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) { } +} \ No newline at end of file diff --git a/Platforms/Android/Resources/AboutResources.txt b/Platforms/Android/Resources/AboutResources.txt new file mode 100644 index 0000000..d2d3ddd --- /dev/null +++ b/Platforms/Android/Resources/AboutResources.txt @@ -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. \ No newline at end of file diff --git a/Platforms/Android/Resources/layout/activity_main.xml b/Platforms/Android/Resources/layout/activity_main.xml new file mode 100644 index 0000000..f949852 --- /dev/null +++ b/Platforms/Android/Resources/layout/activity_main.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/Platforms/Android/Resources/mipmap-anydpi-v26/appicon.xml b/Platforms/Android/Resources/mipmap-anydpi-v26/appicon.xml new file mode 100644 index 0000000..7751f69 --- /dev/null +++ b/Platforms/Android/Resources/mipmap-anydpi-v26/appicon.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Platforms/Android/Resources/mipmap-anydpi-v26/appicon_round.xml b/Platforms/Android/Resources/mipmap-anydpi-v26/appicon_round.xml new file mode 100644 index 0000000..7751f69 --- /dev/null +++ b/Platforms/Android/Resources/mipmap-anydpi-v26/appicon_round.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Platforms/Android/Resources/mipmap-hdpi/appicon_background.png b/Platforms/Android/Resources/mipmap-hdpi/appicon_background.png new file mode 100644 index 0000000..3b49ece --- /dev/null +++ b/Platforms/Android/Resources/mipmap-hdpi/appicon_background.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:001a5186cdbc0aa22d44f21c8f460c038cb3eecb001527edb6d2292ee793f98a +size 571 diff --git a/Platforms/Android/Resources/mipmap-hdpi/appicon_foreground.png b/Platforms/Android/Resources/mipmap-hdpi/appicon_foreground.png new file mode 100644 index 0000000..6e90911 --- /dev/null +++ b/Platforms/Android/Resources/mipmap-hdpi/appicon_foreground.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8feb8d71289b44e6007064235c9c071b050361cd8f42fd669331b279746b220a +size 1526 diff --git a/Platforms/Android/Resources/mipmap-mdpi/appicon_background.png b/Platforms/Android/Resources/mipmap-mdpi/appicon_background.png new file mode 100644 index 0000000..f6efb10 --- /dev/null +++ b/Platforms/Android/Resources/mipmap-mdpi/appicon_background.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a80d5c8958bc64ff34b20c02c739a1a6bc516c13e94d28e98181c2d45c7e7b7 +size 390 diff --git a/Platforms/Android/Resources/mipmap-mdpi/appicon_foreground.png b/Platforms/Android/Resources/mipmap-mdpi/appicon_foreground.png new file mode 100644 index 0000000..cfa7cf0 --- /dev/null +++ b/Platforms/Android/Resources/mipmap-mdpi/appicon_foreground.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:32eaa8cf1d7363736563c176efc84a99c6b2b75216e7f40b56f66d6e65386bcc +size 1040 diff --git a/Platforms/Android/Resources/mipmap-xhdpi/appicon_background.png b/Platforms/Android/Resources/mipmap-xhdpi/appicon_background.png new file mode 100644 index 0000000..46270d0 --- /dev/null +++ b/Platforms/Android/Resources/mipmap-xhdpi/appicon_background.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dddcb9cbdd733c23a1e6d180dc253a73c077915961b704dff43d861060ce746a +size 741 diff --git a/Platforms/Android/Resources/mipmap-xhdpi/appicon_foreground.png b/Platforms/Android/Resources/mipmap-xhdpi/appicon_foreground.png new file mode 100644 index 0000000..e3af9b7 --- /dev/null +++ b/Platforms/Android/Resources/mipmap-xhdpi/appicon_foreground.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3cfcbd53ddc0d532a587ef5047701e815c1dd01e09334ea410075e98a6749182 +size 2013 diff --git a/Platforms/Android/Resources/mipmap-xxhdpi/appicon_background.png b/Platforms/Android/Resources/mipmap-xxhdpi/appicon_background.png new file mode 100644 index 0000000..9cf42b1 --- /dev/null +++ b/Platforms/Android/Resources/mipmap-xxhdpi/appicon_background.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8db3b1755c49b6c38474fe970f50d125d95389007ebe3f93e3e63f4fa94d13b2 +size 1079 diff --git a/Platforms/Android/Resources/mipmap-xxhdpi/appicon_foreground.png b/Platforms/Android/Resources/mipmap-xxhdpi/appicon_foreground.png new file mode 100644 index 0000000..9b0abe6 --- /dev/null +++ b/Platforms/Android/Resources/mipmap-xxhdpi/appicon_foreground.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70b729b17de042fcaaa58dccc414b7e710ffab97ac926ab02660ecadd8dd682c +size 2937 diff --git a/Platforms/Android/Resources/mipmap-xxxhdpi/appicon_background.png b/Platforms/Android/Resources/mipmap-xxxhdpi/appicon_background.png new file mode 100644 index 0000000..c62e37d --- /dev/null +++ b/Platforms/Android/Resources/mipmap-xxxhdpi/appicon_background.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db83004ecbfef7d636546b76352d4e038fc96177ad063be371a81be12c5ef07b +size 1767 diff --git a/Platforms/Android/Resources/mipmap-xxxhdpi/appicon_foreground.png b/Platforms/Android/Resources/mipmap-xxxhdpi/appicon_foreground.png new file mode 100644 index 0000000..ff196bf --- /dev/null +++ b/Platforms/Android/Resources/mipmap-xxxhdpi/appicon_foreground.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1970b876622d57d14e58db2d3229ee3f46111b4458168b97ae24a0dd6a06d860 +size 3911 diff --git a/Platforms/Android/Resources/values/ic_launcher_background.xml b/Platforms/Android/Resources/values/ic_launcher_background.xml new file mode 100644 index 0000000..6ec24e6 --- /dev/null +++ b/Platforms/Android/Resources/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #2C3E50 + \ No newline at end of file diff --git a/Platforms/Android/Resources/values/strings.xml b/Platforms/Android/Resources/values/strings.xml new file mode 100644 index 0000000..43a0ae5 --- /dev/null +++ b/Platforms/Android/Resources/values/strings.xml @@ -0,0 +1,4 @@ + + snake + snake + diff --git a/Platforms/Net/App.ico b/Platforms/Net/App.ico new file mode 100644 index 0000000..dbd447e Binary files /dev/null and b/Platforms/Net/App.ico differ diff --git a/Platforms/Net/SKApp.ico b/Platforms/Net/SKApp.ico new file mode 100644 index 0000000..5c45461 Binary files /dev/null and b/Platforms/Net/SKApp.ico differ diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..8413b20 --- /dev/null +++ b/Program.cs @@ -0,0 +1,58 @@ +using StereoKit; +using StereoKit.Framework; + + +namespace snake; + +class Program +{ + static void Main(string[] args) + { + SK.AddStepper(); + + // 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(); + }); + } +} \ No newline at end of file diff --git a/Projects/Android/snake.Android.csproj b/Projects/Android/snake.Android.csproj new file mode 100644 index 0000000..daf4b96 --- /dev/null +++ b/Projects/Android/snake.Android.csproj @@ -0,0 +1,50 @@ + + + net7.0-android + android-arm64 + 29 + Exe + com.dofdev.snake + 1 + 1.0 + snake + true + + Standard + ..\..\Assets + Assets + + + XA4211;XA1006;XA4301 + + + + + + + + + ..\..\Platforms\Android\AndroidManifest.xml + ..\..\Platforms\Android\Resources + ..\..\Platforms\Android\Assets + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android_icons.sh b/android_icons.sh new file mode 100755 index 0000000..8433855 --- /dev/null +++ b/android_icons.sh @@ -0,0 +1,56 @@ +#!/bin/bash + +# Check if input file is provided +if [ $# -eq 0 ]; then + echo "Usage: $0 " + 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/" \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..58030bb --- /dev/null +++ b/readme.md @@ -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 +``` \ No newline at end of file diff --git a/snake.csproj b/snake.csproj new file mode 100644 index 0000000..13d68ea --- /dev/null +++ b/snake.csproj @@ -0,0 +1,25 @@ + + + net7.0 + Exe + + Platforms\Net\App.ico + + + + + + + + + + + + + + + + + + \ No newline at end of file