Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Texture related improvements to Gltf and USDZ Exporters #4690

Merged
merged 3 commits into from
Oct 3, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion examples/src/examples/loaders/gltf-export.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,22 @@ class GltfExportExample {
entity3.setLocalScale(0.01, 0.01, 0.01);
app.root.addChild(entity3);

// a render component with a sphere and cone primitives
const material = new pc.StandardMaterial();
material.diffuse = pc.Color.RED;
material.update();

const entity = new pc.Entity("TwoMeshInstances");
entity.addComponent('render', {
type: 'asset',
meshInstances: [
new pc.MeshInstance(pc.createSphere(app.graphicsDevice), material),
new pc.MeshInstance(pc.createCone(app.graphicsDevice), material)
]
});
app.root.addChild(entity);
entity.setLocalPosition(0, 1.5, -1.5);

// Create an Entity with a camera component
const camera = new pc.Entity();
camera.addComponent("camera", {
Expand All @@ -79,7 +95,11 @@ class GltfExportExample {
const link = document.getElementById('ar-link');

// export the whole scene into a glb format
new pcx.GltfExporter().build(app.root).then((arrayBuffer: any) => {
const options = {
maxTextureSize: 1024
};

new pcx.GltfExporter().build(app.root, options).then((arrayBuffer: any) => {

const blob = new Blob([arrayBuffer], { type: 'application/octet-stream' });

Expand Down
6 changes: 5 additions & 1 deletion examples/src/examples/loaders/usdz-export.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,11 @@ class UsdzExportExample {
const link = document.getElementById('ar-link');

// convert the loaded entity into asdz file
new pcx.UsdzExporter().build(entity).then((arrayBuffer: any) => {
const options = {
maxTextureSize: 1024
};

new pcx.UsdzExporter().build(entity, options).then((arrayBuffer: any) => {
const blob = new Blob([arrayBuffer], { type: 'application/octet-stream' });

// On iPhone Safari, this link creates a clickable AR link on the screen. When this is clicked,
Expand Down
55 changes: 55 additions & 0 deletions extras/exporters/core-exporter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
class CoreExporter {
/**
* Converts a source image specified in multiple formats to a canvas.
*
* @param {any} image - The source image to be converted.
* @param {object} options - Object for passing optional arguments.
* @param {Color} [options.color] - The tint color to modify the texture with.
* @param {number} [options.maxTextureSize] - Maximum texture size. Texture is resized if over the size.
* @returns {any} - The canvas element containing the image.
*/
imageToCanvas(image, options = {}) {

if ((typeof HTMLImageElement !== 'undefined' && image instanceof HTMLImageElement) ||
(typeof HTMLCanvasElement !== 'undefined' && image instanceof HTMLCanvasElement) ||
(typeof OffscreenCanvas !== 'undefined' && image instanceof OffscreenCanvas) ||
(typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap)) {

// texture dimensions
let { width, height } = image;
const maxTextureSize = options.maxTextureSize;
if (maxTextureSize) {
const scale = Math.min(maxTextureSize / Math.max(width, height), 1);
width = Math.round(width * scale);
height = Math.round(height * scale);
}

// convert to a canvas
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const context = canvas.getContext('2d');
context.drawImage(image, 0, 0, canvas.width, canvas.height);

// tint the texture by specified color
if (options.color) {
const { r, g, b } = options.color;

const imagedata = context.getImageData(0, 0, width, height);
const data = imagedata.data;

for (let i = 0; i < data.length; i += 4) {
data[i + 0] = data[i + 0] * r;
data[i + 1] = data[i + 1] * g;
data[i + 2] = data[i + 2] * b;
}

context.putImageData(imagedata, 0, 0);
}

return canvas;
}
}
}

export { CoreExporter };
120 changes: 112 additions & 8 deletions extras/exporters/gltf-exporter.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { CoreExporter } from "./core-exporter.js";

const ARRAY_BUFFER = 34962;
const ELEMENT_ARRAY_BUFFER = 34963;

Expand Down Expand Up @@ -52,19 +54,44 @@ const getSemantic = (engineSemantic) => {
}
};

class GltfExporter {
const getFilter = function (filter) {
switch (filter) {
case pc.FILTER_NEAREST: return 9728;
case pc.FILTER_LINEAR: return 9729;
case pc.FILTER_NEAREST_MIPMAP_NEAREST: return 9984;
case pc.FILTER_LINEAR_MIPMAP_NEAREST: return 9985;
case pc.FILTER_NEAREST_MIPMAP_LINEAR: return 9986;
case pc.FILTER_LINEAR_MIPMAP_LINEAR: return 9987;
}
};

const getWrap = function (wrap) {
switch (wrap) {
case pc.ADDRESS_CLAMP_TO_EDGE: return 33071;
case pc.ADDRESS_MIRRORED_REPEAT: return 33648;
case pc.ADDRESS_REPEAT: return 10497;
}
};

// supported texture semantics on a material
const textureSemantics = [
'diffuseMap'
];

class GltfExporter extends CoreExporter {
collectResources(root) {
const resources = {
buffers: [],
cameras: [],
entities: [],
materials: [],
textures: [],

// entry: { node, meshInstances}
entityMeshInstances: []
};

const { materials, buffers, entityMeshInstances } = resources;
const { materials, buffers, entityMeshInstances, textures } = resources;

// Collect entities
root.forEach((entity) => {
Expand All @@ -78,6 +105,14 @@ class GltfExporter {
const material = meshInstance.material;
if (materials.indexOf(material) < 0) {
resources.materials.push(material);

// collect textures
textureSemantics.forEach((semantic) => {
const texture = material[semantic];
if (texture && textures.indexOf(texture) < 0) {
textures.push(texture);
}
});
}

// collect mesh instances per node
Expand Down Expand Up @@ -204,20 +239,36 @@ class GltfExporter {
}

writeMaterials(resources, json) {

const attachTexture = (material, destination, name, textureSemantic) => {
const texture = material[textureSemantic];
if (texture) {
const textureIndex = resources.textures.indexOf(texture);
if (textureIndex < 0) console.logWarn(`Texture ${texture.name} wasn't collected.`);
destination[name] = {
"index": textureIndex
};
}
};

if (resources.materials.length > 0) {
json.materials = resources.materials.map((mat) => {
const { name, diffuse, emissive, opacity, blendType, cull } = mat;
const material = {};
const material = {
pbrMetallicRoughness: {}
};
const pbr = material.pbrMetallicRoughness;

if (name && name.length > 0) {
material.name = name;
}

if (!diffuse.equals(pc.Color.WHITE) || opacity !== 1) {
material.pbrMetallicRoughness = {};
material.pbrMetallicRoughness.baseColorFactor = [diffuse.r, diffuse.g, diffuse.b, opacity];
pbr.baseColorFactor = [diffuse.r, diffuse.g, diffuse.b, opacity];
}

attachTexture(mat, pbr, 'baseColorTexture', 'diffuseMap');

if (!emissive.equals(pc.Color.BLACK)) {
material.emissiveFactor = [emissive.r, emissive.g, emissive.b];
}
Expand Down Expand Up @@ -365,7 +416,45 @@ class GltfExporter {
}
}

buildJson(resources) {
convertTextures(textures, json, options) {

const textureOptions = {
maxTextureSize: options.maxTextureSize
};

for (let i = 0; i < textures.length; i++) {

// for now store all textures as png
// TODO: consider jpg if the alpha channel is not used
mvaligursky marked this conversation as resolved.
Show resolved Hide resolved
const isRGBA = true;
const mimeType = isRGBA ? 'image/png' : 'image/jpeg';

const texture = textures[i];
const mipObject = texture._levels[0];

// convert texture data to uri
const canvas = this.imageToCanvas(mipObject, textureOptions);
const uri = canvas.toDataURL(mimeType);

json.images[i] = {
'uri': uri
};

json.samplers[i] = {
'minFilter': getFilter(texture.minFilter),
'magFilter': getFilter(texture.magFilter),
'wrapS': getWrap(texture.addressU),
'wrapT': getWrap(texture.addressV)
};

json.textures[i] = {
'sampler': i,
'source': i
};
}
}

buildJson(resources, options) {
const json = {
asset: {
version: "2.0",
Expand All @@ -378,6 +467,12 @@ class GltfExporter {
]
}
],
images: [
],
samplers: [
],
textures: [
],
scene: 0
};

Expand All @@ -387,14 +482,23 @@ class GltfExporter {
this.writeNodes(resources, json);
this.writeMaterials(resources, json);
this.writeMeshes(resources, json);
this.convertTextures(resources.textures, json, options);

return json;
}

build(entity) {
/**
* Converts a hierarchy of entities to GLB format.
*
* @param {Entity} entity - The root of the entity hierarchy to convert.
* @param {object} options - Object for passing optional arguments.
* @param {Color} [options.maxTextureSize] - Maximum texture size. Texture is resized if over the size.
mvaligursky marked this conversation as resolved.
Show resolved Hide resolved
* @returns {ArrayBuffer} - The GLB file content.
*/
build(entity, options = {}) {
const resources = this.collectResources(entity);

const json = this.buildJson(resources);
const json = this.buildJson(resources, options);
const jsonText = JSON.stringify(json);

const headerLength = 12;
Expand Down
Loading