UNPKG

x3d-image

Version:
445 lines (372 loc) 12.8 kB
"use strict"; const X3D = require ("x_ite"), pkg = require ("../package.json"), electron = require ("electron"), yargs = require ("yargs"), path = require ("path"), url = require ("url"), fs = require ("fs"), DEBUG = false; // Redirect console messages. process .exit = (code = 0) => { throw code }; console .log = (... messages) => electron .ipcRenderer .send ("log", messages); console .warn = (... messages) => electron .ipcRenderer .send ("warn", messages); console .error = (... messages) => electron .ipcRenderer .send ("error", messages); electron .ipcRenderer .on ("main", async (event, argv) => main (argv)); async function main (argv) { try { await generate (argv); process .exit (); } catch (error) { if (typeof error === "number") { electron .ipcRenderer .send ("exit", error); } else { console .error (error .message || error); electron .ipcRenderer .send ("exit", 1); } } } async function generate (argv) { const args = yargs (argv .slice (2)) .scriptName ("x3d-image") .usage ("$0 [options] -i input-file -o output-file [-i input-file -o output-file ...]") .wrap (yargs () .terminalWidth ()) .command ("Render image files from X3D") .version (pkg .version) .alias ("v", "version") .fail ((msg, error, yargs) => { console .error (msg); process .exit (1); }) .option ("view-all", { type: "boolean", alias: "a", description: "Modify the current view so that all objects fit in view volume.", array: true, default: [false], }) .option ("background", { type: "string", alias: "b", description: `Set background to specified color. Color can be any X3D RGBA color or any CSS color. Use PNG as output image format for transparent backgrounds.`, array: true, default: [ ], requiresArg: true, }) .option ("colorSpace", { type: "string", alias: "c", description: `The color space in which color calculations take place.`, array: true, default: ["LINEAR_WHEN_PHYSICAL_MATERIAL"], choices: ["SRGB", "LINEAR_WHEN_PHYSICAL_MATERIAL", "LINEAR"], requiresArg: true, }) .option ("delay", { type: "number", alias: "d", description: "Wait the specified number of seconds before generating the image.", array: true, default: [0], requiresArg: true, }) .option ("extension", { type: "string", alias: "e", description: `Set output file extension(s), e.g. ".x3dv" or ".tidy.x3d". The output file will have the same basename as the input file.`, array: true, requiresArg: true, implies: "input", conflicts: "output", }) .option ("logarithmic-depth-buffer", { type: "boolean", alias: "g", description: `Whether to use a logarithmic depth buffer. It may be necessary to use this if dealing with huge differences in scale in a single scene. It is automatically enabled if a GeoViewpoint is bound.`, array: true, default: [false], }) .option ("input", { type: "string", alias: "i", description: "Set input file(s). If there are less input files than output files, the last input file is used for the remaining output files.", array: true, requiresArg: true, demandOption: true, }) .option ("log", { type: "boolean", alias: "l", description: `Log output filenames to stdout.`, implies: "input", }) .option ("tone-mapping", { type: "string", alias: "m", description: `Whether tone mapping should be applied.`, array: true, default: ["NONE"], choices: ["NONE", "ACES_NARKOWICZ", "ACES_HILL", "ACES_HILL_EXPOSURE_BOOST", "KHR_PBR_NEUTRAL"], requiresArg: true, }) .option ("order-independent-transparency", { type: "boolean", alias: "n", description: `Whether to use order independent transparency rendering technique.`, array: true, default: [false], }) .option ("output", { type: "string", alias: "o", description: "Set output file(s). This can be either a *.png or *.jpg file.", array: true, requiresArg: true, implies: "input", }) .option ("quality", { type: "number", alias: "q", description: "A Number between 0 and 1 indicating the image quality to be used when creating images using file formats that support lossy compression (such as JPEG).", array: true, default: [1], requiresArg: true, }) .option ("rotation", { type: "string", alias: "r", description: `Creates a parent group with the model as children and sets the specified X3D rotation value.`, array: true, default: [ ], requiresArg: true, }) .option ("size", { type: "string", alias: "s", description: "Set image size in pixels.", array: true, default: ["1280x720"], requiresArg: true, }) .option ("text-compression", { type: "string", alias: "t", description: `Controls how Text.length and Text.maxExtent are handled. Either by adjusting char spacing or by scaling text letters.`, array: true, default: ["CHAR_SPACING"], choices: ["CHAR_SPACING", "SCALING"], requiresArg: true, }) .option ("environment-light", { type: "string", alias: "w", description: `Add an EnvironmentLight node to scene. Useful when rendering glTF files with PhysicalMaterial nodes.`, array: true, default: [ ], choices: ["CANNON", "HELIPAD", "FOOTPRINT"], requiresArg: true, }) .option ("exposure", { type: "number", alias: "x", description: `The exposure of an image describes the amount of light that is captured.`, array: true, default: [1], requiresArg: true, }) .check (args => { if (!args .output && !args .extension) throw new Error ("Missing argument output or extension."); return true; }) .example ([ [ "npx x3d-image -s 1600x900 -i file.x3d -o file.jpg", "Render a JPEG image from X3D with size 1600x900." ], [ "npx x3d-image -s 1600x900 -i file1.x3d -o file1.png -i file2.x3d -o file2.png", "Render two PNG images from two X3D files." ], [ "npx x3d-image -a -w CANNON -i file.gltf -e .png", "Render image of glTF file with view-all and environment light." ], ]) .help () .alias ("help", "h") .argv; const canvas = document .getElementById ("browser"), browser = canvas .browser; browser .setBrowserOption ("PrimitiveQuality", "HIGH"); browser .setBrowserOption ("TextureQuality", "HIGH"); browser .setBrowserOption ("Mute", true); const argc = Math .max (args .input .length, args .output ?.length ?? args .extension ?.length); for (let i = 0; i < argc; ++ i) { // Create input filename. const input = new URL (arg (args .input, i), url .pathToFileURL (path .join (process .cwd (), "/"))); // Create output filename. let output; if (args .output) { output = path .resolve (process .cwd (), arg (args .output, i)); } else if (args .extension) { const filename = url .fileURLToPath (input), extension = arg (args .extension, i); output = `${filename .slice (0, -path. extname (filename) .length)}${extension}`; } if (args .log) console .log (output); // Load scene. const mimeType = mimeTypeFromPath (output), size = arg (args .size, i) .split ("x"), width = parseInt (size [0]) || 1280, height = parseInt (size [1]) || 720; browser .endUpdate (); await browser .resize (width, height); await browser .loadURL (new X3D .MFString (input)) .catch (Function .prototype); if (arg (args .background, i)) await addBackground (browser, browser .currentScene, arg (args .background, i)); if (arg (args ["environment-light"], i)) await addEnvironmentLight (browser, browser .currentScene, arg (args ["environment-light"], i)); if (arg (args .rotation, i)) await addTransform (browser, browser .currentScene, arg (args .rotation, i)); if (arg (args .colorSpace, i)) browser .setBrowserOption ("ColorSpace", arg (args .colorSpace, i)); if (arg (args .exposure, i)) browser .setBrowserOption ("Exposure", arg (args .exposure, i)); if (arg (args ["logarithmic-depth-buffer"], i)) browser .setBrowserOption ("LogarithmicDepthBuffer", arg (args ["logarithmic-depth-buffer"], i)); if (arg (args ["order-independent-transparency"], i)) browser .setBrowserOption ("OrderIndependentTransparency", arg (args ["order-independent-transparency"], i)); if (arg (args ["text-compression"], i)) browser .setBrowserOption ("TextCompression", arg (args ["text-compression"], i)); if (arg (args ["tone-mapping"], i)) browser .setBrowserOption ("ToneMapping", arg (args ["tone-mapping"], i)); if (arg (args ["view-all"], i)) browser .viewAll (0); browser .beginUpdate (); if (arg (args .delay, i)) await sleep (arg (args .delay, i) * 1000); await browser .nextFrame (); // Generate image. const blob = await generateImage (canvas, mimeType, arg (args .quality, i)); fs .writeFileSync (output, new DataView (await blob .arrayBuffer ())); } browser .dispose (); } function arg (arg, i) { return arg [i] ?? arg .at (-1); } async function generateImage (canvas, mimeType, quality) { return new Promise ((resolve, reject) => { canvas .toBlob (blob => resolve (blob), mimeType, quality); }); } const sleep = delay => new Promise (resolve => setTimeout (resolve, delay)); function mimeTypeFromPath (filename) { switch (path .extname (filename) .toLowerCase ()) { case ".jpg": case ".jpeg": return "image/jpeg"; default: return "image/png"; } } async function addTransform (browser, scene, rotation) { scene .addComponent (browser .getComponent ("Grouping")); await browser .loadComponents (scene); const transform = scene .createNode ("Transform"); const r = new X3D .SFRotation (); r .setName ("rotation"); r .fromString (rotation, scene); transform .children = scene .rootNodes; transform .rotation = r; scene .rootNodes = new X3D .MFNode (transform); } let background = null; async function addBackground (browser, scene, color) { if (!background) { scene .addComponent (browser .getComponent ("EnvironmentalEffects")); await browser .loadComponents (scene); background = scene .createNode ("Background"); background .transparency = 1; } const c = new X3D .SFColorRGBA (); c .setName ("background"); c .fromString (color, scene); background .set_bind = true; background .skyColor = [c .r, c .g, c .b]; background .transparency = 1 - c .a; scene .addRootNode (background); } const EnvironmentLights = new Map ([ ["CANNON", "cannon-exterior:2"], ["HELIPAD", "helipad:1"], ["FOOTPRINT", "footprint-court:1"], ]); let environmentLight = null; async function addEnvironmentLight (browser, scene, name) { if (!environmentLight) { scene .addComponent (browser .getComponent ("CubeMapTexturing")); await browser .loadComponents (scene); environmentLight = scene .createNode ("EnvironmentLight"); const specularTexture = scene .createNode ("ImageCubeMapTexture") environmentLight .intensity = 1; environmentLight .color = new X3D .SFColor (1, 1, 1); environmentLight .specularTexture = specularTexture; } const [image, intensity] = (EnvironmentLights .get (name) ?? EnvironmentLights .get ("CANNON")) .split (":"); environmentLight .intensity = parseFloat (intensity); const fileURL = new URL (`images/${image}`, url .pathToFileURL (path .join (__dirname, "/"))), specularURL = new X3D .MFString (`${fileURL}.avif`, `${fileURL}.jpg`); if (!environmentLight .specularTexture .url .equals (specularURL)) environmentLight .specularTexture .url = specularURL; scene .addRootNode (environmentLight); await environmentLight .specularTexture .getValue () .requestImmediateLoad (); }