From 6b782a9f4b7be6ea11b9e9011a99c4daea6ab184 Mon Sep 17 00:00:00 2001 From: yellows111 Date: Wed, 25 Oct 2023 15:16:28 +0100 Subject: [PATCH] Add a glTF exporter to project. Has a few bugs, however: * Doesn't support textures. * Exports upside down, and attempting to fix it inverts geometry. This is probably my last commit to this project from amaryllis. At least for a while. --- gltf-exporter.js | 222 +++++++++++++++++++++++++++++++++++++++++++++++ index.js | 4 +- 2 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 gltf-exporter.js diff --git a/gltf-exporter.js b/gltf-exporter.js new file mode 100644 index 0000000..7ec5b29 --- /dev/null +++ b/gltf-exporter.js @@ -0,0 +1,222 @@ +const icondumper2 = require("./icon.js"); +const iconjs = icondumper2.readers; +const filesystem = require("fs"); +const processObj = require("process"); + +const gltfConstants = { + "FLOAT": 5126, + "ARRAY_BUFFER": 34962, + "LINEAR": 9729, + "NEAREST_MIPMAP_LINEAR": 9986, + "REPEAT": 10497 +}; + +function rgb5a1_rgb8(colour) { + let b = ( colour & 0b11111); + let g = ((colour >> 5) & 0b11111); + let r = ((colour >> 10) & 0b11111); + //let a = ( color >> 15 ); + let output = new Number(); + output |= ((r * 8) << 16); + output |= ((g * 8) << 8); + output |= ((b * 8) << 0); + output *= 256; + output += (255); // couldn't do a |= as that converts it to signed, (a+1)*255 + return output; +} // TODO: support textures :P + +function imf2gltf(icon = null, shape = 0, filename = "untitled") { + if (icon === null) { + throw "Missing first argument, of which should be a icondumper2 Intermediate Model Format object."; + } + if (icon.hasOwnProperty("numberOfShapes") === false) { + throw "Expected a icondumper2 Intermediate Model Format object."; + } + if(shape >= icon.numberOfShapes) { + throw `Shape out of bounds. (object has ${icon.numberOfShapes} shapes.)`; + } + const gltfOutput = new Object(); + //setting up GLTF + gltfOutput.scene = 0; + gltfOutput.scenes = [{"nodes": [0]}]; + gltfOutput.nodes = [{"mesh": 0}]; + gltfOutput.meshes = [{ + "primitives": [{ + "attributes": { + "POSITION": 0, + "NORMAL": 1, + "TEXCOORD_0": 2, + "COLOR_0": 3, + }, + "material": 0 + }] + }]; // no indices because who needs indexing when you're transcoding? + gltfOutput.materials = [{ + "pbrMetallicRoughness": { + "baseColorFactor": [1.0, 1.0, 1.0, 1.0], + "metallicFactor": 0.0, + "roughnessFactor": 1.0 + } + }]; + let verticesArray = new Array(); + let normalsArray = new Array(); + let uvArray = new Array(); + let colourArray = new Array(); + icon.vertices.forEach(function(vertexObject){ + verticesArray.push(vertexObject.shapes[shape].x); + //verticesArray.push(-vertexObject.shapes[shape].y); // -2.75 + //if the above is applied, it completely breaks the orientation of the triangles + //just rotate 180deg in an authoring tool + verticesArray.push(vertexObject.shapes[shape].y); + verticesArray.push(vertexObject.shapes[shape].z); + normalsArray.push(vertexObject.normal.x); + normalsArray.push(vertexObject.normal.y); + normalsArray.push(vertexObject.normal.z); + uvArray.push(vertexObject.uv.u); + uvArray.push(vertexObject.uv.v); + colourArray.push(vertexObject.color.r/255); + colourArray.push(vertexObject.color.g/255); + colourArray.push(vertexObject.color.b/255); + colourArray.push((vertexObject.color.a > 1) ? (vertexObject.color.a/255): 1); + }); + let outputFloatArray = new Float32Array([...verticesArray, ...normalsArray, ...uvArray, ...colourArray]); // 3, 3, 2, 4 + gltfOutput.buffers = [{"uri": `${filename}.bin`, "byteLength": outputFloatArray.byteLength}]; + gltfOutput.bufferViews = [ + { + "buffer": 0, + "byteOffset": 0, + "byteLength": (verticesArray.length*4), + "target": gltfConstants.ARRAY_BUFFER + }, + { + "buffer": 0, + "byteOffset": (verticesArray.length*4), + "byteLength": (normalsArray.length*4), + "target": gltfConstants.ARRAY_BUFFER + }, + { + "buffer": 0, + "byteOffset": ((verticesArray.length*4)+(normalsArray.length*4)), + "byteLength": (uvArray.length*4), + "target": gltfConstants.ARRAY_BUFFER + }, + { + "buffer": 0, + "byteOffset": (((verticesArray.length*4)+(normalsArray.length*4))+(uvArray.length*4)), + "byteLength": (colourArray.length*4), + "target": gltfConstants.ARRAY_BUFFER + } + ]; + gltfOutput.accessors = [ + { + "bufferView": 0, + "componentType": gltfConstants.FLOAT, + "count": verticesArray.length/3, + "type": "VEC3", + "max": [ 4.0, 4.0, 4.0], + "min": [-4.0, -4.0, -4.0], + "name": "Vertex Position Accessor" + }, + { + "bufferView": 1, + "componentType": gltfConstants.FLOAT, + "count": (normalsArray.length/3), + "type": "VEC3", + "max": [ 1.0, 1.0, 1.0], + "min": [-1.0, -1.0, -1.0], + "name": "Normal Accessor" + }, + { + "bufferView": 2, + "componentType": gltfConstants.FLOAT, + "count": (uvArray.length/2), + "type": "VEC2", + "max": [ 1.0, 1.0], + "min": [ 0.0, 0.0], + "name": "Texture Coordinate Accessor" + }, + { + "bufferView": 3, + "componentType": gltfConstants.FLOAT, + "count": (colourArray.length/4), + "type": "VEC4", + "max": [ 1.0, 1.0, 1.0, 1.0], + "min": [ 0.0, 0.0, 0.0, 1.0], + "name": "Colour Accessor" + } + ]; + gltfOutput.asset = {"version": "2.0", "extras": {"IU_rotation": [0, 180.0, 0]}} + return {object: gltfOutput, buffer: outputFloatArray}; +} + +function loadAndConvertIcon(inputData, state = "-") { + if (inputData.hasOwnProperty("numberOfShapes") === false) { + throw "Expected a icondumper2 Intermediate Model Format object."; + } + for (let index = 0; index < (inputData.numberOfShapes); index++) { + const filename = `export_${state}_${index}`; + const gltfFile = imf2gltf(inputData, index, filename); + + (require("fs")).writeFileSync(`${filename}.gltf`, new TextEncoder().encode(JSON.stringify(gltfFile.object))); + (require("fs")).writeFileSync(`${filename}.bin`, gltfFile.buffer); + console.log(`Saved shape ${index} as "${filename}.gltf", "${filename}.bin" pair.`); + } +} + +// can anything de-dupe this code somehow? (index.js) +console.log(`icon.js version ${icondumper2.version}, 2023 (c) yellows111`); +switch(processObj.argv[2]) { + case "psu": { + let inputFile = filesystem.readFileSync(processObj.argv[3] ? processObj.argv[3] : "file.psu"); + const parsed = iconjs.readEmsPsuFile(inputFile.buffer.slice(inputFile.byteOffset, inputFile.byteOffset + inputFile.byteLength)); + const PS2D = iconjs.readPS2D(parsed[parsed.rootDirectory]["icon.sys"].data); + Object.keys(PS2D.filenames).forEach(function(file) { + loadAndConvertIcon(iconjs.readIconFile(parsed[parsed.rootDirectory][PS2D.filenames[file]].data), file); + }); + break; + } + case "psv": { + let inputFile = filesystem.readFileSync(processObj.argv[3] ? processObj.argv[3] : "file.psv"); + const parsed = iconjs.readPsvFile(inputFile.buffer.slice(inputFile.byteOffset, inputFile.byteOffset + inputFile.byteLength)); + loadAndConvertIcon(iconjs.readIconFile(parsed.icons.n), "n") + loadAndConvertIcon(iconjs.readIconFile(parsed.icons.c), "c") + loadAndConvertIcon(iconjs.readIconFile(parsed.icons.d), "d") + break; + } + case "sps": + case "xps": { + let inputFile = filesystem.readFileSync(processObj.argv[3] ? processObj.argv[3] : "file.sps"); + const parsed = iconjs.readSharkXPortSxpsFile(inputFile.buffer.slice(inputFile.byteOffset, inputFile.byteOffset + inputFile.byteLength)); + const PS2D = iconjs.readPS2D(parsed[parsed.rootDirectory]["icon.sys"].data); + Object.keys(PS2D.filenames).forEach(function(file) { + loadAndConvertIcon(iconjs.readIconFile(parsed[parsed.rootDirectory][PS2D.filenames[file]].data), file); + }); + break; + } + case "sys": { + let inputFile = filesystem.readFileSync(processObj.argv[3] ? processObj.argv[3] : "icon.sys"); + const metadata = iconjs.readPS2D(inputFile.buffer.slice(inputFile.byteOffset, inputFile.byteOffset + inputFile.byteLength)); + Object.keys(metadata.filenames).forEach(function(file) { + let getFile = filesystem.readFileSync(metadata.filenames[file]); + loadAndConvertIcon(iconjs.readIconFile(getFile.buffer.slice(getFile.byteOffset, getFile.byteOffset + getFile.byteLength)), file); + //console.log(individialIcon); + }); + break; + } + default: { + //Template literal goes here. + console.log( +`${(processObj.argv.length > 2) ? "Unknown argument: "+processObj.argv[2]+"\n\n": ""}icondumper2 node.js client (glTF exporter version) subcommands: +psu: Read a EMS Memory Adapter export file. +psv: Read a PS3 export file. +sps: Read a SharkPort export file. +xps: Read a X-Port export file. + +sys: Read a icon.sys (964 bytes) file, and attempt + to read icon files from the current directory. +` ); // end of template + processObj.exit(1); + } +} +console.log("All shapes were exported upside down. I recommend you rotate them yourself."); +processObj.exit(0); \ No newline at end of file diff --git a/index.js b/index.js index 2de910a..6779644 100644 --- a/index.js +++ b/index.js @@ -75,4 +75,6 @@ sys: Read a icon.sys (964 bytes) file, and attempt (suppress behaviour with --no-read-models) ` ); // end of template } -} \ No newline at end of file + processObj.exit(1); +} +processObj.exit(0); \ No newline at end of file