---
title: "Epic 3D | Interactive 3D Model & AR Viewer for YOOtheme Pro Sites"
description: "Add interactive 3D models to YOOtheme Pro. Let visitors rotate, zoom and explore products in 3D or AR with a fast, privacy-friendly viewer for websites."
url: "https://www.yoozecase.com/?app_id=1&item_id=4"
date: "2026-09-28T03:40:46+00:00"
language: "en-GB"
---

#  Epic 3D

Give your website a third dimension.

**Latest Version**
3.0.2
**Requires**
Joomla 4, 5, 6, PHP 8.1 or newer
**Release date**

**Made for**
Joomla, Wordpress
**Price**
€25.00 (incl. 6 months of updates)

 [ 🛒 Get it for Joomla 👍 ](https://yoozecase.lemonsqueezy.com/checkout/buy/94767982-abeb-4f93-8604-66e4ac725f14)

 [ 🛒 Get it for WordPress👍 ](https://yoozecase.lemonsqueezy.com/checkout/buy/41212378-7949-49d0-8b0e-8781b70fd881)

 [ Demo page ](https://www.yoozecase.com/epic-3d-demo)

Your page just gained depth. Drop a real 3D model straight into the YOOtheme builder and hand visitors the controls. They spin it. They zoom into every detail. On a phone they place it in their own room at true size with AR. This is Joomla in three dimensions.

##  What's new in version 3.0.2

- **Changed:** Malware scanners no longer flag the HDR encoder. The code that builds run-length markers for the generated lighting environment used arithmetic inside chr(), which scanner heuristics report as obfuscation. The values are now byte literals; the generated HDR bytes are unchanged.
- **Note:** No behaviour change. Rendering, elements and install logic are identical to 3.0.1.

 ![Epic 3d 1](https://www.yoozecase.com/files/epic-3d_1.png)

 [](#)

 [](#)

---

##  Description

Real 3D. Right inside the YOOtheme builder.
Yoo 3D turns a plain GLB file into a living object on your page. Upload it. Drop the element into the builder. Done. Visitors grab the model with their mouse and turn it through full space. They zoom into the stitching the grain the polish. On a phone one tap drops the model into their own living room at real size with AR.

Show one hero model under the spotlight. Or line up a whole collection and let people move through it.

The 3D Model Gallery runs a slideshow of models through a single viewer so the page stays fast even with ten of them loaded. The 3D Model Grid lays your catalogue out as clean cards and opens each one big in a pop-up the moment someone clicks. Cards with no poster get a thumbnail rendered straight from the model.

Light it your way with soft studio shadows or a colour you choose. Pick a look preset and skip the fiddling. Add a custom loading animation for the heavy files. Everything runs from your own server so no visitor data leaves for anyone else.

This is your product seen from every angle. Depth is now part of the page.

##  Feature request

 ### What is the next feature for this plugin?

Submit your idea or vote on existing ones.

  #### Top voted

   Website (leave empty)

'; } html += ''; if (oc.title) { html += '##### ' + escapeHtml(oc.title) + '

'; } if (oc.text) { html += '' + escapeHtml(oc.text).replace(/\\n/g, '
') + '

'; } html += '

'; } // Auto winner(s) - top N from the already vote-sorted features if (mode === 'auto_winner' || mode === 'both') { var n = oc.winnerCount | 0; if (n '; for (var i = 0; i ' + (w.image_url ? '![](https://www.yoozecase.com/'%20+%20escapeHtml(w.image_url)%20+%20')

' : '') + '' + (i + 1) + '' + '' + escapeHtml(w.title) + '' + '' + getIcon(this.cfg.voteIcon) + ' ' + (w.votes | 0) + '' + ''; } html += ''; } } if (html === '') { this.elOutcome.hidden = true; return; } this.elOutcome.innerHTML = html; this.elOutcome.hidden = false; }; /** * Roadmap view (v3.2.0): renders the items as status columns, in the * canonical status order from cfg.statuses. Each column is an 12. inside \* the existing .yc-fb-list , holding its own inner  of items - \* this keeps the delegated click/change handlers on elList working \* unchanged. Unknown statuses fall back into the "open" column so no item \* can silently disappear. Empty columns are skipped when \* cfg.roadmapHideEmpty is set. \*/ Board.prototype.renderRoadmap = function (items) { var stats = (this.cfg.statuses && this.cfg.statuses.length) ? this.cfg.statuses : \['open', 'planned', 'in\_progress', 'shipped', 'declined'\]; var labels = this.cfg.statusLabels || {}; var groups = {}; var i, st; for (i = 0; i ' + '##### ' + '' + escapeHtml(labels\[sv\] || sv) + '' + arr.length + '' + '

    ' + '' + itemsHtml + '
    ' + '
'; } return html; }; Board.prototype.renderItem = function (f, index) { var disabled = this.cfg.isClosed ? 'disabled' : ''; var voted = f.voted ? ' yc-voted' : ''; var descHtml = f.description ? '' + escapeHtml(f.description).replace(/\\n/g, '
') + '

' : ''; var thumbHtml = f.image_url ? '![](https://www.yoozecase.com/'%20+%20escapeHtml(f.image_url)%20+%20')

' : ''; // Status workflow (v3.0.0): badge for any non-default status. The // roadmap view (v3.2.0) skips the badge - the column already // communicates the status, a badge would be redundant noise. var st = (f.status || 'open'); var labels = this.cfg.statusLabels || {}; var statusBadge = (!this.isRoadmap && st && st !== 'open' && labels[st]) ? '' + escapeHtml(labels\[st\]) + '' : ''; // Design themes (v3.1.0): arena / neon / pixel render extra per-item // furniture - rank number, avatar (image or initials), a "leader" pill // on rank 1, and a "votes" label. Classic/colour themes are untouched. var t = this.cfg.theme || 'light'; var themed = (t === 'arena' || t === 'neon' || t === 'pixel'); var rank = (typeof index === 'number') ? (index + 1) : 0; var rankHtml = '', leaderHtml = '', avatarHtml = '', votesLabel = ''; if (themed) { rankHtml = '' + pad2(rank) + ''; if (rank === 1) { leaderHtml = '' + escapeHtml(this.cfg.leaderLabel || 'Leader') + ''; } if (f.image_url) { avatarHtml = '![](https://www.yoozecase.com/'%20+%20escapeHtml(f.image_url)%20+%20')

'; } else { avatarHtml = '' + escapeHtml(initialsOf(f.title)) + '

'; } votesLabel = '' + escapeHtml(this.cfg.votesLabel || 'votes') + ''; } // Moderation overlay: subtle corner buttons, only when canModerate. // Buttons appear on hover/focus (styled in CSS). Even in customizer // preview we render them when canModerate is true so the moderator // can see what visitors with moderation rights will see. var modHtml = ''; if (this.cfg.canModerate) { var parts = []; var hideTitle = escapeHtml(this.t('hideTitle', 'Hide this feature')); var deleteTitle = escapeHtml(this.t('deleteTitle', 'Delete this feature')); if (this.cfg.allowSoftHide) { parts.push( '' ); } if (this.cfg.allowHardDelete) { parts.push( '' ); } if (parts.length || (this.cfg.statuses && this.cfg.statuses.length)) { var sParts = parts.slice(); if (this.cfg.statuses && this.cfg.statuses.length) { var opts = ''; for (var si = 0; si ' + escapeHtml(sl) + ''; } var setStatus = escapeHtml(this.t('setStatus', 'Set status')); sParts.push( '' ); } modHtml = '' + sParts.join('') + '

'; } } var mediaHtml = themed ? avatarHtml : thumbHtml; return ( '32. ' + leaderHtml + rankHtml + '' + mediaHtml + '' + '##### ' + escapeHtml(f.title) + '

    ' + statusBadge + descHtml + '

    ' + modHtml + '
' ); }; /** * Detects whether a JSON response looks like a CSRF/token rejection. * Two cases: * 1. com_ajax-level rejection (Joomla's auto-token-check on POST): top * level message contains the JINVALID_TOKEN_NOTICE text and HTTP * status is typically 403. * 2. Plugin-level rejection from requireToken(): payload.error === "Invalid token". */ Board.prototype.isTokenError = function (status, raw, payload) { if (status === 403) return true; var hay = ''; try { hay = JSON.stringify(raw || {}) + '|' + JSON.stringify(payload || {}); } catch (e) {} return /invalid (security )?token/i.test(hay); }; /** * Fetch a fresh CSRF token from the server. Updates this.cfg.csrfToken so * the next call (and any future calls in this Board's lifetime) use the * correct one. Cache-bust with a timestamp so no upstream CDN caches the * response. */ Board.prototype.fetchFreshToken = function () { var self = this; var url = this.cfg.ajaxUrl + '&task=token&_=' + Date.now(); return fetch(url, { method: 'GET', credentials: 'same-origin', headers: { 'Accept': 'application/json', 'Cache-Control': 'no-cache' } }) .then(function (r) { return r.json(); }) .then(function (data) { var p = self.unwrapAjax(data); if (p && p.ok === true && typeof p.token === 'string' && p.token.length >= 16) { self.cfg.csrfToken = p.token; return p.token; } throw new Error('No fresh token in response'); }); }; /** * Generic POST-with-token helper. Builds FormData from a plain object, * adds the current token, posts. On 403 / token-error response, fetches a * fresh token and retries exactly once. * * @param {string} task The task name (appended to URL and body) * @param {object} fields Key-value form fields (no token) * @return {Promise} Resolves to the unwrapped payload on success. \* Rejects with Error containing .reason on failure. \*/ Board.prototype.postWithToken = function (task, fields) { var self = this; function attempt(token) { var fd = new FormData(); fd.append('task', task); for (var k in fields) { if (Object.prototype.hasOwnProperty.call(fields, k)) { fd.append(k, fields\[k\]); } } fd.append(token, '1'); return fetch(self.cfg.ajaxUrl + '&task=' + encodeURIComponent(task), { method: 'POST', credentials: 'same-origin', body: fd, headers: { 'Accept': 'application/json' } }).then(function (r) { return r.json().then(function (data) { return { status: r.status, raw: data, payload: self.unwrapAjax(data) }; }); }); } return attempt(this.cfg.csrfToken).then(function (res) { // Success path if (res.payload && res.payload.ok === true) { return res.payload; } // Token rejection? Fetch fresh and retry once. if (self.isTokenError(res.status, res.raw, res.payload)) { return self.fetchFreshToken().then(function (fresh) { return attempt(fresh).then(function (res2) { if (res2.payload && res2.payload.ok === true) { return res2.payload; } var err = new Error( (res2.payload && res2.payload.error) || (res2.raw && res2.raw.message) || self.t('requestFailed', 'Request failed.') ); err.payload = res2.payload || null; throw err; }); }); } // Some other error - surface it var err = new Error( (res.payload && res.payload.error) || (res.raw && res.raw.message) || self.t('requestFailed', 'Request failed.') ); err.payload = res.payload || null; throw err; }); }; Board.prototype.handleSubmit = function () { var self = this; if (isInBuilder()) { this.showStatus(this.t('previewSubmit', 'Preview mode - submissions are disabled in the YOOtheme builder. Try this on the published page.'), 'info'); return; } if (this.cfg.isClosed) { this.showStatus(this.cfg.closedMessage || this.t('votingClosed', 'Voting is closed.'), 'info'); return; } var title = (this.elTitle && this.elTitle.value || '').trim(); var desc = (this.elDesc && this.elDesc.value || '').trim(); // Client-side validation: minimum title length. Server re-validates. var minLen = (this.cfg.minTitleLength | 0) || 2; if (title.length for hide/delete, remember parent + sibling for rollback. var li = btn.closest('li'); var parentEl = li ? li.parentNode : null; var nextEl = li ? li.nextSibling : null; if (li && (action === 'hide' || action === 'delete')) { li.remove(); } btn.disabled = true; var task = action === 'hide' ? 'moderate\_hide' : action === 'delete' ? 'moderate\_delete' : action === 'restore' ? 'moderate\_restore' : ''; if (!task) { btn.disabled = false; return; } this.postWithToken(task, { feature\_id: featureId }) .then(function (payload) { // Success - keep the UI as-is (already removed) and refresh // so vote counts, top box, and hidden-features panel reflect // the new state. self.refresh(); if (self.hiddenOpen) { self.hiddenLoaded = false; // force a refetch self.fetchHiddenFeatures(); } var msg = action === 'delete' ? self.t('deleted', 'Feature deleted.') : action === 'hide' ? self.t('hidden', 'Feature hidden.') : action === 'restore' ? self.t('restored', 'Feature restored.') : self.t('done', 'Done.'); self.showStatus(msg, 'success'); setTimeout(function () { self.hideStatus(); }, 3000); }) .catch(function (err) { // Rollback: re-insert the row if we removed it. if (li && parentEl) { if (nextEl) { parentEl.insertBefore(li, nextEl); } else { parentEl.appendChild(li); } } btn.disabled = false; self.showStatus(err && err.message ? err.message : self.t('modFailed', 'Moderation action failed.'), 'error'); }); }; Board.prototype.handleStatusChange = function (featureId, status, sel) { var self = this; if (!featureId || !status) return; sel.disabled = true; this.postWithToken('moderate\_status', { feature\_id: featureId, status: status }) .then(function () { self.refresh(); var label = (self.cfg.statusLabels && self.cfg.statusLabels\[status\]) ? self.cfg.statusLabels\[status\] : status; self.showStatus(self.tf('statusSet', 'Status: %s', label), 'success'); setTimeout(function () { self.hideStatus(); }, 3000); }) .catch(function (err) { sel.disabled = false; self.showStatus(err && err.message ? err.message : self.t('statusFailed', 'Could not set status.'), 'error'); }); }; /\*\* \* Toggle the hidden-features panel open/closed. First open triggers \* an AJAX fetch; subsequent toggles just hide/show the cached list. \*/ Board.prototype.toggleHiddenPanel = function () { if (!this.elHiddenList || !this.elHiddenLabel) return; var arrow = this.elHiddenToggle && this.elHiddenToggle.querySelector('.yc-fb-hidden-arrow'); if (this.hiddenOpen) { this.hiddenOpen = false; this.elHiddenList.hidden = true; this.elHiddenLabel.textContent = this.t('showHidden', 'Show hidden features'); if (arrow) arrow.classList.remove('yc-open'); return; } this.hiddenOpen = true; this.elHiddenList.hidden = false; this.elHiddenLabel.textContent = this.t('hideHidden', 'Hide hidden features'); if (arrow) arrow.classList.add('yc-open'); if (!this.hiddenLoaded) { this.fetchHiddenFeatures(); } }; /\*\* \* Fetch the hidden-features list via the moderator-only endpoint. \* Re-callable to refresh the list after a restore. \*/ Board.prototype.fetchHiddenFeatures = function () { var self = this; if (!this.elHiddenList) return; this.elHiddenList.innerHTML = '2. ' + escapeHtml(this.t('loading', 'Loading...')) + '
'; var url = this.cfg.ajaxUrl + '&task=moderate\_list\_hidden&board\_id=' + encodeURIComponent(this.cfg.boardId); fetch(url, { credentials: 'same-origin', headers: { 'Accept': 'application/json' } }) .then(function (r) { return r.json(); }) .then(function (data) { var payload = self.unwrapAjax(data); if (!payload || payload.ok !== true) { self.elHiddenList.innerHTML = '4. ' + escapeHtml((payload && payload.error) ? payload.error : self.t('hiddenLoadFailed', 'Failed to load hidden features.')) + '
'; return; } self.hiddenLoaded = true; var features = payload.features || \[\]; if (features.length === 0) { self.elHiddenList.innerHTML = '6. ' + escapeHtml(self.t('noHidden', 'No hidden features.')) + '
'; return; } var html = ''; for (var i = 0; i ' + escapeHtml(f.description).replace(/\\n/g, '
') + '' : ''; var restoreTitle = escapeHtml(self.t('restoreTitle', 'Restore this feature')); html += '9. ' + '' + '##### ' + escapeHtml(f.title) + '

    ' + descHtml + '' + escapeHtml(self.tf('votesMeta', '%s votes', f.votes | 0)) + '' + '

    ' + '' + '
'; } self.elHiddenList.innerHTML = html; }) .catch(function () { self.elHiddenList.innerHTML = '11. ' + escapeHtml(self.t('hiddenNetwork', 'Network error while loading hidden features.')) + '
'; }); }; function initAll(scope) { var roots = (scope || document).querySelectorAll('.yc-fb-root'); for (var i = 0; i ---

##  Changelog

---

 [ v2.4.14 Breaking 16/07/2026 ](#) Fixed- \*\*Environment = Custom Color showed "3D model could not be loaded" even

 [ v2.4.13 Breaking 08/07/2026 ](#) Fixed- \*\*Empty-state placeholder text overlapped itself.\*\* The stage sets

 [ v2.4.12 Breaking 08/07/2026 ](#) Fixed- \*\*Generated thumbnails showed the model tiny and off-centre\*\* (grid and

 [ v2.4.11 Breaking 08/07/2026 ](#) Added- \*\*Auto-thumbnails for the gallery's Thumbnails navigation strip.\*\* When

- \*\*"Auto-generate Missing Thumbnails" now defaults to ON\*\* on both the

 Fixed- \*\*Auto-thumbnails never appeared (grid) and were missing (gallery).\*\*

 [ v2.4.10 Breaking 08/07/2026 ](#) Fixed- \*\*3D Model Gallery showed no model at all (regression from 2.4.8).\*\* The

- \*\*Grid auto-thumbnails captured a blank image.\*\* The hidden snapshot

 [ v2.4.9 Breaking 08/07/2026 ](#) Added- \*\*3D Model Grid: automatic thumbnails for models without a poster.\*\* New

- Lazy - a model is only generated when its card scrolls near the
- Serialised - one hidden worker at a time, so there is never more than
- Cached - each result is stored in sessionStorage for the visit, keyed

 Fixed- \*\*Grid: modal preload used fetch mode no-cors\*\*, the same opaque-response

 [ v2.4.8 Breaking 08/07/2026 ](#) Fixed- \*\*3D Model Gallery: slides visibly overlapped during a swap ("the

- \*\*Gallery: a later slide could stop loading.\*\* The next-model preload

 [ v2.4.7 Breaking 08/07/2026 ](#) Changed- \*\*Settings panels consolidated to 5 tabs each\*\* (4 plus the built-in

- 3D Model: Model | Motion | Camera & Lighting | Style & Loader
- 3D Model Gallery: Content & Gallery | Motion | Camera & Lighting |
- 3D Model Grid: Content & Grid | Modal | Motion & Camera |

 [ v2.4.6 Breaking 08/07/2026 ](#) Changed- \*\*Removed the diagnostic `data-yc-build` / `data-yc-anim` /

- \*\*Moved the stylesheet read out of the element templates.\*\* The three

 [ v2.4.5 Breaking 08/07/2026 ](#) Changed- \*\*All element icons redrawn from Tabler Icons\*\* (MIT, stroke-based

- \*\*The three elements now share one product picker colour\*\*, teal
- \*\*NOTICE.txt\*\*: added the required "Third-party assets" section

 Fixed- \*\*Builder element icons rendered far too large.\*\* The SVGs carried no
- \*\*Icons are now referenced as inline base64 `data:` URLs\*\* in

## Schema

```json
{
    "@context": "https://schema.org",
    "@type": "Organization",
    "@id": "https://www.yoozecase.com/#organization",
    "name": "YOOzeCase",
    "alternateName": "A YOOzeCase Company",
    "description": "Premium plugins, extensions and YOOtheme Pro elements for WordPress and Joomla. A YOOzeCase Company: polished tools built for usability and performance.",
    "email": "info@yoozecase.com",
    "url": "https://www.yoozecase.com",
    "foundingDate": "2026-01-01",
    "logo": "https://www.yoozecase.com/images/yoozecase/Yoozecase logo.png",
    "image": "https://www.yoozecase.com/images/yoozecase/Yoozecase logo.png",
    "address": {
        "@type": "PostalAddress",
        "addressCountry": "NL",
        "addressLocality": "Rotterdam"
    },
    "sameAs": [
        "https://www.facebook.com/yoozecase",
        "https://x.com/yoozecase",
        "https://www.instagram.com/yoozecase",
        "https://www.youtube.com/@yoozecase",
        "https://bsky.app/profile/yoozecase.bsky.social",
        "https://www.threads.com/@yoozecase",
        "https://mastodon.social/@yoozecase",
        "https://www.reddit.com/user/yoozecase"
    ]
}
```

```json
{
    "@context": "https://schema.org",
    "@type": "BreadcrumbList",
    "itemListElement": [
        {
            "@type": "ListItem",
            "position": 1,
            "name": "Home",
            "item": "https://www.yoozecase.com"
        },
        {
            "@type": "ListItem",
            "position": 2,
            "name": "Extensions",
            "item": "https://www.yoozecase.com/extensions"
        },
        {
            "@type": "ListItem",
            "position": 3,
            "name": "Epic 3D",
            "item": "https://www.yoozecase.com/"
        }
    ]
}
```
