Back to list
Nfemz

blockbench-texture

by Nfemz

Factory automation mod for Hytale - from steam age to quantum tech

0🍴 1📅 Jan 19, 2026

SKILL.md


name: blockbench-texture description: Create, apply, and manage textures on Blockbench models using the Material Palette approach.

Blockbench Texture - 256x256 Palette System

Create and apply textures to an existing Blockbench model using a 256x256 texture atlas with 16 procedurally generated material regions.

Overview

Hytale Prop format requires a single texture. This skill:

  1. Interviews the user about their texture needs
  2. Creates a 256x256 texture atlas with 16 regions (64x64 each)
  3. Generates procedural textures for each region
  4. Maps cube UVs to the correct material region

Architecture: The 256x256 Palette

256x256 Texture Atlas (4x4 grid of 64x64 regions):

     Col 0      Col 1      Col 2      Col 3
    (0-64)    (64-128)  (128-192)  (192-256)
   ┌──────────┬──────────┬──────────┬──────────┐
R0 │ Slot 0   │ Slot 1   │ Slot 2   │ Slot 3   │  (0-64)
   │          │          │          │          │
   ├──────────┼──────────┼──────────┼──────────┤
R1 │ Slot 4   │ Slot 5   │ Slot 6   │ Slot 7   │  (64-128)
   │          │          │          │          │
   ├──────────┼──────────┼──────────┼──────────┤
R2 │ Slot 8   │ Slot 9   │ Slot 10  │ Slot 11  │  (128-192)
   │          │          │          │          │
   ├──────────┼──────────┼──────────┼──────────┤
R3 │ Slot 12  │ Slot 13  │ Slot 14  │ Slot 15  │  (192-256)
   │          │          │          │          │
   └──────────┴──────────┴──────────┴──────────┘

Each slot: 64x64 pixels - enough for detailed procedural patterns

Slot Coordinates Reference

SlotX StartY StartUV Coordinates
000[0, 0, 64, 64]
1640[64, 0, 128, 64]
21280[128, 0, 192, 64]
31920[192, 0, 256, 64]
4064[0, 64, 64, 128]
56464[64, 64, 128, 128]
612864[128, 64, 192, 128]
719264[192, 64, 256, 128]
80128[0, 128, 64, 192]
964128[64, 128, 128, 192]
10128128[128, 128, 192, 192]
11192128[192, 128, 256, 192]
120192[0, 192, 64, 256]
1364192[64, 192, 128, 256]
14128192[128, 192, 192, 256]
15192192[192, 192, 256, 256]

Phase 1: Interview User

Use AskUserQuestion to determine palette contents. Ask about:

Question 1: Visual Style

  • Industrial/mechanical
  • Fantasy/magical
  • Natural/organic
  • Sci-fi/futuristic
  • Mixed

Question 2: Primary Materials Needed

Based on the model elements, ask which material types they need:

  • Metals: Steel, copper, brass, bronze, dark iron, rust
  • Surfaces: Riveted panels, vents/slats, grates, pipes
  • Controls: Button panels, gauges, switches, displays
  • Effects: Glow, fire, warning stripes, hazard markings
  • Natural: Wood, stone, brick, fabric

Question 3: Color Palette Preference

  • Cool tones (grays, blues, silvers)
  • Warm tones (coppers, oranges, browns)
  • Dark/gritty
  • Bright/clean
  • Custom colors

Question 4: Specific Slots

For each slot (0-15), confirm what texture should go there, or use defaults.


Phase 2: Create Atlas Texture

2.1 Create Base Texture

mcp__blockbench__create_texture({
  name: "palette",
  width: 256,
  height: 256,
  fill_color: "#808080"
})

2.2 CRITICAL: Resize Canvas (MCP Bug Workaround)

BUG: The create_texture MCP tool creates 16x16 textures regardless of requested size.

const SIZE = 256;
const tex = Texture.all.find(t => t.name.includes('palette'));
const newCanvas = document.createElement('canvas');
newCanvas.width = SIZE;
newCanvas.height = SIZE;
const newCtx = newCanvas.getContext('2d');
newCtx.fillStyle = '#808080';
newCtx.fillRect(0, 0, SIZE, SIZE);
tex.canvas = newCanvas;
tex.ctx = newCtx;
tex.width = SIZE;
tex.height = SIZE;
tex.uv_width = SIZE;
tex.uv_height = SIZE;
Project.texture_width = SIZE;
Project.texture_height = SIZE;
tex.updateSource(newCanvas.toDataURL());
'Canvas resized to 256x256'

Phase 3: Generate Textures

Available Pattern Generators

Brushed Metal

function brushedMetal(x, y, w, h, r, g, b) {
  for (let row = 0; row < h; row++) {
    const v = Math.floor(Math.random() * 20) - 10;
    ctx.fillStyle = 'rgb('+Math.max(0,Math.min(255,r+v))+','+Math.max(0,Math.min(255,g+v))+','+Math.max(0,Math.min(255,b+v))+')';
    ctx.fillRect(x, y + row, w, 1);
  }
  ctx.fillStyle = 'rgb('+(r+20)+','+(g+20)+','+(b+20)+')';
  ctx.fillRect(x, y, w, 2);
  ctx.fillRect(x, y, 2, h);
  ctx.fillStyle = 'rgb('+Math.max(0,r-20)+','+Math.max(0,g-20)+','+Math.max(0,b-20)+')';
  ctx.fillRect(x+w-2, y, 2, h);
  ctx.fillRect(x, y+h-2, w, 2);
}

Vent Slats

function ventSlats(x, y, w, h, metalR, metalG, metalB) {
  ctx.fillStyle = '#1a1a1a';
  ctx.fillRect(x, y, w, h);
  const slatHeight = 6;
  const gapHeight = 4;
  for (let row = 2; row < h - 2; row += slatHeight + gapHeight) {
    for (let col = 0; col < w; col++) {
      const v = Math.floor(Math.random() * 15) - 7;
      ctx.fillStyle = 'rgb('+Math.max(0,metalR+v)+','+Math.max(0,metalG+v)+','+Math.max(0,metalB+v)+')';
      ctx.fillRect(x + col, y + row, 1, slatHeight);
    }
    ctx.fillStyle = 'rgb('+(metalR+25)+','+(metalG+25)+','+(metalB+25)+')';
    ctx.fillRect(x, y + row, w, 1);
    ctx.fillStyle = 'rgb('+Math.max(0,metalR-25)+','+Math.max(0,metalG-25)+','+Math.max(0,metalB-25)+')';
    ctx.fillRect(x, y + row + slatHeight - 1, w, 1);
  }
}

Button Panel

function buttonPanel(x, y, w, h, baseR, baseG, baseB, cols, rows, btnColors) {
  ctx.fillStyle = 'rgb('+baseR+','+baseG+','+baseB+')';
  ctx.fillRect(x, y, w, h);
  const btnSize = 12;
  const btnGap = (w - cols * btnSize) / (cols + 1);
  const rowGap = (h - rows * btnSize) / (rows + 1);
  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) {
      const bx = x + btnGap + c * (btnSize + btnGap);
      const by = y + rowGap + r * (btnSize + rowGap);
      ctx.fillStyle = '#222222';
      ctx.fillRect(bx - 1, by - 1, btnSize + 2, btnSize + 2);
      ctx.fillStyle = btnColors[(r * cols + c) % btnColors.length];
      ctx.fillRect(bx, by, btnSize, btnSize);
      ctx.fillStyle = 'rgba(255,255,255,0.3)';
      ctx.fillRect(bx, by, btnSize, 2);
      ctx.fillRect(bx, by, 2, btnSize);
      ctx.fillStyle = 'rgba(0,0,0,0.3)';
      ctx.fillRect(bx, by + btnSize - 2, btnSize, 2);
      ctx.fillRect(bx + btnSize - 2, by, 2, btnSize);
    }
  }
}

Riveted Panel

function rivetPanel(x, y, w, h, r, g, b, rivetSpacing) {
  for (let row = 0; row < h; row++) {
    const v = Math.floor(Math.random() * 15) - 7;
    ctx.fillStyle = 'rgb('+Math.max(0,r+v)+','+Math.max(0,g+v)+','+Math.max(0,b+v)+')';
    ctx.fillRect(x, y + row, w, 1);
  }
  ctx.fillStyle = 'rgb('+(r+20)+','+(g+20)+','+(b+20)+')';
  ctx.fillRect(x, y, w, 3);
  ctx.fillRect(x, y, 3, h);
  ctx.fillStyle = 'rgb('+Math.max(0,r-20)+','+Math.max(0,g-20)+','+Math.max(0,b-20)+')';
  ctx.fillRect(x+w-3, y, 3, h);
  ctx.fillRect(x, y+h-3, w, 3);
  for (let ry = rivetSpacing; ry < h - 4; ry += rivetSpacing) {
    for (let rx = rivetSpacing; rx < w - 4; rx += rivetSpacing) {
      ctx.fillStyle = 'rgb('+Math.max(0,r-30)+','+Math.max(0,g-30)+','+Math.max(0,b-30)+')';
      ctx.beginPath();
      ctx.arc(x + rx, y + ry, 3, 0, Math.PI * 2);
      ctx.fill();
      ctx.fillStyle = 'rgb('+(r+10)+','+(g+10)+','+(b+10)+')';
      ctx.beginPath();
      ctx.arc(x + rx - 1, y + ry - 1, 1, 0, Math.PI * 2);
      ctx.fill();
    }
  }
}

Gauge/Dial

function gaugeTexture(x, y, w, h) {
  ctx.fillStyle = '#1a1a1a';
  ctx.fillRect(x, y, w, h);
  const cx = x + w/2;
  const cy = y + h/2;
  const radius = Math.min(w, h) / 2 - 4;
  ctx.fillStyle = '#333333';
  ctx.beginPath();
  ctx.arc(cx, cy, radius, 0, Math.PI * 2);
  ctx.fill();
  ctx.fillStyle = '#111111';
  ctx.beginPath();
  ctx.arc(cx, cy, radius - 3, 0, Math.PI * 2);
  ctx.fill();
  for (let i = 0; i < 10; i++) {
    const angle = (Math.PI * 0.75) + (i / 9) * (Math.PI * 1.5);
    const x1 = cx + Math.cos(angle) * (radius - 8);
    const y1 = cy + Math.sin(angle) * (radius - 8);
    const x2 = cx + Math.cos(angle) * (radius - 4);
    const y2 = cy + Math.sin(angle) * (radius - 4);
    ctx.strokeStyle = '#aaaaaa';
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.moveTo(x1, y1);
    ctx.lineTo(x2, y2);
    ctx.stroke();
  }
  const needleAngle = Math.PI * 1.2;
  ctx.strokeStyle = '#ff3333';
  ctx.lineWidth = 2;
  ctx.beginPath();
  ctx.moveTo(cx, cy);
  ctx.lineTo(cx + Math.cos(needleAngle) * (radius - 12), cy + Math.sin(needleAngle) * (radius - 12));
  ctx.stroke();
}

Glow/Emissive

function glowTexture(x, y, w, h, r, g, b) {
  for (let py = 0; py < h; py++) {
    for (let px = 0; px < w; px++) {
      const dx = px - w/2;
      const dy = py - h/2;
      const dist = Math.sqrt(dx*dx + dy*dy) / (w/2);
      const intensity = Math.max(0, 1 - dist * 0.7);
      const flicker = Math.random() * 0.15;
      const fr = Math.min(255, Math.floor(r * (intensity + flicker)));
      const fg = Math.min(255, Math.floor(g * (intensity + flicker)));
      const fb = Math.min(255, Math.floor(b * (intensity + flicker)));
      ctx.fillStyle = 'rgb('+fr+','+fg+','+fb+')';
      ctx.fillRect(x + px, y + py, 1, 1);
    }
  }
}

Warning Stripes

function warningStripes(x, y, w, h) {
  ctx.fillStyle = '#222222';
  ctx.fillRect(x, y, w, h);
  const stripeW = 12;
  ctx.fillStyle = '#ffcc00';
  for (let i = -h; i < w + h; i += stripeW * 2) {
    ctx.beginPath();
    ctx.moveTo(x + i, y + h);
    ctx.lineTo(x + i + stripeW, y + h);
    ctx.lineTo(x + i + h + stripeW, y);
    ctx.lineTo(x + i + h, y);
    ctx.closePath();
    ctx.fill();
  }
}

Metal Grate

function grateTexture(x, y, w, h, gridSize) {
  ctx.fillStyle = '#0a0a0a';
  ctx.fillRect(x, y, w, h);
  const barWidth = 2;
  ctx.fillStyle = '#3a3a3a';
  for (let gx = 0; gx < w; gx += gridSize) {
    ctx.fillRect(x + gx, y, barWidth, h);
  }
  for (let gy = 0; gy < h; gy += gridSize) {
    ctx.fillRect(x, y + gy, w, barWidth);
  }
  ctx.fillStyle = '#4a4a4a';
  for (let gx = 0; gx < w; gx += gridSize) {
    ctx.fillRect(x + gx, y, 1, h);
  }
}

Pipe/Cylinder

function pipeTexture(x, y, w, h, r, g, b) {
  for (let py = 0; py < h; py++) {
    const curve = Math.sin((py / h) * Math.PI);
    const shade = Math.floor(curve * 40) - 20;
    for (let px = 0; px < w; px++) {
      const v = Math.floor(Math.random() * 10) - 5;
      const fr = Math.max(0, Math.min(255, r + shade + v));
      const fg = Math.max(0, Math.min(255, g + shade + v));
      const fb = Math.max(0, Math.min(255, b + shade + v));
      ctx.fillStyle = 'rgb('+fr+','+fg+','+fb+')';
      ctx.fillRect(x + px, y + py, 1, 1);
    }
  }
  ctx.fillStyle = 'rgb('+(r+30)+','+(g+30)+','+(b+30)+')';
  ctx.fillRect(x, y, w, 4);
  ctx.fillRect(x, y + h - 4, w, 4);
}

Material Color Presets

MaterialRGBHex
steel747482#4a4a52
copper18411551#b87333
dark_iron424248#2a2a30
brass18116666#b5a642
bronze14012083#8c7853
rust1836514#b7410e
light_steel100100110#64646e

Phase 4: Apply UVs to Cubes

Define Palette Mapping

Create a mapping from element names to slot numbers:

const atlas = Texture.all.find(t => t.name.includes('palette'));
const SLOT = 64;

function getSlotUV(slot) {
  const col = slot % 4;
  const row = Math.floor(slot / 4);
  return [col * SLOT, row * SLOT, (col + 1) * SLOT, (row + 1) * SLOT];
}

const materialMap = {
  steel: 0,
  copper: 1,
  dark_iron: 2,
  light_steel: 3,
  vent: 4,
  buttons: 5,
  riveted: 6,
  glow_warm: 7,
  pipe: 8,
  trim: 9,
  glow_hot: 10,
  gauge: 11,
  switch_on: 12,
  switch_off: 13,
  warning: 14,
  grate: 15
};

function getMaterial(name) {
  const n = name.toLowerCase();
  if (n.includes('vent')) return 'vent';
  if (n.includes('grate')) return 'grate';
  if (n.includes('gauge') || n.includes('dial')) return 'gauge';
  if (n.includes('switch')) return 'switch_on';
  if (n.includes('button') || n.includes('control_panel')) return 'buttons';
  if (n.includes('pipe')) return 'pipe';
  if (n.includes('glow') || n.includes('fire')) return 'glow_hot';
  if (n.includes('warning') || n.includes('hazard')) return 'warning';
  if (n.includes('trim')) return 'trim';
  if (n.includes('base') || n.includes('frame') || n.includes('corner')) return 'dark_iron';
  if (n.includes('body') || n.includes('panel')) return 'riveted';
  return 'steel';
}

Cube.all.forEach(cube => {
  const mat = getMaterial(cube.name);
  const slot = materialMap[mat] || 0;
  const uv = getSlotUV(slot);
  Object.keys(cube.faces).forEach(face => {
    cube.faces[face].texture = atlas.uuid;
    cube.faces[face].uv = [...uv];
  });
});

Canvas.updateAllFaces();

Phase 5: Refresh and Verify

const tex = Texture.all[0];
tex.updateSource(tex.canvas.toDataURL());
tex.updateMaterial();
Canvas.updateAllFaces();

Take screenshots from multiple angles to verify.


Example Industrial Palette

A typical industrial/mechanical palette:

SlotTextureUse For
0Brushed steelDefault surfaces
1Brushed copperAccent metal
2Dark ironStructural frames
3Light steelSecondary panels
4Vent slatsAir vents
5Button panelControl interfaces
6Riveted panelMain body panels
7Warm glowLights, indicators
8Copper pipePiping, tubes
9Dark trimEdge trim, borders
10Hot glowFire, heat sources
11Gauge faceDials, meters
12Switch ONToggle switches
13Switch OFFToggle switches
14Warning stripesHazard markings
15Metal grateFloors, walkways

Troubleshooting

IssueSolution
All cubes same textureRe-run UV mapping code
Texture not updatingCall tex.updateSource(tex.canvas.toDataURL())
"Resolution does not match UV size"Set BOTH tex.uv_width/height AND Project.texture_width/height to 256
Preview wrong after changesCall tex.updateMaterial() then Canvas.updateAllFaces()
create_texture ignores sizeKnown MCP bug - always resize canvas manually

DO NOT

  • Use multiple textures (Hytale requires single texture)
  • Use apply_texture MCP tool (broken)
  • Click Validator "Fix UV Size" button (destroys all UV assignments)
  • Forget to resize canvas after create_texture
  • Apply detailed textures (like button panels) to tiny elements - they won't be visible

Score

Total Score

60/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

Reviews

💬

Reviews coming soon