// SPDX-License-Identifier: GPL-3.0-or-later // Ported from heurazy's mario-kart-wii-VR-port (GPL-3.0-or-later). // // VR cockpit overlay: the synthetic steering wheel or handlebar (used when the // vehicle's own wheel cannot be animated) and the tracked hands, drawn per eye // in metres against the replayed scene's depth. See OPENXR.md, "Steering wheel // and hand steering". #pragma once #include "common.hpp" #include "../webgpu/gpu.hpp" #include #include #include #include #include #include #include namespace aurora::gfx::cockpit { using V = std::array; using M = std::array; inline V add(V a, V b) { return {a[0]+b[0], a[1]+b[1], a[2]+b[2]}; } inline V sub(V a, V b) { return {a[0]-b[0], a[1]-b[1], a[2]-b[2]}; } inline V mul(V a, float b) { return {a[0]*b, a[1]*b, a[2]*b}; } inline float dot(V a, V b) { return a[0]*b[0]+a[1]*b[1]+a[2]*b[2]; } inline V cross(V a, V b) { return {a[1]*b[2]-a[2]*b[1],a[2]*b[0]-a[0]*b[2],a[0]*b[1]-a[1]*b[0]}; } inline V norm(V a) { return mul(a, 1/std::sqrt(std::max(dot(a,a), 1e-10f))); } inline V point(const float* m, V p) { return {m[0]*p[0]+m[1]*p[1]+m[2]*p[2]+m[3], m[4]*p[0]+m[5]*p[1]+m[6]*p[2]+m[7], m[8]*p[0]+m[9]*p[1]+m[10]*p[2]+m[11]}; } inline M identity() { return {1,0,0,0,0,1,0,0,0,0,1,0}; } inline M compose(const M& a, const M& b) { M result{}; for(int r=0;r<3;++r) { for(int c=0;c<3;++c) for(int k=0;k<3;++k) result[r*4+c]+=a[r*4+k]*b[k*4+c]; result[r*4+3]=a[r*4+3]; for(int k=0;k<3;++k) result[r*4+3]+=a[r*4+k]*b[k*4+3]; } return result; } inline M inverse(const M& m) { M out=identity(); for(int r=0;r<3;++r) for(int c=0;c<3;++c) out[r*4+c]=m[c*4+r]; const auto p=point(out.data(), {-m[3],-m[7],-m[11]}); out[3]=p[0];out[7]=p[1];out[11]=p[2];return out; } inline M from_pose(const float* p) { const float x=p[0],y=p[1],z=p[2],w=p[3]; return {1-2*(y*y+z*z),2*(x*y-z*w),2*(x*z+y*w),p[4], 2*(x*y+z*w),1-2*(x*x+z*z),2*(y*z-x*w),p[5], 2*(x*z-y*w),2*(y*z+x*w),1-2*(x*x+y*y),p[6]}; } struct HandMesh { std::vector vertices; std::vector indices; std::array bind{}, inverseBind{}; std::array parents{}; }; inline std::mutex meshMutex; inline std::array,2> meshes; struct Vertex { V position, color; }; inline void triangle(std::vector& vertices, V a, V b, V c, V color) { const V normal=norm(cross(sub(b,a),sub(c,a))); const float light=0.55f+0.45f*std::abs(dot(normal,norm({0.3f,0.8f,0.5f}))); color=mul(color,light); vertices.insert(vertices.end(),{{a,color},{b,color},{c,color}}); } inline void tube(std::vector& v, V a, V b, float radius, V color, int sides=8) { const auto direction=norm(sub(b,a)); const auto u=norm(cross(direction,std::abs(direction[1])<0.9f?V{0,1,0}:V{1,0,0})); const auto w=cross(direction,u); for(int i=0;i& vertices,V center,V radii,V color) { const auto surface=[&](int ring,int segment) { const float latitude=float(ring)*3.14159265f/6,longitude=float(segment)*6.2831853f/12; return add(center,{radii[0]*std::sin(latitude)*std::cos(longitude),radii[1]*std::cos(latitude), radii[2]*std::sin(latitude)*std::sin(longitude)}); }; for(int ring=0;ring<6;++ring) for(int segment=0;segment<12;++segment) { const auto a=surface(ring,segment),b=surface(ring+1,segment),c=surface(ring+1,segment+1),d=surface(ring,segment+1); if(ring>0) triangle(vertices,a,b,d,color); if(ring<5) triangle(vertices,b,c,d,color); } } // Rounded palm and individually articulated fingers, in the controller's grip // space as OpenXR defines it: the origin is the palm centroid, -Z runs up the // tube the curled fingers form (little finger towards thumb), and +X is the // palm's outward normal, so the fingers close towards +X. +Y completes the // right-handed frame, which leaves it along the fingers on the right hand and // against them on the left. Building the fingers on any other axis bends them // out of the back of the hand (seen on a Quest 3 on 2026-09-22). inline void glove(std::vector& v, const AuroraCockpitHand& hand, int side) { const size_t start=v.size(); const V white{0.91f,0.95f,1.0f}; const float forward=side==0?-1.0f:1.0f; // hand 0 is the left one const float curl=std::clamp(hand.held?0.85f:hand.squeeze,0.0f,1.0f); // Thin through the palm's normal, a little wider across the knuckles than // the palm is long. ellipsoid(v,{0,0,0},{0.018f,0.043f,0.041f},white); for(int finger=0;finger<4;++finger) { // Index finger nearest the thumb (-Z), little finger last. V a{0.0f,forward*0.030f,-0.025f+finger*0.017f}; const float length=finger==0||finger==3?0.021f:0.026f; for(int joint=0;joint<3;++joint) { const float angle=curl*(0.55f+joint*0.8f); V b=add(a,{std::sin(angle)*length,forward*std::cos(angle)*length,0.0f}); tube(v,a,b,0.008f,white); ellipsoid(v,b,{0.008f,0.008f,0.008f},white);a=b; } } // Thumb: out of the palm's thumb side, closing across the fingers. const V thumbKnuckle{0.026f,forward*0.034f,-0.030f}; tube(v,{0.010f,forward*0.012f,-0.034f},thumbKnuckle,0.010f,white); tube(v,thumbKnuckle,{0.030f+0.014f*curl,forward*(0.052f-0.016f*curl),-0.020f},0.009f,white); for(size_t i=start;i& out, const AuroraCockpitHand& hand, const HandMesh& mesh) { std::array posed{}, skin{}; std::array done{}; const float curl=std::clamp(hand.held?0.85f:hand.squeeze,0.0f,1.0f); // Bind hierarchy is supplied by the runtime. Root and wrist stay rigid; // finger joints curl locally when controllers provide squeeze input. for(int pass=0;pass<26;++pass) for(int j=0;j<26;++j) { if(done[j]) continue; const int parent=mesh.parents[j]; if(parent>=0&&parent<26&&!done[parent]) continue; M local=parent>=0&&parent<26?compose(mesh.inverseBind[parent],mesh.bind[j]):mesh.bind[j]; const bool fingerJoint=j>=2 && j!=6 && j!=11 && j!=16 && j!=21; if(fingerJoint) { const float a=curl*(j<6?0.3f:0.75f),c=std::cos(a),s=std::sin(a); local=compose(local,M{1,0,0,0,0,c,-s,0,0,s,c,0}); } posed[j]=parent>=0&&parent<26?compose(posed[parent],local):local; skin[j]=compose(mesh.inverseBind[1],compose(posed[j],mesh.inverseBind[j])); done[j]=true; } std::vector points(mesh.vertices.size()); for(size_t i=0;i=0&&v.joints[w]<26&&done[v.joints[w]]&&v.weights[w]>0) { p=add(p,mul(point(skin[v.joints[w]].data(),{v.position[0],v.position[1],v.position[2]}),v.weights[w])); total+=v.weights[w]; } if(total>0) p=mul(p,1/total); p=add(p,{0,0,0.04f}); // wrist behind the controller grip/palm origin. points[i]=point(hand.seatFromGrip,p); } for(size_t i=0;i+2& vertices) { vertices.clear();vertices.reserve(12000); // The visible radius and position must match runtime/vr/steering_wheel.h. if (!cockpit.nativeWheel && cockpit.bike) { const float c=std::cos(cockpit.wheelAngle),s=std::sin(cockpit.wheelAngle); const auto barPoint=[&](float x,float y,float z) { return point(cockpit.seatFromHandlebar,{c*x+s*y,-s*x+c*y,z}); }; const float radius=cockpit.handlebarRadius; tube(vertices,barPoint(-radius,0,0),barPoint(radius,0,0),0.013f,{0.45f,0.48f,0.52f}); for(float side:{-1.0f,1.0f}) tube(vertices,barPoint(side*std::max(radius-0.10f,0.0f),0,0),barPoint(side*radius,0,0),0.024f,{0.12f,0.18f,0.19f}); tube(vertices,barPoint(0,0,-0.13f),barPoint(0,0,0),0.023f,{0.12f,0.65f,0.61f}); } else if (!cockpit.nativeWheel) { const auto rim=[&](float angle) -> V { return {0.18f*std::cos(angle),-0.30f+0.18f*std::sin(angle),-0.42f}; }; for(int i=0;i<64;++i) { const float angle=float(i)*6.2831853f/64-cockpit.wheelAngle; const V color=i>=15&&i<=17?V{0.2f,0.9f,0.8f}:V{0.14f,0.17f,0.20f}; tube(vertices,rim(angle),rim(angle+6.2831853f/64),0.016f,color,6); } for(float a : {0.0f,3.14159265f,4.71238898f}) tube(vertices,{0,-0.30f,-0.42f},rim(a-cockpit.wheelAngle),0.011f,{0.45f,0.48f,0.52f}); tube(vertices,{0,-0.30f,-0.445f},{0,-0.30f,-0.395f},0.035f,{0.12f,0.65f,0.61f},16); } std::array,2> current; { std::lock_guard lock(meshMutex);current=meshes; } for(int side=0;side<2;++side) if(cockpit.hands[side].tracked) { if(current[side]) runtime_hand(vertices,cockpit.hands[side],*current[side]); else glove(vertices,cockpit.hands[side],side); } } inline std::vector geometry(const AuroraCockpit& cockpit) { std::vector result;build_geometry(cockpit,result);return result; } inline std::atomic meshRevision{1}; inline std::vector frameVertices; inline AuroraCockpit cachedCockpit{}; inline uint64_t cachedMeshRevision=0; inline wgpu::RenderPipeline pipeline; struct SceneDepth { float z=0, constant=0; bool valid=false; }; inline uint32_t pipelineSamples=0; inline bool pipelineReversedDepth=false; inline wgpu::TextureFormat pipelineFormat{}; inline std::array vertexBuffers; inline std::array vertexCapacity{}; inline void shutdown() { pipeline=nullptr;pipelineSamples=0;vertexBuffers={};vertexCapacity={};cachedMeshRevision=0;frameVertices.clear(); } inline void render(wgpu::CommandEncoder& cmd,const StereoReplayFrame& frame,uint32_t eye,SceneDepth sceneDepth={}, const wgpu::RenderPassEncoder* existingPass=nullptr) { if(!frame.cockpit.active || !sceneDepth.valid) return; using namespace webgpu; const auto& target=frame.eyes[eye].target; const auto format=g_graphicsConfig.surfaceConfiguration.format; // The guest can reverse its viewport depth independently of Aurora's // global reversed-Z convention. The final 1/d coefficient is authoritative. const bool reversedDepth=sceneDepth.constant>0; if(!pipeline||pipelineSamples!=target.msaaSamples||pipelineFormat!=format||pipelineReversedDepth!=reversedDepth) { wgpu::ShaderSourceWGSL source{}; source.code=R"( struct Out { @builtin(position) position: vec4f, @location(0) color: vec3f }; @vertex fn vs(@location(0) position: vec4f, @location(1) color: vec3f) -> Out { var o: Out; o.position=position; o.color=color; return o; } @fragment fn fs(i: Out) -> @location(0) vec4f { return vec4f(i.color,1); } )"; wgpu::ShaderModuleDescriptor md{};md.nextInChain=&source;md.label="VR cockpit hands and wheel"; auto shader=g_device.CreateShaderModule(&md); const wgpu::VertexAttribute attrs[]={{.format=wgpu::VertexFormat::Float32x4,.offset=0,.shaderLocation=0}, {.format=wgpu::VertexFormat::Float32x3,.offset=16,.shaderLocation=1}}; const wgpu::VertexBufferLayout layout{.arrayStride=28,.attributeCount=2,.attributes=attrs}; const wgpu::ColorTargetState color{.format=format}; const wgpu::FragmentState fragment{.module=shader,.entryPoint="fs",.targetCount=1,.targets=&color}; const wgpu::DepthStencilState depth{.format=g_graphicsConfig.depthFormat,.depthWriteEnabled=true, .depthCompare=reversedDepth?wgpu::CompareFunction::GreaterEqual:wgpu::CompareFunction::LessEqual}; wgpu::RenderPipelineDescriptor desc{};desc.label="VR cockpit"; desc.vertex={.module=shader,.entryPoint="vs",.bufferCount=1,.buffers=&layout}; desc.fragment=&fragment;desc.depthStencil=&depth;desc.multisample.count=target.msaaSamples; desc.primitive.topology=wgpu::PrimitiveTopology::TriangleList; pipeline=g_device.CreateRenderPipeline(&desc);pipelineSamples=target.msaaSamples;pipelineFormat=format; pipelineReversedDepth=reversedDepth; } const auto revision=meshRevision.load(); if(cachedMeshRevision!=revision || std::memcmp(&cachedCockpit,&frame.cockpit,sizeof(AuroraCockpit))!=0) { build_geometry(frame.cockpit,frameVertices); cachedCockpit=frame.cockpit;cachedMeshRevision=revision; } const auto& vertices=frameVertices; if(vertices.empty()) return; struct ClipVertex { float p[4]; V color; }; static std::vector clip; clip.resize(vertices.size()); const auto& projection=frame.eyes[eye].projection; for(size_t i=0;i