How to use from
Docker Model Runner
docker model run hf.co/townbox/lowpoly-coder-4b
Quick Links

LowPoly Coder 4B

LowPoly Coder 4B turns a one-sentence request, such as "Make a low-poly okapi.", into JavaScript for Three.js r180 that builds that object as a small low-poly 3D asset. The script export_glb.mjs in this repo turns the reply into a .glb file.

It is a fine-tune of Qwen3.5-4B, and it is often wrong. In a blinded test on objects it had never seen, about 1 in 4 outputs was recognisable as the requested object (24.0%). DeepSeek V4.1 Flash, which wrote the model's first-stage training examples, scored 93.7% on the same test. Sample several attempts per request and keep the best one.

Six rendered views (front, back, left, right, front-left, back-right) of one output for "I need a low-poly lunar rover.": a cream chassis with a dark navy cab, four chunky black wheels, a brass antenna and a rust-coloured disc at the back. Mean rater score 2.0 of 3.

Input A one-sentence text request, e.g. "Build a low-poly washing machine."
Output A 4-line spec, then one JavaScript block defining buildAsset(THREE)
Asset A THREE.Group of named, flat-shaded meshes, with colours only (no textures)
Size The model picks its own triangle budget: 250 to 1,100 on the test set, median 550
Quality 24% of outputs recognisable; under 1% rated "good"
Weights Qwen3.5-4B with LoRA adapters merged in; bf16, 2 safetensors shards, about 9.3 GB
Licence Apache-2.0

What the model writes

A reply has two parts:

  1. A short spec, which the model fills in itself:
    Asset: …
    Triangle budget: …
    Brief: …
    Required named parts: …
    
  2. One javascript code block that defines function buildAsset(THREE). It returns a THREE.Group of named meshes, each with one flat-shaded MeshStandardMaterial.
  • Conventions: Y is up, the object rests on y = 0, the front faces +Z, and scale is free.
  • Style: always the same hand-crafted "woodland collection" look, with a fixed palette: oak, darkWood, ivory, iron, brass, sage, navy, black and rust.

The full output contract is in system_prompt.md.

How to use

1. Generate replies

  • System prompt: send the exact contents of system_prompt.md as the system message. The model was trained with it.
  • User message: just the request.
  • Thinking: off (enable_thinking=False in the chat template).
  • Sampling: temperature 0.7, top_p 0.8, top_k 20, max_tokens 8192.

This is the tested setup (vLLM 0.33.0). Loading the model by repo id this way reproduced the evaluation outputs exactly.

from huggingface_hub import hf_hub_download
from vllm import LLM, SamplingParams
REPO = "townbox/lowpoly-coder-4b"
llm = LLM(model=REPO, max_model_len=12288, limit_mm_per_prompt={"image": 0, "video": 0})
system = open(hf_hub_download(REPO, "system_prompt.md")).read()
msgs = [{"role": "system", "content": system}, {"role": "user", "content": "Build a low-poly washing machine."}]
prompt = llm.get_tokenizer().apply_chat_template(msgs, tokenize=False, add_generation_prompt=True, enable_thinking=False)
out = llm.generate([prompt], SamplingParams(temperature=0.7, top_p=0.8, top_k=20, max_tokens=8192, n=4))
for i, o in enumerate(out[0].outputs):
    open(f"reply_{i}.txt", "w").write(o.text)

The model keeps Qwen3.5-4B's architecture (Qwen3_5ForConditionalGeneration), but it is used text-only, so the tested setup switches image and video inputs off.

2. Turn a reply into a .glb file

npm install three@0.180.0
node export_glb.mjs reply_0.txt asset.glb

export_glb.mjs extracts the code block, runs buildAsset(THREE) in a node:vm context with a 10-second limit, and writes a GLB with one node per named mesh, flat-shaded, with base colours. In testing, all 12 real replies converted. The files passed the Khronos glTF validator with 0 errors and 0 warnings, and their mesh and triangle counts matched the evaluation runtime exactly.

Safety: the generated code is untrusted, and node:vm is not a security boundary. Run the export in a container or VM with no network access and no secrets.

3. Sample several and keep the best

A single attempt is often weak or broken. Sample several, for example 4 to 8 (the snippet above draws 4), and keep the best one. The snippet's own request, "Build a low-poly washing machine.", was also in the test set. One of its outputs is in the typical outputs below, with a mean score of 1.0 of 3.

Earlier tests of closely related models found that picking the best of 8 with a vision model (GPT-6.1 Terra scoring rendered views) raised the recognisable rate by about 23 to 28 points. This was not measured for this exact model.

Results

How it was tested

  • Requests: 90 requests for objects whose names appear nowhere in the training data, in 10 categories. The model made 2 attempts at each, giving 180 outputs.

  • Renders: every output was rendered from 6 views: front, back, left, right, front-left and back-right.

  • Raters: 5 independent blinded raters, all AI models (3 Claude Opus 5.5 instances, Gemini 3.8 Flash and Claude Sonnet 5.5), scored every output from 0 to 3:

    • 0: wrong, broken or empty;
    • 1: weak, the object can be guessed;
    • 2: clearly the requested object, an acceptable asset;
    • 3: good.

    Agreement between raters was Krippendorff's α = 0.81. A score of 2 or more counts as recognisable.

Main result

LowPoly Coder 4B DeepSeek V4.1 Flash (wrote the first-stage training examples)
Recognisable (score ≥ 2) 24.0% (95% CI 18.0–30.2%) 93.7%

Score 3 ("good") is under 1% of outputs.

Technical checks

Check Outputs (of 180)
Ran and rendered 172
Passed every check (closed parts, within its budget, all parts connected) 116
Had detached, floating parts 51

By category

Category Recognisable
birds, sea creatures and bugs 56%
animals 47%
containers and food 29%
props 20%
architecture 17%
fantasy and game 17%
vehicles 14%
furniture 14%
plants 14%
tools and gear 6%

Each category has 10 to 20 outputs, so these rates are noisy.

What the outputs look like

These are real test-set outputs, shown from the front-left view and cropped from the evaluation renders. Each label gives the request, the mean of the 5 raters' scores (0 to 3), how many of the 5 raters scored it 2 or more, and the result of the technical checks.

Best outputs

Six of the best test outputs: a bungalow, an omelette, an oriole, a canary, a quokka and a flower in a pot, with mean rater scores from 2.0 to 2.6 of 3.

These are among the highest-scoring outputs. They include a brown bungalow with a gable roof, chimney, door and windows (which still has detached parts), a yellow omelette, an orange-and-black oriole, a yellow canary, a round-eared brown quokka and a red flower in a pot (the request asked for a vase). Even the best outputs are simple shapes in flat colours.

Typical outputs

Six typical test outputs: a washing machine, an okapi, a cement mixer truck, a typewriter, a bowl of ramen and a dugong, with mean rater scores from 1.0 to 1.4 of 3.

About 3 in 4 outputs fall below the recognisable bar, like these. They are often the right kind of thing but not clearly the requested object:

  • the washing machine is a beige cabinet with a knob and a curved line, but no round door;
  • the okapi is a plain brown, deer-like animal with no stripes, and has detached parts;
  • the cement mixer's drum lies sideways across the truck;
  • the typewriter is a dark block with a row of keys and a paper bar on top;
  • the ramen is a bowl of red broth with a few sticks in it;
  • the dugong is a thin, dark, torpedo-like shape.

Failures

Six failed test outputs, all with a mean rater score of 0.2 of 3: a radiator, a playpen, a troika sleigh and a stockade fort made of loose floating pieces, an upside-down pyramid for a teepee, and an empty plate for a steak.

These are among the lowest-scoring outputs that rendered:

  • the radiator, playpen, troika sleigh and stockade are loose pieces floating in the air;
  • the teepee is an upside-down pyramid;
  • "steak on a plate" is an empty plate.

The teepee and the plate passed every technical check, so passing the checks does not mean the object is right. Outputs that did not run or render at all are not shown; 172 of the 180 outputs ran and rendered.

Limitations

  • Low success rate: about 3 in 4 outputs are not recognisable, and under 1% are rated "good". Plan to sample several attempts per request and pick the best by eye or with a vision model.
  • Broken geometry is common: only 116 of 180 test outputs passed every technical check, and 51 had detached, floating parts.
  • Uneven across categories: birds, sea creatures and bugs (56%) and animals (47%) do best. Tools and gear (6%) and furniture, plants and vehicles (14% each) do worst.
  • Not a scene generator: when asked for scenes with hundreds of objects (aquarium, reef, pond, forest, village, city block; 24 attempts), the model builds one small asset of about 1,000 triangles with roughly 11 to 25 objects. A small village block worked best, and the aquarium tanks came out as opaque boxes.
  • One look: every output uses the same flat-shaded "woodland collection" style and fixed palette, with no textures.
  • Untrusted code: the model writes code that you then execute. Always run it in an isolated container or VM, as described above.

Training

The model was fine-tuned on request → spec + code examples in two stages: 7,599 written by DeepSeek V4.1 Flash, then 4,245 of its own outputs that a vision model (GPT-6.1 Terra) rated as recognisable, all kept only if they passed the technical checks. Both stages are published, with the held-out validation rows, in townbox/lowpoly-coder-4b-data.

Files

File What it is
*.safetensors (2 shards) Merged bf16 weights, about 9.3 GB
system_prompt.md The exact system prompt the model was trained with; always send it
export_glb.mjs Converts a model reply into a .glb file
images/ The render images on this card

Licence

Apache-2.0, inherited from Qwen3.5-4B. The training data contains code written by deepseek/deepseek-v4.1-flash-1017, so that model's terms may also apply to the data.

Downloads last month
21
Safetensors
Model size
5B params
Tensor type
BF16
·
F32
·
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Model tree for townbox/lowpoly-coder-4b

Finetuned
Qwen/Qwen3.5-4B
Finetuned
(1,259)
this model
Quantizations
1 model

Dataset used to train townbox/lowpoly-coder-4b