GLTFExporter是一个用于将3D场景导出为glTF格式的JavaScript库。下面我将逐个讲解其入参、出参、属性、方法以及API使用方式。
入参(Input Parameters):
GLTFExporter的主要入参是要导出的场景对象和一些导出选项。具体来说:
- scene(场景对象): 这是要导出的3D场景对象,通常是使用Three.js等库构建的场景。
- options(导出选项): 这是一个可选的对象,其中包含一些配置项,用于控制导出的行为。例如,您可以指定是否将纹理嵌入到输出文件中、是否包含额外的信息等。
出参(Output):
GLTFExporter的出参是导出的glTF文件。根据您的配置,它可能是一个二进制文件(.glb)或包含多个文件的文件夹(.gltf)。
属性(Properties):
GLTFExporter对象可能具有一些属性,用于配置导出的行为。这些属性通常是一些默认设置,如缩放系数等。
方法(Methods):
GLTFExporter包含了执行导出的方法。
- parse(scene, options, onCompleted, onError): 这个方法执行实际的导出过程。它接受场景对象、导出选项以及两个回调函数作为参数。第一个回调函数(onCompleted)在导出成功完成时调用,并接收导出的结果作为参数。第二个回调函数(onError)在导出过程中出现错误时调用。
API方式使用(API Usage):
使用GLTFExporter的基本流程通常如下:
- 创建GLTFExporter对象。
- 定义导出选项(可选)。
- 使用parse方法将场景导出为glTF格式。
- 处理导出的结果,如保存到文件或进一步处理。
示例代码:
javascript
// 导入GLTFExporter库
import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js';
// 创建GLTFExporter对象
const exporter = new GLTFExporter();
// 定义导出选项(可选)
const options = {
binary: true, // 是否以二进制格式输出(默认为false,输出为.gltf文件)
includeCustomExtensions: true, // 是否包含自定义扩展信息
trs: true, // 是否将几何体的位置、旋转和缩放信息导出
animations: [], // 要导出的动画(如果有的话)
embedImages: false // 是否将图片嵌入到gltf文件中
};
// 使用parse方法导出场景为glTF格式
exporter.parse(scene, options, function (result) {
// 处理导出的结果
if (result instanceof ArrayBuffer) {
// 以二进制格式输出
saveArrayBuffer(result, 'scene.glb');
} else {
// 以JSON格式输出
const output = JSON.stringify(result, null, 2);
saveString(output, 'scene.gltf');
}
}, function (error) {
// 处理导出过程中出现的错误
console.error('Export error:', error);
});
// 保存导出的文件
function saveArrayBuffer(buffer, filename) {
// 实现保存二进制文件的逻辑
}
function saveString(text, filename) {
// 实现保存JSON文件的逻辑
}
所有源码
html
```html
<!DOCTYPE html>
<html lang="en">
<head>
<!-- 页面标题 -->
<title>three.js webgl - exporter - gltf</title>
<meta charset="utf-8">
<!-- 响应式布局 -->
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<!-- 引入 CSS 文件 -->
<link type="text/css" rel="stylesheet" href="main.css">
</head>
<body>
<!-- 信息提示 -->
<div id="info">
<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> webgl - exporter - gltf
</div>
<!-- 导入映射 -->
<script type="importmap">
{
"imports": {
"three": "../build/three.module.js",
"three/addons/": "./jsm/"
}
}
</script>
<!-- 主要 JavaScript 代码 -->
<script type="module">
// 导入所需的模块
import * as THREE from 'three'; // 导入 three.js 库
import { GLTFExporter } from 'three/addons/exporters/GLTFExporter.js'; // 导入 GLTFExporter 模块
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; // 导入 GLTFLoader 模块
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js'; // 导入 KTX2Loader 模块
import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js'; // 导入 MeshoptDecoder 模块
import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; // 导入 GUI 模块
// 导出 GLTF 文件的函数
function exportGLTF( input ) {
const gltfExporter = new GLTFExporter();
const options = {
trs: params.trs,
onlyVisible: params.onlyVisible,
binary: params.binary,
maxTextureSize: params.maxTextureSize
};
gltfExporter.parse(
input,
function ( result ) {
if ( result instanceof ArrayBuffer ) {
saveArrayBuffer( result, 'scene.glb' );
} else {
const output = JSON.stringify( result, null, 2 );
console.log( output );
saveString( output, 'scene.gltf' );
}
},
function ( error ) {
console.log( 'An error happened during parsing', error );
},
options
);
}
// 创建保存链接的函数
const link = document.createElement( 'a' );
link.style.display = 'none';
document.body.appendChild( link ); // Firefox 的兼容性解决方案,见 #6594
// 保存文件的函数
function save( blob, filename ) {
link.href = URL.createObjectURL( blob );
link.download = filename;
link.click();
// URL.revokeObjectURL( url ); breaks Firefox...
}
// 保存字符串到文件的函数
function saveString( text, filename ) {
save( new Blob( [ text ], { type: 'text/plain' } ), filename );
}
// 保存 ArrayBuffer 到文件的函数
function saveArrayBuffer( buffer, filename ) {
save( new Blob( [ buffer ], { type: 'application/octet-stream' } ), filename );
}
// 全局变量定义
let container; // 容器
let camera, object, object2, material, geometry, scene1, scene2, renderer; // 相机、物体、材质、几何体、场景、渲染器等
let gridHelper, sphere, model, coffeemat; // 网格帮助器、球体、模型、材质等
// 参数定义
const params = {
trs: false,
onlyVisible: true,
binary: false,
maxTextureSize: 4096,
exportScene1: exportScene1,
exportScenes: exportScenes,
exportSphere: exportSphere,
exportModel: exportModel,
exportObjects: exportObjects,
exportSceneObject: exportSceneObject,
exportCompressedObject: exportCompressedObject,
};
// 初始化函数
init();
animate();
// 初始化函数
function init() {
container = document.createElement( 'div' );
document.body.appendChild( container );
// 纹理数据
const data = new Uint8ClampedArray( 100 * 100 * 4 );
for ( let y = 0; y < 100; y ++ ) {
for ( let x = 0; x < 100; x ++ ) {
const stride = 4 * ( 100 * y + x );
data[ stride ] = Math.round( 255 * y / 99 );
data[ stride + 1 ] = Math.round( 255 - 255 * y / 99 );
data[ stride + 2 ] = 0;
data[ stride + 3 ] = 255;
}
}
// 渐变纹理
const gradientTexture = new THREE.DataTexture( data, 100, 100, THREE.RGBAFormat );
gradientTexture.minFilter = THREE.LinearFilter;
gradientTexture.magFilter = THREE.LinearFilter;
gradientTexture.needsUpdate = true;
// 第一个场景
scene1 = new THREE.Scene();
scene1.name = 'Scene1';
// 透视相机
camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 2000 );
camera.position.set( 600, 400, 0 );
camera.name = 'PerspectiveCamera';
scene1.add( camera );
// 环境光
const ambientLight = new THREE.AmbientLight( 0xcccccc );
ambientLight.name = 'AmbientLight';
scene1.add( ambientLight );
// 平行光
const dirLight = new THREE.DirectionalLight( 0xffffff, 3 );
dirLight.target.position.set( 0, 0, - 1 );
dirLight.add( dirLight.target );
dirLight.lookAt( - 1, - 1, 0 );
dirLight.name = 'DirectionalLight';
scene1.add( dirLight );
// 网格辅助器
gridHelper = new THREE.GridHelper( 2000, 20, 0xc1c1c1, 0x8d8d8d );
gridHelper.position.y = - 50;
gridHelper.name = 'Grid';
scene1.add( gridHelper );
// 坐标轴辅助器
const axes = new THREE.AxesHelper( 500 );
axes.name = 'AxesHelper';
scene1.add( axes );
// 基本材质的简单几何体
// 二十面体
const mapGrid = new THREE.TextureLoader().load( 'textures/uv_grid_opengl.jpg' );
mapGrid.wrapS = mapGrid.wrapT = THREE.RepeatWrapping;
mapGrid.colorSpace = THREE.SRGBColorSpace;
material = new THREE.MeshBasicMaterial( {
color: 0xffffff,
map: mapGrid
} );
object = new THREE.Mesh( new THREE.IcosahedronGeometry( 75, 0 ), material );
object.position.set( - 200, 0, 200 );
object.name = 'Icosahedron';
scene1.add( object );
// 八面体
material = new THREE.MeshBasicMaterial( {
color: 0x0000ff,
wireframe: true
} );
object = new THREE.Mesh( new THREE.OctahedronGeometry( 75, 1 ), material );
object.position.set( 0, 0, 200 );
object.name = 'Octahedron';
scene1.add( object );
// 四面体
material = new THREE.MeshBasicMaterial( {
color: 0xff0000,
transparent: true,
opacity: 0.5
} );
object = new THREE.Mesh( new THREE.TetrahedronGeometry( 75, 0 ), material );
object.position.set( 200, 0, 200 );
object.name = 'Tetrahedron';
scene1.add( object );
// 缓冲几何体原语
// 球体
material = new THREE.MeshStandardMaterial( {
color: 0xffff00,
metalness: 0.5,
roughness: 1.0,
flatShading: true,
} );
material.map = gradientTexture;
material.bumpMap = mapGrid;
sphere = new THREE.Mesh( new THREE.SphereGeometry( 70, 10, 10 ), material );
sphere.position.set( 0, 0, 0 );
sphere.name = 'Sphere';
scene1.add( sphere );
// 圆柱体
material = new THREE.MeshStandardMaterial( {
color: 0xff00ff,
flatShading: true
} );
object = new THREE.Mesh( new THREE.CylinderGeometry( 10, 80, 100 ), material );
object.position.set( 200, 0, 0 );
object.name = 'Cylinder';
scene1.add( object );
// 环面纹理
material = new THREE.MeshStandardMaterial( {
color: 0xff0000,
roughness: 1
} );
object = new THREE.Mesh( new THREE.TorusKnotGeometry( 50, 15, 40, 10 ), material );
object.position.set( - 200, 0, 0 );
object.name = 'Cylinder';
scene1.add( object );
// 组合体
const mapWood = new THREE.TextureLoader().load( 'textures/hardwood2_diffuse.jpg' );
material = new THREE.MeshStandardMaterial( { map: mapWood, side: THREE.DoubleSide } );
object = new THREE.Mesh( new THREE.BoxGeometry( 40, 100, 100 ), material );
object.position.set( - 200, 0, 400 );
object.name = 'Cube';
scene1.add( object );
object2 = new THREE.Mesh( new THREE.BoxGeometry( 40, 40, 40, 2, 2, 2 ), material );
object2.position.set( 0, 0, 50 );
object2.rotation.set( 0, 45, 0 );
object2.name = 'SubCube';
object.add( object2 );
// 群组
const group1 = new THREE.Group();
group1.name = 'Group';
scene1.add( group1 );
const group2 = new THREE.Group();
group2.name = 'subGroup';
group2.position.set( 0, 50, 0 );
group1.add( group2 );
object2 = new THREE.Mesh( new THREE.BoxGeometry( 30, 30, 30 ), material );
object2.name = 'Cube in group';
object2.position.set( 0, 0, 400 );
group2.add( object2 );
// 线条
geometry = new THREE.BufferGeometry();
let numPoints = 100;
let positions = new Float32Array( numPoints * 3 );
for ( let i = 0; i < numPoints; i ++ ) {
positions[ i * 3 ] = i;
positions[ i * 3 + 1 ] = Math.sin( i / 2 ) * 20;
positions[ i * 3 + 2 ] = 0;
}
geometry.setAttribute( 'position', new THREE.BufferAttribute( positions, 3 ) );
object = new THREE.Line( geometry, new THREE.LineBasicMaterial( { color: 0xffff00 } ) );
object.position.set( - 50, 0, - 200 );
scene1.add( object );
// 线环
geometry = new THREE.BufferGeometry();
numPoints = 5;
const radius = 70;
positions = new Float32Array( numPoints * 3 );
for ( let i = 0; i < numPoints; i ++ ) {
const s = i * Math.PI * 2 / numPoints;
positions[ i * 3 ] = radius * Math.sin( s );
positions[ i * 3 + 1 ] = radius * Math.cos( s );
positions[ i * 3 + 2 ] = 0;
}
geometry.setAttribute( 'position', new THREE.BufferAttribute( positions, 3 ) );
object = new THREE.LineLoop( geometry, new THREE.LineBasicMaterial( { color: 0xffff00 } ) );
object.position.set( 0, 0, - 200 );
scene1.add( object );
// 点
numPoints = 100;
const pointsArray = new Float32Array( numPoints * 3 );
for ( let i = 0; i < numPoints; i ++ ) {
pointsArray[ 3 * i ] = - 50 + Math.random() * 100;
pointsArray[ 3 * i + 1 ] = Math.random() * 100;
pointsArray[ 3 * i + 2 ] = - 50 + Math.random() * 100;
}
const pointsGeo = new THREE.BufferGeometry();
pointsGeo.setAttribute( 'position', new THREE.BufferAttribute( pointsArray, 3 ) );
const pointsMaterial = new THREE.PointsMaterial( { color: 0xffff00, size: 5 } );
const pointCloud = new THREE.Points( pointsGeo, pointsMaterial );
pointCloud.name = 'Points';
pointCloud.position.set( - 200, 0, - 200 );
scene1.add( pointCloud );
// 正交相机
const cameraOrtho = new THREE.OrthographicCamera( window.innerWidth / - 2, window.innerWidth / 2, window.innerHeight / 2, window.innerHeight / - 2, 0.1, 10 );
scene1.add( cameraOrtho );
cameraOrtho.name = 'OrthographicCamera';
material = new THREE.MeshLambertMaterial( {
color: 0xffff00,
side: THREE.DoubleSide
} );
object = new THREE.Mesh( new THREE.CircleGeometry( 50, 20, 0, Math.PI * 2 ), material );
object.position.set( 200, 0, - 400 );
scene1.add( object );
object = new THREE.Mesh( new THREE.RingGeometry( 10, 50, 20, 5, 0, Math.PI * 2 ), material );
object.position.set( 0, 0, - 400 );
scene1.add( object );
object = new THREE.Mesh( new THREE.CylinderGeometry( 25, 75, 100, 40, 5 ), material );
object.position.set( - 200, 0, - 400 );
scene1.add( object );
//
const points = [];
for ( let i = 0; i < 50; i ++ ) {
points.push( new THREE.Vector2( Math.sin( i * 0.2 ) * Math.sin( i * 0.1 ) * 15 + 50, ( i - 5 ) * 2 ) );
}
object = new THREE.Mesh( new THREE.LatheGeometry( points, 20 ), material );
object.position.set( 200, 0, 400 );
scene1.add( object );
// 用于测试 `onlyVisible` 选项的隐藏的大红色盒子
material = new THREE.MeshBasicMaterial( {
color: 0xff0000
} );
object = new THREE.Mesh( new THREE.BoxGeometry( 200, 200, 200 ), material );
object.position.set( 0, 0, 0 );
object.name = 'CubeHidden';
object.visible = false;
scene1.add( object );
// 需要 KHR_mesh_quantization 的模型
const loader = new GLTFLoader();
loader.load( 'models/gltf/ShaderBall.glb', function ( gltf ) {
model = gltf.scene;
model.scale.setScalar( 50 );
model.position.set( 200, - 40, - 200 );
scene1.add( model );
} );
// 需要 KHR_mesh_quantization 的模型
material = new THREE.MeshBasicMaterial( {
color: 0xffffff,
} );
object = new THREE.InstancedMesh( new THREE.BoxGeometry( 10, 10, 10, 2, 2, 2 ), material, 50 );
const matrix = new THREE.Matrix4();
const color = new THREE.Color();
for ( let i = 0; i < 50; i ++ ) {
matrix.setPosition( Math.random() * 100 - 50, Math.random() * 100 - 50, Math.random() * 100 - 50 );
object.setMatrixAt( i, matrix );
object.setColorAt( i, color.setHSL( i / 50, 1, 0.5 ) );
}
object.position.set( 400, 0, 200 );
scene1.add( object );
// 第二个 THREE.Scene
scene2 = new THREE.Scene();
object = new THREE.Mesh( new THREE.BoxGeometry( 100, 100, 100 ), material );
object.position.set( 0, 0, 0 );
object.name = 'Cube2ndScene';
scene2.name = 'Scene2';
scene2.add( object );
renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1;
container.appendChild( renderer.domElement );
window.addEventListener( 'resize', onWindowResize );
// 导出压缩的纹理和网格(KTX2 / Draco / Meshopt)
const ktx2Loader = new KTX2Loader()
.setTranscoderPath( 'jsm/libs/basis/' )
.detectSupport( renderer );
const gltfLoader = new GLTFLoader().setPath( 'models/gltf/' );
gltfLoader.setKTX2Loader( ktx2Loader );
gltfLoader.setMeshoptDecoder( MeshoptDecoder );
gltfLoader.load( 'coffeemat.glb', function ( gltf ) {
gltf.scene.position.x = 400;
gltf.scene.position.z = - 200;
scene1.add( gltf.scene );
coffeemat = gltf.scene;
} );
const gui = new GUI();
let h = gui.addFolder( 'Settings' );
h.add( params, 'trs' ).name( 'Use TRS' );
h.add( params, 'onlyVisible' ).name( 'Only Visible Objects' );
h.add( params, 'binary' ).name( 'Binary (GLB)' );
h.add( params, 'maxTextureSize', 2, 8192 ).name( 'Max Texture Size' ).step( 1 );
h = gui.addFolder( 'Export' );
h.add( params, 'exportScene1' ).name( 'Export Scene 1' );
h.add( params, 'exportScenes' ).name( 'Export Scene 1 and 2' );
h.add( params, 'exportSphere' ).name( 'Export Sphere' );
h.add( params, 'exportModel' ).name( 'Export Model' );
h.add( params, 'exportObjects' ).name( 'Export Sphere With Grid' );
h.add( params, 'exportSceneObject' ).name( 'Export Scene 1 and Object' );
h.add( params, 'exportCompressedObject' ).name( 'Export Coffeemat (from compressed data)' );
gui.open();
}
function exportScene1() {
exportGLTF( scene1 );
}
function exportScenes() {
exportGLTF( [ scene1, scene2 ] );
}
function exportSphere() {
exportGLTF( sphere );
}
function exportModel() {
exportGLTF( model );
}
function exportObjects() {
exportGLTF( [ sphere, gridHelper ] );
}
function exportSceneObject() {
exportGLTF( [ scene1, gridHelper ] );
}
function exportCompressedObject() {
exportGLTF( [ coffeemat ] );
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
}
function animate() {
requestAnimationFrame( animate );
render();
}
function render() {
const timer = Date.now() * 0.0001;
camera.position.x = Math.cos( timer ) * 800;
camera.position.z = Math.sin( timer ) * 800;
camera.lookAt( scene1.position );
renderer.render( scene1, camera );
}
</script>
</body>
</html>