ahg3DModelPlugin - Technical Documentation¶
Version: 1.2.0 Category: Digital Asset Management Dependencies: atom-framework, ahgCorePlugin
Overview¶
A comprehensive 3D model viewing plugin for Access to Memory (AtoM) providing WebGL-based visualization, augmented reality (AR) capabilities, interactive hotspot annotations, Gaussian Splat support, and IIIF 3D manifest generation for museum and archival collections.
Architecture¶
+---------------------------------------------------------------------+
| ahg3DModelPlugin |
+---------------------------------------------------------------------+
| |
| +---------------------------------------------------------------+ |
| | Symfony Plugin Layer | |
| | * ahg3DModelPluginConfiguration (routes, modules) | |
| | * model3d module (CRUD actions) | |
| | * model3dSettings module (admin configuration) | |
| +---------------------------------------------------------------+ |
| | |
| v |
| +---------------------------------------------------------------+ |
| | Service Layer | |
| | * Model3DService (CRUD, viewer HTML generation) | |
| | * Ar3dService (format detection, config) | |
| | * ThreeDThumbnailService (thumbnail generation) | |
| +---------------------------------------------------------------+ |
| | |
| v |
| +---------------------------------------------------------------+ |
| | Provider Layer | |
| | * Model3DProvider (framework integration) | |
| +---------------------------------------------------------------+ |
| | |
| v |
| +---------------------------------------------------------------+ |
| | Frontend Viewers | |
| | * Google Model Viewer (WebXR/AR) | |
| | * Three.js (fallback) | |
| | * GaussianSplats3D (point cloud) | |
| +---------------------------------------------------------------+ |
| | |
| v |
| +---------------------------------------------------------------+ |
| | Database Layer | |
| | * object_3d_model, object_3d_hotspot | |
| | * viewer_3d_settings, iiif_3d_manifest | |
| +---------------------------------------------------------------+ |
| |
+---------------------------------------------------------------------+
Supported Formats¶
| Format | Extension | MIME Type | AR Support | Notes |
|---|---|---|---|---|
| glTF Binary | .glb |
model/gltf-binary |
Yes | Recommended. Single file with embedded textures |
| glTF JSON | .gltf |
model/gltf+json |
Yes | Multiple files, external textures |
| Apple AR | .usdz |
model/vnd.usdz+zip |
iOS only | Required for iOS Quick Look |
| Wavefront | .obj |
model/obj |
Limited | No animations, widely supported |
| Stereolithography | .stl |
model/stl |
Limited | 3D printing standard, no textures |
| Autodesk FBX | .fbx |
application/octet-stream |
No | Proprietary format |
| Polygon File | .ply |
application/x-ply |
No | Point cloud support |
| Gaussian Splat | .splat, .ksplat |
N/A | No | Neural radiance fields |
Database Schema¶
ERD Diagram¶
+----------------------------------+
| object_3d_model |
+----------------------------------+
| PK id INT |
| FK object_id INT |---> information_object
| |
| -- File Info -- |
| filename VARCHAR(255) |
| original_filename VARCHAR(255)|
| file_path VARCHAR(500) |
| file_size BIGINT |
| mime_type VARCHAR(100) |
| format ENUM(glb,gltf,...) |
| |
| -- Model Metadata -- |
| vertex_count INT |
| face_count INT |
| texture_count INT |
| animation_count INT |
| has_materials TINYINT |
| |
| -- Viewer Settings -- |
| auto_rotate TINYINT |
| rotation_speed DECIMAL(3,2) |
| camera_orbit VARCHAR(100) |
| field_of_view VARCHAR(20) |
| exposure DECIMAL(3,2) |
| shadow_intensity DECIMAL(3,2) |
| background_color VARCHAR(20) |
| |
| -- AR Settings -- |
| ar_enabled TINYINT |
| ar_scale VARCHAR(20) |
| ar_placement ENUM(floor,wall) |
| |
| -- Display -- |
| poster_image VARCHAR(500) |
| thumbnail VARCHAR(500) |
| is_primary TINYINT |
| is_public TINYINT |
| display_order INT |
| |
| FK created_by INT |
| FK updated_by INT |
| created_at TIMESTAMP |
| updated_at TIMESTAMP |
+----------------------------------+
|
| 1:N
v
+----------------------------------+
| object_3d_model_i18n |
+----------------------------------+
| PK id INT |
| FK model_id INT |
| culture VARCHAR(10) |
| title VARCHAR(255) |
| description TEXT |
| alt_text VARCHAR(500) |
+----------------------------------+
|
| 1:N (from object_3d_model)
v
+----------------------------------+
| object_3d_hotspot |
+----------------------------------+
| PK id INT |
| FK model_id INT |
| hotspot_type ENUM |
| position_x DECIMAL(10,6) |
| position_y DECIMAL(10,6) |
| position_z DECIMAL(10,6) |
| normal_x DECIMAL(10,6) |
| normal_y DECIMAL(10,6) |
| normal_z DECIMAL(10,6) |
| icon VARCHAR(50) |
| color VARCHAR(20) |
| link_url VARCHAR(500) |
| link_target ENUM |
| display_order INT |
| is_visible TINYINT |
| created_at TIMESTAMP |
+----------------------------------+
|
| 1:N
v
+----------------------------------+
| object_3d_hotspot_i18n |
+----------------------------------+
| PK id INT |
| FK hotspot_id INT |
| culture VARCHAR(10) |
| title VARCHAR(255) |
| description TEXT |
+----------------------------------+
+----------------------------------+
| object_3d_texture |
+----------------------------------+
| PK id INT |
| FK model_id INT |
| texture_type ENUM |
| filename VARCHAR(255) |
| file_path VARCHAR(500) |
| width INT |
| height INT |
| created_at TIMESTAMP |
+----------------------------------+
+----------------------------------+
| viewer_3d_settings |
+----------------------------------+
| PK id INT |
| setting_key VARCHAR(100) |
| setting_value TEXT |
| setting_type ENUM |
| description VARCHAR(500) |
| updated_at TIMESTAMP |
+----------------------------------+
+----------------------------------+
| iiif_3d_manifest |
+----------------------------------+
| PK id INT |
| FK model_id INT (UNIQUE) |
| manifest_json LONGTEXT |
| manifest_hash VARCHAR(64) |
| generated_at TIMESTAMP |
+----------------------------------+
+----------------------------------+
| object_3d_audit_log |
+----------------------------------+
| PK id INT |
| FK model_id INT |
| FK object_id INT |
| FK user_id INT |
| user_name VARCHAR(255) |
| action ENUM |
| details JSON |
| ip_address VARCHAR(45) |
| user_agent VARCHAR(500) |
| created_at TIMESTAMP |
+----------------------------------+
Hotspot Types¶
| Type | Color | Icon | Use Case |
|---|---|---|---|
| annotation | #1a73e8 (Blue) |
info | General notes and comments |
| info | #34a853 (Green) |
info-circle | Information points |
| damage | #ea4335 (Red) |
exclamation-triangle | Condition documentation |
| detail | #fbbc04 (Yellow) |
search | Highlight features |
| link | #4285f4 (Blue) |
link | External URL links |
Texture Types¶
| Type | Description |
|---|---|
| diffuse | Base color/albedo map |
| normal | Normal/bump map |
| roughness | Surface roughness map |
| metallic | Metallic/reflectivity map |
| ao | Ambient occlusion map |
| emissive | Self-illumination map |
| environment | Environment/reflection map |
Service Methods¶
Model3DService¶
namespace Ahg3DModel\Services;
class Model3DService
{
// Model CRUD
public function getModelsForObject(int $objectId): array
public function getPrimaryModel(int $objectId): ?object
public function getModel(int $modelId): ?object
public function createModel(int $objectId, array $data): int
public function updateModel(int $modelId, array $data): bool
public function deleteModel(int $modelId): bool
// Viewer HTML Generation
public function getModelViewerHtml(int $modelId, array $options = []): string
public function getThreeJsViewerHtml(int $modelId, array $options = []): string
// Hotspots
public function getHotspots(int $modelId): array
public function addHotspot(int $modelId, array $data): int
public function updateHotspot(int $hotspotId, array $data): bool
public function deleteHotspot(int $hotspotId): bool
// Settings
public function getSetting(string $key, $default = null): mixed
public function saveSetting(string $key, $value, string $type = 'string'): bool
// IIIF
public function generateIiif3dManifest(int $modelId): array
}
Ar3dService¶
namespace AtomExtensions\Extensions\Ar3dViewer\Services;
class Ar3dService implements Ar3dServiceInterface
{
public function is3dModel(int $digitalObjectId): bool
public function getViewerConfig(int $digitalObjectId): array
public function getSupportedFormats(): array
}
ThreeDThumbnailService¶
namespace AtomExtensions\Services;
class ThreeDThumbnailService
{
// Model detection
public function is3DModel(string $filename): bool
public function is3DMimeType(string $mime): bool
public function getSupportedMimeTypes(): array
// Single thumbnail generation
public function generateThumbnail(string $inputPath, string $outputPath, int $width = 512, int $height = 512): bool
// Derivative generation (thumbnail + reference image)
public function createDerivatives(int $digitalObjectId): bool
// Multi-angle rendering (6 views via Blender)
public function generateMultiAngle(string $inputPath, string $outputDir, int $size = 1024): array
// Returns: ['front' => '/path/front.png', 'back' => '...', 'left' => '...', 'right' => '...', 'top' => '...', 'detail' => '...']
// Multi-angle directory for a digital object
public function getMultiAngleDir(int $digitalObjectId): string
// Batch processing
public function batchProcessExisting(): array
// Returns: ['processed' => N, 'success' => N, 'failed' => N]
}
Supported MIME Types¶
| MIME Type | Format |
|---|---|
model/obj |
Wavefront OBJ |
model/gltf-binary |
GLB |
model/gltf+json |
GLTF |
model/stl |
STL |
application/x-tgif |
OBJ (common mislabel) |
model/vnd.usdz+zip |
USDZ |
application/x-ply |
PLY |
Multi-Angle Camera Positions¶
| View | Azimuth | Elevation | Purpose |
|---|---|---|---|
| Front | 0° | 15° | Primary view |
| Back | 180° | 15° | Reverse view |
| Left | 270° | 15° | Left profile |
| Right | 90° | 15° | Right profile |
| Top | 0° | 80° | Bird's-eye |
| Detail | 45° | 35° | Close-up detail |
Model3DProvider¶
namespace ahg3DModelPlugin\Provider;
class Model3DProvider implements Model3DProviderInterface
{
public function is3dModel(int $digitalObjectId): bool
public function getViewerConfig(int $digitalObjectId): array
public function getSupportedFormats(): array
public function generateThumbnail(int $digitalObjectId, array $options = []): array
}
Helper Functions¶
Include the helper file in templates:
| Function | Description | Returns |
|---|---|---|
has_3d_model($resource) |
Check if object has 3D models | bool |
get_3d_models($resource) |
Get all 3D models for object | array |
get_primary_3d_model($resource) |
Get primary 3D model | ?object |
render_3d_model($resource, $options) |
Render primary model viewer | string |
render_3d_model_viewer($modelId, $options) |
Render viewer by model ID | string |
render_3d_model_gallery($resource, $options) |
Render tabbed gallery | string |
get_3d_model_upload_url($resource) |
Get upload URL for object | string |
get_iiif_3d_manifest_url($modelId) |
Get IIIF manifest URL | string |
is_3d_format($extension) |
Check if extension is 3D format | bool |
is_splat_format($extension) |
Check if extension is Gaussian Splat | bool |
render_splat_viewer($url, $options) |
Render Gaussian Splat viewer | string |
get_3d_format_label($format) |
Get human-readable format label | string |
render_3d_upload_button($resource) |
Render upload button (editors) | string |
Viewer Options¶
| Option | Type | Default | Description |
|---|---|---|---|
height |
string | '500px' |
Viewer container height |
viewer_type |
string | 'model-viewer' |
'model-viewer' or 'threejs' |
show_hotspots |
bool | true |
Display hotspot annotations |
base_url |
string | auto | Base URL for model files |
Routes¶
Web Routes¶
| Route | Method | URL | Description |
|---|---|---|---|
ar3d_model_index |
GET | /ahg3DModel/index |
List all 3D models |
ar3d_model_view |
GET | /ahg3DModel/view/:id |
View single model |
ar3d_model_upload |
GET/POST | /ahg3DModel/upload |
Upload new model |
ar3d_model_edit |
GET/POST | /ahg3DModel/edit/:id |
Edit model settings |
ar3d_model_delete |
POST | /ahg3DModel/delete/:id |
Delete model |
ar3d_viewer_embed |
GET | /ahg3DModel/embed/:id |
Embeddable viewer |
ar3d_settings |
GET/POST | /ahg3DSettings/index |
Global settings |
API Routes¶
| Route | Method | URL | Response |
|---|---|---|---|
ar3d_api_models |
GET | /api/3d/models/:object_id |
JSON array of models |
ar3d_api_hotspots |
GET | /api/3d/hotspots/:model_id |
JSON array of hotspots |
ar3d_hotspot_add |
POST | /ahg3DModel/addHotspot/:id |
JSON {success, id} |
ar3d_hotspot_delete |
POST | /ahg3DModel/deleteHotspot/:id |
JSON {success} |
ar3d_iiif_manifest |
GET | /iiif/3d/:id/manifest.json |
IIIF 3D manifest |
IIIF 3D Manifest¶
Each model has an IIIF 3D-compliant manifest at /iiif/3d/{model_id}/manifest.json:
{
"@context": [
"http://iiif.io/api/presentation/3/context.json",
"http://iiif.io/api/extension/3d/context.json"
],
"id": "https://example.com/iiif/3d/1/manifest.json",
"type": "Manifest",
"label": { "en": ["Bronze Statue"] },
"summary": { "en": ["19th century bronze figure"] },
"metadata": [
{ "label": { "en": ["Format"] }, "value": { "en": ["GLB"] } },
{ "label": { "en": ["File Size"] }, "value": { "en": ["5.2 MB"] } }
],
"items": [
{
"id": "https://example.com/iiif/3d/1/scene/1",
"type": "Scene",
"items": [
{
"id": "https://example.com/iiif/3d/1/annotation/1",
"type": "Annotation",
"motivation": "painting",
"body": {
"id": "https://example.com/uploads/3d/1/model.glb",
"type": "Model",
"format": "model/gltf-binary"
},
"target": "https://example.com/iiif/3d/1/scene/1"
}
]
}
],
"annotations": [
{
"id": "https://example.com/iiif/3d/1/annotations/1",
"type": "AnnotationPage",
"items": [
{
"id": "https://example.com/iiif/3d/1/hotspot/1",
"type": "Annotation",
"motivation": "commenting",
"body": {
"type": "TextualBody",
"value": "Surface detail annotation",
"format": "text/plain"
},
"target": {
"type": "PointSelector",
"x": 0.5,
"y": 0.2,
"z": 0.3
}
}
]
}
],
"extensions": {
"viewer": {
"autoRotate": true,
"rotationSpeed": 30,
"cameraOrbit": "0deg 75deg 105%",
"fieldOfView": "30deg",
"arEnabled": true
}
}
}
JavaScript API¶
The Model3D namespace provides client-side utilities:
// Initialize all viewers on page
Model3D.initViewers();
// Setup a single viewer
Model3D.setupViewer(viewerElement);
// Toggle fullscreen
Model3D.toggleFullscreen(containerElement);
// Check AR support
Model3D.isARSupported(); // returns boolean
// Hotspot management
Model3D.addHotspot(viewer, {
id: 'hotspot-1',
position_x: 0.5,
position_y: 0.2,
position_z: 0.3,
normal_x: 0,
normal_y: 1,
normal_z: 0,
hotspot_type: 'annotation',
color: '#1a73e8',
title: 'Surface Detail',
description: 'Notable feature'
});
Model3D.removeHotspot(viewer, 'hotspot-1');
Model3D.focusHotspot(viewer, 'hotspot-1');
// AJAX operations
Model3D.loadModel(modelId, callback);
Model3D.loadHotspots(modelId, callback);
Model3D.saveHotspot(modelId, hotspotData, callback);
Model3D.deleteHotspot(hotspotId, callback);
// Gaussian Splat viewer
Model3D.initSplatViewer(container, splatUrl, {
cameraUp: [0, 1, 0],
cameraPosition: [0, 0, 5],
cameraLookAt: [0, 0, 0],
alphaThreshold: 5,
onLoad: function(viewer) { },
onError: function(err) { }
});
// Format utilities
Model3D.getSupportedExtensions(); // ['glb', 'gltf', ...]
Model3D.isSupportedFormat('model.glb'); // true
Model3D.isSplatFormat('ksplat'); // true
Model3D.formatBytes(5242880); // '5.00 MB'
Configuration¶
Global Settings (viewer_3d_settings table)¶
| Setting Key | Type | Default | Description |
|---|---|---|---|
default_viewer |
string | model-viewer |
Default viewer type |
default_background |
string | #f5f5f5 |
Background color |
default_exposure |
string | 1.0 |
Light exposure |
default_shadow_intensity |
string | 1.0 |
Shadow darkness |
rotation_speed |
integer | 30 |
Degrees per second |
enable_auto_rotate |
boolean | 1 |
Auto-rotate default |
enable_fullscreen |
boolean | 1 |
Show fullscreen button |
enable_ar |
boolean | 1 |
Enable AR viewing |
enable_download |
boolean | 0 |
Allow downloads |
enable_annotations |
boolean | 1 |
Enable hotspots |
max_file_size_mb |
integer | 100 |
Upload size limit |
allowed_formats |
json | ["glb","gltf","usdz"] |
Permitted formats |
watermark_enabled |
boolean | 0 |
Show watermark |
watermark_text |
string | '' |
Watermark text |
Per-Model Settings (object_3d_model table)¶
| Setting | Type | Default | Description |
|---|---|---|---|
auto_rotate |
boolean | 1 |
Enable rotation |
rotation_speed |
decimal | 1.00 |
Rotation multiplier |
camera_orbit |
string | 0deg 75deg 105% |
Initial camera position |
min_camera_orbit |
string | null | Minimum orbit bounds |
max_camera_orbit |
string | null | Maximum orbit bounds |
field_of_view |
string | 30deg |
Camera FOV |
exposure |
decimal | 1.00 |
Light exposure |
shadow_intensity |
decimal | 1.00 |
Shadow darkness |
shadow_softness |
decimal | 1.00 |
Shadow blur |
background_color |
string | #f5f5f5 |
Background |
environment_image |
string | null | HDR environment |
skybox_image |
string | null | Skybox image |
ar_enabled |
boolean | 1 |
Enable AR |
ar_scale |
string | auto |
AR scale mode |
ar_placement |
enum | floor |
AR placement (floor/wall) |
Viewer Integration¶
Model Viewer (Google WebXR)¶
Primary viewer using <model-viewer> web component:
<model-viewer
id="viewer-1"
src="/uploads/3d/1/model.glb"
ios-src="/uploads/3d/1/model.usdz"
alt="3D Model"
camera-controls
auto-rotate
rotation-per-second="30deg"
camera-orbit="0deg 75deg 105%"
field-of-view="30deg"
exposure="1"
shadow-intensity="1"
ar
ar-modes="webxr scene-viewer quick-look"
style="width: 100%; height: 500px;">
<!-- AR Button -->
<button slot="ar-button" class="ar-button">
View in AR
</button>
<!-- Hotspots -->
<button class="hotspot"
slot="hotspot-1"
data-position="0.5m 0.2m 0.3m"
data-normal="0m 1m 0m">
<div class="hotspot-annotation">
<strong>Title</strong>
<p>Description</p>
</div>
</button>
</model-viewer>
Three.js Fallback¶
Alternative viewer for older browsers:
<div id="threejs-viewer" style="width: 100%; height: 500px;"></div>
<script>
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(30, w/h, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
const controls = new THREE.OrbitControls(camera, renderer.domElement);
const loader = new THREE.GLTFLoader();
loader.load('/uploads/3d/1/model.glb', function(gltf) {
scene.add(gltf.scene);
});
</script>
Gaussian Splat Viewer¶
For neural radiance field (NeRF) captured models:
<div id="splat-viewer" style="width: 100%; height: 500px;"></div>
<script src="/plugins/ahg3DModelPlugin/web/vendor/gaussian-splats3d/gaussian-splats-3d.umd.js"></script>
<script>
const viewer = new GaussianSplats3D.Viewer({
rootElement: document.getElementById('splat-viewer'),
selfDrivenMode: true
});
viewer.addSplatScene('/uploads/3d/1/model.splat').then(() => {
viewer.start();
});
</script>
Template Integration¶
Display 3D Model in Object View¶
Manual Integration¶
<?php
include_once sfConfig::get('sf_plugins_dir') . '/ahg3DModelPlugin/lib/helper/Model3DHelper.php';
if (has_3d_model($resource)): ?>
<div class="model-3d-section">
<?php echo render_3d_model($resource, [
'height' => '500px',
'viewer_type' => 'model-viewer'
]); ?>
</div>
<?php endif; ?>
Embed in External Site¶
<iframe
src="https://your-atom-site/ahg3DModel/embed/123"
width="100%"
height="500"
frameborder="0"
allowfullscreen
allow="xr-spatial-tracking">
</iframe>
AR Requirements¶
| Platform | Requirements |
|---|---|
| iOS | Safari 12+, HTTPS, .usdz file for Quick Look |
| Android | Chrome 79+, HTTPS, .glb/.gltf for Scene Viewer |
| WebXR | WebXR-capable browser, HTTPS, secure context |
AR Modes Priority¶
- WebXR - Immersive AR in browser (emerging standard)
- Scene Viewer - Android native AR viewer
- Quick Look - iOS native AR viewer
Audit Trail Integration¶
Actions logged to object_3d_audit_log:
| Action | Trigger |
|---|---|
upload |
New model uploaded |
update |
Model settings changed |
delete |
Model deleted |
view |
Model viewed |
ar_view |
AR session started |
download |
Model file downloaded |
hotspot_add |
Hotspot created |
hotspot_delete |
Hotspot removed |
Integration with ahgAuditTrailPlugin for central logging is automatic when available.
NGINX Configuration¶
Add MIME types for 3D files:
types {
model/gltf-binary glb;
model/gltf+json gltf;
model/vnd.usdz+zip usdz;
model/obj obj;
model/stl stl;
}
Increase upload limits:
CORS for IIIF manifests (optional):
location ~ ^/iiif/3d/ {
add_header Access-Control-Allow-Origin "*";
add_header Access-Control-Allow-Methods "GET, OPTIONS";
}
File Size Guidelines¶
| Model Type | Recommended Size | Max Polygons |
|---|---|---|
| Simple objects | < 5 MB | 50,000 |
| Detailed objects | 5-20 MB | 200,000 |
| High-fidelity scans | 20-50 MB | 500,000 |
| Museum quality | 50-100 MB | 1,000,000 |
Optimization Tips¶
- Use GLB format with Draco compression
- Reduce texture resolution (2K max for web)
- Decimate high-poly meshes
- Merge materials where possible
- Remove hidden geometry
CLI Commands¶
3d:derivatives¶
Generates thumbnail and reference image derivatives for 3D model files using Blender.
php atom-framework/bin/atom 3d:derivatives # Process all 3D objects missing derivatives
php atom-framework/bin/atom 3d:derivatives --id=123 # Process specific digital object
php atom-framework/bin/atom 3d:derivatives --force # Regenerate even if derivatives exist
php atom-framework/bin/atom 3d:derivatives --dry-run # Preview without generating
| Option | Description |
|---|---|
--id=N |
Process only the specified digital object ID |
--force |
Regenerate even if derivatives already exist |
--dry-run |
List objects that would be processed without generating |
3d:multiangle¶
Generates 6 multi-angle renders (front, back, left, right, top, detail) using Blender. Optionally sends renders to LLM for AI description.
php atom-framework/bin/atom 3d:multiangle # Render all 3D objects missing multi-angle views
php atom-framework/bin/atom 3d:multiangle --id=123 # Render specific digital object
php atom-framework/bin/atom 3d:multiangle --describe # Render + generate AI description
php atom-framework/bin/atom 3d:multiangle --force # Regenerate even if renders exist
php atom-framework/bin/atom 3d:multiangle --dry-run # Preview without rendering
| Option | Description |
|---|---|
--id=N |
Process only the specified digital object ID |
--force |
Regenerate even if renders already exist |
--describe |
After rendering, send images to LLM and output AI description |
--dry-run |
List objects that would be processed without rendering |
Cron Examples¶
# Nightly derivative generation
0 2 * * * cd /usr/share/nginx/archive && php atom-framework/bin/atom 3d:derivatives >> /var/log/atom/3d-derivatives.log 2>&1
# Nightly multi-angle rendering
0 3 * * * cd /usr/share/nginx/archive && php atom-framework/bin/atom 3d:multiangle >> /var/log/atom/3d-multiangle.log 2>&1
# Weekly multi-angle + AI description batch
0 4 * * 0 cd /usr/share/nginx/archive && php atom-framework/bin/atom 3d:multiangle --describe >> /var/log/atom/3d-describe.log 2>&1
Voice Integration¶
AI 3D Object Description¶
The voice command system (ahgVoice module in ahgThemeB5Plugin) supports AI-powered description of 3D objects via multi-angle renders.
Endpoint¶
| Parameter | Description |
|---|---|
digital_object_id |
Direct digital object ID |
information_object_id |
Information object ID (resolves to digital object) |
slug |
Record slug (resolves to information object, then digital object) |
Response:
{
"success": true,
"description": "This object is a...",
"source": "local",
"model": "llava:7b",
"render_count": 6,
"cached": false,
"information_object_id": 456
}
Voice Commands¶
| Command | Action |
|---|---|
| "describe object" / "describe 3D" | Auto-detects 3D viewer, generates multi-angle renders, sends to LLM |
| "describe model" / "what is this object" | Same as above |
| "save to description" | Save AI description to scope_and_content |
| "save to alt text" | Save as digital object alt text |
| "save to both" | Save to both fields |
| "discard" | Discard the AI description |
LLM Configuration¶
Uses the same hybrid LLM configuration as image description:
| Provider | Model | Timeout | Notes |
|---|---|---|---|
| Local (Ollama) | llava:7b | 180s | All 6 images sent in single request |
| Cloud (Anthropic) | claude-sonnet | 120s | 6 image content blocks per request |
Context-aware prompts adjust vocabulary for GLAM sector: CCO (museum), ISAD(G) (archive), MARC (library), VRA (gallery), IPTC (DAM).
Multi-Angle Gallery Partial¶
<?php include_partial('model3d/multiAngleGallery', [
'digitalObject' => $digitalObject,
'informationObject' => $informationObject,
]); ?>
Renders a thumbnail row with lightbox modal for the 6 views if renders exist. Purely additive — shows nothing if {derivative_dir}/multiangle/ doesn't exist.
Blender Integration¶
Requirements¶
- Blender 4.2+ (5.0+ recommended)
- Installed via snap (
/snap/bin/blender) or system package
Scripts¶
| Script | Location | Purpose |
|---|---|---|
blender_thumbnail.py |
tools/3d-thumbnail/ |
Single thumbnail render |
render_multiangle.py |
tools/3d-thumbnail/ |
6-angle orbit renders |
generate-thumbnail.sh |
tools/3d-thumbnail/ |
Shell wrapper for single thumbnail |
generate-multiangle.sh |
tools/3d-thumbnail/ |
Shell wrapper for multi-angle renders |
EEVEE Compatibility¶
Blender 4.2+ renamed the render engine from BLENDER_EEVEE to BLENDER_EEVEE_NEXT. Both Python scripts handle this automatically with version detection.
Troubleshooting¶
| Issue | Solution |
|---|---|
| Model not loading | Check MIME types in nginx, verify file path |
| AR button not showing | Ensure HTTPS, check device compatibility |
| AR session fails | Verify GLB/USDZ format, check file integrity |
| Hotspots not visible | Verify is_visible=1, check z-index |
| Slow performance | Optimize model, reduce polygon count |
| CORS errors | Add Access-Control headers to nginx |
| Manifest 404 | Check route configuration, clear cache |
| Thumbnails not generating | Check Blender installed (/snap/bin/blender --version), verify script permissions |
| Multi-angle renders missing | Run php atom-framework/bin/atom 3d:multiangle --id=N, check Blender logs |
| AI describe returns empty | Check Ollama running (curl localhost:11434), verify LLM model pulled |
| OBJ MIME type wrong | OBJ files often mislabeled as application/x-tgif — handled automatically |
| Blender snap permission | Snap Blender may need --no-sandbox or run as correct user |
Related Plugins¶
| Plugin | Integration |
|---|---|
| ahgConditionPlugin | Damage hotspots linked to condition reports |
| ahgAuditTrailPlugin | Central audit logging |
| ahgCorePlugin | Database initialization, helper functions |
| ahgThemeB5Plugin | Voice AI 3D description (ahgVoice module) |
| atom-framework | ThreeDThumbnailService, CLI commands (3d:derivatives, 3d:multiangle) |
Part of the AtoM AHG Framework