Caracteristici și Funcționalități
Aspose.3D FOSS pentru TypeScript este o bibliotecă Node.js licențiată MIT pentru încărcarea, construirea și exportarea scenelor 3D. Vine cu definiții complete de tip TypeScript, o singură dependență la runtime (xmldom), și suport pentru șase formate majore de fișiere 3D. Această pagină este referința principală pentru toate zonele de funcționalitate și include exemple de cod TypeScript executabile pentru fiecare.
Instalare și configurare
Instalați pachetul din npm folosind o singură comandă:
npm install @aspose/3dPachetul vizează CommonJS și necesită Node.js 18 sau o versiune ulterioară. După instalare, verificați că tsconfig.json include următoarele opțiuni ale compilatorului pentru compatibilitate completă:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"moduleResolution": "node",
"esModuleInterop": true,
"strict": true
}
}Importați principalul Scene clasă din rădăcina pachetului. Clasele de opțiuni specifice formatului sunt importate din sub-căile lor corespunzătoare:
import { Scene } from '@aspose/3d';
import { ObjLoadOptions } from '@aspose/3d/formats/obj';
import { GltfSaveOptions, GltfFormat } from '@aspose/3d/formats/gltf';Caracteristici și Funcționalități
Suport pentru formate
Aspose.3D FOSS pentru TypeScript citește și scrie șase formate majore de fișiere 3D. Detectarea formatului este automată pe baza numerelor magice binare la încărcare, astfel că nu trebuie să specificați explicit formatul sursă.
| Format | Citire | Scriere | Note |
|---|---|---|---|
| OBJ (Wavefront) | Da | Da | Citește/scrie .mtl materiale; utilizare ObjLoadOptions.enableMaterials pentru import |
| glTF 2.0 | Da | Da | format text JSON; materiale PBR |
| GLB | Da | Da | Binar glTF; setat GltfSaveOptions.binaryMode = true |
| STL | Da | Da | Binar și ASCII; roundtrip complet verificat |
| 3MF | Da | Da | 3D Manufacturing Format with color and material metadata |
| FBX | Nu* | Nu* | Importator/exportator există, dar detectarea automată a formatului nu este conectată |
| COLLADA (DAE) | Da | Da | Scalarea unității, geometrie, materiale și clipuri de animație |
Încărcarea unui fișier OBJ cu materiale:
import { Scene } from '@aspose/3d';
import { ObjLoadOptions } from '@aspose/3d/formats/obj';
const scene = new Scene();
const options = new ObjLoadOptions();
options.enableMaterials = true;
options.flipCoordinateSystem = false;
options.scale = 1.0;
options.normalizeNormal = true;
scene.open('model.obj', options);Salvarea în GLB (glTF binar):
import { Scene } from '@aspose/3d';
import { GltfSaveOptions, GltfFormat } from '@aspose/3d/formats/gltf';
const scene = new Scene();
// ... build or load scene content
const opts = new GltfSaveOptions();
opts.binaryMode = true;
scene.save('output.glb', GltfFormat.getInstance(), opts);Graf de scenă
Tot conținutul 3D este organizat ca un arbore de Node obiecte cu rădăcina la scene.rootNode. Fiecare nod poate conține un Entity (un Mesh, Camera, Light, sau alt SceneObject) și un Transform care îl poziționează relativ la părintele său.
Clase cheie ale graficului de scenă:
Scene: containerul de nivel superior; conținerootNodeșianimationClipsNode: un nod de arbore denumit cuchildNodes,entity,transform, șimaterialsEntity: clasă de bază pentru obiecte atașabile (Mesh,Camera,Light)SceneObject: clasă de bază partajată deNodeșiEntityA3DObject: clasă de bază rădăcină cunameși container de proprietățiTransform: translație locală, rotație (Euler și Quaternion) și scară
Traversarea graficului de scenă:
import { Scene, Node, Mesh } from '@aspose/3d';
const scene = new Scene();
scene.open('model.obj');
function visit(node: Node, depth: number = 0): void {
const indent = ' '.repeat(depth);
console.log(`${indent}Node: ${node.name}`);
if (node.entity) {
console.log(`${indent} Entity: ${node.entity.constructor.name}`);
}
for (const child of node.childNodes) {
visit(child, depth + 1);
}
}
visit(scene.rootNode);Crearea ierarhiei scenei programatic:
import { Scene, Node } from '@aspose/3d';
const scene = new Scene();
const parent = scene.rootNode.createChildNode('chassis');
const wheel = parent.createChildNode('wheel_fl');
wheel.transform.translation.set(0.9, -0.3, 1.4);Geometrie și Mesh
Mesh este tipul principal de geometrie. Extinde Geometry și expune puncte de control (vârfuri), indici de poligon și elemente de vârf pentru normale, UV-uri și culori de vârf.
Clase cheie de geometrie:
Mesh: rețea de poligoane cucontrolPointsșipolygonCountGeometry: clasă de bază cu gestionarea elementelor de vârfVertexElementNormal: normale per-vârf sau per-vârf-poligonVertexElementUV: coordonate de textură (unul sau mai multe canale UV)VertexElementVertexColor: date de culoare per-vârfMappingMode: controlează modul în care datele elementului se mapă pe poligoane (CONTROL_POINT,POLYGON_VERTEX,POLYGON,EDGE,ALL_SAME)ReferenceMode: controlează strategia de indexare (DIRECT,INDEX,INDEX_TO_DIRECT)VertexElementType: identifică semantica unui element de vârfTextureMapping: enumerarea canalelor de textură
Citirea datelor de plasă dintr-o scenă încărcată:
import { Scene, Mesh, VertexElementType } from '@aspose/3d';
const scene = new Scene();
scene.open('model.stl');
for (const node of scene.rootNode.childNodes) {
if (node.entity instanceof Mesh) {
const mesh = node.entity as Mesh;
console.log(`Mesh "${node.name}": ${mesh.controlPoints.length} vertices, ${mesh.polygonCount} polygons`);
const normals = mesh.getElement(VertexElementType.NORMAL);
if (normals) {
console.log(` Normal mapping: ${normals.mappingMode}`);
}
}
}Sistem de materiale
Aspose.3D FOSS pentru TypeScript suportă trei tipuri de materiale care acoperă întreaga gamă, de la umbrire Phong moștenită la redare bazată pe fizică:
LambertMaterial: culoare difuză și culoare ambientală; se mapă la materiale simple OBJ/DAEPhongMaterial: adaugă culoare speculară, luciu și emisivă; tipul de material implicit OBJPbrMaterial: model fizic de rugozitate/metalic; utilizat pentru importul și exportul glTF 2.0
Citirea materialelor dintr-o scenă OBJ încărcată:
import { Scene, PhongMaterial, LambertMaterial } from '@aspose/3d';
import { ObjLoadOptions } from '@aspose/3d/formats/obj';
const scene = new Scene();
const options = new ObjLoadOptions();
options.enableMaterials = true;
scene.open('model.obj', options);
for (const node of scene.rootNode.childNodes) {
for (const mat of node.materials) {
if (mat instanceof PhongMaterial) {
const phong = mat as PhongMaterial;
console.log(` Phong: diffuse=${JSON.stringify(phong.diffuseColor)}, shininess=${phong.shininess}`);
} else if (mat instanceof LambertMaterial) {
console.log(` Lambert: diffuse=${JSON.stringify((mat as LambertMaterial).diffuseColor)}`);
}
}
}Aplicarea unui material PBR la construirea unei scene glTF:
import { Scene, Node, PbrMaterial } from '@aspose/3d';
import { Vector3 } from '@aspose/3d';
import { GltfSaveOptions, GltfFormat } from '@aspose/3d/formats/gltf';
const scene = new Scene();
const node = scene.rootNode.createChildNode('sphere');
const mat = new PbrMaterial();
mat.albedo = new Vector3(0.8, 0.2, 0.2); // red-tinted albedo; albedo starts null, must assign
mat.metallicFactor = 0.0;
mat.roughnessFactor = 0.5;
node.material = mat;
const opts = new GltfSaveOptions();
opts.binaryMode = false;
scene.save('output.gltf', GltfFormat.getInstance(), opts);Utilitare matematice
Biblioteca este livrată cu un set complet de tipuri matematice 3D, toate complet tipizate:
Vector3: vector cu 3 componente; suportăminus(),times(),dot(),cross(),normalize(),length,angleBetween()Vector4: vector cu 4 componente pentru coordonate omogeneMatrix4: matrice de transformare 4×4 cuconcatenate(),transpose,decompose,setTRSQuaternion: cuaternion de rotație cufromEulerAngle()(static, singular),eulerAngles()(instance method),slerp(),normalize()BoundingBox: cutie de delimitare aliniată pe axe cuminimum,maximum,center,size,mergeFVector3: variantă cu precizie simplă aVector3utilizat în datele elementelor de vârf
Calcularea unei cutii delimitatoare din vârfurile rețelei:
import { Scene, Mesh, Vector3, BoundingBox } from '@aspose/3d';
const scene = new Scene();
scene.open('model.obj');
let box = new BoundingBox();
for (const node of scene.rootNode.childNodes) {
if (node.entity instanceof Mesh) {
for (const pt of (node.entity as Mesh).controlPoints) {
box.merge(new Vector3(pt.x, pt.y, pt.z));
}
}
}
console.log('Center:', box.center);
console.log('Extents:', box.size);Construirea unei transformări din unghiuri Euler:
import { Quaternion, Vector3, Matrix4 } from '@aspose/3d';
const rot = Quaternion.fromEulerAngle(0, Math.PI / 4, 0); // 45° around Y
const mat = new Matrix4();
mat.setTRS(new Vector3(0, 0, 0), rot, new Vector3(1, 1, 1));Sistem de animație
API-ul de animație modelează clipuri, noduri, canale și secvențe de cadre cheie:
AnimationClip: colecție numită de noduri de animație; accesată prinscene.animationClips; expuneanimations: AnimationNode[]AnimationNode: grup denumitBindPoint: s; creat princlip.createAnimationNode(name), accesat princlip.animationsBindPoint: leagă unAnimationNode: la o proprietate specifică a unui obiect de scenă; expuneproperty: șichannelsCountAnimationChannel: extindeKeyframeSequence; deține un separatkeyframeSequence; accesat prinbindPoint.getChannel(name)KeyFrame: o singură pereche timp/valoare; transportă per-keyframeinterpolation: InterpolationKeyframeSequence: listă ordonată deKeyFrameobiecte prinkeyFrames; arepreBehaviorșipostBehavior(Extrapolation)Interpolation: enum:LINEAR,CONSTANT,BEZIER,B_SPLINE,CARDINAL_SPLINE,TCB_SPLINEExtrapolation: class cutype: ExtrapolationTypeșirepeatCount: numberExtrapolationType: enum:CONSTANT,GRADIENT,CYCLE,CYCLE_RELATIVE,OSCILLATE
Citirea datelor de animație dintr-o scenă încărcată:
import { Scene, AnimationNode, BindPoint } from '@aspose/3d';
const scene = new Scene();
scene.open('animated.dae'); // COLLADA animation import is supported
for (const clip of scene.animationClips) {
console.log(`Clip: "${clip.name}"`);
for (const animNode of clip.animations) { // clip.animations, not clip.nodes
console.log(` AnimationNode: ${animNode.name}`);
for (const bp of animNode.bindPoints) { // animNode.bindPoints, not animNode.channels
console.log(` BindPoint: property="${bp.property.name}", channels=${bp.channelsCount}`);
}
}
}Suport pentru fluxuri și buffer-e
Utilizați scene.openFromBuffer() pentru a încărca o scenă 3D direct din memorie Buffer. Acesta este modelul recomandat pentru funcții serverless, conducte de streaming și procesarea activelor preluate prin HTTP fără a scrie pe disc.
import { Scene } from '@aspose/3d';
import { ObjLoadOptions } from '@aspose/3d/formats/obj';
import * as fs from 'fs';
// Load file into memory, then parse from buffer
const buffer: Buffer = fs.readFileSync('model.obj');
const scene = new Scene();
const options = new ObjLoadOptions();
options.enableMaterials = true;
scene.openFromBuffer(buffer, options);
for (const node of scene.rootNode.childNodes) {
if (node.entity) {
console.log(node.name, node.entity.constructor.name);
}
}Detectarea automată a formatului pe baza numerelor magice binare se aplică la încărcarea din buffer, astfel încât fișierele GLB, STL binare și 3MF sunt recunoscute fără a specifica un parametru de format.
Exemple de utilizare
Exemplul 1: Încărcare OBJ și export în GLB
Acest exemplu încarcă un fișier Wavefront OBJ cu materiale, apoi reexportă scena ca fișier binar glTF (GLB) potrivit pentru utilizarea pe web și în motoare de joc.
import { Scene } from '@aspose/3d';
import { ObjLoadOptions } from '@aspose/3d/formats/obj';
import { GltfSaveOptions, GltfFormat } from '@aspose/3d/formats/gltf';
function convertObjToGlb(inputPath: string, outputPath: string): void {
const scene = new Scene();
const loadOpts = new ObjLoadOptions();
loadOpts.enableMaterials = true;
loadOpts.flipCoordinateSystem = false;
loadOpts.normalizeNormal = true;
scene.open(inputPath, loadOpts);
// Report what was loaded
for (const node of scene.rootNode.childNodes) {
if (node.entity) {
console.log(`Loaded: ${node.name} (${node.entity.constructor.name})`);
}
}
const saveOpts = new GltfSaveOptions();
saveOpts.binaryMode = true; // write .glb instead of .gltf + .bin
scene.save(outputPath, GltfFormat.getInstance(), saveOpts);
console.log(`Exported GLB to: ${outputPath}`);
}
convertObjToGlb('input.obj', 'output.glb');Exemplul 2: Tur complet STL cu validare a normalelor
Acest exemplu încarcă un fișier STL binar, afișează informațiile de normală per vertex, apoi reexportă scena ca STL ASCII și verifică turul complet.
import { Scene, Mesh, VertexElementNormal, VertexElementType } from '@aspose/3d';
import { StlLoadOptions, StlSaveOptions } from '@aspose/3d/formats/stl';
const scene = new Scene();
const loadOpts = new StlLoadOptions();
scene.open('model.stl', loadOpts);
let totalPolygons = 0;
for (const node of scene.rootNode.childNodes) {
if (node.entity instanceof Mesh) {
const mesh = node.entity as Mesh;
totalPolygons += mesh.polygonCount;
const normElem = mesh.getElement(VertexElementType.NORMAL) as VertexElementNormal | null;
if (normElem) {
console.log(` Normals: ${normElem.data.length} entries, mapping=${normElem.mappingMode}`);
}
}
}
console.log(`Total polygons: ${totalPolygons}`);
// Re-export as ASCII STL
const saveOpts = new StlSaveOptions();
saveOpts.binaryMode = false; // ASCII output
scene.save('output_ascii.stl', saveOpts);Exemplul 3: Construiește o scenă programatic și salveaz-o ca glTF
Acest exemplu construiește o scenă cu un material PBR de la zero și o salvează ca un fișier JSON glTF.
import { Scene, Mesh, PbrMaterial, Vector4, Vector3 } from '@aspose/3d';
import { GltfSaveOptions, GltfFormat } from '@aspose/3d/formats/gltf';
const scene = new Scene();
const node = scene.rootNode.createChildNode('floor');
// Build a simple quad mesh (two triangles)
// controlPoints are Vector4 (x, y, z, w) where w=1 for positions
const mesh = new Mesh();
mesh.controlPoints.push(
new Vector4(-1, 0, -1, 1),
new Vector4( 1, 0, -1, 1),
new Vector4( 1, 0, 1, 1),
new Vector4(-1, 0, 1, 1),
);
mesh.createPolygon([0, 1, 2]);
mesh.createPolygon([0, 2, 3]);
node.entity = mesh;
// Apply a PBR material
const mat = new PbrMaterial();
mat.albedo = new Vector3(0.6, 0.6, 0.6); // albedo starts null, must assign
mat.metallicFactor = 0.0;
mat.roughnessFactor = 0.8;
node.material = mat;
// Save as JSON glTF
const opts = new GltfSaveOptions();
opts.binaryMode = false;
scene.save('floor.gltf', GltfFormat.getInstance(), opts);
console.log('Scene written to floor.gltf');Sfaturi și bune practici
- Utilizați
ObjLoadOptions.enableMaterials = truede fiecare dată când aveți nevoie de date de material din fișierele .mtl. Fără ele, lista de materiale de pe fiecare nod va fi goală. - Preferă
binaryMode = truepentru GLB când produceți active pentru web sau motoare de jocuri. GLB binar este un singur fișier auto‑conținut și se încarcă mai repede în browsere și motoare decât versiunea JSON + .bin. - Utilizați
openFromBuffer()în medii serverless pentru a evita I/O‑ul fișierelor temporare. Preluați activul, transmitețiBufferdirect, și scrieți ieșirea într-un flux sau alt buffer. - Verificați
node.entityînainte de a face casting: nu toate nodurile conțin o entitate. Întotdeauna protejați cu uninstanceofverifică înainte de a accesaMesh-proprietăți specifice, cum ar ficontrolPoints. - Setare
normalizeNormal = trueînObjLoadOptionscând fișierele tale OBJ sursă provin din surse neîncredere. Acest lucru împiedică propagarea normalelor degenerate în etapele ulterioare de redare sau validare. - Păstrează
strict: trueîn tsconfig.json: biblioteca este scrisă cunoImplicitAnyșistrictNullChecks. Dezactivareastrictmaschează erorile reale de tip și diminuează valoarea API-ului tipizat. - Parcurge prin
childNodes, nu o buclă de index: thechildNodesproprietatea returnează un iterabil; evită să te bazezi pe indexarea numerică pentru compatibilitate viitoare.
Probleme comune
| Simptom | Cauză Probabilă | Remediere |
|---|---|---|
| Lista de materiale este goală după încărcarea OBJ | enableMaterials nu este setat | Setează options.enableMaterials = true |
| Fișierul GLB conține un fișier .bin separat | binaryMode se setează implicit la false | Setează opts.binaryMode = true |
| Normalele vârfurilor lipsesc în ieșirea STL | Modului STL ASCII omite normalele pe fiecare față | Comută la binaryMode = true sau calculează normalele înainte de export |
node.entity este întotdeauna null | Se parcurge doar rootNode, nu copiii săi | Recursiv în node.childNodes |
| Eroare TypeScript: proprietatea nu există | Vechi @types cache | Rulează npm install @aspose/3d din nou; fără separare @types pachetul este necesar |
openFromBuffer aruncă eroare de format | Formatul nu poate fi detectat automat din magic | Transmite clasa de opțiune de format explicită ca al doilea argument |
Întrebări frecvente
Necesită biblioteca vreun add-on nativ sau pachete de sistem? Nu. Aspose.3D FOSS pentru TypeScript are o singură dependență la runtime: xmldom, care este JavaScript pur și este instalat automat prin npm. Nu există .node native addons și fără pachete de sistem de instalat.
Ce versiuni de Node.js sunt suportate? Node.js 18, 20 și 22 LTS. Biblioteca vizează ieșire CommonJS și folosește caracteristici de limbaj ES2020 în interior.
Pot folosi biblioteca într-un pachet pentru browser (webpack/esbuild)? Biblioteca vizează Node.js și folosește Node.js fs și Buffer API-uri. Crearea de pachete pentru browser nu este suportată oficial. Pentru utilizarea în browser, încarcă scena pe server și transmite rezultatul (de ex., ca GLB) clientului.
Care este diferența dintre GltfSaveOptions.binaryMode = true și false? binaryMode = false produce un .gltf fișier JSON plus un separat .bin sidecar de buffer binar. binaryMode = true produce un singur element auto-conținut .glb fișier. Utilizați true pentru livrarea activelor în producție.
Pot încărca un fișier dintr-un răspuns HTTP fără să îl salvez pe disc? Da. Preiați răspunsul ca un Buffer (de exemplu, utilizând node-fetch sau încorporată fetch în Node 18+), apoi apelați scene.openFromBuffer(buffer, options).
Este suportul FBX complet? Nu. Clasele de import și export FBX există în bibliotecă, dar FBX nu este integrat în Scene.open() sau Scene.save() detectare automată. Apelarea scene.open('file.fbx') nu va invoca importatorul FBX; fișierul va fi gestionat prin calea de rezervă STL. Folosiți direct clasele de import/export specifice FBX dacă aveți nevoie de I/O FBX. Consultați tabelul de suport al formatelor de mai sus care marchează FBX ca No*.
Suportă biblioteca TypeScript 4.x? Se recomandă TypeScript 5.0+. TypeScript 4.7+ ar trebui să funcționeze în practică, dar biblioteca este testată și dezvoltată pentru 5.0+.
Rezumat al referinței API
| Clasă | Modul | Scop |
|---|---|---|
Scene | @aspose/3d | Container de scenă la nivel superior; open(), openFromBuffer(), save(), rootNode, animationClips |
Node | @aspose/3d | Nod al graficului de scenă; childNodes, entity, transform, materials, createChildNode() |
Entity | @aspose/3d | Clasă de bază pentru obiecte atașabile la scenă |
SceneObject | @aspose/3d | Clasa de bază partajată de Node și Entity |
A3DObject | @aspose/3d | Baza rădăcină cu name și pungă de proprietăți |
Transform | @aspose/3d | Translație, rotație și scară locale |
Mesh | @aspose/3d | Plasă poligonală; controlPoints, polygonCount, createPolygon(), elemente de vârf |
Geometry | @aspose/3d | Clasă de bază pentru tipuri de geometrie |
Camera | @aspose/3d | Entitate cameră cu unghi de vizualizare și setări de proiecție |
Light | @aspose/3d | Entitate lumină (punct, direcțional, spot) |
LambertMaterial | @aspose/3d | Model de umbrire difuz + ambiental |
PhongMaterial | @aspose/3d | Umbrire Phong cu componentă speculară și emisivă |
PbrMaterial | @aspose/3d | Model fizic bazat pe rugozitate/metalic pentru glTF |
Vector3 | @aspose/3d | 3-component double-precision vector |
Vector4 | @aspose/3d | 4-component vector for homogeneous math |
Matrix4 | @aspose/3d | 4×4 transformation matrix |
Quaternion | @aspose/3d | Cuaternion de rotație |
BoundingBox | @aspose/3d | Cutie de delimitare aliniată pe axe |
FVector3 | @aspose/3d | Variantă cu precizie simplă a Vector3 |
VertexElementNormal | @aspose/3d | Normale per-vertex sau per-polygon-vertex |
VertexElementUV | @aspose/3d | Element de vertex pentru coordonatele texturii |
VertexElementVertexColor | @aspose/3d | Element de vertex pentru culoarea per-vertex |
MappingMode | @aspose/3d | Enum: CONTROL_POINT, POLYGON_VERTEX, POLYGON, ALL_SAME |
ReferenceMode | @aspose/3d | Enum: DIRECT, INDEX, INDEX_TO_DIRECT |
AnimationClip | @aspose/3d | Animație numită; expune animations: AnimationNode[]; creat prin scene.createAnimationClip(name) |
AnimationNode | @aspose/3d | Grup denumit de BindPoints; creat prin clip.createAnimationNode(name) |
BindPoint | @aspose/3d | Leagă un AnimationNode la o proprietate a obiectului de scenă; expune property și channelsCount |
AnimationChannel | @aspose/3d | Extinde KeyframeSequence; conține un keyframeSequence; accesat prin bindPoint.getChannel(name) |
KeyFrame | @aspose/3d | Pereche unică de cadru cheie timp/valoare; conține interpolation: Interpolation |
KeyframeSequence | @aspose/3d | Ordinate keyFrames listă; preBehavior/postBehavior sunt Extrapolation obiecte |
Interpolation | @aspose/3d | Enum: LINEAR, CONSTANT, BEZIER, B_SPLINE, CARDINAL_SPLINE, TCB_SPLINE |
Extrapolation | @aspose/3d | Clasă cu type: ExtrapolationType și repeatCount: number |
ExtrapolationType | @aspose/3d | Enum: CONSTANT, GRADIENT, CYCLE, CYCLE_RELATIVE, OSCILLATE |
ObjLoadOptions | @aspose/3d/formats/obj | Opțiuni de import OBJ: enableMaterials, flipCoordinateSystem, scale, normalizeNormal |
GltfSaveOptions | @aspose/3d/formats/gltf | Opțiuni de export glTF/GLB: binaryMode |
GltfFormat | @aspose/3d/formats/gltf | Instanță de format pentru glTF/GLB; transmite la scene.save() |
StlLoadOptions | @aspose/3d/formats/stl | Opțiuni de import STL |
StlSaveOptions | @aspose/3d/formats/stl | Opțiuni de export STL: binaryMode |
StlImporter | @aspose/3d/formats/stl | Cititor STL de nivel scăzut |
StlExporter | @aspose/3d/formats/stl | Scriitor STL de nivel scăzut |