mirror of
https://github.com/MichaelScottsman/LiquidAss-Frame.git
synced 2026-10-20 08:01:06 +02:00
- assPod Model on the main menu switches between Video Gen 1 and Nano Gen 2; Color lists the current body's finishes (the nano: silver, green, blue, pink, black, (PRODUCT)RED). The choice is kept in ~/.config/glass-shell/asspod.json beside the glass tune. - The nano: a procedural 2nd-generation model (tools/nano2-model) exported as render models, drawn by podd with anodized aluminum (tinted, bead-blasted, the laser-etched back paler), white end caps and a clickable wheel. Same menus on a 240 x 180 logical screen, so its text is larger, as on the real one; it plays no video. Shown 52.5 mm wide. - podd: one box layout that holds either body, so a switch needs no restart; 64 px between atlas tiles and a 4 px clear band around each eye's image, which removes the hairline rectangle around the body (filtering at the stereo split took the other eye's edge); texture dump and debug modes. - The left stick acts like the right: Steam's d-pad directions are no longer relayed (tilting the left stick down or left pressed the centre button or Menu).
1041 lines
52 KiB
C++
1041 lines
52 KiB
C++
// podd: the assPod's body with real shaders, for SteamVR's scene graph.
|
|
//
|
|
// SteamVR draws render models with fixed lighting only, so this renders the models (device/asspod/
|
|
// model) itself on the GPU, lit by a studio environment and one key light. Two bodies, picked by the
|
|
// spec's "model":
|
|
// video the Video (tools/asspod-model): clearcoat plastic on the front, mirror-polished steel on
|
|
// the back and edges, the engraved peach as satin, glass over the screen
|
|
// nano the nano, 2nd generation (tools/nano2-model): anodized aluminum (a tinted, bead-blasted
|
|
// metal under a thin oxide) with the laser-etched back paler and smoother, white plastic
|
|
// end caps, the click wheel and an aluminum center button
|
|
//
|
|
// How it reaches the headset: the body sits in a box of six panels in SteamVR's scene graph
|
|
// (device/asspod/src/sg.js), rigid with the hand. For each face and each eye, podd renders the model
|
|
// as seen from that eye through the face (an off-axis projection onto the face's plane), from
|
|
// predicted head and hand poses. A face's panel shows its eye pair with SteamVR's "Parallel"
|
|
// stereoscopy, so the body looks solid from any side, and the panels themselves move with the hand
|
|
// at the compositor's rate. The screen is not drawn here: the window shows the live screen panel
|
|
// inside the box (glass reflections over it, transparent otherwise).
|
|
//
|
|
// Input: --spec /dev/shm/lgs/podd.json {open, hand, rot [w,x,y,z], centre [x,y,z], scale, nanoScale, model, finish}
|
|
// (the body in the controller's frame, written by device/shell_ext/asspod.py; scale is the
|
|
// Video's, nanoScale the nano's; finish is the current model's colour)
|
|
// Output: --out /dev/shm/lgs/podd-out.json {pid, ok, key, texW, texH, box [hx,hy,hz], tiles {face: [x,y,w,h]}}
|
|
// tiles are the left eye's rect in texture pixels (v = 0 at the top), with `pad` px around
|
|
// each eye (PAD px of overscan, then BAND px clear); the right eye's starts tw + 2 pad to its right. A face's panel
|
|
// shows [tx - pad, tx + 2 tw + 3 pad] x [ty - pad, ty + th + pad]. The overlay's mouse scale
|
|
// is half the texture's width (one eye).
|
|
//
|
|
// Build on the Frame: sh native/podd/build.sh (GBM, EGL, GLES 3.2, SteamVR's libopenvr_api).
|
|
#include <openvr.h>
|
|
#include <signal.h>
|
|
#include <sys/file.h>
|
|
#include <sys/prctl.h>
|
|
#include <sys/stat.h>
|
|
#include <time.h>
|
|
|
|
#include <array>
|
|
#include <cctype>
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "gfx.h"
|
|
#include "json.h"
|
|
#include "vmath.h"
|
|
|
|
// ------------------------------------------------------------------ the models (model metres)
|
|
struct Geo {
|
|
float w, h, d; // body
|
|
float lcdCy, lcdW, lcdH; // the LCD (transparent: the screen panel shows through)
|
|
float winW, winH, winR; // its window (winR: rounded corners drawn here; 0 = the plate's own hole)
|
|
float wheelCy, wheelRm; // the wheel's centre and the touch marker's radius
|
|
float markerSigma; // the marker's glow
|
|
float rockDeg, sink; // press motion
|
|
};
|
|
// tools/asspod-model GEOMETRY
|
|
static const Geo VIDEO = {0.0618f, 0.1035f, 0.0110f, 0.0242f, 0.0508f, 0.0381f, 0.0532f, 0.0405f, 0.f,
|
|
-0.0240f, 0.0135f, 0.0028f, 2.5f, 0.0005f};
|
|
// tools/nano2-model GEOMETRY
|
|
static const Geo NANO = {0.040f, 0.090f, 0.0065f, 0.02888f, 0.0296f, 0.0222f, 0.03162f, 0.02404f, 0.0006f,
|
|
-0.0133f, 0.0108f, 0.0021f, 2.2f, 0.0004f};
|
|
// The nano's anodized colours (tools/nano2-model FINISHES metal, sRGB) and roughness.
|
|
struct Finish { const char *name; float r, g, b, rough; };
|
|
static const Finish NANO_FINISHES[] = {
|
|
{"silver", 0xd3 / 255.f, 0xd6 / 255.f, 0xda / 255.f, 0.42f}, {"green", 0x7f / 255.f, 0xd1 / 255.f, 0x2a / 255.f, 0.40f},
|
|
{"blue", 0x1e / 255.f, 0xa9 / 255.f, 0xe3 / 255.f, 0.40f}, {"pink", 0xec / 255.f, 0x2c / 255.f, 0x95 / 255.f, 0.40f},
|
|
{"black", 0x16 / 255.f, 0x17 / 255.f, 0x1a / 255.f, 0.46f}, {"red", 0xe0 / 255.f, 0x12 / 255.f, 0x1b / 255.f, 0.40f},
|
|
};
|
|
static const float MARGIN = 0.004f; // m around the body: the box of panels
|
|
static const float PX_PER_M = 4200.f; // panel pixels per metre (about 1 px per 0.24 mm)
|
|
static const int PAD = 3; // px rendered past each face's edge: neighbouring faces overlap,
|
|
// so no panel edge (where texels blend with the gap) shows
|
|
static const int BAND = 4; // px left clear around each eye's image (outside its PAD): SteamVR
|
|
// splits a panel's rect between the eyes at its middle, and
|
|
// filtering at that seam took a sliver of the other eye's image,
|
|
// whose body colour at its edge showed as a faint line along the
|
|
// box's edge, about a cm off the body
|
|
static const int SPAN = PAD + BAND; // each eye's image to its block's edge
|
|
|
|
static const float kPi = 3.14159265f;
|
|
static volatile sig_atomic_t g_stop = 0;
|
|
static void onSignal(int) { g_stop = 1; }
|
|
|
|
static uint64_t nowNs() {
|
|
timespec ts;
|
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
return uint64_t(ts.tv_sec) * 1000000000ull + uint64_t(ts.tv_nsec);
|
|
}
|
|
|
|
static bool readFile(const std::string &p, std::string &out) {
|
|
std::ifstream f(p, std::ios::binary);
|
|
if (!f) return false;
|
|
std::stringstream ss;
|
|
ss << f.rdbuf();
|
|
out = ss.str();
|
|
return true;
|
|
}
|
|
|
|
static void writeAtomic(const std::string &path, const std::string &text) {
|
|
std::string tmp = path + ".tmp";
|
|
FILE *f = std::fopen(tmp.c_str(), "w");
|
|
if (!f) return;
|
|
std::fwrite(text.data(), 1, text.size(), f);
|
|
std::fclose(f);
|
|
std::rename(tmp.c_str(), path.c_str());
|
|
}
|
|
|
|
// ------------------------------------------------------------------ meshes
|
|
struct Mesh {
|
|
GLuint vao = 0, vbo = 0;
|
|
GLsizei count = 0;
|
|
};
|
|
|
|
// Our exporter's OBJ: v / vt / vn, then non-indexed triangles "f i/i/i i/i/i i/i/i".
|
|
static bool loadObj(const std::string &path, Mesh &m) {
|
|
std::string text;
|
|
if (!readFile(path, text)) return false;
|
|
std::vector<float> pos, uv, nrm, out;
|
|
std::istringstream in(text);
|
|
std::string line;
|
|
while (std::getline(in, line)) {
|
|
if (line.size() < 2) continue;
|
|
if (line[0] == 'v' && line[1] == ' ') {
|
|
float x, y, z;
|
|
if (std::sscanf(line.c_str() + 2, "%f %f %f", &x, &y, &z) == 3) pos.insert(pos.end(), {x, y, z});
|
|
} else if (line[0] == 'v' && line[1] == 't') {
|
|
float u, v;
|
|
if (std::sscanf(line.c_str() + 3, "%f %f", &u, &v) == 2) uv.insert(uv.end(), {u, v});
|
|
} else if (line[0] == 'v' && line[1] == 'n') {
|
|
float x, y, z;
|
|
if (std::sscanf(line.c_str() + 3, "%f %f %f", &x, &y, &z) == 3) nrm.insert(nrm.end(), {x, y, z});
|
|
} else if (line[0] == 'f' && line[1] == ' ') {
|
|
int a[3][3];
|
|
if (std::sscanf(line.c_str() + 2, "%d/%d/%d %d/%d/%d %d/%d/%d", &a[0][0], &a[0][1], &a[0][2], &a[1][0], &a[1][1],
|
|
&a[1][2], &a[2][0], &a[2][1], &a[2][2]) != 9)
|
|
continue;
|
|
for (auto &c : a) {
|
|
int p = c[0] - 1, t = c[1] - 1, n = c[2] - 1;
|
|
if (p < 0 || size_t(p) * 3 + 2 >= pos.size() || t < 0 || size_t(t) * 2 + 1 >= uv.size() || n < 0 ||
|
|
size_t(n) * 3 + 2 >= nrm.size())
|
|
return false;
|
|
out.insert(out.end(), {pos[p * 3], pos[p * 3 + 1], pos[p * 3 + 2], nrm[n * 3], nrm[n * 3 + 1], nrm[n * 3 + 2],
|
|
uv[t * 2], uv[t * 2 + 1]});
|
|
}
|
|
}
|
|
}
|
|
if (out.empty()) return false;
|
|
glGenVertexArrays(1, &m.vao);
|
|
glGenBuffers(1, &m.vbo);
|
|
glBindVertexArray(m.vao);
|
|
glBindBuffer(GL_ARRAY_BUFFER, m.vbo);
|
|
glBufferData(GL_ARRAY_BUFFER, GLsizeiptr(out.size() * sizeof(float)), out.data(), GL_STATIC_DRAW);
|
|
for (int i = 0; i < 3; i++) glEnableVertexAttribArray(GLuint(i));
|
|
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 32, reinterpret_cast<void *>(0));
|
|
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 32, reinterpret_cast<void *>(12));
|
|
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 32, reinterpret_cast<void *>(24));
|
|
glBindVertexArray(0);
|
|
m.count = GLsizei(out.size() / 8);
|
|
return true;
|
|
}
|
|
|
|
// Raw RGBA from the exporter: "RGBA", u32 width, u32 height, rows top first.
|
|
static GLuint loadRgba(const std::string &path) {
|
|
std::string d;
|
|
if (!readFile(path, d) || d.size() < 12 || d.compare(0, 4, "RGBA") != 0) return 0;
|
|
uint32_t w, h;
|
|
std::memcpy(&w, d.data() + 4, 4);
|
|
std::memcpy(&h, d.data() + 8, 4);
|
|
if (d.size() < 12 + size_t(w) * h * 4) return 0;
|
|
GLuint t;
|
|
glGenTextures(1, &t);
|
|
glBindTexture(GL_TEXTURE_2D, t);
|
|
int levels = 1;
|
|
for (uint32_t m = std::max(w, h); m > 1; m >>= 1) levels++;
|
|
glTexStorage2D(GL_TEXTURE_2D, levels, GL_RGBA8, GLsizei(w), GLsizei(h));
|
|
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
|
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, GLsizei(w), GLsizei(h), GL_RGBA, GL_UNSIGNED_BYTE, d.data() + 12);
|
|
glGenerateMipmap(GL_TEXTURE_2D);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
return t;
|
|
}
|
|
|
|
// ------------------------------------------------------------------ shaders
|
|
static const char *VS = R"GLSL(#version 320 es
|
|
layout(location = 0) in vec3 aPos;
|
|
layout(location = 1) in vec3 aNrm;
|
|
layout(location = 2) in vec2 aUv;
|
|
uniform mat4 uProj, uView, uModel; // model: the part's own motion (wheel rock, button sink), in body units
|
|
uniform float uScale; // model metres -> body metres
|
|
out vec3 vPos; // body space (metres)
|
|
out vec3 vNrm;
|
|
out vec2 vUv;
|
|
out vec3 vModel; // the model's own coordinates (metres, unscaled)
|
|
void main() {
|
|
vec4 p = uModel * vec4(aPos, 1.0);
|
|
vModel = p.xyz;
|
|
vPos = p.xyz * uScale;
|
|
vNrm = mat3(uModel) * aNrm;
|
|
vUv = aUv;
|
|
gl_Position = uProj * uView * vec4(vPos, 1.0);
|
|
}
|
|
)GLSL";
|
|
|
|
static const char *FS = R"GLSL(#version 320 es
|
|
precision highp float;
|
|
in vec3 vPos;
|
|
in vec3 vNrm;
|
|
in vec2 vUv;
|
|
in vec3 vModel;
|
|
uniform vec3 uEye; // body space
|
|
uniform mat3 uToWorld; // body -> world rotation (the environment is fixed in the room)
|
|
uniform int uMat; // 0 plate, 1 steel, 2 steel back (engraving), 3 wheel, 4 well, 5 ports, 6 window glass,
|
|
// 7 anodized aluminum, 8 anodized back (engraving), 9 white plastic (texture), 10 palette (texture)
|
|
uniform vec3 uBase; // base colour (linear)
|
|
uniform float uRough; // anodized: the blasted metal's roughness
|
|
uniform sampler2D uTex; // wheel labels / engraving mask / cap print / palette
|
|
uniform vec4 uMarker; // x angle (rad, 0 = top, clockwise), y opacity
|
|
uniform vec3 uWheel; // the wheel's centre y, the marker's radius, its glow (model metres)
|
|
uniform vec3 uLcd; // the LCD's centre y, half width, half height
|
|
uniform vec3 uWin; // the window's half width, half height, corner radius (0: square)
|
|
out vec4 frag;
|
|
|
|
const float PI = 3.14159265;
|
|
const vec3 KEY = normalize(vec3(-0.45, 0.80, 0.40));
|
|
const vec3 FILL = normalize(vec3(0.70, 0.35, 0.60));
|
|
|
|
// The studio, as seen along world direction r, blurred for roughness: dark surroundings (so glossy
|
|
// black stays black and polished steel has contrast), a soft grey ceiling, and bright soft boxes.
|
|
vec3 envColor(vec3 r, float rough) {
|
|
float y = r.y;
|
|
float w = 0.03 + rough * 0.6;
|
|
float up = smoothstep(0.05 - w, 0.55 + w, y);
|
|
vec3 c = mix(vec3(0.016, 0.017, 0.019), vec3(0.10, 0.105, 0.115), up);
|
|
c = mix(c, vec3(0.05, 0.048, 0.045), smoothstep(-0.05 + w, -0.35 - w, y)); // the floor
|
|
// the key soft box above-left, a fill on the right, a long strip light behind
|
|
float lk = 0.004 + rough * rough * 0.9;
|
|
float key = exp(-(1.0 - dot(r, KEY)) / lk) * (0.004 / lk) * 6.0;
|
|
float lf = 0.012 + rough * rough * 0.9;
|
|
float fill = exp(-(1.0 - dot(r, FILL)) / lf) * (0.012 / lf) * 1.5;
|
|
float sw = 0.0015 + rough * 0.25;
|
|
float strip = exp(-pow(r.x + 0.55, 2.0) / sw) * smoothstep(-0.2, 0.3, y) * smoothstep(0.95, 0.5, y) * 1.1 * (0.0015 / sw + 0.15 * rough);
|
|
return c + vec3(key + fill) + vec3(1.0, 0.98, 0.95) * strip;
|
|
}
|
|
vec3 irradiance(vec3 n) {
|
|
vec3 c = mix(vec3(0.05, 0.05, 0.05), vec3(0.22, 0.23, 0.25), n.y * 0.5 + 0.5);
|
|
return c + vec3(1.0, 0.98, 0.95) * max(dot(n, KEY), 0.0) * 0.32 + vec3(0.14) * max(dot(n, FILL), 0.0);
|
|
}
|
|
float fresnel(float f0, float cosT) { return f0 + (1.0 - f0) * pow(1.0 - cosT, 5.0); }
|
|
// value noise for the blasted aluminum's grain
|
|
float hash3(vec3 p) { return fract(sin(dot(p, vec3(127.1, 311.7, 74.7))) * 43758.5453); }
|
|
float vnoise(vec3 p) {
|
|
vec3 i = floor(p), f = fract(p);
|
|
f = f * f * (3.0 - 2.0 * f);
|
|
float a = mix(mix(hash3(i), hash3(i + vec3(1, 0, 0)), f.x), mix(hash3(i + vec3(0, 1, 0)), hash3(i + vec3(1, 1, 0)), f.x), f.y);
|
|
float b = mix(mix(hash3(i + vec3(0, 0, 1)), hash3(i + vec3(1, 0, 1)), f.x), mix(hash3(i + vec3(0, 1, 1)), hash3(i + vec3(1, 1, 1)), f.x), f.y);
|
|
return mix(a, b, f.z);
|
|
}
|
|
vec3 fresnel3(vec3 f0, float cosT) { return f0 + (1.0 - f0) * pow(1.0 - cosT, 5.0); }
|
|
|
|
void main() {
|
|
vec3 n = normalize(vNrm);
|
|
vec3 v = normalize(uEye - vPos);
|
|
if (dot(n, v) < 0.0) n = -n; // two-sided (thin parts)
|
|
vec3 nw = normalize(uToWorld * n);
|
|
vec3 r = normalize(uToWorld * reflect(-v, n));
|
|
float cosT = clamp(dot(n, v), 0.0, 1.0);
|
|
vec3 col;
|
|
float alpha = 1.0;
|
|
if (uMat == 1 || uMat == 2) {
|
|
// polished stainless steel; the engraving is a satin etch
|
|
float etch = uMat == 2 ? texture(uTex, vec2(vUv.x, 1.0 - vUv.y)).r : 0.0;
|
|
float rough = mix(0.06, 0.38, etch);
|
|
vec3 f0 = mix(vec3(0.90, 0.91, 0.93), vec3(0.74, 0.75, 0.78), etch);
|
|
col = fresnel3(f0, cosT) * envColor(r, rough);
|
|
} else if (uMat == 3) {
|
|
// the click wheel: printed labels on satin plastic
|
|
vec3 base = texture(uTex, vec2(vUv.x, 1.0 - vUv.y)).rgb;
|
|
base = pow(base, vec3(2.2));
|
|
col = base * irradiance(nw) + fresnel(0.04, cosT) * envColor(r, 0.45);
|
|
// the touch marker: a soft glow where the thumb is
|
|
if (uMarker.y > 0.001) {
|
|
vec2 p = vModel.xy - vec2(0.0, uWheel.x);
|
|
vec2 m = vec2(sin(uMarker.x), cos(uMarker.x)) * uWheel.y;
|
|
float d = length(p - m);
|
|
col += vec3(0.55, 0.72, 1.0) * uMarker.y * exp(-d * d / (2.0 * uWheel.z * uWheel.z)) * 0.9;
|
|
}
|
|
} else if (uMat == 4) {
|
|
col = uBase * irradiance(nw);
|
|
} else if (uMat == 5) {
|
|
col = uBase * irradiance(nw) + fresnel(0.3, cosT) * envColor(r, 0.35) * 0.5;
|
|
} else if (uMat == 6) {
|
|
// the window: black glossy border; the LCD itself transparent, with the glass's reflection
|
|
vec2 p = vModel.xy - vec2(0.0, uLcd.x);
|
|
if (uWin.z > 0.0) { // rounded corners (the nano's window lies on the aluminum)
|
|
vec2 q = abs(p) - (uWin.xy - uWin.z);
|
|
if (length(max(q, 0.0)) > uWin.z) discard;
|
|
}
|
|
bool lcd = abs(p.x) < uLcd.y && abs(p.y) < uLcd.z;
|
|
vec3 refl = fresnel(0.04, cosT) * envColor(r, 0.02);
|
|
if (lcd) {
|
|
float a = clamp(max(refl.r, max(refl.g, refl.b)), 0.0, 1.0);
|
|
frag = vec4(refl, a); // premultiplied: the reflection over whatever is behind
|
|
frag.rgb = pow(frag.rgb / (1.0 + frag.rgb), vec3(1.0 / 2.2)) * a;
|
|
return;
|
|
}
|
|
col = vec3(0.004) + refl;
|
|
} else if (uMat == 7 || uMat == 8) {
|
|
// anodized aluminum: a dyed metal, bead-blasted (fine grain), under a thin clear oxide. The
|
|
// laser engraving on the back takes the dye and the blasting with it: paler and smoother.
|
|
float etch = uMat == 8 ? texture(uTex, vec2(vUv.x, 1.0 - vUv.y)).r : 0.0;
|
|
vec3 gp = vModel * 5200.0; // grain cells about 0.2 mm
|
|
float grain = vnoise(gp) * 0.65 + vnoise(gp * 2.3 + 7.1) * 0.35 - 0.5;
|
|
vec3 jitter = vec3(vnoise(gp + 3.1), vnoise(gp + 5.7), vnoise(gp + 9.2)) - 0.5;
|
|
vec3 ng = normalize(n + jitter * 0.07 * (1.0 - etch));
|
|
vec3 rg = normalize(uToWorld * reflect(-v, ng));
|
|
float cosG = clamp(dot(ng, v), 0.0, 1.0);
|
|
float rough = mix(uRough + grain * 0.08, 0.16, etch);
|
|
vec3 tint = mix(uBase, mix(uBase, vec3(0.85), 0.55), etch);
|
|
vec3 spec = fresnel3(tint, cosG) * envColor(rg, rough);
|
|
vec3 diff = tint * irradiance(nw) * 0.85; // the blasted surface scatters: its colour shows from any side
|
|
vec3 coat = fresnel(0.04, cosT) * envColor(r, 0.1) * 0.5;
|
|
col = (spec + diff) * (1.0 + grain * 0.1) + coat;
|
|
} else if (uMat == 9) {
|
|
// the nano's end caps: white satin plastic (the print from the texture)
|
|
vec3 base = pow(texture(uTex, vec2(vUv.x, 1.0 - vUv.y)).rgb, vec3(2.2));
|
|
col = base * irradiance(nw) * (1.0 - fresnel(0.04, cosT)) + fresnel(0.04, cosT) * envColor(r, 0.3) * 0.6;
|
|
} else if (uMat == 10) {
|
|
// the nano's ports: each part's colour from the palette texture
|
|
vec3 base = pow(texture(uTex, vec2(vUv.x, 1.0 - vUv.y)).rgb, vec3(2.2));
|
|
col = base * irradiance(nw) + fresnel(0.3, cosT) * envColor(r, 0.35) * 0.5 * max(base.r, 0.1);
|
|
} else {
|
|
// the front plate and centre button: plastic under a clear coat
|
|
vec3 diffuse = uBase * irradiance(nw);
|
|
vec3 spec = fresnel(0.04, cosT) * envColor(r, uBase.r > 0.5 ? 0.18 : 0.12);
|
|
vec3 coat = fresnel(0.04, cosT) * envColor(r, 0.03);
|
|
col = diffuse * (1.0 - fresnel(0.04, cosT)) + spec * 0.5 + coat;
|
|
}
|
|
// filmic tone map and gamma (the overlay is submitted as gamma colour)
|
|
col = col / (1.0 + col * 0.55) * 1.1;
|
|
frag = vec4(pow(max(col, 0.0), vec3(1.0 / 2.2)), alpha);
|
|
}
|
|
)GLSL";
|
|
|
|
// ------------------------------------------------------------------ the box and its atlas
|
|
struct Face {
|
|
const char *name;
|
|
v3 c, R, U, N; // body space: centre, right, up, outward normal
|
|
float w, h; // metres
|
|
int tx, ty, tw, th; // the left eye's tile (texture px, top-left origin) inside PAD px of overscan
|
|
// and BAND px clear; the right eye's at tx + tw + 2 SPAN
|
|
};
|
|
|
|
struct Layout {
|
|
std::array<Face, 6> f;
|
|
int texW = 0, texH = 0;
|
|
float hx = 0, hy = 0, hz = 0;
|
|
int vx = 0, vy = 0; // the video tile: the playing video's picture twice (left eye, right eye), like a face block
|
|
};
|
|
|
|
// The video's picture: device/shell_ext/asspod.py writes the latest frame to VIDEO_FILE as
|
|
// "PVID", u32 seq, u32 width, u32 height, then RGBA rows top first (letterboxed to 4:3).
|
|
static const int VIDEO_W = 320, VIDEO_H = 240;
|
|
static const char *VIDEO_FILE = "/dev/shm/lgs/podd-video";
|
|
|
|
// The box holds either body at its scale, so switching models keeps the layout (and the page's panels).
|
|
static Layout makeLayout(float scale, float nanoScale) {
|
|
Layout L;
|
|
L.hx = std::max(VIDEO.w * scale, NANO.w * nanoScale) / 2 + MARGIN;
|
|
L.hy = std::max(VIDEO.h * scale, NANO.h * nanoScale) / 2 + MARGIN;
|
|
L.hz = std::max(VIDEO.d * scale, NANO.d * nanoScale) / 2 + MARGIN;
|
|
const float hx = L.hx, hy = L.hy, hz = L.hz;
|
|
L.f = {{
|
|
{"front", {0, 0, hz}, {1, 0, 0}, {0, 1, 0}, {0, 0, 1}, 2 * hx, 2 * hy},
|
|
{"back", {0, 0, -hz}, {-1, 0, 0}, {0, 1, 0}, {0, 0, -1}, 2 * hx, 2 * hy},
|
|
{"right", {hx, 0, 0}, {0, 0, -1}, {0, 1, 0}, {1, 0, 0}, 2 * hz, 2 * hy},
|
|
{"left", {-hx, 0, 0}, {0, 0, 1}, {0, 1, 0}, {-1, 0, 0}, 2 * hz, 2 * hy},
|
|
{"top", {0, hy, 0}, {1, 0, 0}, {0, 0, -1}, {0, 1, 0}, 2 * hx, 2 * hz},
|
|
{"bottom", {0, -hy, 0}, {1, 0, 0}, {0, 0, 1}, {0, -1, 0}, 2 * hx, 2 * hz},
|
|
}};
|
|
// two rows: front, back, right, left (all as tall as the body); then top and bottom. A face's block
|
|
// is both eyes, each in PAD px of overscan and BAND px clear: [SPAN | left eye | 2 SPAN | right eye | SPAN]. The blocks (and
|
|
// the opaque video tile) stand GAP px apart: a panel seen at a slant samples well past its own edge
|
|
// (coarse texture levels), and 4 px let it pick up a neighbour's body or the black video tile, a
|
|
// thin flickering line along the box's edges, a few mm off the body.
|
|
const int gap = 64;
|
|
int x = 0, row0 = 0;
|
|
for (int i = 0; i < 4; i++) {
|
|
Face &F = L.f[size_t(i)];
|
|
F.tw = int(std::lround(F.w * PX_PER_M));
|
|
F.th = int(std::lround(F.h * PX_PER_M));
|
|
F.tx = x + SPAN;
|
|
F.ty = SPAN;
|
|
x += 2 * F.tw + 4 * SPAN + gap;
|
|
row0 = std::max(row0, F.th + 2 * SPAN);
|
|
}
|
|
int w0 = x;
|
|
x = 0;
|
|
int row1 = 0;
|
|
for (int i = 4; i < 6; i++) {
|
|
Face &F = L.f[size_t(i)];
|
|
F.tw = int(std::lround(F.w * PX_PER_M));
|
|
F.th = int(std::lround(F.h * PX_PER_M));
|
|
F.tx = x + SPAN;
|
|
F.ty = row0 + gap + SPAN;
|
|
x += 2 * F.tw + 4 * SPAN + gap;
|
|
row1 = std::max(row1, F.th + 2 * SPAN);
|
|
}
|
|
// the video tile under the second row
|
|
L.vx = x;
|
|
L.vy = row0 + gap;
|
|
x += 2 * VIDEO_W + gap;
|
|
L.texW = (std::max(w0, x) + 63) / 64 * 64;
|
|
L.texH = (row0 + gap + std::max(row1, VIDEO_H) + 7) / 8 * 8;
|
|
return L;
|
|
}
|
|
|
|
// Off-axis projection through a face (Kooima's generalized perspective), eye in body space.
|
|
static bool faceMatrices(const Face &F, v3 e, float proj[16], float view[16]) {
|
|
v3 pa = F.c - F.R * (F.w / 2) - F.U * (F.h / 2);
|
|
v3 pb = F.c + F.R * (F.w / 2) - F.U * (F.h / 2);
|
|
v3 pc = F.c - F.R * (F.w / 2) + F.U * (F.h / 2);
|
|
v3 va = pa - e, vb = pb - e, vc = pc - e;
|
|
float d = -dot(va, F.N);
|
|
if (d < 0.002f) return false; // the eye is behind (or in) this face's plane
|
|
const float n = 0.002f, f = 4.0f;
|
|
float l = dot(F.R, va) * n / d, r = dot(F.R, vb) * n / d;
|
|
float b = dot(F.U, va) * n / d, t = dot(F.U, vc) * n / d;
|
|
// memory row 0 is the texture's top and GL's y = 0: map the face's top (+U) to y = -1
|
|
std::swap(b, t);
|
|
std::fill(proj, proj + 16, 0.f);
|
|
proj[0] = 2 * n / (r - l);
|
|
proj[5] = 2 * n / (t - b);
|
|
proj[8] = (r + l) / (r - l);
|
|
proj[9] = (t + b) / (t - b);
|
|
proj[10] = -(f + n) / (f - n);
|
|
proj[11] = -1;
|
|
proj[14] = -2 * f * n / (f - n);
|
|
// view: rotate into the face's frame (rows R, U, N), then move the eye to the origin
|
|
const v3 R = F.R, U = F.U, N = F.N;
|
|
view[0] = R.x; view[4] = R.y; view[8] = R.z;
|
|
view[1] = U.x; view[5] = U.y; view[9] = U.z;
|
|
view[2] = N.x; view[6] = N.y; view[10] = N.z;
|
|
view[3] = view[7] = view[11] = 0;
|
|
view[12] = -dot(R, e);
|
|
view[13] = -dot(U, e);
|
|
view[14] = -dot(N, e);
|
|
view[15] = 1;
|
|
return true;
|
|
}
|
|
|
|
// ------------------------------------------------------------------ the thumbstick (as device/asspod/src/input/controllers.js)
|
|
struct Stick {
|
|
v3 P, axis;
|
|
};
|
|
static const Stick STICK_R = {{-0.0218f, -0.0049f, 0.0564f}, {-0.173f, 0.807f, -0.565f}};
|
|
static const Stick STICK_L = {{0.0218f, -0.0049f, 0.0565f}, {0.168f, 0.797f, -0.581f}};
|
|
|
|
struct HandInput {
|
|
std::string rm;
|
|
vr::VRInputValueHandle_t path = 0;
|
|
float x = 0, y = 0;
|
|
bool clicked = false;
|
|
};
|
|
|
|
static void readStick(HandInput &h, const Stick &S) {
|
|
h.x = h.y = 0;
|
|
h.clicked = false;
|
|
if (h.rm.empty() || !h.path || !vr::VRRenderModels()) return;
|
|
vr::RenderModel_ComponentState_t cs{};
|
|
vr::RenderModel_ControllerMode_State_t mode{};
|
|
if (!vr::VRRenderModels()->GetComponentStateForDevicePath(h.rm.c_str(), "thumbstick", h.path, &mode, &cs)) return;
|
|
Pose xf = fromVR(cs.mTrackingToComponentRenderModel);
|
|
quat q = quatFrom(xf);
|
|
if (q.w < 0) q = {-q.w, -q.x, -q.y, -q.z};
|
|
v3 s = normalize(S.axis);
|
|
auto proj = [&](v3 v) { return normalize(v - s * dot(v, s)); };
|
|
v3 right = proj({1, 0, 0}), fwd = proj({0, 0, -1});
|
|
v3 d = cross({q.x, q.y, q.z}, s);
|
|
float x = dot(d, right) / 0.165f, y = dot(d, fwd) / 0.165f;
|
|
float r = std::sqrt(x * x + y * y);
|
|
if (r > 1) { x /= r; y /= r; }
|
|
h.x = x;
|
|
h.y = y;
|
|
v3 rp = poseFrom(q, {}).rotate(S.P);
|
|
v3 resid = xf.t - (S.P - rp);
|
|
h.clicked = -dot(resid, s) > 0.00025f;
|
|
}
|
|
|
|
// ------------------------------------------------------------------ main
|
|
struct Spec {
|
|
bool open = false;
|
|
std::string hand = "right";
|
|
quat rot;
|
|
v3 centre;
|
|
float scale = 1.375f;
|
|
float nanoScale = 1.3125f;
|
|
std::string model = "video";
|
|
std::string finish = "black";
|
|
bool world = false; // pinned in the room (Reposition): wrot / wcentre in standing space
|
|
quat wrot;
|
|
v3 wcentre;
|
|
};
|
|
|
|
static bool readSpec(const std::string &path, Spec &s) {
|
|
std::string t, err;
|
|
if (!readFile(path, t)) return false;
|
|
JVal j;
|
|
JParser p(t);
|
|
if (!p.parse(j, err)) return false;
|
|
s.open = j.boolean("open", false);
|
|
s.hand = j.str("hand", "right");
|
|
s.scale = float(j.num("scale", 1.375));
|
|
s.nanoScale = float(j.num("nanoScale", 1.3125));
|
|
s.model = j.str("model", "video");
|
|
s.finish = j.str("finish", "black");
|
|
const JVal *r = j.get("rot"), *c = j.get("centre");
|
|
if (r && r->type == JVal::Arr && r->a.size() == 4) s.rot = qnorm({float(r->a[0].n), float(r->a[1].n), float(r->a[2].n), float(r->a[3].n)});
|
|
if (c && c->type == JVal::Arr && c->a.size() == 3) s.centre = {float(c->a[0].n), float(c->a[1].n), float(c->a[2].n)};
|
|
s.world = false;
|
|
const JVal *w = j.get("world");
|
|
if (w && w->type == JVal::Obj) {
|
|
const JVal *wr = w->get("rot"), *wc = w->get("centre");
|
|
if (wr && wr->type == JVal::Arr && wr->a.size() == 4 && wc && wc->type == JVal::Arr && wc->a.size() == 3) {
|
|
s.world = true;
|
|
s.wrot = qnorm({float(wr->a[0].n), float(wr->a[1].n), float(wr->a[2].n), float(wr->a[3].n)});
|
|
s.wcentre = {float(wc->a[0].n), float(wc->a[1].n), float(wc->a[2].n)};
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
std::string specPath = "/dev/shm/lgs/podd.json", outPath = "/dev/shm/lgs/podd-out.json", modelDir, key = "podd.pod";
|
|
float fps = 72;
|
|
bool orphanOk = false;
|
|
for (int i = 1; i < argc; i++) {
|
|
std::string a = argv[i];
|
|
auto next = [&]() -> std::string { return i + 1 < argc ? argv[++i] : ""; };
|
|
if (a == "--spec") specPath = next();
|
|
else if (a == "--out") outPath = next();
|
|
else if (a == "--model") modelDir = next();
|
|
else if (a == "--key") key = next();
|
|
else if (a == "--fps") fps = std::stof(next());
|
|
else if (a == "--orphan-ok") orphanOk = true;
|
|
else { std::fprintf(stderr, "unknown option %s\n", a.c_str()); return 2; }
|
|
}
|
|
if (modelDir.empty()) { std::fprintf(stderr, "--model DIR is required\n"); return 2; }
|
|
if (!orphanOk) prctl(PR_SET_PDEATHSIG, SIGTERM);
|
|
signal(SIGTERM, onSignal);
|
|
signal(SIGINT, onSignal);
|
|
signal(SIGHUP, onSignal);
|
|
setvbuf(stdout, nullptr, _IOLBF, 0);
|
|
|
|
int lock = open((outPath + ".lock").c_str(), O_CREAT | O_RDWR | O_CLOEXEC, 0644);
|
|
if (lock < 0 || flock(lock, LOCK_EX | LOCK_NB) != 0) { std::printf("another podd is running\n"); return 75; }
|
|
|
|
vr::EVRInitError err = vr::VRInitError_None;
|
|
vr::VR_Init(&err, vr::VRApplication_Overlay);
|
|
if (err != vr::VRInitError_None) { std::printf("VR_Init: %s\n", vr::VR_GetVRInitErrorAsEnglishDescription(err)); return 75; }
|
|
if (!vr::VROverlay() || !vr::VRSystem() || !vr::VRIPCResourceManager()) { std::printf("OpenVR interfaces missing\n"); return 1; }
|
|
|
|
Gfx gfx;
|
|
if (!gfx.init()) return 1;
|
|
std::printf("podd: %s\n", gfx.renderer.c_str());
|
|
|
|
Program prog;
|
|
if (!linkProgram(prog, VS, FS, "pod")) return 1;
|
|
|
|
// The parts of both models. A part with a variant is drawn only in that finish (the Video's
|
|
// plates; the nano's wheels: black on the black one, white on the others); a tinted part takes the
|
|
// nano's finish colour (one mesh for every colour). tex: the RGBA podd samples (a path in the
|
|
// model dir), if any.
|
|
struct Part {
|
|
std::string model, name, variant, tex;
|
|
int mat;
|
|
v3 base;
|
|
bool tint = false;
|
|
Mesh mesh;
|
|
GLuint texId = 0;
|
|
bool wheel = false, center = false;
|
|
};
|
|
const v3 W1 = {1, 1, 1};
|
|
std::vector<Part> parts = {
|
|
{"video", "asspod_steel_back", "", "asspod_steel_back/asspod_steel_back_mask.rgba", 2, {0.9f, 0.9f, 0.9f}},
|
|
{"video", "asspod_steel_edge", "", "", 1, {0.9f, 0.9f, 0.9f}},
|
|
{"video", "asspod_ports", "", "", 5, {0.03f, 0.03f, 0.035f}},
|
|
{"video", "asspod_plate_black", "black", "", 0, {0.004f, 0.004f, 0.0045f}},
|
|
{"video", "asspod_plate_white", "white", "", 0, {0.86f, 0.86f, 0.84f}},
|
|
{"video", "asspod_well_black", "black", "", 4, {0.002f, 0.002f, 0.002f}},
|
|
{"video", "asspod_well_white", "white", "", 4, {0.25f, 0.25f, 0.24f}},
|
|
{"video", "asspod_wheel_black", "black", "asspod_wheel_black/asspod_wheel_black_color.rgba", 3, W1},
|
|
{"video", "asspod_wheel_white", "white", "asspod_wheel_white/asspod_wheel_white_color.rgba", 3, W1},
|
|
{"video", "asspod_center_black", "black", "", 0, {0.004f, 0.004f, 0.0045f}},
|
|
{"video", "asspod_center_white", "white", "", 0, {0.86f, 0.86f, 0.84f}},
|
|
{"nano", "nano_shell_silver", "", "", 7, W1, true},
|
|
{"nano", "nano_back_silver", "", "nano_back_mask/nano_back_mask.rgba", 8, W1, true},
|
|
{"nano", "nano_center_silver", "", "", 7, W1, true},
|
|
{"nano", "nano_wheel_white", "white", "nano_wheel_white/nano_wheel_white_color.rgba", 3, W1},
|
|
{"nano", "nano_wheel_black", "black", "nano_wheel_black/nano_wheel_black_color.rgba", 3, W1},
|
|
{"nano", "nano_cap_top", "", "nano_cap_top/nano_cap_top_color.rgba", 9, W1},
|
|
{"nano", "nano_cap_bottom", "", "nano_cap_bottom/nano_cap_bottom_color.rgba", 9, W1},
|
|
{"nano", "nano_ports", "", "nano_ports/nano_ports_color.rgba", 10, W1},
|
|
{"nano", "nano_well", "", "", 4, {0.002f, 0.002f, 0.002f}},
|
|
};
|
|
for (auto &p : parts) {
|
|
std::string dir = modelDir + "/" + p.name + "/";
|
|
if (!loadObj(dir + p.name + ".obj", p.mesh)) { std::printf("cannot load %s\n", p.name.c_str()); return 1; }
|
|
if (!p.tex.empty()) p.texId = loadRgba(modelDir + "/" + p.tex);
|
|
p.wheel = p.name.find("wheel") != std::string::npos;
|
|
p.center = p.name.find("center") != std::string::npos;
|
|
}
|
|
// the windows: a quad over each model's screen (border + transparent LCD), in model metres
|
|
auto windowQuad = [](const Geo &G) {
|
|
Mesh m;
|
|
const float z = G.d / 2 + 0.00008f, hw = G.winW / 2, hh = G.winH / 2, cy = G.lcdCy;
|
|
const float q[] = {-hw, cy - hh, z, 0, 0, 1, 0, 0, hw, cy - hh, z, 0, 0, 1, 1, 0, hw, cy + hh, z, 0, 0, 1, 1, 1,
|
|
-hw, cy - hh, z, 0, 0, 1, 0, 0, hw, cy + hh, z, 0, 0, 1, 1, 1, -hw, cy + hh, z, 0, 0, 1, 0, 1};
|
|
glGenVertexArrays(1, &m.vao);
|
|
glGenBuffers(1, &m.vbo);
|
|
glBindVertexArray(m.vao);
|
|
glBindBuffer(GL_ARRAY_BUFFER, m.vbo);
|
|
glBufferData(GL_ARRAY_BUFFER, sizeof q, q, GL_STATIC_DRAW);
|
|
for (int i = 0; i < 3; i++) glEnableVertexAttribArray(GLuint(i));
|
|
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 32, reinterpret_cast<void *>(0));
|
|
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 32, reinterpret_cast<void *>(12));
|
|
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 32, reinterpret_cast<void *>(24));
|
|
glBindVertexArray(0);
|
|
m.count = 6;
|
|
return m;
|
|
};
|
|
const Mesh windowVideo = windowQuad(VIDEO), windowNano = windowQuad(NANO);
|
|
|
|
Spec spec;
|
|
struct stat lastSt{};
|
|
Layout L = makeLayout(spec.scale, spec.nanoScale);
|
|
float layoutScale = -1, layoutNanoScale = -1;
|
|
|
|
vr::VROverlayHandle_t ov = vr::k_ulOverlayHandleInvalid;
|
|
if (vr::VROverlay()->CreateOverlay(key.c_str(), "assPod", &ov) != vr::VROverlayError_None) {
|
|
std::printf("CreateOverlay(%s) failed\n", key.c_str());
|
|
return 75;
|
|
}
|
|
vr::VROverlay()->SetOverlayFlag(ov, vr::VROverlayFlags_IsPremultiplied, true);
|
|
vr::VROverlay()->SetOverlayWidthInMeters(ov, 1.0f);
|
|
|
|
std::array<DmaTarget, 3> bufs;
|
|
GLuint depthRb = 0, msColor = 0, msFbo = 0; // 4x MSAA, resolved into the dmabuf SteamVR reads
|
|
GLint samples = 4;
|
|
int cur = 0;
|
|
bool mouseSet = false;
|
|
auto release = [&]() {
|
|
for (auto &b : bufs) {
|
|
if (b.vrHandle) vr::VRIPCResourceManager()->UnrefResource(vr::SharedTextureHandle_t(b.vrHandle));
|
|
if (b.bo) b.destroy(gfx);
|
|
}
|
|
if (depthRb) { glDeleteRenderbuffers(1, &depthRb); depthRb = 0; }
|
|
if (msColor) { glDeleteRenderbuffers(1, &msColor); msColor = 0; }
|
|
if (msFbo) { glDeleteFramebuffers(1, &msFbo); msFbo = 0; }
|
|
};
|
|
auto allocate = [&]() -> bool {
|
|
release();
|
|
for (auto &b : bufs) {
|
|
if (!b.create(gfx, L.texW, L.texH)) return false;
|
|
vr::DmabufAttributes_t a{};
|
|
a.unWidth = uint32_t(L.texW);
|
|
a.unHeight = uint32_t(L.texH);
|
|
a.unDepth = a.unMipLevels = a.unArrayLayers = a.unSampleCount = 1;
|
|
a.unFormat = GBM_FORMAT_ABGR8888;
|
|
a.ulModifier = DRM_FORMAT_MOD_LINEAR;
|
|
a.unPlaneCount = 1;
|
|
a.plane[0].unOffset = b.offset;
|
|
a.plane[0].unStride = b.stride;
|
|
a.plane[0].nFd = b.fd;
|
|
vr::SharedTextureHandle_t h = 0;
|
|
if (!vr::VRIPCResourceManager()->ImportDmabuf(vr::VRApplication_Overlay, &a, &h) || !h) {
|
|
std::printf("ImportDmabuf failed\n");
|
|
return false;
|
|
}
|
|
b.vrHandle = h;
|
|
}
|
|
GLint maxS = 0;
|
|
glGetIntegerv(GL_MAX_SAMPLES, &maxS);
|
|
samples = std::min<GLint>(4, std::max<GLint>(1, maxS));
|
|
glGenRenderbuffers(1, &msColor);
|
|
glBindRenderbuffer(GL_RENDERBUFFER, msColor);
|
|
glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples, GL_RGBA8, L.texW, L.texH);
|
|
glGenRenderbuffers(1, &depthRb);
|
|
glBindRenderbuffer(GL_RENDERBUFFER, depthRb);
|
|
glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples, GL_DEPTH_COMPONENT24, L.texW, L.texH);
|
|
glGenFramebuffers(1, &msFbo);
|
|
glBindFramebuffer(GL_FRAMEBUFFER, msFbo);
|
|
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, msColor);
|
|
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthRb);
|
|
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { std::printf("MSAA framebuffer incomplete\n"); return false; }
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
std::printf("MSAA x%d\n", samples);
|
|
mouseSet = false;
|
|
return true;
|
|
};
|
|
|
|
auto writeOut = [&](bool ok, long frames, float fpsNow, float ms) {
|
|
std::string o = "{\"pid\": " + std::to_string(getpid()) + ", \"ok\": " + (ok ? "true" : "false") + ", \"key\": \"" + key +
|
|
"\", \"texW\": " + std::to_string(L.texW) + ", \"texH\": " + std::to_string(L.texH) + ", \"pad\": " + std::to_string(SPAN) + ", \"box\": [" +
|
|
std::to_string(L.hx) + ", " + std::to_string(L.hy) + ", " + std::to_string(L.hz) + "], \"tiles\": {";
|
|
for (size_t i = 0; i < L.f.size(); i++) {
|
|
const Face &F = L.f[i];
|
|
o += std::string(i ? ", " : "") + "\"" + F.name + "\": [" + std::to_string(F.tx) + ", " + std::to_string(F.ty) + ", " +
|
|
std::to_string(F.tw) + ", " + std::to_string(F.th) + "]";
|
|
}
|
|
o += "}, \"video\": [" + std::to_string(L.vx) + ", " + std::to_string(L.vy) + ", " + std::to_string(VIDEO_W) + ", " +
|
|
std::to_string(VIDEO_H) + "]";
|
|
o += ", \"frames\": " + std::to_string(frames) + ", \"fps\": " + std::to_string(fpsNow) + ", \"cpu_ms\": " + std::to_string(ms) +
|
|
", \"updated\": " + std::to_string(double(time(nullptr))) + "}\n";
|
|
writeAtomic(outPath, o);
|
|
};
|
|
|
|
// the video's picture: a texture the frames go into, and a framebuffer to copy it into its tile
|
|
GLuint videoTex = 0, videoFbo = 0;
|
|
long videoFrames = 0;
|
|
struct stat videoSt{};
|
|
{
|
|
glGenTextures(1, &videoTex);
|
|
glBindTexture(GL_TEXTURE_2D, videoTex);
|
|
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, VIDEO_W, VIDEO_H);
|
|
std::vector<uint8_t> black(size_t(VIDEO_W) * VIDEO_H * 4, 0);
|
|
for (size_t i = 3; i < black.size(); i += 4) black[i] = 255;
|
|
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, VIDEO_W, VIDEO_H, GL_RGBA, GL_UNSIGNED_BYTE, black.data());
|
|
glGenFramebuffers(1, &videoFbo);
|
|
glBindFramebuffer(GL_FRAMEBUFFER, videoFbo);
|
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, videoTex, 0);
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
}
|
|
(void)videoFrames;
|
|
|
|
HandInput handIn[2];
|
|
vr::VRInputValueHandle_t pathR = 0, pathL = 0;
|
|
if (vr::VRInput()) {
|
|
vr::VRInput()->GetInputSourceHandle("/user/hand/right", &pathR);
|
|
vr::VRInput()->GetInputSourceHandle("/user/hand/left", &pathL);
|
|
}
|
|
handIn[0].path = pathL;
|
|
handIn[1].path = pathR;
|
|
float marker = 0, markerAngle = 0, sink = 0, rock = 0, rockAngle = 0;
|
|
std::string debugMode;
|
|
struct stat debugSt{};
|
|
|
|
long frames = 0;
|
|
uint64_t lastOut = 0, fpsT0 = nowNs();
|
|
long fpsFrames = 0;
|
|
float fpsNow = 0, cpuMs = 0;
|
|
const uint64_t frameNs = uint64_t(1e9 / std::max(10.f, fps));
|
|
uint64_t lastFrameNs = nowNs();
|
|
std::printf("podd: ready (%s)\n", key.c_str());
|
|
|
|
while (!g_stop) {
|
|
vr::VREvent_t ev;
|
|
while (vr::VRSystem()->PollNextEvent(&ev, sizeof ev)) {
|
|
if (ev.eventType == vr::VREvent_Quit) { g_stop = 1; break; }
|
|
}
|
|
if (!orphanOk && getppid() == 1) break;
|
|
// the spec, when it changed
|
|
struct stat st{};
|
|
if (stat(specPath.c_str(), &st) == 0 && (st.st_mtim.tv_sec != lastSt.st_mtim.tv_sec || st.st_mtim.tv_nsec != lastSt.st_mtim.tv_nsec)) {
|
|
lastSt = st;
|
|
readSpec(specPath, spec);
|
|
}
|
|
if (std::fabs(spec.scale - layoutScale) > 1e-5f || std::fabs(spec.nanoScale - layoutNanoScale) > 1e-5f) {
|
|
L = makeLayout(spec.scale, spec.nanoScale);
|
|
layoutScale = spec.scale;
|
|
layoutNanoScale = spec.nanoScale;
|
|
if (!allocate()) { std::printf("cannot allocate %dx%d\n", L.texW, L.texH); return 1; }
|
|
writeOut(true, frames, 0, 0);
|
|
std::printf("layout %dx%d (scale %.3f)\n", L.texW, L.texH, double(spec.scale));
|
|
}
|
|
const bool visible = spec.open && vr::VROverlay()->IsDashboardVisible();
|
|
const uint64_t t0 = nowNs();
|
|
if (!visible) {
|
|
if (t0 - lastOut > 2000000000ull) { writeOut(true, frames, 0, cpuMs); lastOut = t0; }
|
|
struct timespec ts = {0, 50000000};
|
|
nanosleep(&ts, nullptr);
|
|
continue;
|
|
}
|
|
// poses at the time these pixels reach the eyes
|
|
float sinceVsync = 0;
|
|
uint64_t frameCounter = 0;
|
|
vr::VRSystem()->GetTimeSinceLastVsync(&sinceVsync, &frameCounter);
|
|
float hz = vr::VRSystem()->GetFloatTrackedDeviceProperty(vr::k_unTrackedDeviceIndex_Hmd, vr::Prop_DisplayFrequency_Float);
|
|
float toPhotons = vr::VRSystem()->GetFloatTrackedDeviceProperty(vr::k_unTrackedDeviceIndex_Hmd, vr::Prop_SecondsFromVsyncToPhotons_Float);
|
|
float frameS = hz > 1 ? 1.f / hz : 1.f / 90.f;
|
|
float predict = frameS * 2 - sinceVsync + toPhotons;
|
|
vr::TrackedDevicePose_t poses[vr::k_unMaxTrackedDeviceCount];
|
|
vr::VRSystem()->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding, predict, poses, vr::k_unMaxTrackedDeviceCount);
|
|
const vr::ETrackedControllerRole role = spec.hand == "left" ? vr::TrackedControllerRole_LeftHand : vr::TrackedControllerRole_RightHand;
|
|
const vr::TrackedDeviceIndex_t hi = vr::VRSystem()->GetTrackedDeviceIndexForControllerRole(role);
|
|
if (hi == vr::k_unTrackedDeviceIndexInvalid || !poses[hi].bPoseIsValid || !poses[0].bPoseIsValid) {
|
|
struct timespec ts = {0, 10000000};
|
|
nanosleep(&ts, nullptr);
|
|
continue;
|
|
}
|
|
const Pose hmd = fromVR(poses[0].mDeviceToAbsoluteTracking);
|
|
const Pose hand = fromVR(poses[hi].mDeviceToAbsoluteTracking);
|
|
const Pose body = spec.world ? poseFrom(spec.wrot, spec.wcentre) : hand * poseFrom(spec.rot, spec.centre);
|
|
const Pose toBody = body.inverse();
|
|
|
|
// the wheel: marker and press motion from both sticks (the one tilted further)
|
|
for (int k = 0; k < 2; k++) {
|
|
HandInput &h = handIn[k];
|
|
if (h.rm.empty()) {
|
|
vr::TrackedDeviceIndex_t di = vr::VRSystem()->GetTrackedDeviceIndexForControllerRole(
|
|
k ? vr::TrackedControllerRole_RightHand : vr::TrackedControllerRole_LeftHand);
|
|
if (di != vr::k_unTrackedDeviceIndexInvalid) {
|
|
char buf[256] = {};
|
|
vr::VRSystem()->GetStringTrackedDeviceProperty(di, vr::Prop_RenderModelName_String, buf, sizeof buf);
|
|
h.rm = buf;
|
|
}
|
|
}
|
|
readStick(h, k ? STICK_R : STICK_L);
|
|
}
|
|
const HandInput &src = std::hypot(handIn[1].x, handIn[1].y) >= std::hypot(handIn[0].x, handIn[0].y) ? handIn[1] : handIn[0];
|
|
const float rad = std::hypot(src.x, src.y);
|
|
const bool touching = rad >= 0.45f;
|
|
if (rad >= 0.3f) markerAngle = std::atan2(src.x, src.y);
|
|
const float dt = float(double(t0 - lastFrameNs) / 1e9);
|
|
lastFrameNs = t0;
|
|
marker += ((touching ? 1.f : 0.f) - marker) * std::min(1.f, dt / (touching ? 0.06f : 0.15f));
|
|
const bool clicked = handIn[0].clicked || handIn[1].clicked;
|
|
const bool centred = rad < 0.45f;
|
|
sink += (((clicked && centred) ? 1.f : 0.f) - sink) * std::min(1.f, dt / 0.05f);
|
|
rock += (((clicked && !centred) ? 1.f : 0.f) - rock) * std::min(1.f, dt / 0.05f);
|
|
if (clicked && !centred) rockAngle = std::round(markerAngle / (kPi / 2)) * (kPi / 2);
|
|
|
|
// debugging: /tmp/lgs/podd-debug "blank" draws nothing; "dot" only a white dot at the front face's
|
|
// centre (each eye); "faces:front,left" only those faces
|
|
{
|
|
struct stat ds{};
|
|
if (stat("/tmp/lgs/podd-debug", &ds) == 0) {
|
|
if (ds.st_mtim.tv_sec != debugSt.st_mtim.tv_sec || ds.st_mtim.tv_nsec != debugSt.st_mtim.tv_nsec) {
|
|
debugSt = ds;
|
|
readFile("/tmp/lgs/podd-debug", debugMode);
|
|
while (!debugMode.empty() && std::isspace(static_cast<unsigned char>(debugMode.back()))) debugMode.pop_back();
|
|
}
|
|
} else {
|
|
debugMode.clear();
|
|
debugSt = {};
|
|
}
|
|
}
|
|
// render: every face, both eyes
|
|
DmaTarget &B = bufs[size_t(cur)];
|
|
glBindFramebuffer(GL_FRAMEBUFFER, msFbo);
|
|
glViewport(0, 0, L.texW, L.texH);
|
|
glDisable(GL_SCISSOR_TEST);
|
|
glClearColor(0, 0, 0, 0);
|
|
glClearDepthf(1);
|
|
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
|
glEnable(GL_DEPTH_TEST);
|
|
glDisable(GL_CULL_FACE);
|
|
glDisable(GL_BLEND);
|
|
const bool nano = spec.model == "nano";
|
|
const Geo &G = nano ? NANO : VIDEO;
|
|
Finish fin = NANO_FINISHES[0];
|
|
for (const Finish &f : NANO_FINISHES)
|
|
if (spec.finish == f.name) fin = f;
|
|
auto lin = [](float c) { return std::pow(c, 2.2f); };
|
|
prog.use();
|
|
prog.set("uScale", nano ? spec.nanoScale : spec.scale);
|
|
prog.set("uRough", fin.rough);
|
|
prog.set("uWheel", G.wheelCy, G.wheelRm, G.markerSigma);
|
|
prog.set("uLcd", G.lcdCy, G.lcdW / 2, G.lcdH / 2);
|
|
prog.set("uWin", G.winW / 2, G.winH / 2, G.winR);
|
|
prog.set("uTex", 0);
|
|
float toWorld[9];
|
|
for (int i = 0; i < 3; i++)
|
|
for (int j = 0; j < 3; j++) toWorld[j * 3 + i] = body.r[i][j]; // column-major
|
|
glUniformMatrix3fv(prog.loc("uToWorld"), 1, GL_FALSE, toWorld);
|
|
prog.set("uMarker", markerAngle, marker, 0.f, 0.f);
|
|
// the variant drawn: the Video's finish; the nano's wheel is black only on the black one
|
|
const std::string variant = nano ? (spec.finish == "black" ? "black" : "white") : (spec.finish == "white" ? "white" : "black");
|
|
for (int eye = 0; eye < 2; eye++) {
|
|
const Pose e2h = fromVR(vr::VRSystem()->GetEyeToHeadTransform(eye ? vr::Eye_Right : vr::Eye_Left));
|
|
const v3 eyeW = (hmd * e2h).t;
|
|
const v3 eyeB = toBody.apply(eyeW);
|
|
prog.set("uEye", eyeB.x, eyeB.y, eyeB.z);
|
|
for (const Face &F : L.f) {
|
|
if (debugMode == "blank" || debugMode == "dot") continue;
|
|
if (debugMode.rfind("faces:", 0) == 0 && debugMode.find(F.name) == std::string::npos) continue;
|
|
float P[16], V[16];
|
|
// the face grown by PAD px on every side (the panel shows it all)
|
|
Face grown = F;
|
|
grown.w = F.w * float(F.tw + 2 * PAD) / float(F.tw);
|
|
grown.h = F.h * float(F.th + 2 * PAD) / float(F.th);
|
|
if (!faceMatrices(grown, eyeB, P, V)) continue;
|
|
const int x = F.tx - PAD + eye * (F.tw + 2 * SPAN);
|
|
const int y = F.ty - PAD, vw = F.tw + 2 * PAD, vh = F.th + 2 * PAD;
|
|
// GL rows count from the buffer's first row (the texture's top)
|
|
glViewport(x, y, vw, vh);
|
|
glEnable(GL_SCISSOR_TEST);
|
|
glScissor(x, y, vw, vh);
|
|
prog.mat4("uProj", P);
|
|
prog.mat4("uView", V);
|
|
for (auto &p : parts) {
|
|
if (p.model != (nano ? "nano" : "video")) continue;
|
|
if (!p.variant.empty() && p.variant != variant) continue;
|
|
Pose m; // the part's own motion, about the wheel's centre (model metres)
|
|
if (p.center) m.t = {0, 0, -G.sink * sink};
|
|
if (p.wheel && rock > 0.001f) {
|
|
const float ang = -G.rockDeg * kPi / 180 * rock;
|
|
const v3 axis = normalize(v3{std::cos(rockAngle), -std::sin(rockAngle), 0});
|
|
Pose R = poseFrom(qnorm({std::cos(ang / 2), axis.x * std::sin(ang / 2), axis.y * std::sin(ang / 2), 0}), {});
|
|
const v3 pivot = {0, G.wheelCy, G.d / 2};
|
|
R.t = pivot - R.rotate(pivot);
|
|
m = R;
|
|
}
|
|
float M[16];
|
|
m.toGL(M);
|
|
prog.mat4("uModel", M);
|
|
prog.set("uMat", p.mat);
|
|
if (p.tint) prog.set("uBase", lin(fin.r), lin(fin.g), lin(fin.b));
|
|
else prog.set("uBase", p.base.x, p.base.y, p.base.z);
|
|
glActiveTexture(GL_TEXTURE0);
|
|
glBindTexture(GL_TEXTURE_2D, p.texId);
|
|
glBindVertexArray(p.mesh.vao);
|
|
glDrawArrays(GL_TRIANGLES, 0, p.mesh.count);
|
|
}
|
|
// the window last: it replaces what lies behind it (the screen panel shows through the LCD)
|
|
Pose id;
|
|
float M[16];
|
|
id.toGL(M);
|
|
prog.mat4("uModel", M);
|
|
prog.set("uMat", 6);
|
|
const Mesh &win = nano ? windowNano : windowVideo;
|
|
glBindVertexArray(win.vao);
|
|
glDrawArrays(GL_TRIANGLES, 0, win.count);
|
|
}
|
|
}
|
|
if (debugMode == "dot") {
|
|
const Face &F = L.f[0];
|
|
glEnable(GL_SCISSOR_TEST);
|
|
glClearColor(1, 1, 1, 1);
|
|
for (int eye = 0; eye < 2; eye++) {
|
|
glScissor(F.tx + eye * (F.tw + 2 * SPAN) + F.tw / 2 - 4, F.ty + F.th / 2 - 4, 8, 8);
|
|
glClear(GL_COLOR_BUFFER_BIT);
|
|
}
|
|
glClearColor(0, 0, 0, 0);
|
|
}
|
|
glDisable(GL_SCISSOR_TEST);
|
|
glBindVertexArray(0);
|
|
// resolve the samples into the buffer SteamVR reads
|
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, msFbo);
|
|
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, B.fbo);
|
|
glBlitFramebuffer(0, 0, L.texW, L.texH, 0, 0, L.texW, L.texH, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
|
// the video's latest frame, when there is a new one, into its tile (rows top first both ways)
|
|
{
|
|
struct stat vs{};
|
|
if (stat(VIDEO_FILE, &vs) == 0 && (vs.st_mtim.tv_sec != videoSt.st_mtim.tv_sec || vs.st_mtim.tv_nsec != videoSt.st_mtim.tv_nsec)) {
|
|
videoSt = vs;
|
|
std::string d;
|
|
uint32_t vw = 0, vh = 0;
|
|
if (readFile(VIDEO_FILE, d) && d.size() >= 16 && d.compare(0, 4, "PVID") == 0) {
|
|
std::memcpy(&vw, d.data() + 8, 4);
|
|
std::memcpy(&vh, d.data() + 12, 4);
|
|
if (int(vw) == VIDEO_W && int(vh) == VIDEO_H && d.size() >= 16 + size_t(vw) * vh * 4) {
|
|
glBindTexture(GL_TEXTURE_2D, videoTex);
|
|
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
|
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, VIDEO_W, VIDEO_H, GL_RGBA, GL_UNSIGNED_BYTE, d.data() + 16);
|
|
videoFrames++;
|
|
}
|
|
}
|
|
}
|
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, videoFbo);
|
|
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, B.fbo);
|
|
for (int eye = 0; eye < 2; eye++) {
|
|
const int x0 = L.vx + eye * VIDEO_W;
|
|
glBlitFramebuffer(0, 0, VIDEO_W, VIDEO_H, x0, L.vy, x0 + VIDEO_W, L.vy + VIDEO_H, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
|
}
|
|
}
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
glFinish(); // SteamVR samples the dmabuf from another process
|
|
// debugging: the texture as SteamVR gets it, once, when /tmp/lgs/podd-dump.req appears
|
|
// ("RGBA", u32 width, u32 height, rows as stored: row 0 is the texture's top), with the spec
|
|
// and the eyes in body space beside it
|
|
if (access("/tmp/lgs/podd-dump.req", F_OK) == 0) {
|
|
std::remove("/tmp/lgs/podd-dump.req");
|
|
std::vector<uint8_t> px(size_t(L.texW) * L.texH * 4);
|
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, B.fbo);
|
|
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
|
glReadPixels(0, 0, L.texW, L.texH, GL_RGBA, GL_UNSIGNED_BYTE, px.data());
|
|
std::string d = "RGBA";
|
|
uint32_t wh[2] = {uint32_t(L.texW), uint32_t(L.texH)};
|
|
d.append(reinterpret_cast<const char *>(wh), 8);
|
|
d.append(reinterpret_cast<const char *>(px.data()), px.size());
|
|
writeAtomic("/tmp/lgs/podd-dump.rgba", d);
|
|
std::string eyes;
|
|
for (int eye = 0; eye < 2; eye++) {
|
|
const Pose e2h = fromVR(vr::VRSystem()->GetEyeToHeadTransform(eye ? vr::Eye_Right : vr::Eye_Left));
|
|
const v3 eb = toBody.apply((hmd * e2h).t);
|
|
eyes += std::string(eye ? ", " : "") + "[" + std::to_string(eb.x) + ", " + std::to_string(eb.y) + ", " + std::to_string(eb.z) + "]";
|
|
}
|
|
writeAtomic("/tmp/lgs/podd-dump.json", "{\"eyes\": [" + eyes + "], \"model\": \"" + spec.model + "\"}\n");
|
|
}
|
|
vr::SharedTextureHandle_t h = vr::SharedTextureHandle_t(B.vrHandle);
|
|
vr::Texture_t tex = {&h, vr::TextureType_SharedTextureHandle, vr::ColorSpace_Gamma};
|
|
auto e = vr::VROverlay()->SetOverlayTexture(ov, &tex);
|
|
if (e != vr::VROverlayError_None) std::printf("SetOverlayTexture: %s\n", vr::VROverlay()->GetOverlayErrorNameFromEnum(e));
|
|
if (!mouseSet) {
|
|
vr::HmdVector2_t mouse = {float(L.texW / 2), float(L.texH)}; // one eye's width: Parallel stereo
|
|
vr::VROverlay()->SetOverlayMouseScale(ov, &mouse);
|
|
mouseSet = true;
|
|
}
|
|
cur = (cur + 1) % int(bufs.size());
|
|
frames++;
|
|
fpsFrames++;
|
|
const uint64_t t1 = nowNs();
|
|
cpuMs = cpuMs * 0.9f + float(double(t1 - t0) / 1e6) * 0.1f;
|
|
if (t1 - fpsT0 > 1000000000ull) {
|
|
fpsNow = float(fpsFrames * 1e9 / double(t1 - fpsT0));
|
|
fpsFrames = 0;
|
|
fpsT0 = t1;
|
|
}
|
|
if (t1 - lastOut > 1000000000ull) { writeOut(true, frames, fpsNow, cpuMs); lastOut = t1; }
|
|
if (t1 - t0 < frameNs) {
|
|
uint64_t left = frameNs - (t1 - t0);
|
|
struct timespec ts = {time_t(left / 1000000000ull), long(left % 1000000000ull)};
|
|
nanosleep(&ts, nullptr);
|
|
}
|
|
}
|
|
std::printf("podd: stopping\n");
|
|
if (ov != vr::k_ulOverlayHandleInvalid) vr::VROverlay()->DestroyOverlay(ov);
|
|
release();
|
|
writeOut(false, frames, 0, 0);
|
|
vr::VR_Shutdown();
|
|
gfx.shutdown();
|
|
return 0;
|
|
}
|