2026-11-09 · 18 min · qwen · threejs · gltf · 3d · fine-tuning · datasets · game-dev
Why read this
Solidtop 85%Validates all 417 shipped GLBs, measures their scale, pivots and draw calls, and merges 278 by material; the model and the pages are not run.
- Original analysis
- Runs on a consumer GPU
- Concrete numbers to act on
3D & spatialMixed licencesPractitioner model
How this was scored
- Is it new?
- 1 of 3: An incremental tweak
- Can I trust it?
- 2 of 3: Measures key facts from files, code or configs
- Can I run it?
- 2 of 3: Open code or weights with real limits
- Will I understand it?
- 2 of 3: Mechanism from first principles with figures
- Can I act on it?
- 2 of 3: A concrete recipe, numbers or comparison
- Will it last?
- 1 of 3: Relevant for months
- Does it affect many?
- 1 of 3: A specialist community
- Only here?
- 2 of 3: A teardown or measurement few others did
Score 59 of 100, ranked 438 of 701 rated articles. Each question is answered 0–3 by hand, and a 3 is rare. How articles are scored
A post by @dextune on 2 November says CCORE, the author of Qwen3.5-4B-3D-Coder, has a second fine-tune out, this one for game assets. The model turns a text prompt into a standalone HTML and JavaScript page built on Three.js, and each page has a GLB export button, so the asset can be imported into Godot. The post says most generations work on the first attempt, especially with English prompts, and that the full weights and the training dataset would follow once training finished.
Both have since followed. The model is ccore/Qwen3.5-4B-GLB-3D-Coder and the data is ccore/ThreeJS-3D-GLB-Assets. I wrote up the first model from its weights, data and technical report; this piece does not repeat the architecture or the KV-cache arithmetic, which have not changed. It asks a narrower question: what does a game developer actually get out of this, and is it usable?
Labels, as before. Measured means I ran code on a file and this is what it printed. Reported means the author says so and I did not check. Reasoned means arithmetic or inference on numbers of either kind.
- architecture
- Qwen3_5ForConditionalGeneration
- task
- text-generation
- license
- apache-2.0
- safetensors
- 5 shards
- gguf files
- 3
- largest file
- 4.48 GB
- files
- 36
- downloads
- 38.7K
- likes
- 291
- languages
- en
repo last modified 2026-11-06
What the model outputs
The tags say glb, so it is worth being precise. The model does not emit GLB bytes, glTF JSON or a Blender script. It writes one HTML file. The file loads Three.js from a CDN, builds the asset as a tree of Three.js objects, shows it in a preview with lights and orbit controls, and wires a button to Three.js's own GLTFExporter. Clicking the button serialises the asset subtree to a binary glTF in the browser and downloads it. The GLB is a by-product of running the code.
This is the system prompt, from the card, and it is the whole contract:
Create game assets for the Godot editor, which uses GLB files. Create whatever the user requests as a good-quality low-poly asset using only Three.js in one complete standalone HTML file. The HTML must show the asset and include an Export GLB button that downloads a working GLB. If the requested asset needs animation, add it and include the animation in the exported GLB; otherwise make it static.
Measured: all 417 rows of the validation split carry that same 399-character string as their system message. Every one of the 417 answers is a fenced HTML block, and the export is always the same shape. Here it is from the first row, a merchant, trimmed:
// Everything below this group is exported.
const exportRoot = new THREE.Group();
exportRoot.name = "LowPoly_Merchant";
scene.add(exportRoot);
// ...
exporter.parse(
exportRoot,
(result) => {
const blob = new Blob([result], { type: "model/gltf-binary" });
// ... object URL, <a download="low_poly_merchant.glb">, click
},
(error) => { /* "Export failed — see console" */ },
{ binary: true, onlyVisible: true /* , animations: [...] */ },
);The pattern that matters is the split between the preview and the asset. In the merchant's page the lights, the ground and the backdrop live outside the exported group ("Preview environment — deliberately kept outside the export root"), so they do not end up in your game. Four GLBs in the split do carry a light, as KHR_lights_punctual. Measured, over 417 pages: none passes scene to the exporter. The first argument is a named group (asset in 178 pages, assetRoot in 84, model in 33, exportRoot in 15, and so on). 416 set binary: true, 413 set onlyVisible: true, and 228 set trs: true, which writes each node as translation, rotation and scale rather than a matrix.
The idiom also moved away from the first model's. That one imported only the Three.js core from one cdnjs URL and wrote its own camera controls. This one uses an import map pointing at jsDelivr, pulls three addons, and does not agree with itself on a version:
| Three.js version in the import map | Pages |
|---|---|
| 0.160.0 | 115 |
| 0.164.1 | 89 |
| 0.169.0 | 78 |
| 0.170.0 | 60 |
| 0.166.1 | 28 |
| 0.165.0, 0.160.1, 0.161.0, 0.167.1, 0.168.0 | 47 between them |
All 417 import GLTFExporter, 404 import OrbitControls, 100 import RoundedBoxGeometry and 22 RoomEnvironment. Reasoned: "only Three.js" here means Three.js plus its own addons, which the exporter makes unavoidable. Ten pinned versions are harmless for a page that runs once and is thrown away. They do mean that the generator string inside each GLB records which exporter wrote it, and that the model was trained on a mixture, so its own pages will pin a version from roughly that range.
What was released
| Artefact | What it is | Licence |
|---|---|---|
| Safetensors | five shards, 17,490,033,664 bytes, FP32 language weights and BF16 vision tower | Apache-2.0 |
| GGUF | Q4_K_M (2,708,804,128 bytes) and Q8_0 (4,482,402,848 bytes), plus the base model's BF16 vision projector | Apache-2.0 |
| Dataset | 3,918 rows: 3,501 train, 417 validation; conversations, a preview render and the exported GLB for every row | MIT |
| Technical report | none this time |
Measured, from the safetensors headers by HTTP range request: 4,539,265,536 parameters, 4,205,751,296 of them language weights in F32 and 333,514,240 vision weights in BF16. The 723 tensors are the same 723 as the first model's; the 15 mtp. tensors of the base Qwen3.5-4B are still missing while config.json still declares mtp_num_hidden_layers: 1. The full-precision checkpoint is twice the size of the first model's FP16 one for no change in content: F32 is how it was saved, not a property of the weights. generation_config.json carries the card's sampling settings, temperature 0.8, top-k 40, top-p 0.95, min-p 0, no repetition penalty.
Reported: the card says the release is from 5 November and that anyone who downloaded the GGUFs before then should delete them and download again, because the filenames did not change. There is no training recipe, no loss curve and no evaluation table in the repo. What the card offers instead is a set of side-by-side images against DeepSeek V4.1 Flash 1017 on prompts "not included in its training dataset", a playable browser arena built from its assets with help from "an orchestration and validation harness", and one line of limitations: "Some requests need a retry or a small code correction." None of that is something I could check, and the card itself says a harness did part of the arena's work.
The dataset
The dataset card describes 3,918 examples across 40 domains and 2,903 distinct subjects, and says all of them "were tested and rendered, and their exported GLB files were validated before inclusion". It ships three configurations that share ids and row order: conversations (prompt, thinking trace, answer), previews (one rendered image per row) and meshes (the GLB each row's own page exported).

Forty domains is generous. Several are the same category twice, once with about 120 to 220 rows and once with about 40: companions_and_creatures (139) and creatures_and_companions (43), nature_and_environment (119) and nature_and_garden (40), enemies (220) and enemies_and_bosses (38), weapons_and_armor (179) and weapons_armor_and_equipment (41). Reasoned: two generation batches with different labels, merged without folding the names together. It does not affect training; it does mean "40 domains" is closer to 30.
Measured, on the validation split. I pulled the 8.6 MB conversations shard and the 77 MB meshes shard and nothing else. Prompts come in two kinds. 275 are ten words or fewer ("Create a merchant.", "Create a mug.") and 142 are briefs of 54 to 78 words that name parts, materials, colours and motion. Titles are visibly templated: a subject recurs with a prefix, and "Magical", "Ancient", "Enchanted", "Weathered" and "Compact" lead dozens of them. No prompt is repeated. The thinking traces are terse and planning-shaped; the merchant's begins "We need create HTML standalone using Three.js. Need infer merchant likely low-poly character." The card does not say which model wrote the answers.
How long an answer is
I counted every row with the model's own tokenizer.json, split into prompt, thinking and answer.
| Per row, 417 rows | Mean | Median | Longest |
|---|---|---|---|
| Prompt (system + user) | 113 | 90 | 177 |
| Thinking trace | 10,490 | 9,507 | 40,307 |
| Answer (the HTML) | 9,241 | 9,067 | 16,675 |
| Total | 19,843 | 19,222 | 52,909 |
The card says responses "average around 20K tokens", which these rows bear out, and that training examples reach 72K, which the training split may; the validation split's longest is 52,909. Against the first model's data this is a lighter task. Those rows averaged 29,001 tokens, 16,066 of thinking and 12,822 of code. One low-poly prop is a smaller program than one lit, fogged, animated scene.
Two patterns are worth knowing before you set a token limit. Long briefs make the model think more and write less: the median thinking trace is 12,038 tokens for a long brief against 8,246 for a short prompt, and the median answer 7,933 against 9,916. Reasoned: a short prompt leaves the design to the answer, and a long brief moves it into the plan. The other pattern is that the thinking comes first and is long, so a cap stops a generation in the worst place.
The card warns that "an 8K output limit is too restrictive" and asks for unlimited output. The numbers are blunter than the warning. At 8,192 tokens, 268 of 417 answers would still be thinking when they were cut, which means no HTML at all, and 145 would stop partway through the page. Four finish. At 16,384, 119 finish. At 32,768, 396 do. Reasoned: set the limit to at least 32K, and to 64K if the brief is long, and treat these as the teacher's lengths, not a ceiling on the student's: the card publishes no length statistics for the model's own answers.
The GLBs, through a validator
The meshes configuration is the part of this release I can test without running anyone's code, because a GLB is data. I extracted all 417 validation GLBs and ran each through the Khronos glTF-Validator (the npm build, version 2.0.0-dev.3.10) with no issue cap.
Measured:
- 417 of 417 have zero errors and zero warnings. The dataset card's claim that its GLBs were validated holds for this split.
- The informational messages are nearly all one kind: 42,776
UNUSED_OBJECTnotes in 412 files, every one I inspected aTEXCOORD_0attribute on a primitive whose material has no texture.GLTFExporterwrites UVs whether or not anything reads them. That is dead weight, not a defect. - 29 images have non-power-of-two sizes, one primitive has a degenerate triangle, and six files use
EXT_materials_bump, an extension the validator does not know. - The
asset.generatorstring matches the page's pinned Three.js version in every file that records one (277 sayTHREE.GLTFExporter r164tor170; the 140 from r160 and r161 print no revision). Reasoned: the shipped GLBs were written by the pages they sit next to, as the card says, not converted from something else.
What is in them, by the validator's own counts:
| Per GLB, 417 files | p10 | Median | p90 | Max |
|---|---|---|---|---|
| Triangles | 1,570 | 4,124 | 14,392 | 69,662 |
| Vertices | 2,629 | 6,675 | 36,718 | 255,932 |
| Materials | 7 | 11 | 22 | 87 |
| Draw calls | 27 | 90 | 212 | 593 |
| File size | 137 KB | 373 KB | 1.39 MB | 7.0 MB |
The triangle counts are honestly low-poly: 349 of 417 assets are under 10,000 triangles. The vertex count runs above the triangle count because 314 of the pages set flatShading: true and many build geometry that does not share vertices across faces. Materials are MeshStandardMaterial in all 417 pages; 207 files use KHR_materials_emissive_strength for glowing parts and 60 use KHR_materials_unlit. 71 carry textures, and all 71 pages draw them on a canvas at runtime (CanvasTexture) rather than loading a file. 66 carry animation, and none carries a skin: when a character waves, a node rotates, no bone bends a mesh.
Animation is the part of the contract I can check against the prompt. Measured: 27 prompts ask for motion in words my keyword search catches ("animated", "opens", "rotating" and the like), and all 27 GLBs carry an animation. 122 prompts say "static" or "keep it static", and none of those GLBs does. Another 39 animate without a request my search found, mostly "magical" companions and creatures given an idle bob. Two pages build an AnimationClip that never reaches the file, both magical companions. That is a careful dataset.
What a validator does not check
A glTF validator asks whether a file is a correct glTF. A game engine asks other questions: how big is this thing, where is its origin, and how many times will I have to draw it? Those are where I found the rough edges.
Scale
The glTF 2.0 specification says the unit is the metre, and Godot imports one unit as one metre. Measured, from the scene bounds after applying every node transform:
A merchant is 3.29 m tall. The median of 65 characters and enemies is 3.37 m, and one of them stands between 1.6 and 1.9 m. Items and weapons have a median of 3.53 m, a treasure key is 7.82 m, a wood axe 3.91 m. The plain mug is 0.11 m, which is a mug; the magical mug is 2.21 m. The pocket watch is 0.12 m; the magical pocket watch is 5.74 m. There is no unit in the system prompt, and the pages do not compute one. Only 7 of 417 pages measure their own asset with a Box3. The camera is placed by hand, 8 to 13 units from the origin in 211 of the 307 pages whose camera position I could parse, and the asset is built to fill that view. The mug's page, modelled in real centimetres, puts its camera 0.33 units away.

Reasoned: none of this is wrong as glTF, and every asset is one scale factor from correct. It is still a step each import needs, and it cannot be automated from the file alone, because the file does not know whether it is a mug or a monument. A level designer dropping a 3.3 m shopkeeper behind a 2.1 m anvil has to rescale both, by different factors.
Origin
A prop should usually sit on its origin, so that placing it at a point on the floor puts it on the floor. Measured: 177 of 417 GLBs have their lowest point within 1 cm of y = 0, and 260 within 5 cm. 138 extend more than 5 cm below the origin, and 56 have more than 40% of their height below it, which is an asset centred on its origin rather than standing on it; the wood axe and the treasure key in the figure are two. Horizontally the data is tidier: 379 of 417 are centred on x and z to within a tenth of their size.
Draw calls
Each THREE.Mesh in the page becomes a glTF node with its own mesh, and a Three.js page builds a merchant out of dozens of boxes, cylinders and tori. The median GLB has 102 nodes, 89 meshes and 11 materials, and the validator's draw-call count, one per primitive per node, has a median of 90. Reasoned: Godot keeps the node tree on import, so each mesh node becomes its own MeshInstance3D, and a scene with fifty such props starts at thousands of draw calls before anything else is drawn.
It is fixable offline, and I wanted to know by how much. My script, 72 lines of Python and NumPy over the GLB's own JSON and binary chunk, walks the node tree, bakes each node's transform into its vertices and normals (flipping the winding where a transform mirrors), and joins every primitive that shares a material into one. It only touches assets where that is safe: no animation, no skin, no texture, no lights. 278 of the 417 qualify; three more failed on a missing normal or a morph target and were left alone.
Measured: all 278 merged files pass the same validator with zero errors and zero warnings, and their bounds agree with the originals' to within a millimetre in the 28 I spot-checked with trimesh. Draw calls across the 278 go from 27,950 to 3,436, a median of 87 down to 11. File size drops to 71.8% of the original in total, because one primitive per material carries far less accessor and node overhead than a hundred. The triangle count is identical in 213 of the 278. In the other 65 it is higher, and that is correct: the original reused one mesh in several nodes, and a baked merge writes every copy out.
The cost is editability. A merged asset is one node per material, and the part names the teacher gave each node (the merchant's run Boot_Toe, Apron_Pocket, Hat_Band, Feather_Pin) are gone. For an animated asset you cannot do it at all without keeping the moving parts separate. So the decision is per asset: merge static props and scenery, keep characters and anything that opens.
What I could not run
- The pages. The plan was to open sampled validation pages in headless Chromium, click Export GLB, and check that the code in the dataset reproduces the GLB the dataset ships. The sandbox's permission layer refused to run the dataset's HTML in a browser, and I did not try to work around it. What I have instead is the generator-string match above, which shows the exporter versions line up, not that the bytes do.
- The model. The full-precision weights are 17.49 GB and an answer averages about 20,000 tokens, most of it thinking. That is hours a generation on the shared CPU I work on, and the memory to hold the model would have exceeded what I am allowed. Every statement about the student's own output is the card's, and the card publishes no numbers for it.
- The training split. 417 rows of 3,918 are what I measured. The validation split is drawn from the same domains in the same proportions, by the dataset's own stats file, but the 72K-token example the card mentions is not in it.
- Godot. I reasoned about Godot's importer from how it maps glTF nodes; I did not import anything into the editor.
- Visual quality. The validator and my bounds check say nothing about whether a goblin looks like a goblin. The dataset's preview renders and the card's comparisons are the only evidence for that, and they are the author's selection.
What to do with it
If you want assets out of this model:
- Run the Q8_0 GGUF (the card's evaluated quantisation) with the system prompt verbatim and the card's sampling settings. Give it an output limit of at least 32K tokens; 8K finishes almost nothing.
- Save the HTML, open it in a browser with network access, and click Export GLB. The page fetches Three.js and three addons from jsDelivr, so it will not work offline as written.
- On import, fix scale first. Decide the real height of the thing and scale the root to it; nothing in the file can tell you.
- Check the origin. If the asset is centred rather than grounded, move the root so its lowest point is at zero.
- For static props, merge by material, in Godot or in a preprocessing script like mine; median draw calls drop from 87 to 11 in this data. Keep animated assets as they are.
If you want to train on the data: it is unusually clean. Every shipped GLB I checked is valid, every validation prompt is unique, the animation contract holds, and it comes with a rendered reference per row. Fold the duplicate domain labels together first, and consider adding a scale to the prompt, because the model has learnt the teacher's camera, not the world's.
Related reading: the first CCORE model, whose 213 teacher scenes I ran in a browser; Construct 1.0, where an agent writes a JSON scene and a deterministic compiler writes the glTF with pivots and an opening animation, the opposite design choice to letting the model write the exporter call; and 3DCodeVerse, a much larger corpus of executable 3D programs.