Skip to content

Instantly share code, notes, and snippets.

@yakumo-proj
Created December 14, 2020 11:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save yakumo-proj/3f729319e734293e0b073d2139a82ace to your computer and use it in GitHub Desktop.
Save yakumo-proj/3f729319e734293e0b073d2139a82ace to your computer and use it in GitHub Desktop.
UniVRMを使わずにC#でVRMの画像リサイズを実装してみたやつ
using System;
using System.Json;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
namespace VRMOptimizer4Cluster
{
/// <summary>
/// 縮小アルゴリズム選択用enum値
/// </summary>
public enum ResamplerType
{
BiCubic = 0,
Lanczos2,
Lanczos3,
Lanczos5,
BiLinear, //triangle
Welch,
}
/// <summary>
/// ビューに渡す用データ
/// </summary>
public struct TextureInfo
{
public readonly byte[] Buf;
public readonly int BufferIndex;
public readonly int ImageIndex;
public readonly string Name;
public readonly int Width;
public readonly int Height;
public TextureInfo(byte[] buf, int bufferIndex, int imageIndex, string name)
{
Buf = buf;
BufferIndex = bufferIndex;
ImageIndex = imageIndex;
Name = name;
using var image = Image.Load(buf);
Width = image.Width;
Height = image.Height;
}
}
public class TinyVRMFileUtil
{
/// <summary>
/// VRM/GLBのヘッダ
/// </summary>
public class GlbHeader
{
public UInt32 Magic { get; set; }
public UInt32 Version { get; set; }
public UInt32 Length { get; set; }
public GlbHeader(UInt32 magic, UInt32 version, UInt32 length)
{
Magic = magic;
Version = version;
Length = length;
}
public void Write(StreamWriter stream)
{
stream.Write(Magic);
stream.Write(Version);
stream.Write(Length);
}
}
/// <summary>
/// VRM/GLBのチャンク(JSON/BINARY)
/// </summary>
public class GlbChunk
{
public UInt32 Length { get; set; }
public UInt32 ChunkType { get; set; }
public byte[] Data { get; set; }
public GlbChunk(UInt32 length, UInt32 chunkType, byte[] data)
{
Length = length;
ChunkType = chunkType;
Data = data;
}
public void Write(StreamWriter stream)
{
stream.Write(Length);
stream.Write(ChunkType);
stream.Write(Data);
}
}
/// <summary>
/// View用表示データ
/// </summary>
public List<TextureInfo> TextureList =>
ImageIndexes.Select((x, imgIndex) => new TextureInfo(
BinaryBuffers[x.index], x.index, imgIndex, x.name)
).ToList();
/// <summary>
/// GLBデータのインスタンス用
/// </summary>
public GlbHeader Header { get; set; }
public List<GlbChunk> Chunks { get; set; }
public List<byte[]> BinaryBuffers { get; set; }
JsonValue GlTFPart { get; set; } = null;
//JsonArray BufferViews { get; set; } = null;
/// <summary>
/// GLTFのimagesを解析して一時格納用
/// </summary>
public struct ImageIndex
{
public int index;
public string name;
}
List<ImageIndex> ImageIndexes { get; set; } = null;
/// <summary>
/// VRMファイルのヘッダ・チャンクを分割
/// </summary>
/// <param name="fileName">開くVRMファイル</param>
/// <returns></returns>
bool ChunkParser(string fileName)
{
using BinaryReader br = new BinaryReader(File.OpenRead(fileName));
Header = new GlbHeader(
br.ReadUInt32(),
br.ReadUInt32(),
br.ReadUInt32()
);
Chunks = new List<GlbChunk>();
for (int i = 0; i < 2; i++)
{
UInt32 length = br.ReadUInt32();
UInt32 chunkType = br.ReadUInt32();
var chunk = new GlbChunk(
length,
chunkType,
br.ReadBytes((int)length)
);
Chunks.Add(chunk);
}
return true;
}
/// <summary>
/// GLTF解析
/// </summary>
void GltfParser()
{
using MemoryStream memoryStream = new MemoryStream(Chunks[0].Data);
GlTFPart = JsonValue.Load(memoryStream);
BinaryBuffers = new List<byte[]>();
foreach (var bv in GlTFPart["bufferViews"] as JsonArray)
{
int offset = bv["byteOffset"];
int length = bv["byteLength"];
var buffer = new byte[length];
Array.Copy(Chunks[1].Data, offset, buffer, 0, length);
BinaryBuffers.Add(buffer);
}
Chunks[1].Data = null; //NOTE 使わないので開放しておく。
ImageIndexes = (GlTFPart["images"] as JsonArray)
.Select(x => new ImageIndex {
index = x["bufferView"],
name = x["name"]
}).ToList();
}
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="fileName"></param>
public TinyVRMFileUtil(string fileName)
{
ChunkParser(fileName); //ヘッダ・チャンクの解析
GltfParser(); //テクスチャ画像情報の解析
}
/// <summary>
/// 規定の最大サイズで縮小+保存
/// </summary>
/// <param name="filePath"></param>
/// <param name="maxWidth"></param>
/// <param name="type"></param>
public void ChangeAndSave(string filePath, int maxWidth, ResamplerType type)
{
// 画像のリサイズ
IResampler resampler = GetResampler(type);
foreach (var texture in TextureList.Where(x => x.Width > maxWidth))
{
using var image = Image.Load(BinaryBuffers[texture.BufferIndex]);
image.Mutate(x => x.Resize(maxWidth, maxWidth, resampler));
using MemoryStream ms = new MemoryStream();
image.SaveAsPng(ms);
BinaryBuffers[texture.BufferIndex] = ms.ToArray();
}
//bufferViewオフセットを再計算
int nOffset = 0;
for (int i = 0; i < BinaryBuffers.Count(); i++)
{
var buffer = BinaryBuffers[i];
GlTFPart["bufferViews"][i]["byteOffset"] = nOffset;
GlTFPart["bufferViews"][i]["byteLength"] = buffer.Count();
nOffset += buffer.Count();
}
//長さ情報の再計算など
Chunks[1].Length = (uint)nOffset;
Chunks[0].Data = System.Text.Encoding.UTF8.GetBytes(GlTFPart.ToString());
Chunks[0].Length = (UInt32)Chunks[0].Data.Count();
Header.Length = 12 + 8 + Chunks[0].Length + 8 + Chunks[1].Length;
// ファイルに書き出し
using var stream = File.OpenWrite(filePath);
stream.Write(BitConverter.GetBytes(Header.Magic));
stream.Write(BitConverter.GetBytes(Header.Version));
stream.Write(BitConverter.GetBytes(Header.Length));
stream.Write(BitConverter.GetBytes(Chunks[0].Length));
stream.Write(BitConverter.GetBytes(Chunks[0].ChunkType));
stream.Write(Chunks[0].Data);
stream.Write(BitConverter.GetBytes(Chunks[1].Length));
stream.Write(BitConverter.GetBytes(Chunks[1].ChunkType));
foreach (var buf in BinaryBuffers) { stream.Write(buf); }
}
/// <summary>
/// 縮小アルゴリズムの選択
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
protected IResampler GetResampler(ResamplerType type)
{
switch (type)
{
case ResamplerType.Lanczos2: return new LanczosResampler(2);
case ResamplerType.Lanczos3: return new LanczosResampler(3);
case ResamplerType.Lanczos5: return new LanczosResampler(5);
case ResamplerType.BiLinear: return new TriangleResampler();
case ResamplerType.Welch: return new WelchResampler();
/* case ResamplerType.BiCubic: */
default: return new BicubicResampler();
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment