Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ cmake_minimum_required(VERSION 3.24.0)
cmake_policy(SET CMP0005 NEW)
cmake_policy(SET CMP0048 NEW) # manages project version

project(QtMeshEditor VERSION 3.2.1 LANGUAGES C CXX)
project(QtMeshEditor VERSION 3.3.0 LANGUAGES C CXX)
message(STATUS "Building QtMeshEditor version ${PROJECT_VERSION}")

set(QTMESHEDITOR_VERSION_STRING "\"${PROJECT_VERSION}\"")
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ jobs:
- uses: actions/checkout@v4

- name: Run QtMesh scan
uses: fernandotonon/QtMeshEditor@3.2.1
uses: fernandotonon/QtMeshEditor@3.3.0
with:
command: scan
image-tag: "3.1.0"
Expand Down Expand Up @@ -79,22 +79,22 @@ Release tags are listed on the [releases page](https://github.com/fernandotonon/

```yaml
# Validate a specific mesh
- uses: fernandotonon/QtMeshEditor@3.2.1
- uses: fernandotonon/QtMeshEditor@3.3.0
with:
command: validate
input-file: ./models/character.fbx
image-tag: "3.1.0"

# Convert FBX → glTF
- uses: fernandotonon/QtMeshEditor@3.2.1
- uses: fernandotonon/QtMeshEditor@3.3.0
with:
command: convert
input-file: ./models/character.fbx
output-file: ./output/character.gltf2
image-tag: "3.1.0"

# Resample Mixamo animations (200+ keyframes → 30)
- uses: fernandotonon/QtMeshEditor@3.2.1
- uses: fernandotonon/QtMeshEditor@3.3.0
with:
command: anim
input-file: ./animations/dance.fbx
Expand All @@ -103,7 +103,7 @@ Release tags are listed on the [releases page](https://github.com/fernandotonon/
image-tag: "3.1.0"

# Get mesh info as JSON
- uses: fernandotonon/QtMeshEditor@3.2.1
- uses: fernandotonon/QtMeshEditor@3.3.0
id: info
with:
command: info
Expand Down
40 changes: 40 additions & 0 deletions website/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ function App() {
{ id: 'convert', label: 'Convert', title: 'Convert formats', code: pipelineExamples.convert, language: 'convert' },
{ id: 'merge', label: 'Merge', title: 'Merge animation clips', code: pipelineExamples.merge, language: 'anim' },
{ id: 'turntable', label: 'Turntable', title: 'Render turntable PNG', code: pipelineExamples.turntable, language: 'turntable' },
{ id: 'vat', label: 'VAT', title: 'Bake to vertex animation texture', code: pipelineExamples.vat, language: 'bash' },
{ id: 'docker', label: 'Docker', title: 'Docker workflow', code: pipelineExamples.docker, language: 'docker' },
{ id: 'gha', label: 'GitHub Actions', title: 'GitHub Actions workflow', code: githubActionExample, language: 'yaml' },
],
Expand Down Expand Up @@ -247,6 +248,45 @@ function App() {
return () => window.clearTimeout(timer);
}, [copyState]);

// Deep-link to a section by URL fragment, e.g. `/#vat-demo`. The
// browser's native fragment-scroll runs before React mounts the
// sections, so the initial scroll lands on an empty page. After
// mount, we re-resolve the hash and scroll to the matching element.
// Also re-fires on hashchange so in-page navigation (clicking a
// header anchor while staying on this page) works the same way.
useEffect(() => {
if (typeof window === 'undefined') {
return undefined;
}
const scrollToHash = () => {
const id = window.location.hash.replace(/^#/, '');
if (!id) return;
const target = document.getElementById(id);
if (!target) return;
// `behavior: 'auto'` for the initial scroll feels right —
// smooth-scroll on a cold page is animated jank. Subsequent
// in-page hashchanges use smooth.
target.scrollIntoView({ behavior: 'auto', block: 'start' });
};
// requestAnimationFrame gives the DOM a tick to lay out before
// we measure scroll positions; otherwise on slow CPU the target
// may not have its final height yet.
const raf = window.requestAnimationFrame(scrollToHash);
const onHashChange = () => {
const id = window.location.hash.replace(/^#/, '');
if (!id) return;
const target = document.getElementById(id);
if (target) {
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
};
window.addEventListener('hashchange', onHashChange);
return () => {
window.cancelAnimationFrame(raf);
window.removeEventListener('hashchange', onHashChange);
};
}, []);

async function handleCopyInstallCommand() {
if (!recommendedInstall) {
return;
Expand Down
16 changes: 16 additions & 0 deletions website/src/DocsApp.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const NAV = [
{ id: 'cmd-lod', label: 'lod' },
{ id: 'cmd-pose', label: 'pose' },
{ id: 'cmd-turntable', label: 'turntable' },
{ id: 'cmd-vat', label: 'vat' },
{ id: 'cmd-scan', label: 'scan' },
]},
{ section: 'Performance', items: [
Expand Down Expand Up @@ -437,6 +438,21 @@ qtmesh turntable <file> -o frame_%02d.png [--frames N] [--axis y|x|z]`}
</p>
</CmdSection>

<CmdSection id="cmd-vat" name="vat" description={<>Bake a skeletal animation to a <strong>Vertex Animation Texture</strong> (OpenVAT format). The output is a 16-bit PNG storing per-vertex position+normal samples in time, a JSON sidecar with the playback bounds, a vertex-order-aligned glTF, and an Ogre bind sidecar (<Code>{`<basename>_ogre_bind.bin`}</Code>) so engine importers can realign UV2 to the bake's column order on import. Drop-in shader templates for Godot/Unity/Unreal ship at <a href="https://github.com/fernandotonon/QtMeshEditor/tree/master/tools/vat-shaders" className={s.link}>tools/vat-shaders/</a>. See the <a href="/#vat-demo" className={s.link}>live VAT demo</a> for a 1000-instance perf comparison against per-instance skeletal skinning.</>}
synopsis={`qtmesh vat <file> --anim <name> [--fps N] [-o <dir>] [--json]`}
options={[
['--anim <name>', 'Animation clip name to bake (required)'],
['--fps N', 'Frames per second to sample at (default: 30)'],
['-o <dir>', 'Output directory (default: <input>_vat alongside the source)'],
['--json', 'Emit a machine-readable summary instead of human text'],
]}
examples={[
'qtmesh vat character.fbx --anim "Run" -o bakes/run/',
'qtmesh vat character.fbx --anim "Idle" --fps 24 -o bakes/idle/',
'qtmesh vat character.fbx --anim "Dance" --json',
]}
/>

<CmdSection id="cmd-scan" name="scan" description={<>Recursively scan a directory for 3D asset issues. Think of it as <strong>ESLint for 3D assets</strong>. Checks format restrictions, complexity limits, naming conventions, skeleton/animation content, and more. Supports YAML configuration, scoped rules per folder, JSON output, and auto-fix. Also available as a <a href="https://github.com/marketplace/actions/qtmesheditor" className={s.link}>GitHub Action</a>: <Code>{qtmeshActionRef}</Code>.</>}
synopsis={`qtmesh scan [path] [options]`}
options={[
Expand Down
21 changes: 20 additions & 1 deletion website/src/components/Section.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,30 @@
import styles from './Section.module.css';

export default function Section({ id, eyebrow, title, subtitle, children }) {
// Each section gets a hover-visible `#`-link next to its title, so a
// reader can grab a shareable URL (e.g. `/#vat-demo`) without
// resorting to dev-tools. The deep-link effect in App.jsx handles
// scrolling to whatever fragment the URL points at — both on initial
// load and on subsequent `hashchange` events.
return (
<section id={id} className={styles.section}>
<div className={styles.header}>
{eyebrow ? <p className={styles.eyebrow}>{eyebrow}</p> : null}
{title ? <h2 className={styles.title}>{title}</h2> : null}
{title ? (
<h2 className={styles.title}>
{title}
{id ? (
<a
href={`#${id}`}
className={styles.anchorLink}
aria-label={`Permalink to ${typeof title === 'string' ? title : 'this section'}`}
title="Copy link to this section"
>
#
</a>
) : null}
</h2>
) : null}
{subtitle ? <p className={styles.subtitle}>{subtitle}</p> : null}
</div>
{children}
Expand Down
17 changes: 17 additions & 0 deletions website/src/components/Section.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,23 @@
color: var(--text-0);
}

.anchorLink {
margin-left: 0.45rem;
color: var(--text-muted);
opacity: 0;
text-decoration: none;
font-weight: 400;
font-size: 0.6em;
transition: opacity 140ms ease, color 140ms ease;
}

.section:hover .anchorLink,
.title:focus-within .anchorLink,
.anchorLink:focus {
opacity: 1;
color: var(--accent-1);
}

.subtitle {
margin: 0.8rem 0 0;
max-width: 68ch;
Expand Down
1 change: 1 addition & 0 deletions website/src/data/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ export const pipelineExamples = {
convert: `qtmesh convert model.fbx -o model.glb2\nqtmesh convert model.dae -o model.mesh`,
merge: `qtmesh anim base.fbx \\\n --merge walk.fbx run.fbx jump.fbx idle.fbx \\\n -o merged.fbx`,
turntable: `qtmesh turntable character.fbx -o preview.png --frames 12 --size 512\nqtmesh turntable character.fbx -o frames/frame_%02d.png --frames 24`,
vat: `# Bake a skeletal animation to a Vertex Animation Texture (OpenVAT format).\n# Emits a 16-bit PNG, a JSON sidecar, a vertex-order-aligned glTF,\n# and an Ogre bind sidecar so engine importers (Godot/Unity/Unreal)\n# can realign UV2 to the bake's column order on import.\nqtmesh vat character.fbx --anim "Run" --fps 30 -o bakes/run/\n\n# Drop-in shader templates for Godot, Unity, and Unreal:\n# https://github.com/fernandotonon/QtMeshEditor/tree/master/tools/vat-shaders`,
docker: `docker run --rm --user "$(id -u):$(id -g)" -v $(pwd):/workspace \\\n ghcr.io/fernandotonon/qtmesh scan ./assets --fail-on error`,
githubAction: `name: QtMesh Scan\n\non:\n push:\n branches: [ "master" ]\n\njobs:\n scan-assets-qtmesh:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n\n - name: Run QtMesh scan\n uses: __QTMESH_ACTION_REF__\n with:\n command: scan\n image-tag: "__QTMESH_IMAGE_TAG__"\n env:\n QTMESH_CLOUD_TOKEN: \${{ secrets.QTMESH_CLOUD_TOKEN }}`,
scanFixConvert: `qtmesh scan ./assets --fail-on error\nqtmesh fix character.fbx --all -o character_fixed.fbx\nqtmesh convert character_fixed.fbx -o character.glb2`
Expand Down
2 changes: 1 addition & 1 deletion website/src/hooks/useQtmeshActionRef.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';

const QTMESH_RELEASES_LATEST_API = 'https://api.github.com/repos/fernandotonon/QtMeshEditor/releases/latest';
const QTMESH_ACTION_REF_FALLBACK = 'fernandotonon/QtMeshEditor@3.2.1';
const QTMESH_ACTION_REF_FALLBACK = 'fernandotonon/QtMeshEditor@3.3.0';
const CACHE_KEY = 'qtmesh.actionRef.cache.v1';
const CACHE_TTL_MS = 6 * 60 * 60 * 1000;

Expand Down
16 changes: 16 additions & 0 deletions website/src/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,22 @@ body,
min-height: 100%;
}

html {
/* Smooth-scroll anchor links and `scrollIntoView({ behavior: 'smooth' })`.
The deep-link effect in App.jsx forces `behavior: 'auto'` for the
initial cold-load scroll (animating a 0→target scroll on first paint
looks like jank); in-page hashchanges keep the smooth animation. */
scroll-behavior: smooth;
}

/* Give every section a small top breathing-room when scrolled into view
by anchor. Without this the deep-link / in-page anchor lands the
section's first line flush with the viewport's top edge, which reads
as "cut off." */
section[id] {
scroll-margin-top: 1.5rem;
}

body {
font-family: var(--font-ui);
background: var(--surface-0);
Expand Down
Loading