Merge branch 'collada' into next

This commit is contained in:
JannisX11 2021-12-01 22:02:14 +01:00
commit 8495784dba
10 changed files with 1324 additions and 10 deletions

View File

@ -70,6 +70,7 @@
<script src="lib/three.min.js"></script>
<script src="lib/three_custom.js"></script>
<script src="lib/GLTFExporter.js"></script>
<script src="lib/ColladaExporter.js"></script>
<script src="lib/CanvasFrame.js"></script>
<script src="lib/fik.min.js"></script>
<script src="lib/molang.umd.js"></script>
@ -137,6 +138,7 @@
<script src="js/io/formats/bedrock_old.js"></script>
<script src="js/io/formats/obj.js"></script>
<script src="js/io/formats/gltf.js"></script>
<script src="js/io/formats/collada.js"></script>
<script src="js/io/formats/modded_entity.js"></script>
<script src="js/io/formats/optifine_jem.js"></script>
<script src="js/io/formats/optifine_jpm.js"></script>

View File

@ -199,15 +199,16 @@ class Keyframe {
})
return arr;
}
getFixed(data_point = 0) {
getFixed(data_point = 0, do_quaternion = true) {
if (this.channel === 'rotation') {
let fix = this.animator.group.mesh.fix_rotation;
return new THREE.Quaternion().setFromEuler(new THREE.Euler(
let euler = new THREE.Euler(
fix.x - Math.degToRad(this.calc('x', data_point)),
fix.y - Math.degToRad(this.calc('y', data_point)),
fix.z + Math.degToRad(this.calc('z', data_point)),
'ZYX'
));
)
return do_quaternion ? new THREE.Quaternion().setFromEuler(euler) : euler;
} else if (this.channel === 'position') {
let fix = this.animator.group.mesh.fix_position;
return new THREE.Vector3(

View File

@ -601,6 +601,7 @@ const MenuBar = {
'export_minecraft_skin',
'export_gltf',
'export_obj',
'export_collada',
'upload_sketchfab',
'share_model',
]},

806
js/io/formats/collada.js Normal file
View File

@ -0,0 +1,806 @@
(function() {
function arrangeArray(array) {
return array.map(v => Math.roundTo(v, 6)).join(' ');
}
var codec = new Codec('collada', {
name: 'Collada Model',
extension: 'dae',
compile(options = 0) {
let scope = this;
let geometries = [];
let root = [];
let effects = [];
let images = [];
let materials = [];
// Structure
let model = {
type: 'COLLADA',
attributes: {
xmlns: 'http://www.collada.org/2005/11/COLLADASchema',
version: '1.4.1',
'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
},
content: [
{
type: 'asset',
content: [
{
name: 'contributor',
content: [
{type: 'author', content: settings.username.value || 'Blockbench user'},
{type: 'authoring_tool', content: 'Blockbench'},
]
},
{name: 'created', content: new Date().toISOString()},
{name: 'modified', content: new Date().toISOString()},
{name: 'unit', attributes: {name: 'meter', meter: "0.0625"}},
{name: 'up_axis', content: 'Y_UP'}
]
},
{
type: 'library_effects',
content: effects
},
{
type: 'library_images',
content: images
},
{
type: 'library_materials',
content: materials
},
{
type: 'library_geometries',
content: geometries
},
{
type: 'library_visual_scenes',
content: [{
type: 'visual_scene',
attributes: {
id: 'Scene',
name: 'Scene',
},
content: root
}]
},
{
type: 'scene',
content: [{
type: 'instance_visual_scene',
attributes: {
url: '#Scene'
}
}]
}
]
}
// Materials
Texture.all.forEach((texture, i) => {
effects.push({
type: 'effect',
attributes: {id: `Material_${i}-effect`},
content: {
type: 'profile_COMMON',
content: [
{
type: 'newparam',
attributes: {sid: `Image_${i}-surface`},
content: {
type: 'surface',
attributes: {type: '2D'},
content: {type: 'init_from', content: `Image_${i}`}
}
},
{
type: 'newparam',
attributes: {sid: `Image_${i}-sampler`},
content: {
type: 'sampler2D',
content: {type: 'source', content: `Image_${i}-surface`}
}
},
{
type: 'technique',
attributes: {sid: 'common'},
content: {
type: 'lambert',
content: [
{type: 'emission', content: {type: 'color', attributes: {sid: 'emission'}, content: '0 0 0 1'}},
{type: 'diffuse', content: {type: 'texture', attributes: {texture: `Image_${i}-sampler`, texcoord: 'UVMap'}}},
{type: 'index_of_refraction', content: {type: 'float', attributes: {sid: 'ior'}, content: '1.45'}}
]
}
}
]
}
})
images.push({
type: 'image',
attributes: {
id: `Image_${i}`,
name: `Image_${i}`,
},
content: {
type: 'init_from',
content: `${texture.name.replace(/\.png$/, '')}.png`
}
})
materials.push({
type: 'material',
attributes: {
id: `Material_${i}-material`,
name: `Material_${i}`,
},
content: {name: 'instance_effect', attributes: {url: `#Material_${i}-effect`}}
})
})
// Cube Geometry
const cube_face_normals = {
north: [0, 0, -1],
east: [1, 0, 0],
south: [0, 0, 1],
west: [-1, 0, 0],
up: [0, 1, 0],
down: [0, -1, 0],
}
Cube.all.forEach(cube => {
let positions = [];
let normals = [];
let uv = [];
let vcount = [];
let primitive = [];
function addPosition(x, y, z) {
positions.push(x - cube.origin[0], y - cube.origin[1], z - cube.origin[2]);
}
addPosition(cube.to[0] + cube.inflate, cube.to[1] + cube.inflate, cube.to[2] + cube.inflate);
addPosition(cube.to[0] + cube.inflate, cube.to[1] + cube.inflate, cube.from[2] - cube.inflate);
addPosition(cube.to[0] + cube.inflate, cube.from[1] - cube.inflate, cube.to[2] + cube.inflate);
addPosition(cube.to[0] + cube.inflate, cube.from[1] - cube.inflate, cube.from[2] - cube.inflate);
addPosition(cube.from[0] - cube.inflate, cube.to[1] + cube.inflate, cube.from[2] - cube.inflate);
addPosition(cube.from[0] - cube.inflate, cube.to[1] + cube.inflate, cube.to[2] + cube.inflate);
addPosition(cube.from[0] - cube.inflate, cube.from[1] - cube.inflate, cube.from[2] - cube.inflate);
addPosition(cube.from[0] - cube.inflate, cube.from[1] - cube.inflate, cube.to[2] + cube.inflate);
for (let fkey in cube.faces) {
let face = cube.faces[fkey];
if (face.texture === null) continue;
normals.push(...cube_face_normals[fkey]);
let uv_outputs = [
[face.uv[0] / Project.texture_width, 1 - face.uv[1] / Project.texture_height],
[face.uv[2] / Project.texture_width, 1 - face.uv[1] / Project.texture_height],
[face.uv[2] / Project.texture_width, 1 - face.uv[3] / Project.texture_height],
[face.uv[0] / Project.texture_width, 1 - face.uv[3] / Project.texture_height],
];
var rot = face.rotation || 0;
while (rot > 0) {
uv_outputs.splice(0, 0, uv_outputs.pop());
rot -= 90;
}
uv_outputs.forEach(coord => {
uv.push(...coord);
})
vcount.push(4);
let vertices;
switch (fkey) {
case 'north': vertices = [1, 4, 6, 3]; break;
case 'east': vertices = [0, 1, 3, 2]; break;
case 'south': vertices = [5, 0, 2, 7]; break;
case 'west': vertices = [4, 5, 7, 6]; break;
case 'up': vertices = [4, 1, 0, 5]; break;
case 'down': vertices = [7, 2, 3, 6]; break;
}
primitive.push(
vertices[3], (normals.length/3)-1, vcount.length*4 - 1,
vertices[2], (normals.length/3)-1, vcount.length*4 - 2,
vertices[1], (normals.length/3)-1, vcount.length*4 - 3,
vertices[0], (normals.length/3)-1, vcount.length*4 - 4,
)
}
let geometry = {
type: 'geometry',
attributes: {
id: `${cube.uuid}-mesh`,
name: cube.name
},
content: {
type: 'mesh',
content: [
{
type: 'source',
attributes: {id: `${cube.uuid}-mesh-positions`},
content: [
{
type: 'float_array',
attributes: {id: `${cube.uuid}-mesh-positions-array`, count: positions.length},
content: arrangeArray(positions)
},
{
type: 'technique_common',
content: {
type: 'accessor',
attributes: {source: `#${cube.uuid}-mesh-positions-array`, count: positions.length/3, stride: 3},
content: [
{type: 'param', attributes: {name: 'X', type: 'float'}},
{type: 'param', attributes: {name: 'Y', type: 'float'}},
{type: 'param', attributes: {name: 'Z', type: 'float'}},
]
}
}
]
},
{
type: 'source',
attributes: {id: `${cube.uuid}-mesh-normals`},
content: [
{
type: 'float_array',
attributes: {id: `${cube.uuid}-mesh-normals-array`, count: normals.length},
content: arrangeArray(normals)
},
{
type: 'technique_common',
content: {
type: 'accessor',
attributes: {source: `#${cube.uuid}-mesh-normals-array`, count: normals.length/3, stride: 3},
content: [
{type: 'param', attributes: {name: 'X', type: 'float'}},
{type: 'param', attributes: {name: 'Y', type: 'float'}},
{type: 'param', attributes: {name: 'Z', type: 'float'}},
]
}
}
]
},
{
type: 'source',
attributes: {id: `${cube.uuid}-mesh-map-0`},
content: [
{
type: 'float_array',
attributes: {id: `${cube.uuid}-mesh-map-0-array`, count: uv.length},
content: arrangeArray(uv)
},
{
type: 'technique_common',
content: {
type: 'accessor',
attributes: {source: `#${cube.uuid}-mesh-map-0-array`, count: uv.length/2, stride: 2},
content: [
{type: 'param', attributes: {name: 'S', type: 'float'}},
{type: 'param', attributes: {name: 'T', type: 'float'}},
]
}
}
]
},
{
type: 'vertices',
attributes: {id: `${cube.uuid}-mesh-vertices`},
content: [
{
type: 'input',
attributes: {semantic: 'POSITION', source: `#${cube.uuid}-mesh-positions`}
}
]
}
]
}
}
let j = 0;
let last_tex;
let render_groups = [];
for (let fkey in cube.faces) {
let face = cube.faces[fkey];
if (face.texture !== null) {
let vcount_here = vcount[j];
let p_here = primitive.slice(j * 12, j * 12 + 12);
if (last_tex && face.texture === last_tex) {
render_groups.last().vcount.push(vcount_here);
render_groups.last().primitive.push(...p_here);
} else {
render_groups.push({
texture: face.getTexture(),
vcount: [vcount_here],
primitive: p_here,
})
last_tex = face.texture;
}
j++;
}
}
render_groups.forEach(render_group => {
geometry.content.content.push({
type: 'polylist',
attributes: {
material: `Material_${Texture.all.indexOf(render_group.texture)}-material`,
count: 6
},
content: [
{type: 'input', attributes: {semantic: 'VERTEX', source: `#${cube.uuid}-mesh-vertices`, offset: 0}},
{type: 'input', attributes: {semantic: 'NORMAL', source: `#${cube.uuid}-mesh-normals`, offset: 1}},
{type: 'input', attributes: {semantic: 'TEXCOORD', source: `#${cube.uuid}-mesh-map-0`, offset: 2, set: 0}},
{type: 'vcount', content: arrangeArray(render_group.vcount)},
{type: 'p', content: arrangeArray(render_group.primitive)}
]
})
})
geometries.push(geometry);
})
// Mesh Geo
Mesh.all.forEach(mesh => {
let positions = [];
let normals = [];
let uv = [];
let vertex_keys = [];
function addPosition(x, y, z) {
positions.push(x, y, z);
}
for (let vkey in mesh.vertices) {
addPosition(...mesh.vertices[vkey]);
vertex_keys.push(vkey);
}
let texture;
let j = 0;
let last_tex;
let render_groups = [];
let primitive_count = 0;
for (let key in mesh.faces) {
if (mesh.faces[key].vertices.length >= 3) {
let face = mesh.faces[key];
let vertices = face.getSortedVertices();
let tex = mesh.faces[key].getTexture();
vertices.forEach(vkey => {
uv.push(face.uv[vkey][0] / Project.texture_width, 1 - face.uv[vkey][1] / Project.texture_height);
})
normals.push(...face.getNormal(true));
let face_primitives = [];
vertices.forEach((vkey, vi) => {
face_primitives.push(
vertex_keys.indexOf(vkey),
(normals.length/3)-1,
(uv.length/2)-vertices.length+vi,
)
})
if (last_tex && face.texture === last_tex) {
render_groups.last().vcount.push(vertices.length);
render_groups.last().primitive.push(...face_primitives);
} else {
render_groups.push({
texture: face.getTexture(),
vcount: [vertices.length],
primitive: face_primitives,
})
last_tex = face.texture;
}
primitive_count += face.vertices.length;
i++;
}
}
let geometry = {
type: 'geometry',
attributes: {
id: `${mesh.uuid}-mesh`,
name: mesh.name
},
content: {
type: 'mesh',
content: [
{
type: 'source',
attributes: {id: `${mesh.uuid}-mesh-positions`},
content: [
{
type: 'float_array',
attributes: {id: `${mesh.uuid}-mesh-positions-array`, count: positions.length},
content: arrangeArray(positions)
},
{
type: 'technique_common',
content: {
type: 'accessor',
attributes: {source: `#${mesh.uuid}-mesh-positions-array`, count: positions.length/3, stride: 3},
content: [
{type: 'param', attributes: {name: 'X', type: 'float'}},
{type: 'param', attributes: {name: 'Y', type: 'float'}},
{type: 'param', attributes: {name: 'Z', type: 'float'}},
]
}
}
]
},
{
type: 'source',
attributes: {id: `${mesh.uuid}-mesh-normals`},
content: [
{
type: 'float_array',
attributes: {id: `${mesh.uuid}-mesh-normals-array`, count: normals.length},
content: arrangeArray(normals)
},
{
type: 'technique_common',
content: {
type: 'accessor',
attributes: {source: `#${mesh.uuid}-mesh-normals-array`, count: normals.length/3, stride: 3},
content: [
{type: 'param', attributes: {name: 'X', type: 'float'}},
{type: 'param', attributes: {name: 'Y', type: 'float'}},
{type: 'param', attributes: {name: 'Z', type: 'float'}},
]
}
}
]
},
{
type: 'source',
attributes: {id: `${mesh.uuid}-mesh-map-0`},
content: [
{
type: 'float_array',
attributes: {id: `${mesh.uuid}-mesh-map-0-array`, count: uv.length},
content: arrangeArray(uv)
},
{
type: 'technique_common',
content: {
type: 'accessor',
attributes: {source: `#${mesh.uuid}-mesh-map-0-array`, count: uv.length/2, stride: 2},
content: [
{type: 'param', attributes: {name: 'S', type: 'float'}},
{type: 'param', attributes: {name: 'T', type: 'float'}},
]
}
}
]
},
{
type: 'vertices',
attributes: {id: `${mesh.uuid}-mesh-vertices`},
content: [
{
type: 'input',
attributes: {semantic: 'POSITION', source: `#${mesh.uuid}-mesh-positions`}
}
]
}
]
}
}
render_groups.forEach(render_group => {
geometry.content.content.push({
type: 'polylist',
attributes: {
material: `Material_${Texture.all.indexOf(render_group.texture)}-material`,
count: 6
},
content: [
{type: 'input', attributes: {semantic: 'VERTEX', source: `#${mesh.uuid}-mesh-vertices`, offset: 0}},
{type: 'input', attributes: {semantic: 'NORMAL', source: `#${mesh.uuid}-mesh-normals`, offset: 1}},
{type: 'input', attributes: {semantic: 'TEXCOORD', source: `#${mesh.uuid}-mesh-map-0`, offset: 2, set: 0}},
{type: 'vcount', content: arrangeArray(render_group.vcount)},
{type: 'p', content: arrangeArray(render_group.primitive)}
]
})
})
geometries.push(geometry);
})
// Object Hierarchy
function processNode(node) {
let position = node.origin.slice();
if (node.parent instanceof Group) position.V3_subtract(node.parent.origin);
let tag = {
name: 'node',
attributes: {
id: node.uuid,
name: node.name,
type: 'NODE'
},
content: [
{type: 'scale', attributes: {sid: 'scale'}, content: '1 1 1'},
{type: 'translate', attributes: {sid: 'location'}, content: position.join(' ')},
]
}
if (node.rotatable) {
tag.content.push(
{type: 'rotate', attributes: {sid: 'rotationZ'}, content: `0 0 1 ${node.rotation[2]}`},
{type: 'rotate', attributes: {sid: 'rotationY'}, content: `0 1 0 ${node.rotation[1]}`},
{type: 'rotate', attributes: {sid: 'rotationX'}, content: `1 0 0 ${node.rotation[0]}`},
)
}
if (node instanceof Cube || node instanceof Mesh) {
let textures = [];
for (let fkey in node.faces) {
let tex = node.faces[fkey].getTexture();
if (tex instanceof Texture) textures.safePush(tex);
}
tag.content.push({
type: 'instance_geometry',
attributes: {url: `#${node.uuid}-mesh`, name: node.name},
content: {
name: 'bind_material',
content: {
name: 'technique_common',
content: textures.map(tex => {
let index = Texture.all.indexOf(tex);
return {
name: 'instance_material',
attributes: {symbol: `Material_${index}-material`, target: `#Material_${index}-material`},
content: {
name: 'bind_vertex_input',
attributes: {semantic: 'UVMap', input_semantic: 'TEXCOORD', input_set: '0'}
}
}
})
}
}
});
}
if (node instanceof Group) {
node.children.forEach(node => {
tag.content.push(processNode(node));
})
}
return tag;
}
Outliner.root.forEach(node => {
root.push(processNode(node))
})
/*
let compiled_animations = Codecs.gltf.buildAnimationTracks();
if (compiled_animations.length) {
let animations_tag = {
type: 'library_animations',
content: []
}
let animation_clips_tag = {
type: 'library_animation_clips',
content: []
}
compiled_animations.forEach(anim_obj => {
let anim_tag = {
type: 'animation',
attributes: {
id: `animation-${anim_obj.name}`,
name: anim_obj.name
},
content: []
}
anim_obj.tracks.forEach(track => {
let group = OutlinerNode.uuids[track.group_uuid];
let collada_channel = track.channel;
if (collada_channel == 'position') collada_channel = 'location';
let track_name = `${group.name}_${collada_channel}`
let track_tag = {
type: 'animation',
attributes: {id: `${group.name}`, name: group.name},
content: [
{
type: 'source',
attributes: {id: track_name+'-input'},
content: [
{
type: 'float_array',
attributes: {id: track_name+'-input-array', count: track.times.length},
content: arrangeArray(track.times)
},
{
type: 'technique_common',
content: {
type: 'accessor',
attributes: {source: '#'+track_name+'-input-array', count: track.times.length, stride: 1},
content: {type: 'param', attributes: {name: 'TIME', type: 'float'}}
}
}
]
},
{
type: 'source',
attributes: {id: track_name+'-output'},
content: [
{
type: 'float_array',
attributes: {id: track_name+'-output-array', count: track.values.length},
content: arrangeArray(track.values)
},
{
type: 'technique_common',
content: {
type: 'accessor',
attributes: {source: '#'+track_name+'-output-array', count: track.values.length, stride: 3},
content: [
{type: 'param', attributes: {name: 'X', type: 'float'}},
{type: 'param', attributes: {name: 'Y', type: 'float'}},
{type: 'param', attributes: {name: 'Z', type: 'float'}},
]
}
}
]
},
{
type: 'sampler',
attributes: {id: `${track_name}-sampler`},
content: [
{type: 'input', attributes: {semantic: 'INPUT', source: '#'+track_name+'-input'}},
{type: 'input', attributes: {semantic: 'OUTPUT', source: '#'+track_name+'-output'}},
//{type: 'input', attributes: {semantic: 'INTERPOLATION', source: '#'+track_name+'-interpolation'}},
]
}
]
}
track_tag.content.push({
type: 'channel',
attributes: {source: `#${track_name}-sampler`, target: `${group.uuid}/${collada_channel}`}
})
anim_tag.content.push(track_tag)
})
animations_tag.content.push(anim_tag)
let animation_clip_tag = {
type: 'animation_clip',
attributes: {
id: anim_obj.name,
name: anim_obj.name
}
}
})
model.content.push(animations_tag);
}*/
scope.dispatchEvent('compile', {model, options});
if (options.raw) {
return model
} else {
return compileXML(model)
}
},
write(content, path) {
var scope = this;
content = this.compile();
Blockbench.writeFile(path, {content}, path => scope.afterSave(path));
Texture.all.forEach(tex => {
if (tex.error == 1) return;
var name = tex.name;
if (name.substr(-4).toLowerCase() !== '.png') {
name += '.png';
}
var image_path = path.split(osfs);
image_path.splice(-1, 1, name);
Blockbench.writeFile(image_path.join(osfs), {
content: tex.source,
savetype: 'image'
})
})
},
export() {
var scope = this;
if (isApp) {
Blockbench.export({
resource_id: 'dae',
type: this.name,
extensions: [this.extension],
startpath: this.startPath(),
content: this.compile(),
name: this.fileName(),
custom_writer: (a, b) => scope.write(a, b),
}, path => this.afterDownload(path))
} else {
var archive = new JSZip();
var content = this.compile()
archive.file((Project.name||'model')+'.obj', content)
Texture.all.forEach(tex => {
if (texture.error == 1) return;
var name = texture.name;
if (name.substr(-4).toLowerCase() !== '.png') {
name += '.png';
}
archive.file(name, texture.source.replace('data:image/png;base64,', ''), {base64: true});
})
archive.generateAsync({type: 'blob'}).then(content => {
Blockbench.export({
type: 'Zip Archive',
extensions: ['zip'],
name: 'assets',
content: content,
savetype: 'zip'
}, path => scope.afterDownload(path));
})
}
}
})
BARS.defineActions(function() {
codec.export_action = new Action({
id: 'export_collada',
icon: 'fas.fa-sync-alt',
category: 'file',
click: function () {
codec.export()
}
})
})
})()
function compileXML(object) {
let depth = 0;
let output = '<?xml version="1.0" encoding="utf-8"?>\n';
function spaces() {
let s = '';
for (let i = 0; i < depth; i++) {
s += ' ';
}
return s;
}
function handleObject(object) {
let type = object.type || object.name;
let head = `<${type}`;
if (object.attributes) {
for (let key in object.attributes) {
head += ` ${key}="${object.attributes[key]}"`
}
}
output += spaces() + head;
if (typeof object.content == 'string') {
output += '>' + object.content + `</${type}>\n`;
} else if (typeof object.content == 'object') {
depth++;
output += `>\n`;
let list = object.content instanceof Array ? object.content : [object.content];
list.forEach(node => {
if (typeof node == 'object') handleObject(node);
})
depth--;
output += spaces() + `</${type}>\n`;
} else {
output += '/>\n';
}
}
handleObject(object);
return output;
}

View File

@ -1,6 +1,6 @@
(function() {
function buildAnimationTracks() {
function buildAnimationTracks(do_quaternions = true) {
let anims = [];
Animator.animations.forEach(animation => {
@ -53,7 +53,7 @@ function buildAnimationTracks() {
keyframes.sort((a, b) => a.time - b.time)
// Sampling rotation steps that exceed 180 degrees
if (channel === 'rotation' && !contains_script) {
if (channel === 'rotation' && !contains_script && do_quaternions) {
let original_keyframes = keyframes.slice();
original_keyframes.forEach((kf, i) => {
let next = original_keyframes[i+1]
@ -101,10 +101,10 @@ function buildAnimationTracks() {
}
times.push(kf.time);
Timeline.time = kf.time;
kf.getFixed().toArray(values, values.length);
kf.getFixed(0, do_quaternions).toArray(values, values.length);
})
let trackType = THREE.VectorKeyframeTrack;
if (channel === 'rotation') {
if (channel === 'rotation' && do_quaternions) {
trackType = THREE.QuaternionKeyframeTrack;
channel = 'quaternion';
} else if (channel == 'position') {
@ -113,6 +113,8 @@ function buildAnimationTracks() {
})
}
let track = new trackType(animator.group.mesh.uuid+'.'+channel, times, values, interpolation);
track.group_uuid = animator.group.uuid;
track.channel = channel;
tracks.push(track);
}
}
@ -181,6 +183,8 @@ var codec = new Codec('gltf', {
}
})
codec.buildAnimationTracks = buildAnimationTracks;
BARS.defineActions(function() {
codec.export_action = new Action({
id: 'export_gltf',

View File

@ -91,7 +91,7 @@ var codec = new Codec('obj', {
uv_outputs.push(`vt ${face.uv[2] / Project.texture_width} ${1 - face.uv[1] / Project.texture_height}`);
uv_outputs.push(`vt ${face.uv[2] / Project.texture_width} ${1 - face.uv[3] / Project.texture_height}`);
uv_outputs.push(`vt ${face.uv[0] / Project.texture_width} ${1 - face.uv[3] / Project.texture_height}`);
var rot = element.faces[key].rotation || 0;
var rot = face.rotation || 0;
while (rot > 0) {
uv_outputs.splice(0, 0, uv_outputs.pop());
rot -= 90;

View File

@ -873,10 +873,16 @@ new NodePreviewController(Cube, {
let j = 0;
mesh.geometry.faces = [];
mesh.geometry.clearGroups();
let last_tex;
Canvas.face_order.forEach((fkey, i) => {
if (cube.faces[fkey].texture !== null) {
indices.push(0 + i*4, 2 + i*4, 1 + i*4, 2 + i*4, 3 + i*4, 1 + i*4);
mesh.geometry.addGroup(j*6, 6, j)
if (last_tex && cube.faces[fkey].texture === last_tex) {
mesh.geometry.groups[mesh.geometry.groups.length-1].count += 6;
} else {
mesh.geometry.addGroup(j*6, 6, j)
last_tex = cube.faces[fkey].texture;
}
mesh.geometry.faces.push(fkey)
j++;
}

View File

@ -410,6 +410,7 @@ class Group extends OutlinerNode {
Group.prototype.type = 'group';
Group.prototype.icon = 'fa fa-folder';
Group.prototype.isParent = true;
Group.prototype.rotatable = true;
Group.prototype.name_regex = () => Format.bone_rig ? 'a-zA-Z0-9_' : false;
Group.prototype.buttons = [
Outliner.buttons.autouv,

View File

@ -880,8 +880,10 @@
"action.export_minecraft_skin.desc": "Export the Minecraft skin as a PNG texture",
"action.export_obj": "Export OBJ Model",
"action.export_obj.desc": "Export a Wavefront OBJ model for rendering",
"action.export_collada": "Export Collada Model (dae)",
"action.export_collada.desc": "Export model and animations as dae file to use it in other 3D applications",
"action.export_gltf": "Export glTF Model",
"action.export_gltf.desc": "Export model and animations as glTF file to use in other 3D applications",
"action.export_gltf.desc": "Export model and animations as glTF file for sharing and rendering",
"action.upload_sketchfab": "Upload to Sketchfab",
"action.upload_sketchfab.desc": "Upload your model to Sketchfab",
"action.share_model": "Share...",

491
lib/ColladaExporter.js Normal file
View File

@ -0,0 +1,491 @@
( function () {
/**
* https://github.com/gkjohnson/collada-exporter-js
*
* Usage:
* const exporter = new ColladaExporter();
*
* const data = exporter.parse(mesh);
*
* Format Definition:
* https://www.khronos.org/collada/
*/
class ColladaExporter {
parse( object, onDone, options = {} ) {
options = Object.assign( {
version: '1.4.1',
author: null,
textureDirectory: '',
upAxis: 'Y_UP',
unitName: null,
unitMeter: null
}, options );
if ( options.upAxis.match( /^[XYZ]_UP$/ ) === null ) {
console.error( 'ColladaExporter: Invalid upAxis: valid values are X_UP, Y_UP or Z_UP.' );
return null;
}
if ( options.unitName !== null && options.unitMeter === null ) {
console.error( 'ColladaExporter: unitMeter needs to be specified if unitName is specified.' );
return null;
}
if ( options.unitMeter !== null && options.unitName === null ) {
console.error( 'ColladaExporter: unitName needs to be specified if unitMeter is specified.' );
return null;
}
if ( options.textureDirectory !== '' ) {
options.textureDirectory = `${options.textureDirectory}/`.replace( /\\/g, '/' ).replace( /\/+/g, '/' );
}
const version = options.version;
if ( version !== '1.4.1' && version !== '1.5.0' ) {
console.warn( `ColladaExporter : Version ${version} not supported for export. Only 1.4.1 and 1.5.0.` );
return null;
} // Convert the urdf xml into a well-formatted, indented format
function format( urdf ) {
const IS_END_TAG = /^<\//;
const IS_SELF_CLOSING = /(\?>$)|(\/>$)/;
const HAS_TEXT = /<[^>]+>[^<]*<\/[^<]+>/;
const pad = ( ch, num ) => num > 0 ? ch + pad( ch, num - 1 ) : '';
let tagnum = 0;
return urdf.match( /(<[^>]+>[^<]+<\/[^<]+>)|(<[^>]+>)/g ).map( tag => {
if ( ! HAS_TEXT.test( tag ) && ! IS_SELF_CLOSING.test( tag ) && IS_END_TAG.test( tag ) ) {
tagnum --;
}
const res = `${pad( ' ', tagnum )}${tag}`;
if ( ! HAS_TEXT.test( tag ) && ! IS_SELF_CLOSING.test( tag ) && ! IS_END_TAG.test( tag ) ) {
tagnum ++;
}
return res;
} ).join( '\n' );
} // Convert an image into a png format for saving
function base64ToBuffer( str ) {
const b = atob( str );
const buf = new Uint8Array( b.length );
for ( let i = 0, l = buf.length; i < l; i ++ ) {
buf[ i ] = b.charCodeAt( i );
}
return buf;
}
let canvas, ctx;
function imageToData( image, ext ) {
canvas = canvas || document.createElement( 'canvas' );
ctx = ctx || canvas.getContext( '2d' );
canvas.width = image.width;
canvas.height = image.height;
ctx.drawImage( image, 0, 0 ); // Get the base64 encoded data
const base64data = canvas.toDataURL( `image/${ext}`, 1 ).replace( /^data:image\/(png|jpg);base64,/, '' ); // Convert to a uint8 array
return base64ToBuffer( base64data );
} // gets the attribute array. Generate a new array if the attribute is interleaved
const getFuncs = [ 'getX', 'getY', 'getZ', 'getW' ];
function attrBufferToArray( attr ) {
if ( attr.isInterleavedBufferAttribute ) {
// use the typed array constructor to save on memory
const arr = new attr.array.constructor( attr.count * attr.itemSize );
const size = attr.itemSize;
for ( let i = 0, l = attr.count; i < l; i ++ ) {
for ( let j = 0; j < size; j ++ ) {
arr[ i * size + j ] = attr[ getFuncs[ j ] ]( i );
}
}
return arr;
} else {
return attr.array;
}
} // Returns an array of the same type starting at the `st` index,
// and `ct` length
function subArray( arr, st, ct ) {
if ( Array.isArray( arr ) ) return arr.slice( st, st + ct ); else return new arr.constructor( arr.buffer, st * arr.BYTES_PER_ELEMENT, ct );
} // Returns the string for a geometry's attribute
function getAttribute( attr, name, params, type ) {
const array = attrBufferToArray( attr );
const res = `<source id="${name}">` + `<float_array id="${name}-array" count="${array.length}">` + array.join( ' ' ) + '</float_array>' + '<technique_common>' + `<accessor source="#${name}-array" count="${Math.floor( array.length / attr.itemSize )}" stride="${attr.itemSize}">` + params.map( n => `<param name="${n}" type="${type}" />` ).join( '' ) + '</accessor>' + '</technique_common>' + '</source>';
return res;
} // Returns the string for a node's transform information
let transMat;
function getTransform( o ) {
// ensure the object's matrix is up to date
// before saving the transform
o.updateMatrix();
transMat = transMat || new THREE.Matrix4();
transMat.copy( o.matrix );
transMat.transpose();
return `<matrix>${transMat.toArray().join( ' ' )}</matrix>`;
} // Process the given piece of geometry into the geometry library
// Returns the mesh id
function processGeometry( g ) {
let info = geometryInfo.get( g );
if ( ! info ) {
// convert the geometry to bufferGeometry if it isn't already
const bufferGeometry = g;
if ( bufferGeometry.isBufferGeometry !== true ) {
throw new Error( 'THREE.ColladaExporter: Geometry is not of type THREE.BufferGeometry.' );
}
const meshid = `Mesh${libraryGeometries.length + 1}`;
const indexCount = bufferGeometry.index ? bufferGeometry.index.count * bufferGeometry.index.itemSize : bufferGeometry.attributes.position.count;
const groups = bufferGeometry.groups != null && bufferGeometry.groups.length !== 0 ? bufferGeometry.groups : [ {
start: 0,
count: indexCount,
materialIndex: 0
} ];
const gname = g.name ? ` name="${g.name}"` : '';
let gnode = `<geometry id="${meshid}"${gname}><mesh>`; // define the geometry node and the vertices for the geometry
const posName = `${meshid}-position`;
const vertName = `${meshid}-vertices`;
gnode += getAttribute( bufferGeometry.attributes.position, posName, [ 'X', 'Y', 'Z' ], 'float' );
gnode += `<vertices id="${vertName}"><input semantic="POSITION" source="#${posName}" /></vertices>`; // NOTE: We're not optimizing the attribute arrays here, so they're all the same length and
// can therefore share the same triangle indices. However, MeshLab seems to have trouble opening
// models with attributes that share an offset.
// MeshLab Bug#424: https://sourceforge.net/p/meshlab/bugs/424/
// serialize normals
let triangleInputs = `<input semantic="VERTEX" source="#${vertName}" offset="0" />`;
if ( 'normal' in bufferGeometry.attributes ) {
const normName = `${meshid}-normal`;
gnode += getAttribute( bufferGeometry.attributes.normal, normName, [ 'X', 'Y', 'Z' ], 'float' );
triangleInputs += `<input semantic="NORMAL" source="#${normName}" offset="0" />`;
} // serialize uvs
if ( 'uv' in bufferGeometry.attributes ) {
const uvName = `${meshid}-texcoord`;
gnode += getAttribute( bufferGeometry.attributes.uv, uvName, [ 'S', 'T' ], 'float' );
triangleInputs += `<input semantic="TEXCOORD" source="#${uvName}" offset="0" set="0" />`;
} // serialize lightmap uvs
if ( 'uv2' in bufferGeometry.attributes ) {
const uvName = `${meshid}-texcoord2`;
gnode += getAttribute( bufferGeometry.attributes.uv2, uvName, [ 'S', 'T' ], 'float' );
triangleInputs += `<input semantic="TEXCOORD" source="#${uvName}" offset="0" set="1" />`;
} // serialize colors
if ( 'color' in bufferGeometry.attributes ) {
const colName = `${meshid}-color`;
gnode += getAttribute( bufferGeometry.attributes.color, colName, [ 'X', 'Y', 'Z' ], 'uint8' );
triangleInputs += `<input semantic="COLOR" source="#${colName}" offset="0" />`;
}
let indexArray = null;
if ( bufferGeometry.index ) {
indexArray = attrBufferToArray( bufferGeometry.index );
} else {
indexArray = new Array( indexCount );
for ( let i = 0, l = indexArray.length; i < l; i ++ ) indexArray[ i ] = i;
}
for ( let i = 0, l = groups.length; i < l; i ++ ) {
const group = groups[ i ];
const subarr = subArray( indexArray, group.start, group.count );
const polycount = subarr.length / 3;
gnode += `<triangles material="MESH_MATERIAL_${group.materialIndex}" count="${polycount}">`;
gnode += triangleInputs;
gnode += `<p>${subarr.join( ' ' )}</p>`;
gnode += '</triangles>';
}
gnode += '</mesh></geometry>';
libraryGeometries.push( gnode );
info = {
meshid: meshid,
bufferGeometry: bufferGeometry
};
geometryInfo.set( g, info );
}
return info;
} // Process the given texture into the image library
// Returns the image library
function processTexture( tex ) {
let texid = imageMap.get( tex );
if ( texid == null ) {
texid = `image-${libraryImages.length + 1}`;
const ext = 'png';
const name = tex.name || texid;
let imageNode = `<image id="${texid}" name="${name}">`;
if ( version === '1.5.0' ) {
imageNode += `<init_from><ref>${options.textureDirectory}${name}.${ext}</ref></init_from>`;
} else {
// version image node 1.4.1
imageNode += `<init_from>${options.textureDirectory}${name}.${ext}</init_from>`;
}
imageNode += '</image>';
libraryImages.push( imageNode );
imageMap.set( tex, texid );
textures.push( {
directory: options.textureDirectory,
name,
ext,
data: imageToData( tex.image, ext ),
original: tex
} );
}
return texid;
} // Process the given material into the material and effect libraries
// Returns the material id
function processMaterial( m ) {
let matid = materialMap.get( m );
if ( matid == null ) {
matid = `Mat${libraryEffects.length + 1}`;
let type = 'phong';
if ( m.isMeshLambertMaterial === true ) {
type = 'lambert';
} else if ( m.isMeshBasicMaterial === true ) {
type = 'constant';
if ( m.map !== null ) {
// The Collada spec does not support diffuse texture maps with the
// constant shader type.
// mrdoob/three.js#15469
console.warn( 'ColladaExporter: Texture maps not supported with THREE.MeshBasicMaterial.' );
}
}
const emissive = m.emissive ? m.emissive : new THREE.Color( 0, 0, 0 );
const diffuse = m.color ? m.color : new THREE.Color( 0, 0, 0 );
const specular = m.specular ? m.specular : new THREE.Color( 1, 1, 1 );
const shininess = m.shininess || 0;
const reflectivity = m.reflectivity || 0; // Do not export and alpha map for the reasons mentioned in issue (#13792)
// in three.js alpha maps are black and white, but collada expects the alpha
// channel to specify the transparency
let transparencyNode = '';
if ( m.transparent === true ) {
transparencyNode += '<transparent>' + ( m.map ? '<texture texture="diffuse-sampler"></texture>' : '<float>1</float>' ) + '</transparent>';
if ( m.opacity < 1 ) {
transparencyNode += `<transparency><float>${m.opacity}</float></transparency>`;
}
}
const techniqueNode = `<technique sid="common"><${type}>` + '<emission>' + ( m.emissiveMap ? '<texture texture="emissive-sampler" texcoord="TEXCOORD" />' : `<color sid="emission">${emissive.r} ${emissive.g} ${emissive.b} 1</color>` ) + '</emission>' + ( type !== 'constant' ? '<diffuse>' + ( m.map ? '<texture texture="diffuse-sampler" texcoord="TEXCOORD" />' : `<color sid="diffuse">${diffuse.r} ${diffuse.g} ${diffuse.b} 1</color>` ) + '</diffuse>' : '' ) + ( type !== 'constant' ? '<bump>' + ( m.normalMap ? '<texture texture="bump-sampler" texcoord="TEXCOORD" />' : '' ) + '</bump>' : '' ) + ( type === 'phong' ? `<specular><color sid="specular">${specular.r} ${specular.g} ${specular.b} 1</color></specular>` + '<shininess>' + ( m.specularMap ? '<texture texture="specular-sampler" texcoord="TEXCOORD" />' : `<float sid="shininess">${shininess}</float>` ) + '</shininess>' : '' ) + `<reflective><color>${diffuse.r} ${diffuse.g} ${diffuse.b} 1</color></reflective>` + `<reflectivity><float>${reflectivity}</float></reflectivity>` + transparencyNode + `</${type}></technique>`;
const effectnode = `<effect id="${matid}-effect">` + '<profile_COMMON>' + ( m.map ? '<newparam sid="diffuse-surface"><surface type="2D">' + `<init_from>${processTexture( m.map )}</init_from>` + '</surface></newparam>' + '<newparam sid="diffuse-sampler"><sampler2D><source>diffuse-surface</source></sampler2D></newparam>' : '' ) + ( m.specularMap ? '<newparam sid="specular-surface"><surface type="2D">' + `<init_from>${processTexture( m.specularMap )}</init_from>` + '</surface></newparam>' + '<newparam sid="specular-sampler"><sampler2D><source>specular-surface</source></sampler2D></newparam>' : '' ) + ( m.emissiveMap ? '<newparam sid="emissive-surface"><surface type="2D">' + `<init_from>${processTexture( m.emissiveMap )}</init_from>` + '</surface></newparam>' + '<newparam sid="emissive-sampler"><sampler2D><source>emissive-surface</source></sampler2D></newparam>' : '' ) + ( m.normalMap ? '<newparam sid="bump-surface"><surface type="2D">' + `<init_from>${processTexture( m.normalMap )}</init_from>` + '</surface></newparam>' + '<newparam sid="bump-sampler"><sampler2D><source>bump-surface</source></sampler2D></newparam>' : '' ) + techniqueNode + ( m.side === THREE.DoubleSide ? '<extra><technique profile="THREEJS"><double_sided sid="double_sided" type="int">1</double_sided></technique></extra>' : '' ) + '</profile_COMMON>' + '</effect>';
const materialName = m.name ? ` name="${m.name}"` : '';
const materialNode = `<material id="${matid}"${materialName}><instance_effect url="#${matid}-effect" /></material>`;
libraryMaterials.push( materialNode );
libraryEffects.push( effectnode );
materialMap.set( m, matid );
}
return matid;
} // Recursively process the object into a scene
function processObject( o ) {
let node = `<node name="${o.name}">`;
node += getTransform( o );
if ( o.isMesh === true && o.geometry !== null ) {
// function returns the id associated with the mesh and a "BufferGeometry" version
// of the geometry in case it's not a geometry.
const geomInfo = processGeometry( o.geometry );
const meshid = geomInfo.meshid;
const geometry = geomInfo.bufferGeometry; // ids of the materials to bind to the geometry
let matids = null;
let matidsArray; // get a list of materials to bind to the sub groups of the geometry.
// If the amount of subgroups is greater than the materials, than reuse
// the materials.
const mat = o.material || new THREE.MeshBasicMaterial();
const materials = Array.isArray( mat ) ? mat : [ mat ];
if ( geometry.groups.length > materials.length ) {
matidsArray = new Array( geometry.groups.length );
} else {
matidsArray = new Array( materials.length );
}
matids = matidsArray.fill().map( ( v, i ) => processMaterial( materials[ i % materials.length ] ) );
node += `<instance_geometry url="#${meshid}">` + ( matids != null ? '<bind_material><technique_common>' + matids.map( ( id, i ) => `<instance_material symbol="MESH_MATERIAL_${i}" target="#${id}" >` + '<bind_vertex_input semantic="TEXCOORD" input_semantic="TEXCOORD" input_set="0" />' + '</instance_material>' ).join( '' ) + '</technique_common></bind_material>' : '' ) + '</instance_geometry>';
}
o.children.forEach( c => node += processObject( c ) );
node += '</node>';
return node;
}
const geometryInfo = new WeakMap();
const materialMap = new WeakMap();
const imageMap = new WeakMap();
const textures = [];
const libraryImages = [];
const libraryGeometries = [];
const libraryEffects = [];
const libraryMaterials = [];
const libraryVisualScenes = processObject( object );
const specLink = version === '1.4.1' ? 'http://www.collada.org/2005/11/COLLADASchema' : 'https://www.khronos.org/collada/';
let dae = '<?xml version="1.0" encoding="UTF-8" standalone="no" ?>' + `<COLLADA xmlns="${specLink}" version="${version}">` + '<asset>' + ( '<contributor>' + '<authoring_tool>three.js Collada Exporter</authoring_tool>' + ( options.author !== null ? `<author>${options.author}</author>` : '' ) + '</contributor>' + `<created>${new Date().toISOString()}</created>` + `<modified>${new Date().toISOString()}</modified>` + ( options.unitName !== null ? `<unit name="${options.unitName}" meter="${options.unitMeter}" />` : '' ) + `<up_axis>${options.upAxis}</up_axis>` ) + '</asset>';
dae += `<library_images>${libraryImages.join( '' )}</library_images>`;
dae += `<library_effects>${libraryEffects.join( '' )}</library_effects>`;
dae += `<library_materials>${libraryMaterials.join( '' )}</library_materials>`;
dae += `<library_geometries>${libraryGeometries.join( '' )}</library_geometries>`;
dae += `<library_visual_scenes><visual_scene id="Scene" name="scene">${libraryVisualScenes}</visual_scene></library_visual_scenes>`;
dae += '<scene><instance_visual_scene url="#Scene"/></scene>';
dae += '</COLLADA>';
const res = {
data: format( dae ),
textures
};
if ( typeof onDone === 'function' ) {
requestAnimationFrame( () => onDone( res ) );
}
return res;
}
}
THREE.ColladaExporter = ColladaExporter;
} )();