mirror of
https://github.com/simplysanatori/KartFrame.git
synced 2026-10-17 18:03:02 +02:00
290 lines
16 KiB
C++
290 lines
16 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
#pragma once
|
|
#include "vr/body_ik.h"
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <vector>
|
|
#include <stdexcept>
|
|
#include <cstring>
|
|
#include <cctype>
|
|
|
|
namespace mkw::vr::hand_workshop {
|
|
struct Character { const char* name; const char* archive; };
|
|
inline constexpr Character characters[]{
|
|
{"Mario","ma_kart-mr"},{"Luigi","ma_kart-lg"},{"Peach","ma_kart-pc"},{"Daisy","ma_kart-ds"},
|
|
{"Yoshi","ma_kart-ys"},{"Birdo","ma_kart-ca"},{"Diddy Kong","ma_kart-dd"},{"Bowser Jr.","ma_kart-jr"},
|
|
{"Baby Mario","sa_kart-bmr"},{"Baby Luigi","sa_kart-blg"},{"Baby Peach","sa_kart-bpc"},{"Baby Daisy","sa_kart-bds"},
|
|
{"Toad","sa_kart-ko"},{"Toadette","sa_kart-kk"},{"Koopa Troopa","sa_kart-nk"},{"Dry Bones","sa_kart-ka"},
|
|
{"Wario","la_kart-wr"},{"Waluigi","la_kart-wl"},{"Donkey Kong","la_kart-dk"},{"Bowser","la_kart-kp"},
|
|
{"King Boo","la_kart-kt"},{"Rosalina","la_kart-rs"},{"Funky Kong","la_kart-fk"},{"Dry Bowser","la_kart-bk"}
|
|
};
|
|
inline std::string Key(std::string name) {
|
|
const auto delimiter=name.find("__");
|
|
if(name.starts_with("body__")) name=name.substr(6);
|
|
else if(delimiter!=std::string::npos) name.resize(delimiter);
|
|
if(name=="k_hair_model") name="kino_girl_body";
|
|
for(char& c:name) if(!std::isalnum(static_cast<unsigned char>(c)) && c!='_') c='_';
|
|
return name;
|
|
}
|
|
struct Vertex { std::array<float,3> position{}; std::array<float,2> uv{}; };
|
|
// Rigid GX vertices are already local to the selected skinning matrix. They
|
|
// must not be transformed by the model's inverse bind matrix a second time.
|
|
// Native hands extend along +X; their left/right local palm axes are mirrored.
|
|
inline Mtx34 NeutralWrist(unsigned hand) {
|
|
return hand==0?Mtx34{0,1,0,0,0,0,-1,0,-1,0,0,0}:
|
|
Mtx34{0,-1,0,0,0,0,1,0,-1,0,0,0};
|
|
}
|
|
struct PreviewHand {
|
|
Mtx34 wrist=kIdentityMtx34,mesh=kIdentityMtx34;
|
|
};
|
|
inline PreviewHand PlaceHand(const std::vector<Vertex>& vertices,unsigned hand,float scale) {
|
|
if(vertices.empty() || hand>=2 || !detail::IsFiniteFloat(&scale) || scale<=0)
|
|
throw std::runtime_error("Invalid character hand preview");
|
|
PreviewHand result;result.wrist=NeutralWrist(hand);result.mesh=result.wrist;
|
|
for(auto& value:result.mesh)value*=scale;
|
|
std::array<float,3> min{1e6f,1e6f,1e6f},max{-1e6f,-1e6f,-1e6f};
|
|
for(const auto& vertex:vertices) {
|
|
const auto p=detail::TransformPoint(result.mesh,vertex.position[0],vertex.position[1],vertex.position[2]);
|
|
const float coords[]{p.x,p.y,p.z};
|
|
for(int axis=0;axis<3;++axis) {min[axis]=std::min(min[axis],coords[axis]);max[axis]=std::max(max[axis],coords[axis]);}
|
|
}
|
|
const float centre[]{hand==0?-.23f:.23f,-.23f,-.48f};
|
|
for(int axis=0;axis<3;++axis) {
|
|
result.wrist[axis*4+3]=centre[axis]-(min[axis]+max[axis])*.5f;
|
|
result.mesh[axis*4+3]=result.wrist[axis*4+3];
|
|
}
|
|
return result;
|
|
}
|
|
// A near-palm trigger press belongs to calibration, irrespective of where its
|
|
// aim ray intersects the UI. Keep ownership until release so it cannot also
|
|
// click Previous/Next. Distant presses remain available to UI navigation.
|
|
struct CaptureTrigger {
|
|
bool armed=false,consumed=false;
|
|
void Release(float trigger) { if(trigger<.2f) {armed=true;consumed=false;} }
|
|
bool Capture(float trigger,bool tracked,bool alreadyCaptured,const Mtx34& grip,const Mtx34& wrist,Mtx34& offset) {
|
|
Release(trigger);
|
|
if(!armed || alreadyCaptured || !tracked || trigger<.55f)return false;
|
|
armed=false;
|
|
if(!body_ik::CalibrateGripPose(grip,wrist,offset) || body_ik::Length(body_ik::Position(offset))>=.18f)return false;
|
|
consumed=true;return true;
|
|
}
|
|
};
|
|
inline bool ConsumePointer(const std::array<CaptureTrigger,2>& triggers,unsigned hand) {
|
|
return hand<2 && triggers[hand].consumed;
|
|
}
|
|
struct Model {
|
|
std::string key;
|
|
std::array<std::vector<Vertex>,2> hands; // wrist-local coordinates, not model bind space
|
|
std::array<Mtx34,2> wrist{};
|
|
uint32_t width=0,height=0;
|
|
float eye_height=110.f;
|
|
std::vector<uint8_t> rgba;
|
|
};
|
|
// Bounded reader: malformed/mismatched archives never touch guest memory.
|
|
struct Reader {
|
|
const std::vector<uint8_t>& data;
|
|
void Check(size_t at,size_t length) const { if(at>data.size() || length>data.size()-at) throw std::runtime_error("Truncated character archive"); }
|
|
uint8_t U8(size_t at) const { Check(at,1);return data[at]; }
|
|
uint16_t U16(size_t at) const { Check(at,2);return (data[at]<<8)|data[at+1]; }
|
|
uint32_t U32(size_t at) const { Check(at,4);return (uint32_t(data[at])<<24)|(uint32_t(data[at+1])<<16)|(data[at+2]<<8)|data[at+3]; }
|
|
float F32(size_t at) const { auto u=U32(at);float f;std::memcpy(&f,&u,4);if(!detail::IsFiniteFloat(&f)) throw std::runtime_error("Invalid character coordinate");return f; }
|
|
std::string Name(size_t at) const { std::string s;for(int i=0;i<128;++i) { auto c=U8(at+i);if(!c)return s;s+=char(c); }throw std::runtime_error("Invalid character name"); }
|
|
std::vector<size_t> Dictionary(size_t at) const {
|
|
const auto count=U32(at+4);if(count>4096) throw std::runtime_error("Invalid character dictionary");
|
|
Check(at+8,size_t(count+1)*16);std::vector<size_t> result;
|
|
for(uint32_t i=1;i<=count;++i) { const size_t p=at+U32(at+8+i*16+12);Check(p,4);result.push_back(p); }
|
|
return result;
|
|
}
|
|
};
|
|
inline std::vector<uint8_t> Unpack(const std::vector<uint8_t>& input) {
|
|
Reader r{input};if(r.U32(0)!=0x59617a30) throw std::runtime_error("Expected Yaz0 character archive");
|
|
const auto size=r.U32(4);if(!size || size>16*1024*1024) throw std::runtime_error("Character archive exceeds 16 MB limit");
|
|
std::vector<uint8_t> out;out.reserve(size);size_t at=16;
|
|
while(out.size()<size) {
|
|
const auto code=r.U8(at++);
|
|
for(int bit=7;bit>=0 && out.size()<size;--bit) {
|
|
if(code&(1<<bit)) out.push_back(r.U8(at++));
|
|
else {
|
|
const auto a=r.U8(at++),b=r.U8(at++);size_t count=a>>4;
|
|
if(count) count+=2;else count=size_t(r.U8(at++))+18;
|
|
const size_t distance=((a&15)<<8)+b+1;
|
|
if(distance>out.size() || count>size-out.size()) throw std::runtime_error("Invalid Yaz0 back reference");
|
|
while(count--) out.push_back(out[out.size()-distance]);
|
|
}
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
inline std::array<uint8_t,4> RGB565(uint16_t c) {
|
|
return {uint8_t(((c>>11)&31)*255/31),uint8_t(((c>>5)&63)*255/63),uint8_t((c&31)*255/31),255};
|
|
}
|
|
inline void Texture(const Reader& r,size_t tex,Model& model) {
|
|
model.width=r.U16(tex+0x1c);model.height=r.U16(tex+0x1e);
|
|
if(!model.width || !model.height || model.width>1024 || model.height>1024 || r.U32(tex+0x20)!=14)
|
|
throw std::runtime_error("Unsupported character texture (expected CMPR)");
|
|
const size_t start=tex+r.U32(tex+0x10);size_t at=start;
|
|
model.rgba.resize(size_t(model.width)*model.height*4);
|
|
for(unsigned y=0;y<model.height;y+=8) for(unsigned x=0;x<model.width;x+=8)
|
|
for(unsigned sub=0;sub<4;++sub) {
|
|
const auto c0=r.U16(at),c1=r.U16(at+2);std::array<std::array<uint8_t,4>,4> colors{RGB565(c0),RGB565(c1)};
|
|
for(int k=0;k<3;++k) {
|
|
colors[2][k]=(c0>c1?(2*colors[0][k]+colors[1][k])/3:(colors[0][k]+colors[1][k])/2);
|
|
colors[3][k]=(c0>c1?(colors[0][k]+2*colors[1][k])/3:0);
|
|
}
|
|
colors[2][3]=255;colors[3][3]=c0>c1?255:0;
|
|
for(unsigned row=0;row<4;++row) {
|
|
const auto bits=r.U8(at+4+row);
|
|
for(unsigned col=0;col<4;++col) {
|
|
const auto px=x+(sub%2)*4+col,py=y+(sub/2)*4+row;
|
|
if(px<model.width && py<model.height) std::copy(colors[(bits>>(6-2*col))&3].begin(),colors[(bits>>(6-2*col))&3].end(),model.rgba.begin()+(py*model.width+px)*4);
|
|
}
|
|
}
|
|
at+=8;
|
|
}
|
|
}
|
|
inline float Component(const Reader& r,size_t at,uint32_t type,unsigned fraction) {
|
|
const float scale=std::ldexp(1.f,-int(fraction));
|
|
switch(type) {
|
|
case 0:return r.U8(at)*scale;case 1:return int8_t(r.U8(at))*scale;
|
|
case 2:return r.U16(at)*scale;case 3:return int16_t(r.U16(at))*scale;case 4:return r.F32(at);
|
|
default:throw std::runtime_error("Unsupported character vertex format");
|
|
}
|
|
}
|
|
inline std::vector<std::array<float,3>> Array(const Reader& r,size_t header,bool uv) {
|
|
const auto type=r.U32(header+(uv?0x18:0x18));const unsigned stride=r.U8(header+0x1d),fraction=r.U8(header+0x1c);
|
|
const auto count=r.U16(header+0x1e);const size_t data=header+r.U32(header+8);
|
|
const unsigned components=uv?(r.U32(header+0x14)==1?2:1):(r.U32(header+0x14)==1?3:2);
|
|
const unsigned bytes=type==4?4:type>=2?2:1;if(stride<bytes*components) throw std::runtime_error("Invalid character vertex stride");
|
|
r.Check(data,size_t(count)*stride);std::vector<std::array<float,3>> out(count);
|
|
for(unsigned i=0;i<count;++i) for(unsigned j=0;j<components;++j) out[i][j]=Component(r,data+i*stride+j*bytes,type,fraction);
|
|
return out;
|
|
}
|
|
inline Model Parse(const std::vector<uint8_t>& data) {
|
|
Reader r{data};Model out;size_t mdl=0;std::array<uint32_t,2> wristIds{UINT32_MAX,UINT32_MAX};
|
|
Mtx34 face=kIdentityMtx34;bool faceFound=false;
|
|
std::array<Mtx34,2> wristFromSkin{kIdentityMtx34,kIdentityMtx34};
|
|
std::string textureName;
|
|
for(size_t p=0;p+0x90<data.size();p+=4) if(r.U32(p)==0x4d444c30) {
|
|
const auto size=r.U32(p+4),revision=r.U32(p+8);if(revision!=11 || size<0x90)continue;r.Check(p,size);
|
|
const auto bones=r.Dictionary(p+r.U32(p+0x14));
|
|
for(auto bone:bones) {
|
|
const auto name=r.Name(bone+r.U32(bone+8));
|
|
if(name=="face_1" || name=="head" || name=="head1") {
|
|
faceFound=true;for(int i=0;i<12;++i)face[i]=r.F32(bone+0x70+i*4);
|
|
}
|
|
const int hand=name=="wrist_l1"?0:name=="wrist_r1"?1:-1;
|
|
if(hand>=0) {
|
|
wristIds[hand]=r.U32(bone+0x10);
|
|
// King Boo's terminal wrist is not in the skinning palette. Its hand
|
|
// geometry is rigidly owned by the immediately preceding arm segment.
|
|
const auto parent=bone+int32_t(r.U32(bone+0x5c));
|
|
if(r.U32(p+r.U32(p+0x18)+4)>0) {
|
|
const auto arrays=r.Dictionary(p+r.U32(p+0x18));
|
|
if(!arrays.empty() && r.Name(arrays[0]+r.U32(arrays[0]+0xc)).starts_with("king_teresa")) {
|
|
wristIds[hand]=r.U32(parent+0x10);
|
|
Mtx34 wrist{},skin{},inverse{};
|
|
for(int i=0;i<12;++i) {wrist[i]=r.F32(bone+0x70+i*4);skin[i]=r.F32(parent+0x70+i*4);}
|
|
if(!InvertMtx(wrist,inverse))throw std::runtime_error("Invalid character wrist bind matrix");
|
|
wristFromSkin[hand]=ComposeMtx(inverse,skin);
|
|
}
|
|
}
|
|
for(int i=0;i<12;++i) out.wrist[hand][i]=r.F32(bone+0x70+i*4);
|
|
}
|
|
}
|
|
if(wristIds[0]!=UINT32_MAX && wristIds[1]!=UINT32_MAX) { mdl=p;break; }
|
|
}
|
|
if(!mdl) throw std::runtime_error("No character hand skeleton found");
|
|
const auto positions=r.Dictionary(mdl+r.U32(mdl+0x18));
|
|
const auto texcoords=r.Dictionary(mdl+r.U32(mdl+0x24));
|
|
if(positions.empty() || texcoords.empty()) throw std::runtime_error("No character geometry found");
|
|
if(faceFound)for(auto array:positions) {
|
|
if(r.Name(array+r.U32(array+0xc)).find("_eye")==std::string::npos)continue;
|
|
const auto eye=detail::TransformPoint(face,(r.F32(array+0x20)+r.F32(array+0x2c))*.5f,
|
|
(r.F32(array+0x24)+r.F32(array+0x30))*.5f,(r.F32(array+0x28)+r.F32(array+0x34))*.5f);
|
|
out.eye_height=eye.y;break;
|
|
}
|
|
out.key=Key(r.Name(positions[0]+r.U32(positions[0]+0xc)));
|
|
const auto posName=r.Name(positions[0]+r.U32(positions[0]+0xc));const auto delim=posName.find("__");
|
|
if(delim==std::string::npos) throw std::runtime_error("No character material found");
|
|
const auto materials=r.Dictionary(mdl+r.U32(mdl+0x30));
|
|
std::vector<uint32_t> shapeMaterials(256,UINT32_MAX);
|
|
for(auto definition:r.Dictionary(mdl+r.U32(mdl+0x10))) {
|
|
// DrawOpa/DrawXlu streams are identified by their first draw command.
|
|
if(r.U8(definition)!=4)continue;
|
|
for(size_t at=definition;r.U8(at)==4;at+=8) {
|
|
const auto material=r.U16(at+1),shape=r.U16(at+3);
|
|
if(shape<shapeMaterials.size() && material<materials.size())shapeMaterials[shape]=material;
|
|
}
|
|
}
|
|
const auto shapes=r.Dictionary(mdl+r.U32(mdl+0x38));
|
|
for(unsigned shapeIndex=0;shapeIndex<shapes.size();++shapeIndex) {
|
|
const auto shape=shapes[shapeIndex];
|
|
const unsigned posId=r.U16(shape+0x48),uvId=r.U16(shape+0x50);
|
|
if(posId>=positions.size() || uvId>=texcoords.size())continue;
|
|
auto pos=Array(r,positions[posId],false),uv=Array(r,texcoords[uvId],true);
|
|
const auto lo=r.U32(shape+0xc),hi=r.U32(shape+0x10);
|
|
std::array<unsigned,12> offsets{},types{};unsigned stride=0;
|
|
for(int bit=0;bit<9;++bit) stride+=(lo>>bit)&1;
|
|
for(unsigned attr=0;attr<12;++attr) {
|
|
offsets[attr]=stride;types[attr]=attr<4?(lo>>(9+attr*2))&3:(hi>>((attr-4)*2))&3;
|
|
if(types[attr]==1)throw std::runtime_error("Direct character attributes unsupported");
|
|
if(types[attr])stride+=types[attr]-1;
|
|
}
|
|
if(types[0]<2 || types[4]<2 || !stride) continue;
|
|
const size_t group=shape+0x24,start=group+r.U32(group+8),length=r.U32(group+4);r.Check(start,length);
|
|
std::array<uint32_t,10> matrices;matrices.fill(UINT32_MAX);size_t at=start;
|
|
struct Indexed { Vertex vertex; uint32_t bone; };
|
|
const auto triangle=[&](const Indexed& a,const Indexed& b,const Indexed& c) {
|
|
for(int hand=0;hand<2;++hand) if(a.bone==wristIds[hand] && b.bone==wristIds[hand] && c.bone==wristIds[hand]) {
|
|
auto& mesh=out.hands[hand];if(mesh.size()>60000)throw std::runtime_error("Character hand mesh too large");
|
|
if(shapeIndex>=shapeMaterials.size() || shapeMaterials[shapeIndex]>=materials.size())throw std::runtime_error("Missing character hand material");
|
|
const auto material=materials[shapeMaterials[shapeIndex]],sampler=material+r.U32(material+0x30);
|
|
const auto name=r.Name(sampler+r.U32(sampler));
|
|
if(!textureName.empty() && textureName!=name)throw std::runtime_error("Multiple character hand materials unsupported");
|
|
textureName=name;
|
|
for(auto vertex: {a.vertex,b.vertex,c.vertex}) {
|
|
const auto p=detail::TransformPoint(wristFromSkin[hand],vertex.position[0],vertex.position[1],vertex.position[2]);
|
|
vertex.position={p.x,p.y,p.z};mesh.push_back(vertex);
|
|
}
|
|
}
|
|
};
|
|
while(at<start+length) {
|
|
const auto cmd=r.U8(at++);if(!cmd)continue;
|
|
if(cmd==0x20 || cmd==0x28 || cmd==0x30 || cmd==0x38) {
|
|
if(at+4>start+length)throw std::runtime_error("Truncated matrix load");
|
|
if(cmd==0x20) { const unsigned address=r.U16(at+2)&0xfff;if(address%12 || address/12>=10)throw std::runtime_error("Invalid matrix slot");matrices[address/12]=r.U16(at); }
|
|
at+=4;continue;
|
|
}
|
|
const auto primitive=cmd&0xf8;if(primitive!=0x80 && primitive!=0x90 && primitive!=0x98 && primitive!=0xa0)throw std::runtime_error("Unsupported character primitive");
|
|
const auto count=r.U16(at);at+=2;if(count<3 || count>(start+length-at)/stride)throw std::runtime_error("Truncated character primitive");
|
|
std::vector<Indexed> vertices;vertices.reserve(count);
|
|
for(unsigned i=0;i<count;++i) {
|
|
const size_t v=at+i*stride;
|
|
const unsigned pi=types[0]==2?r.U8(v+offsets[0]):r.U16(v+offsets[0]);
|
|
const unsigned ti=types[4]==2?r.U8(v+offsets[4]):r.U16(v+offsets[4]);
|
|
if(pi>=pos.size() || ti>=uv.size())throw std::runtime_error("Invalid character vertex index");
|
|
auto bone=r.U32(shape+8);
|
|
if(lo&1) { const unsigned slot=r.U8(v);if(slot%3 || slot/3>=10)throw std::runtime_error("Invalid character matrix selector");bone=matrices[slot/3]; }
|
|
vertices.push_back({{pos[pi],{uv[ti][0],uv[ti][1]}},bone});
|
|
}
|
|
if(primitive==0x80) { if(count%4)throw std::runtime_error("Invalid quad count");for(unsigned i=0;i<count;i+=4) {triangle(vertices[i],vertices[i+1],vertices[i+2]);triangle(vertices[i],vertices[i+2],vertices[i+3]);} }
|
|
else if(primitive==0x90) {if(count%3)throw std::runtime_error("Invalid triangle count");for(unsigned i=0;i<count;i+=3)triangle(vertices[i],vertices[i+1],vertices[i+2]);}
|
|
else for(unsigned i=2;i<count;++i) { const auto a=primitive==0xa0?0:i-2,b=i-1;if(primitive==0x98 && i%2)triangle(vertices[b],vertices[a],vertices[i]);else triangle(vertices[a],vertices[b],vertices[i]); }
|
|
at+=size_t(count)*stride;
|
|
}
|
|
}
|
|
if(out.hands[0].empty() || out.hands[1].empty())throw std::runtime_error("Character has no rigid hand triangles");
|
|
for(size_t p=0;p+0x40<data.size();p+=4) if(r.U32(p)==0x54455830 && r.Name(p+r.U32(p+0x14))==textureName) {Texture(r,p,out);break;}
|
|
if(out.rgba.empty())throw std::runtime_error("Character hand texture missing");
|
|
return out;
|
|
}
|
|
inline Model Load(const std::filesystem::path& root,unsigned index) {
|
|
if(index>=std::size(characters))throw std::runtime_error("Invalid character selection");
|
|
const auto path=root/"files"/"Race"/"Kart"/(std::string(characters[index].archive)+".szs");
|
|
std::ifstream file(path,std::ios::binary|std::ios::ate);if(!file)throw std::runtime_error("Character archive missing from installed game DATA");
|
|
const auto length=file.tellg();if(length<=0 || length>16*1024*1024)throw std::runtime_error("Invalid character archive size");
|
|
std::vector<uint8_t> bytes(static_cast<size_t>(length));file.seekg(0);if(!file.read(reinterpret_cast<char*>(bytes.data()),length))throw std::runtime_error("Could not read character archive");
|
|
return Parse(Unpack(bytes));
|
|
}
|
|
}
|