FractalFir / tmf

Tight Model format is an experimental lossy 3D model format focused on reducing file size as much as posible without decreasing visual quality of the viewed model or read speeds.
MIT License
100 stars 6 forks source link

Octahedron normals #12

Open CptPotato opened 1 year ago

CptPotato commented 1 year ago

Hey there!

Have you considered encoding normals using octahedron mapping? It's a neat way to map a direction vector into two components and it has a more uniform distribution than storing (angle, z).

I haven't profiled it against the current implementation, but here's some sample code:

code ```rs use glam::{Vec2, Vec3}; /// Encode a 3d direction vector to a 2d vector using octahedron mapping. /// The output vector is in the range [-1..1]. The input vector doesn't have to be normalized. pub fn encode_oct(dir: Vec3) -> Vec2 { let norm = dir.x.abs() + dir.y.abs() + dir.z.abs(); let nx = dir.x / norm; let ny = dir.y / norm; if dir.z.is_sign_positive() { Vec2::new(nx, ny) } else { // fold over negative z Vec2::new( (1.0 - ny.abs()) * nx.signum(), (1.0 - nx.abs()) * ny.signum(), ) } } /// Decode an octahedron mapped direction vector back to the original one. /// The output is normalized. pub fn decode_oct(mut oct: Vec2) -> Vec3 { let z = 1.0 - oct.x.abs() - oct.y.abs(); oct += oct.signum() * z.min(0.0); Vec3::new(oct.x, oct.y, z).normalize() } ```

If you want to, I could open a PR to compare it to the current impl.

FractalFir commented 1 year ago

Great suggestion. From what it looks like, it should be faster (since it uses no trigonometric functions). If you would like to work on it, then I will gladly help. What you would need to implement is mainly 3 functions: one to encode, another to decode, and one which will convert an angle(in radians or degrees) which would return a minimal number of bits required to save the normal with a given precision. Their signature could look something like that:

fn encode(normal:Vec3,prec_bits:u8)->(f64,f64)
fn decode(a:f64,b:f64)->Vector3
fn bits_from_angle(angle:f64)->u8

I can then take those functions and add tmf-specific stuff around them. However, this may take me some time(I am going on a short vacation soon and the project has some work that is has more impact). This is the ranking of size of different segments in the example mesh:

  1. NormalSegment: 16.92 kb
  2. UvSegment: 28.559 kb,
  3. VertexSegment: 41.839 kb,
  4. NormalTriangleSegment: 74.119 kb,
  5. VertexTriangleSegment: 76.101 kb,
  6. UvTriangleSegment: 76.593 kb, Total size: 314.131kb As you can see, normals themselves already do not take all that much space, so the size reduction would be small, but the improvement in speed could be a nice bonus.
novacrazy commented 1 year ago

The hemioct method on page 27 of https://jcgt.org/published/0003/02/01/paper.pdf is also worth looking into. Greater accuracy than oct32p.

CptPotato commented 1 year ago

The hemioct method on page 27 of https://jcgt.org/published/0003/02/01/paper.pdf is also worth looking into. Greater accuracy than oct32p.

This seems like a good idea for normal maps, but I'm not sure about arbitrary vectors since you have to worry about the other hemisphere as well (adding a z sign bit, and at that point the accuracy/size improvement is lost). It could be faster, though.