Skip to content

Commit

Permalink
Merge pull request #150 from M3-org/add-ktx-support
Browse files Browse the repository at this point in the history
Add ktx2 export support
  • Loading branch information
madjin authored Aug 25, 2024
2 parents 9290049 + af46700 commit 5d0cde5
Show file tree
Hide file tree
Showing 7 changed files with 10,722 additions and 27 deletions.
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
</div>

<link rel="stylesheet" href="public/style.css" />
<script src="src/library/ktx2/libktx.js"></script>
<script type="module" src="src/Main.jsx"></script>
</body>
</html>
233 changes: 207 additions & 26 deletions src/library/VRMExporterv0.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { BufferAttribute, Euler, Vector3 } from "three";
import { VRMExpressionPresetName } from "@pixiv/three-vrm";
import { encodeToKTX2 } from 'ktx2-encoder';
import { KtxDecoder } from "./ktx";
import { KTXTools } from "./ktxtools";




function ToOutputVRMMeta(vrmMeta, icon, outputImage) {
return {
allowedUserName: vrmMeta.allowedUserName,
Expand All @@ -17,6 +24,11 @@ function ToOutputVRMMeta(vrmMeta, icon, outputImage) {
violentUssageName: vrmMeta.violentUssageName,
};
}


const debug = false;


// WebGL(OpenGL)マクロ定数
var WEBGL_CONST;
(function (WEBGL_CONST) {
Expand Down Expand Up @@ -97,15 +109,14 @@ function getVRM0BoneName(name) {
return name;
}
export default class VRMExporterv0 {
parse(vrm, avatar, screenshot, rootSpringBones, colliderBones, scale, onDone) {
async parse(vrm, avatar, screenshot, rootSpringBones, isktx2, scale, onDone) {
const vrmMeta = convertMetaToVRM0(vrm.meta);
const humanoid = convertHumanoidToVRM0(vrm.humanoid);

const materials = vrm.materials;
//const expressionsPreset = {};
//const expressionCustom = {};
const blendShapeGroups = [];

// to do, add support to spring bones
//const springBone = vrm.springBoneManager;
const exporterInfo = {
Expand Down Expand Up @@ -174,9 +185,9 @@ export default class VRMExporterv0 {
return { name: material.name + "_normal", imageBitmap: material.normalMap.image };
});
const images = [...mainImages, ...shadeImages, ...ormImages, ...normalImages].filter(element => element !== null);
const outputImages = toOutputImages(images, icon);
const outputImages = toOutputImages(images, icon, isktx2 ? "image/ktx2" : "image/png");
const outputSamplers = toOutputSamplers(outputImages);
const outputTextures = toOutputTextures(outputImages);
const outputTextures = toOutputTextures(outputImages, isktx2);
const outputMaterials = toOutputMaterials(uniqueMaterials, images);
const rootNode = avatar.children.filter((child) => child.children.length > 0 &&
child.children[0].type === VRMObjectType.Bone)[0];
Expand Down Expand Up @@ -601,11 +612,11 @@ export default class VRMExporterv0 {
colliderIndices.push(ind);
}
else {
console.warn("No collider group for bone name: ", springParent.name + " was found");
if (debug) console.warn("No collider group for bone name: ", springParent.name + " was found");
}
}
else {
console.log("No colliders definition were present in vrm file file for: ", springBone.name + " spring bones")
if(debug) console.log("No colliders definition were present in vrm file file for: ", springBone.name + " spring bones")
}
});

Expand Down Expand Up @@ -639,13 +650,14 @@ export default class VRMExporterv0 {


outputVrmMeta.texture = icon ? outputImages.length - 1 : undefined;
const bufferViews = [];
bufferViews.push(...images.map((image) => ({
buffer: imageBitmap2png(image.imageBitmap),
type: MeshDataType.IMAGE,
})));
const bufferViews = await Promise.all(
images.map(async (image) => ({
buffer: isktx2 ? await imageBitmap2ktx2(image.imageBitmap) : imageBitmap2png(image.imageBitmap),
type: MeshDataType.IMAGE,
}))
);

// bufferViews.push(...meshDatas.map((data) => ({ buffer: data.buffer, type: data.type })));
/// continue until code finished assigning buffers

const meshDataBufferViewRelation = [];
meshDatas.forEach((data, i) => {
Expand All @@ -661,7 +673,7 @@ export default class VRMExporterv0 {

if (icon)
bufferViews.push({
buffer: imageBitmap2png(icon.imageBitmap),
buffer: isktx2 ? await imageBitmap2ktx2(icon.imageBitmap) : imageBitmap2png(icon.imageBitmap),
type: MeshDataType.IMAGE,
});
let bufferOffset = 0;
Expand Down Expand Up @@ -742,6 +754,16 @@ export default class VRMExporterv0 {

fillVRMMissingMetaData(outputVrmMeta);

const extensionsUsed = [
"KHR_materials_unlit",
"KHR_texture_transform",
"VRM"
];

if (isktx2){
extensionsUsed.push("KHR_texture_basisu");
}

const outputData = {
accessors: outputAccessors,
asset: exporterInfo,
Expand Down Expand Up @@ -771,11 +793,7 @@ export default class VRMExporterv0 {
specVersion: "0.0"
},
},
extensionsUsed: [
"KHR_materials_unlit",
"KHR_texture_transform",
"VRM",
],
extensionsUsed: extensionsUsed,
images: outputImages,
materials: outputMaterials,
meshes: outputMeshes,
Expand Down Expand Up @@ -820,6 +838,157 @@ function getNodes(parentNode) {
return [parentNode];
return [parentNode].concat(parentNode.children.map((child) => getNodes(child)).flat());
}

// async function imageBitmap2ktx2(image) {
// // Create ImageBitmap from the image
// const bitmap = await createImageBitmap(image);

// // Create a canvas and draw the ImageBitmap onto it
// const canvas = document.createElement('canvas');
// canvas.width = bitmap.width;
// canvas.height = bitmap.height;

// const ctx = canvas.getContext('bitmaprenderer');
// ctx.transferFromImageBitmap(bitmap);

// // Convert the canvas to a Blob
// const blob2 = await new Promise((res) => canvas.toBlob(res));

// // Encode the Blob to KTX2 format
// const ktx2Data = await encodeToKTX2(blob2,{
// mode: 'etc1s', // Use ETC1S for better compression
// quality: 'low', // Adjust based on acceptable quality loss
// compressionLevel: 1 // Lower values can increase compression
// });

// // Return the KTX2 data as an ArrayBuffer or Uint8Array
// return new Uint8Array(ktx2Data);
// }

// Initialize the KTX decoder/compressor

const ktxTools = new KTXTools();

async function imageBitmap2ktx2(image) {
// Create ImageBitmap from the image
const bitmap = await createImageBitmap(image);

// Create a canvas and draw the ImageBitmap onto it
const canvas = document.createElement('canvas');

canvas.width = bitmap.width;
canvas.height = bitmap.height;


const ctx = canvas.getContext('2d');

ctx.drawImage(bitmap, 0, 0);

// Get the image data as a Uint8Array (RGBA format)
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const pixelData = new Uint8Array(imageData.data.buffer);




// Compress the image data to KTX2 format
// reference https://github.khronos.org/KTX-Software/ktxtools/ktx_create.html
const ktx2Data = await ktxTools.compress(pixelData, canvas.width, canvas.height, 4, {
//basisu_options: {
normalMap : false,

uastc: false, // Set to true for higher quality UASTC compression
qualityLevel: 50, // Adjust compression quality (0-100)
compressionLevel: 2,
/** Set the compression quality KTX2 - UASTC */
// compressionUASTC_Rdo: false,
// UASTC_supercmp_scheme: "Zstd",

// --uastc-quality <level>
uastcFlags: "DEFAULT", // "FASTEST", "FASTER", "DEFAULT", "SLOWER", "SLOWEST"

// --astc-quality <level>
// The quality level configures the quality-performance tradeoff for the compressor; more complete searches of the search space improve image quality at the expense of compression time
// fastest == 0, fast == 10, medium == 60, thorough == 98, exhaustive == 100,
compressionUASTC_Rdo_Level: 18,

// --uastc-rdo
// Enable UASTC RDO post-processing.
uastcRDO: false,

// --uastc-rdo-l <lambda>
// Set UASTC RDO quality scalar (lambda) to lambda. Lower values yield higher quality/larger LZ compressed files, higher values yield lower quality/smaller LZ compressed files.
// A good range to try is [.25,10]. For normal maps a good range is [.25,.75]. The full
// Range is [.001,10.0]. Default is 1.0.
uastcRDOQualityScalar: 1.0,

// --uastc-rdo-d <dictsize>
// Set UASTC RDO dictionary size in bytes. Default is 4096. Lower values=faster, but give less compression.
// Range is [64,65536]
uastcRDODictSize: 4096,

// --uastc-rdo-b <scale>
// Set UASTC RDO max smooth block error scale. Default is 10.0, 1.0 is disabled. Larger values suppress more artifacts (and allocate more bits) on smooth blocks.
// Range is [1.0,300.0]
uastcRDOMaxSmoothBlockErrorScale: 10.0,

// --uastc-rdo-s <deviation>
// Set UASTC RDO max smooth block standard deviation. Default is 18.0. Larger values expand the range of blocks considered smooth.
// Range is [.01,65536.0]
uastcRDOMaxSmoothBlockStdDev: 18.0,

// --uastc-rdo-f
uastcRDODontFavorSimplerModes: false,



/** Set the compression quality KTX2 - ETC1S */
// --clevel <level>
// ETC1S / BasisLZ compression level, an encoding speed vs. quality tradeoff. Range is [0,5], default is 1. Higher values are slower but give higher quality.
// ETC1SCompressionLevel: 2,

// --qlevel <level>
// ETC1S / BasisLZ quality level.
//Range is [1,255]. Lower gives better compression/lower quality/faster. Higher gives less compression/higher quality/slower.
//--qlevel automatically determines values for --max-endpoints, --max-selectors, --endpoint-rdo-threshold and --selector-rdo-threshold for the target quality level. Setting these options overrides the values determined by -qlevel which defaults to 128 if neither it nor --max-endpoints and --max-selectors have been set.
ETC1SQualityLevel: 128,

// --max-endpoints <arg>
// Manually set the maximum number of color endpoint clusters.
// Range is [1,16128]. Default is 0, unset.
ETC1SmaxEndpoints: 0,

// --endpoint-rdo-threshold <arg>
// Set endpoint RDO quality threshold.
// The default is 1.25. Lower is higher quality but less quality per output bit (try [1.0,3.0]). This will override the value chosen by --qlevel.
ETC1SEndpointRdoThreshold: 1.25,

// --max-selectors <arg>
// Manually set the maximum number of color selector clusters from [1,16128]. Default is 0, unset.
ETC1SMaxSelectors: 0,

// --selector-rdo-threshold <arg>
// Set selector RDO quality threshold.
// The default is 1.25. Lower is higher quality but less quality per output bit (try [1.0,3.0]). This will override the value chosen by --qlevel.
ETC1SSelectorRdoThreshold: 1.25,

// --no-endpoint-rdo
// Disable endpoint rate distortion optimizations. Slightly faster, less noisy output, but lower quality per output bit. Default is to do endpoint RDO.
ETC1SNoEndpointRdo: false,

// --no-selector-rdo
// Disable selector rate distortion optimizations. Slightly faster, less noisy output, but lower quality per output bit. Default is to do selector RDO.
ETC1SNoSelectorRdo: false,
//},
supercmp_scheme: 'Zstd', // Optional: Enable supercompression Zstd, Zlib, BasisLZ, None

//compression_level: 18
});

return ktx2Data;
}


function imageBitmap2png(image) {
const canvas = document.createElement('canvas');
canvas.width = image.width;
Expand Down Expand Up @@ -1173,7 +1342,7 @@ const toOutputMaterials = (uniqueMaterials, images) => {
KHR_texture_transform: {
offset: [0, 0],
scale: [1, 1],
},
}
},
index: baseTxrIndex,
texCoord: 0, // TODO:
Expand Down Expand Up @@ -1243,12 +1412,12 @@ const toOutputMaterials = (uniqueMaterials, images) => {
return parseMaterial;
});
};
const toOutputImages = (images, icon) => {
const toOutputImages = (images, icon, mimeType) => {
return (icon ? images.concat(icon) : images)
.filter((image) => image && image.imageBitmap)
.map((image) => ({
bufferView: -1,
mimeType: "image/png",
mimeType: mimeType,
name: image.name, // TODO: 取得できないので仮のテクスチャ名としてマテリアル名を入れた
}));
};
Expand All @@ -1260,11 +1429,23 @@ const toOutputSamplers = (outputImages) => {
wrapT: WEBGL_CONST.REPEAT, // TODO: だいたいこれだった
}));
};
const toOutputTextures = (outputImages) => {
return outputImages.map((_, index) => ({
sampler: 0,
source: index, // TODO: 全パターンでindexなのか不明
}));
const toOutputTextures = (outputImages, isktx2) => {
if (isktx2){
return outputImages.map((_, index) => ({
sampler: 0,
extensions: {
KHR_texture_basisu: {
source: index
}
}
}));
}
else{
return outputImages.map((_, index) => ({
sampler: 0,
source: index, // TODO: 全パターンでindexなのか不明
}));
}
};
const toOutputScenes = (avatar, outputNodes) => {
const nodeNames = outputNodes.map((node) => node.name);
Expand Down
2 changes: 1 addition & 1 deletion src/library/download-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ function parseVRM (glbModel, avatar, options){
// XXX collider bones should be taken from springBone.colliderBones
const colliderBones = [];

exporter.parse(vrmData, glbModel, screenshot, rootSpringBones, colliderBones, scale, (vrm) => {
exporter.parse(vrmData, glbModel, screenshot, rootSpringBones, options.ktxCompression, scale, (vrm) => {
resolve(vrm)
})
})
Expand Down
Loading

0 comments on commit 5d0cde5

Please sign in to comment.