OneLauncher-Cracked/packages/oneclient_app/src/components/player_model.rs

329 lines
11 KiB
Rust

use std::cell::RefCell;
use bytes::Bytes;
use freya::engine::prelude::{
Data, Paint, SamplingOptions, Shader, SkData, SkImage, SkRect, TileMode,
};
use freya::prelude::*;
use skia_safe::RuntimeEffect;
use skia_safe::runtime_effect::ChildPtr;
use crate::hooks::use_player_skin;
const CAM_DIST: f32 = 80.0;
const CAM_FOCAL: f32 = 1.85;
const DRAG_SENSITIVITY: f32 = 0.01;
const PITCH_LIMIT: f32 = 1.3;
thread_local! {
static EFFECT: RefCell<Option<RuntimeEffect>> = const { RefCell::new(None) };
}
fn player_effect() -> Option<RuntimeEffect> {
EFFECT.with(|cell| {
let mut slot = cell.borrow_mut();
if slot.is_none() {
match RuntimeEffect::make_for_shader(PLAYER_SKSL, None) {
Ok(effect) => *slot = Some(effect),
Err(err) => tracing::error!("player model SkSL failed to compile: {err}"),
}
}
slot.clone()
})
}
#[derive(PartialEq, Clone)]
pub struct PlayerModel {
uuid: String,
width: Size,
height: Size,
yaw: f32,
pitch: f32,
}
impl PlayerModel {
pub fn new(uuid: impl Into<String>) -> Self {
Self {
uuid: uuid.into(),
width: Size::fill(),
height: Size::fill(),
yaw: 0.5,
pitch: -0.1,
}
}
pub fn width(mut self, width: Size) -> Self {
self.width = width;
self
}
pub fn height(mut self, height: Size) -> Self {
self.height = height;
self
}
#[allow(dead_code)]
pub fn yaw(mut self, yaw: f32) -> Self {
self.yaw = yaw;
self
}
#[allow(dead_code)]
pub fn pitch(mut self, pitch: f32) -> Self {
self.pitch = pitch;
self
}
}
impl Component for PlayerModel {
fn render_key(&self) -> DiffKey {
DiffKey::from(&self.uuid)
}
fn render(&self) -> impl IntoElement {
let (skin_bytes, is_slim) = use_player_skin(self.uuid.clone());
let mut cache = use_state(|| None::<(usize, Shader)>);
let src_ptr = skin_bytes.as_ptr() as usize;
let cache_copy = cache.peek().cloned();
let skin_shader = match cache_copy {
Some((ptr, shader)) if ptr == src_ptr => Some(shader),
_ => {
let shader = decode_skin_shader(&skin_bytes);
if let Some(shader) = &shader {
cache.set(Some((src_ptr, shader.clone())));
}
shader
}
};
let mut yaw = use_state({
let v = self.yaw;
move || v
});
let mut pitch = use_state({
let v = self.pitch;
move || v
});
let mut drag = use_state(|| None::<(f32, f32, f32, f32)>);
let mut last_uuid = use_state({
let u = self.uuid.clone();
move || u
});
if *last_uuid.peek() != self.uuid {
last_uuid.set(self.uuid.clone());
yaw.set(self.yaw);
pitch.set(self.pitch);
drag.set(None);
}
let effect = player_effect();
let render_cb = RenderCallback::new(move |ctx: &mut CanvasContext| {
let (Some(effect), Some(skin)) = (&effect, &skin_shader) else {
return;
};
let (w, h) = (ctx.size.width, ctx.size.height);
if w <= 0.0 || h <= 0.0 {
return;
}
let (cur_yaw, cur_pitch) = (yaw(), pitch());
let slim = if is_slim { 1.0 } else { 0.0 };
let mut uniforms = Vec::with_capacity(28);
for value in [w, h, cur_yaw, cur_pitch, CAM_DIST, CAM_FOCAL, slim] {
uniforms.extend_from_slice(&value.to_le_bytes());
}
let Some(shader) = effect.make_shader(
Data::new_copy(&uniforms),
&[ChildPtr::from(skin.clone())],
None,
) else {
return;
};
let mut paint = Paint::default();
paint.set_anti_alias(true);
paint.set_shader(shader);
ctx.canvas.draw_rect(SkRect::new(0.0, 0.0, w, h), &paint);
});
// `canvas` carries no style data, so the cursor rides on a wrapper `rect` that
// covers exactly the same area
rect()
.width(self.width.clone())
.height(self.height.clone())
.cursor(CursorIcon::Grab)
.child(
canvas(render_cb)
.key(src_ptr as u64)
.width(Size::fill())
.height(Size::fill()),
)
.on_pointer_down(move |e: Event<PointerEventData>| {
let loc = e.global_location();
drag.set(Some((loc.x as f32, loc.y as f32, yaw(), pitch())));
})
.on_global_pointer_move(move |e: Event<PointerEventData>| {
let Some((sx, sy, yaw0, pitch0)) = *drag.read() else {
return;
};
let loc = e.global_location();
if loc.to_tuple() == (-1.0, -1.0) {
return;
}
let dx = loc.x as f32 - sx;
let dy = loc.y as f32 - sy;
yaw.set(yaw0 - dx * DRAG_SENSITIVITY);
pitch.set((pitch0 + dy * DRAG_SENSITIVITY).clamp(-PITCH_LIMIT, PITCH_LIMIT));
Platform::get().send(UserEvent::RequestRedraw);
})
// Global so a release outside the widget still ends the drag
.on_global_pointer_press(move |_: Event<PointerEventData>| {
if drag.peek().is_some() {
drag.set(None);
}
})
}
}
fn decode_skin_shader(skin_bytes: &Bytes) -> Option<Shader> {
let data = unsafe { SkData::new_bytes(skin_bytes) };
let skin = SkImage::from_encoded(data)?;
let skin = skin.make_raster_image(None, None).unwrap_or(skin);
skin.to_shader(
(TileMode::Clamp, TileMode::Clamp),
SamplingOptions::default(),
None,
)
}
const PLAYER_SKSL: &str = r#"
uniform float uResX;
uniform float uResY;
uniform float uYaw;
uniform float uPitch;
uniform float uDist;
uniform float uFocal;
uniform float uSlim;
uniform shader uSkin;
void hitBox(float3 ro, float3 rd, float3 cmin, float3 cmax, float3 dims, float3 td,
float2 base, inout float best, inout half4 col) {
float3 t1 = (cmin - ro) / rd;
float3 t2 = (cmax - ro) / rd;
float3 tmin = min(t1, t2);
float3 tmax = max(t1, t2);
float tN = max(max(tmin.x, tmin.y), tmin.z);
float tF = min(min(tmax.x, tmax.y), tmax.z);
if (tN > tF || tF < 0.0) { return; }
float t = tN;
if (t < 0.0 || t >= best) { return; }
float3 p = ro + rd * t;
float3 n = clamp((p - cmin) / dims, 0.0, 1.0); // 0..1 inside the box
float w = td.x, h = td.y, d = td.z; // atlas region sizes
float2 uv;
float3 nrm;
if (tN == tmin.x) {
if (rd.x > 0.0) { // -X face (player right)
uv = float2(base.x + n.z * d, base.y + d + (1.0 - n.y) * h);
nrm = float3(-1.0, 0.0, 0.0);
} else { // +X face (player left)
uv = float2(base.x + d + w + (1.0 - n.z) * d, base.y + d + (1.0 - n.y) * h);
nrm = float3(1.0, 0.0, 0.0);
}
} else if (tN == tmin.y) {
if (rd.y > 0.0) { // -Y face (bottom): same orientation as the top, shifted by w
uv = float2(base.x + d + w + n.x * w, base.y + n.z * d);
nrm = float3(0.0, -1.0, 0.0);
} else { // +Y face (top)
uv = float2(base.x + d + n.x * w, base.y + n.z * d);
nrm = float3(0.0, 1.0, 0.0);
}
} else {
if (rd.z > 0.0) { // -Z face (back)
uv = float2(base.x + 2.0 * d + w + (1.0 - n.x) * w, base.y + d + (1.0 - n.y) * h);
nrm = float3(0.0, 0.0, -1.0);
} else { // +Z face (front)
uv = float2(base.x + d + n.x * w, base.y + d + (1.0 - n.y) * h);
nrm = float3(0.0, 0.0, 1.0);
}
}
half4 s = uSkin.eval(uv);
if (s.a < 0.5) {
return;
}
float3 L = normalize(float3(0.4, 0.8, 0.6));
float diff = 0.65 + 0.35 * clamp(dot(nrm, L), 0.0, 1.0);
col = half4(s.rgb * half(diff), 1.0);
best = t;
}
half4 main(float2 fragCoord) {
float2 res = float2(uResX, uResY);
float2 uv = (fragCoord - 0.5 * res) / res.y;
uv.y = -uv.y; // screen y is down; world y is up
float cy = cos(uYaw), sy = sin(uYaw);
float cp = cos(uPitch), sp = sin(uPitch);
float3 ro = float3(sy * cp, sp, cy * cp) * uDist;
float3 fwd = normalize(-ro);
float3 right = normalize(cross(fwd, float3(0.0, 1.0, 0.0)));
float3 up = cross(right, fwd);
float3 rd = normalize(fwd * uFocal + right * uv.x + up * uv.y);
float best = 1e9;
half4 col = half4(0.0);
float aw = uSlim > 0.5 ? 3.0 : 4.0; // arm width: slim = 3px, classic = 4px
float sw = aw + 0.5; // arm overlay (sleeve) width
hitBox(ro, rd, float3(-4.0, 8.0, -4.0), float3(4.0, 16.0, 4.0),
float3(8.0, 8.0, 8.0), float3(8.0, 8.0, 8.0), float2(0.0, 0.0), best, col); // head
hitBox(ro, rd, float3(-4.0, -4.0, -2.0), float3(4.0, 8.0, 2.0),
float3(8.0, 12.0, 4.0), float3(8.0, 12.0, 4.0), float2(16.0, 16.0), best, col); // body
hitBox(ro, rd, float3(-4.0 - aw, -4.0, -2.0), float3(-4.0, 8.0, 2.0),
float3(aw, 12.0, 4.0), float3(aw, 12.0, 4.0), float2(40.0, 16.0), best, col); // right arm
hitBox(ro, rd, float3(4.0, -4.0, -2.0), float3(4.0 + aw, 8.0, 2.0),
float3(aw, 12.0, 4.0), float3(aw, 12.0, 4.0), float2(32.0, 48.0), best, col); // left arm
hitBox(ro, rd, float3(-4.0, -16.0, -2.0), float3(0.0, -4.0, 2.0),
float3(4.0, 12.0, 4.0), float3(4.0, 12.0, 4.0), float2(0.0, 16.0), best, col); // right leg
hitBox(ro, rd, float3(0.0, -16.0, -2.0), float3(4.0, -4.0, 2.0),
float3(4.0, 12.0, 4.0), float3(4.0, 12.0, 4.0), float2(16.0, 48.0), best, col); // left leg
hitBox(ro, rd, float3(-4.5, 7.5, -4.5), float3(4.5, 16.5, 4.5),
float3(9.0, 9.0, 9.0), float3(8.0, 8.0, 8.0), float2(32.0, 0.0), best, col); // hat
hitBox(ro, rd, float3(-4.25, -4.25, -2.25), float3(4.25, 8.25, 2.25),
float3(8.5, 12.5, 4.5), float3(8.0, 12.0, 4.0), float2(16.0, 32.0), best, col); // jacket
hitBox(ro, rd, float3(-3.75 - sw, -4.25, -2.25), float3(-3.75, 8.25, 2.25),
float3(sw, 12.5, 4.5), float3(aw, 12.0, 4.0), float2(40.0, 32.0), best, col); // right sleeve
hitBox(ro, rd, float3(3.75, -4.25, -2.25), float3(3.75 + sw, 8.25, 2.25),
float3(sw, 12.5, 4.5), float3(aw, 12.0, 4.0), float2(48.0, 48.0), best, col); // left sleeve
hitBox(ro, rd, float3(-4.25, -16.25, -2.25), float3(0.25, -3.75, 2.25),
float3(4.5, 12.5, 4.5), float3(4.0, 12.0, 4.0), float2(0.0, 32.0), best, col); // right pant
hitBox(ro, rd, float3(-0.25, -16.25, -2.25), float3(4.25, -3.75, 2.25),
float3(4.5, 12.5, 4.5), float3(4.0, 12.0, 4.0), float2(0.0, 48.0), best, col); // left pant
return col;
}
"#;