Merge pull request #503 from Nitero/main

render patterns via textures to boost fps
This commit is contained in:
petrak@ 2023-08-05 13:00:58 -04:00 committed by GitHub
commit 630146956d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 401 additions and 12 deletions

View file

@ -1,5 +1,6 @@
package at.petrak.hexcasting.client.entity;
import at.petrak.hexcasting.client.render.PatternTextureManager;
import at.petrak.hexcasting.client.render.RenderLib;
import at.petrak.hexcasting.common.entities.EntityWallScroll;
import com.mojang.blaze3d.systems.RenderSystem;
@ -97,12 +98,16 @@ public class WallScrollRenderer extends EntityRenderer<EntityWallScroll> {
vertex(mat, norm, light, verts, dx, dy, dz, 1, 1 - margin, 0, 1, 0);
ps.popPose();
if (PatternTextureManager.useTextures && wallScroll.points != null)
PatternTextureManager.renderPatternForScroll(wallScroll.points.pointsKey, ps, bufSource, light, wallScroll.points.zappyPoints, wallScroll.blockSize, wallScroll.getShowsStrokeOrder());
}
if (wallScroll.zappyPoints != null) {
var points = wallScroll.zappyPoints;
ps.pushPose();
//TODO: remove old rendering if not needed anymore for comparison
if(!PatternTextureManager.useTextures && wallScroll.points != null) {
var points = wallScroll.points.zappyPoints;
ps.pushPose();
ps.mulPose(Vector3f.YP.rotationDegrees(180f));
ps.translate(0, 0, 1.1f / 16f);
// make smaller scrolls not be charlie kirk-sized

View file

@ -0,0 +1,15 @@
package at.petrak.hexcasting.client.render;
import net.minecraft.world.phys.Vec2;
import java.util.List;
public class HexPatternPoints {
public List<Vec2> zappyPoints = null;
public String pointsKey = null; //TODO: if a string key isnt performant enough override hashcode for points
public HexPatternPoints(List<Vec2> zappyPoints) {
this.zappyPoints = zappyPoints;
pointsKey = PatternTextureManager.getPointsKey(zappyPoints);
}
}

View file

@ -0,0 +1,307 @@
package at.petrak.hexcasting.client.render;
import at.petrak.hexcasting.api.block.HexBlockEntity;
import at.petrak.hexcasting.api.casting.math.HexPattern;
import at.petrak.hexcasting.common.blocks.akashic.BlockAkashicBookshelf;
import at.petrak.hexcasting.common.blocks.akashic.BlockEntityAkashicBookshelf;
import at.petrak.hexcasting.common.blocks.circles.BlockEntitySlate;
import at.petrak.hexcasting.common.blocks.circles.BlockSlate;
import com.mojang.blaze3d.platform.NativeImage;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import com.mojang.math.Matrix3f;
import com.mojang.math.Matrix4f;
import com.mojang.math.Vector3f;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.GameRenderer;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.texture.DynamicTexture;
import net.minecraft.client.renderer.texture.OverlayTexture;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.Tuple;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.AttachFace;
import net.minecraft.world.phys.Vec2;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.HashMap;
import java.util.List;
import java.util.stream.Collectors;
public class PatternTextureManager {
//TODO: remove if not needed anymore for comparison
public static boolean useTextures = true;
public static int repaintIndex = 0;
public static int resolutionByBlockSize = 512;
public static int paddingByBlockSize = 50;
public static int circleRadiusByBlockSize = 8;
public static int scaleLimit = 16;
private static HashMap<String, ResourceLocation> patternTextures = new HashMap<>();
public static String getPointsKey(List<Vec2> zappyPoints)
{
return zappyPoints.stream()
.map(p -> String.format("(%f,%f)", p.x, p.y))
.collect(Collectors.joining(";"));
}
public static HexPatternPoints generateHexPatternPoints(HexBlockEntity tile, HexPattern pattern, float flowIrregular)
{
var stupidHash = tile.getBlockPos().hashCode();
var lines1 = pattern.toLines(1, Vec2.ZERO);
var zappyPoints = RenderLib.makeZappy(lines1, RenderLib.findDupIndices(pattern.positions()),
10, 0.5f, 0f, flowIrregular, 0f, 1f, stupidHash);
return new HexPatternPoints(zappyPoints);
}
public static void renderPatternForScroll(String pointsKey, PoseStack ps, MultiBufferSource bufSource, int light, List<Vec2> zappyPoints, int blockSize, boolean showStrokeOrder)
{
renderPattern(pointsKey, ps, bufSource, light, zappyPoints, blockSize, showStrokeOrder, false, true, false,false, true,-1);
}
public static void renderPatternForSlate(BlockEntitySlate tile, HexPattern pattern, PoseStack ps, MultiBufferSource buffer, int light, BlockState bs)
{
if(tile.points == null)
tile.points = generateHexPatternPoints(tile, pattern, 0.2f);
boolean isOnWall = bs.getValue(BlockSlate.ATTACH_FACE) == AttachFace.WALL;
boolean isOnCeiling = bs.getValue(BlockSlate.ATTACH_FACE) == AttachFace.CEILING;
int facing = bs.getValue(BlockSlate.FACING).get2DDataValue();
renderPatternForBlockEntity(tile.points, ps, buffer, light, isOnWall, isOnCeiling, true, facing);
}
public static void renderPatternForAkashicBookshelf(BlockEntityAkashicBookshelf tile, HexPattern pattern, PoseStack ps, MultiBufferSource buffer, int light, BlockState bs)
{
if(tile.points == null)
tile.points = generateHexPatternPoints(tile, pattern, 0f);
int facing = bs.getValue(BlockAkashicBookshelf.FACING).get2DDataValue();
renderPatternForBlockEntity(tile.points, ps, buffer, light, true, false, false, facing);
}
public static void renderPatternForBlockEntity(HexPatternPoints points, PoseStack ps, MultiBufferSource buffer, int light, boolean isOnWall, boolean isOnCeiling, boolean isSlate, int facing)
{
var oldShader = RenderSystem.getShader();
ps.pushPose();
RenderSystem.setShader(GameRenderer::getPositionTexShader);
renderPattern(points.pointsKey, ps, buffer, light, points.zappyPoints, 1, false, true, isOnWall, isOnCeiling, isSlate, false, facing);
ps.popPose();
RenderSystem.setShader(() -> oldShader);
}
public static void renderPattern(String pointsKey, PoseStack ps, MultiBufferSource bufSource, int light, List<Vec2> zappyPoints, int blockSize, boolean showStrokeOrder, boolean useFullSize, boolean isOnWall, boolean isOnCeiling, boolean isSlate, boolean isScroll, int facing)
{
ps.pushPose();
PoseStack.Pose last = ps.last();
Matrix4f mat = last.pose();
Matrix3f normal = last.normal();
float x = blockSize, y = blockSize, z = (-1f / 16f) - 0.01f;
float nx = 0, ny = 0, nz = 0;
//TODO: refactor this mess of a method
if(isOnWall)
{
if(isScroll)
{
ps.translate(-blockSize / 2f, -blockSize / 2f, 1f / 32f);
nz = -1;
}
else
{
ps.mulPose(Vector3f.ZP.rotationDegrees(180));
if(isSlate)
{
if(facing == 0)
ps.translate(0,-1,0);
if(facing == 1)
ps.translate(-1,-1,0);
if(facing == 2)
ps.translate(-1,-1,1);
if(facing == 3)
ps.translate(0,-1,1);
}
else
{
z = -0.01f;
if(facing == 0)
ps.translate(0,-1,1);
if(facing == 1)
ps.translate(0,-1,0);
if(facing == 2)
ps.translate(-1,-1,0);
if(facing == 3)
ps.translate(-1,-1,1);
}
if(facing == 0)
ps.mulPose(Vector3f.YP.rotationDegrees(180));
if(facing == 1)
ps.mulPose(Vector3f.YP.rotationDegrees(270));
if(facing == 3)
ps.mulPose(Vector3f.YP.rotationDegrees(90));
if(facing == 0 || facing == 2)
nz = -1;
if(facing == 1 || facing == 3)
nx = -1;
ps.translate(0,0,0);
}
}
else //slates on the floor or ceiling
{
if(facing == 0)
ps.translate(0,0,0);
if(facing == 1)
ps.translate(1,0,0);
if(facing == 2)
ps.translate(1,0,1);
if(facing == 3)
ps.translate(0,0,1);
ps.mulPose(Vector3f.YP.rotationDegrees(facing*-90));
if(isOnCeiling)
{
ps.mulPose(Vector3f.XP.rotationDegrees(-90));
ps.translate(0,-1,1);
}
else
ps.mulPose(Vector3f.XP.rotationDegrees(90));
nz = -1;
}
int lineWidth = 16;
int outerColor = 0xB4B4BE;//0xff_c8c8d2;
int innerColor = 0x2A2A2A;//0xc8_322b33;
if(isScroll)
{
lineWidth = 20;
outerColor = 0xDEDEDE;//0xff_d2c8c8;
innerColor = 0x343434;//0xc8_322b33;
}
ResourceLocation texture = getTexture(zappyPoints, pointsKey, blockSize, showStrokeOrder, lineWidth, useFullSize, new Color(innerColor), new Color(outerColor));
VertexConsumer verts = bufSource.getBuffer(RenderType.entityCutout(texture));
vertex(mat, normal, light, verts, 0, 0, z, 0, 0, nx, ny, nz);
vertex(mat, normal, light, verts, 0, y, z, 0, 1, nx, ny, nz);
vertex(mat, normal, light, verts, x, y, z, 1, 1, nx, ny, nz);
vertex(mat, normal, light, verts, x, 0, z, 1, 0, nx, ny, nz);
ps.popPose();
}
private static void vertex(Matrix4f mat, Matrix3f normal, int light, VertexConsumer verts, float x, float y, float z,
float u, float v, float nx, float ny, float nz) {
verts.vertex(mat, x, y, z)
.color(0xffffffff)
.uv(u, v).overlayCoords(OverlayTexture.NO_OVERLAY).uv2(light)
.normal(normal, nx, ny, nz)
.endVertex();
}
public static ResourceLocation getTexture(List<Vec2> points, String pointsKey, int blockSize, boolean showsStrokeOrder, int lineWidth, boolean useFullSize, Color innerColor, Color outerColor) {
if (patternTextures.containsKey(pointsKey))
return patternTextures.get(pointsKey);
return createTexture(points, pointsKey, blockSize, showsStrokeOrder, lineWidth, useFullSize, innerColor, outerColor);
}
public static ResourceLocation createTexture(List<Vec2> points, String pointsKey, int blockSize, boolean showsStrokeOrder, int lineWidth, boolean useFullSize, Color innerColor, Color outerColor)
{
int resolution = resolutionByBlockSize * blockSize;
int padding = paddingByBlockSize * blockSize;
double minX = Double.MAX_VALUE, maxX = Double.MIN_VALUE, minY = Double.MAX_VALUE, maxY = Double.MIN_VALUE;
for (Vec2 point : points)
{
minX = Math.min(minX, point.x);
maxX = Math.max(maxX, point.x);
minY = Math.min(minY, point.y);
maxY = Math.max(maxY, point.y);
}
double rangeX = maxX - minX;
double rangeY = maxY - minY;
double scale = Math.min((resolution - 2 * padding) / rangeX, (resolution - 2 * padding) / rangeY);
double limit = blockSize * scaleLimit;
if (!useFullSize && scale > limit)
scale = limit;
double offsetX = ((resolution - 2 * padding) - rangeX * scale) / 2;
double offsetY = ((resolution - 2 * padding) - rangeY * scale) / 2;
BufferedImage img = new BufferedImage(resolution, resolution, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = img.createGraphics();
g2d.setColor(outerColor);
g2d.setStroke(new BasicStroke((blockSize * 5f / 3f) * lineWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
drawLines(g2d, points, minX, minY, scale, offsetX, offsetY, padding);
g2d.setColor(innerColor);
g2d.setStroke(new BasicStroke((blockSize * 2f / 3f) * lineWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
drawLines(g2d, points, minX, minY, scale, offsetX, offsetY, padding);
if (showsStrokeOrder) {
g2d.setColor(new Color(0xff_d77b5b));
Tuple<Integer, Integer> point = getTextureCoordinates(points.get(0), minX, minY, scale, offsetX, offsetY, padding);
int spotRadius = circleRadiusByBlockSize * blockSize;
drawHexagon(g2d, point.getA(), point.getB(), spotRadius);
}
g2d.dispose();
NativeImage nativeImage = new NativeImage(img.getWidth(), img.getHeight(), true);
for (int y = 0; y < img.getHeight(); y++)
for (int x = 0; x < img.getWidth(); x++)
nativeImage.setPixelRGBA(x, y, img.getRGB(x, y));
DynamicTexture dynamicTexture = new DynamicTexture(nativeImage);
ResourceLocation resourceLocation = Minecraft.getInstance().getTextureManager().register("hex_pattern_texture_" + points.hashCode() + "_" + repaintIndex + ".png", dynamicTexture);
patternTextures.put(pointsKey, resourceLocation);
return resourceLocation;
}
private static void drawLines(Graphics2D g2d, List<Vec2> points, double minX, double minY, double scale, double offsetX, double offsetY, int padding) {
for (int i = 0; i < points.size() - 1; i++) {
Tuple<Integer, Integer> pointFrom = getTextureCoordinates(points.get(i), minX, minY, scale, offsetX, offsetY, padding);
Tuple<Integer, Integer> pointTo = getTextureCoordinates(points.get(i+1), minX, minY, scale, offsetX, offsetY, padding);
g2d.drawLine(pointFrom.getA(), pointFrom.getB(), pointTo.getA(), pointTo.getB());
}
}
private static Tuple<Integer, Integer> getTextureCoordinates(Vec2 point, double minX, double minY, double scale, double offsetX, double offsetY, int padding) {
int x = (int) ((point.x - minX) * scale + offsetX) + padding;
int y = (int) ((point.y - minY) * scale + offsetY) + padding;
return new Tuple(x, y);
}
private static void drawHexagon(Graphics2D g2d, int x, int y, int radius) {
int fracOfCircle = 6;
Polygon hexagon = new Polygon();
for (int i = 0; i < fracOfCircle; i++) {
double theta = (i / (double) fracOfCircle) * Math.PI * 2;
int hx = (int) (x + Math.cos(theta) * radius);
int hy = (int) (y + Math.sin(theta) * radius);
hexagon.addPoint(hx, hy);
}
g2d.fill(hexagon);
}
public static void repaint() {
repaintIndex++;
patternTextures.clear();
}
}

View file

@ -1,6 +1,7 @@
package at.petrak.hexcasting.client.render.be;
import at.petrak.hexcasting.api.casting.math.HexPattern;
import at.petrak.hexcasting.client.render.PatternTextureManager;
import at.petrak.hexcasting.client.render.RenderLib;
import at.petrak.hexcasting.common.blocks.akashic.BlockAkashicBookshelf;
import at.petrak.hexcasting.common.blocks.akashic.BlockEntityAkashicBookshelf;
@ -16,6 +17,7 @@ import net.minecraft.util.Mth;
import net.minecraft.world.phys.Vec2;
public class BlockEntityAkashicBookshelfRenderer implements BlockEntityRenderer<BlockEntityAkashicBookshelf> {
public BlockEntityAkashicBookshelfRenderer(BlockEntityRendererProvider.Context ctx) {
// NO-OP
}
@ -29,7 +31,12 @@ public class BlockEntityAkashicBookshelfRenderer implements BlockEntityRenderer<
}
var bs = tile.getBlockState();
if(PatternTextureManager.useTextures) {
PatternTextureManager.renderPatternForAkashicBookshelf(tile, pattern, ps, buffer, light, bs);
return;
}
//TODO: remove old rendering if not needed anymore for comparison
var oldShader = RenderSystem.getShader();
RenderSystem.setShader(GameRenderer::getPositionColorShader);
RenderSystem.enableDepthTest();

View file

@ -1,5 +1,6 @@
package at.petrak.hexcasting.client.render.be;
import at.petrak.hexcasting.client.render.PatternTextureManager;
import at.petrak.hexcasting.client.render.RenderLib;
import at.petrak.hexcasting.common.blocks.circles.BlockEntitySlate;
import at.petrak.hexcasting.common.blocks.circles.BlockSlate;
@ -23,11 +24,15 @@ public class BlockEntitySlateRenderer implements BlockEntityRenderer<BlockEntity
@Override
public void render(BlockEntitySlate tile, float pPartialTick, PoseStack ps,
MultiBufferSource buffer, int light, int overlay) {
if (tile.pattern == null) {
if (tile.pattern == null)
return;
}
var bs = tile.getBlockState();
if(PatternTextureManager.useTextures && !bs.getValue(BlockSlate.ENERGIZED)) {
PatternTextureManager.renderPatternForSlate(tile, tile.pattern, ps, buffer, light, bs);
return;
}
var oldShader = RenderSystem.getShader();
RenderSystem.setShader(GameRenderer::getPositionColorShader);

View file

@ -1,6 +1,7 @@
package at.petrak.hexcasting.common.blocks.akashic;
import at.petrak.hexcasting.api.block.HexBlockEntity;
import at.petrak.hexcasting.client.render.HexPatternPoints;
import at.petrak.hexcasting.api.casting.iota.Iota;
import at.petrak.hexcasting.api.casting.iota.IotaType;
import at.petrak.hexcasting.api.casting.math.HexPattern;
@ -22,6 +23,8 @@ public class BlockEntityAkashicBookshelf extends HexBlockEntity {
// For both these cases we save just the tag of the iota.
private CompoundTag iotaTag = null;
public HexPatternPoints points;
public BlockEntityAkashicBookshelf(BlockPos pWorldPosition, BlockState pBlockState) {
super(HexBlockEntities.AKASHIC_BOOKSHELF_TILE, pWorldPosition, pBlockState);
}

View file

@ -1,6 +1,7 @@
package at.petrak.hexcasting.common.blocks.circles;
import at.petrak.hexcasting.api.block.HexBlockEntity;
import at.petrak.hexcasting.client.render.HexPatternPoints;
import at.petrak.hexcasting.api.casting.math.HexPattern;
import at.petrak.hexcasting.common.lib.HexBlockEntities;
import net.minecraft.core.BlockPos;
@ -15,6 +16,8 @@ public class BlockEntitySlate extends HexBlockEntity {
@Nullable
public HexPattern pattern;
public HexPatternPoints points;
public BlockEntitySlate(BlockPos pos, BlockState state) {
super(HexBlockEntities.SLATE_TILE, pos, state);
}

View file

@ -0,0 +1,43 @@
package at.petrak.hexcasting.common.command;
import at.petrak.hexcasting.client.render.PatternTextureManager;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import java.awt.*;
public class PatternTexturesCommand
{
public static void add(LiteralArgumentBuilder<CommandSourceStack> cmd) {
cmd.then(Commands.literal("textureToggle")
.requires(dp -> dp.hasPermission(Commands.LEVEL_ADMINS))
.executes(ctx -> {
PatternTextureManager.useTextures = !PatternTextureManager.useTextures;
return 1;
}));
// cmd.then(Commands.literal("textureSetColor")
// .requires(dp -> dp.hasPermission(Commands.LEVEL_ADMINS))
// .then(Commands.argument("r", IntegerArgumentType.integer())
// .then(Commands.argument("g", IntegerArgumentType.integer())
// .then(Commands.argument("b", IntegerArgumentType.integer()).executes(ctx -> {
// var r = IntegerArgumentType.getInteger(ctx, "r");
// var g = IntegerArgumentType.getInteger(ctx, "g");
// var b = IntegerArgumentType.getInteger(ctx, "b");
// PatternTextureManager.color = new Color(r,g,b,255);
// PatternTextureManager.repaint();
// return 1;
// })))));
//
// cmd.then(Commands.literal("textureSetResolution")
// .requires(dp -> dp.hasPermission(Commands.LEVEL_ADMINS))
// .then(Commands.argument("integer", IntegerArgumentType.integer()).executes(ctx -> {
// var integer = IntegerArgumentType.getInteger(ctx, "integer");
// PatternTextureManager.resolutionByBlockSize = integer;
// PatternTextureManager.repaint();
// return 1;
// })));
}
}

View file

@ -1,5 +1,6 @@
package at.petrak.hexcasting.common.entities;
import at.petrak.hexcasting.client.render.HexPatternPoints;
import at.petrak.hexcasting.api.casting.math.HexPattern;
import at.petrak.hexcasting.api.utils.HexUtils;
import at.petrak.hexcasting.api.utils.NBTHelper;
@ -30,12 +31,9 @@ import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.GameRules;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.Vec2;
import net.minecraft.world.phys.Vec3;
import org.jetbrains.annotations.Nullable;
import java.util.List;
public class EntityWallScroll extends HangingEntity {
private static final EntityDataAccessor<Boolean> SHOWS_STROKE_ORDER = SynchedEntityData.defineId(
EntityWallScroll.class,
@ -46,7 +44,7 @@ public class EntityWallScroll extends HangingEntity {
public boolean isAncient;
public int blockSize;
// Client-side only!
public List<Vec2> zappyPoints;
public HexPatternPoints points;
public EntityWallScroll(EntityType<? extends EntityWallScroll> type, Level world) {
super(type, world);
@ -74,14 +72,15 @@ public class EntityWallScroll extends HangingEntity {
var dots = pair.getSecond();
var readOffset = this.getShowsStrokeOrder() ? RenderLib.DEFAULT_READABILITY_OFFSET : 0f;
var lastProp = this.getShowsStrokeOrder() ? RenderLib.DEFAULT_LAST_SEGMENT_LEN_PROP : 1f;
this.zappyPoints = RenderLib.makeZappy(dots, RenderLib.findDupIndices(pattern.positions()), 10, 0.4f,
var zappyPoints = RenderLib.makeZappy(dots, RenderLib.findDupIndices(pattern.positions()), 10, 0.4f,
0f, 0f, readOffset, lastProp, this.getId());
points = new HexPatternPoints(zappyPoints);
}
this.isAncient = NBTHelper.hasString(scroll, ItemScroll.TAG_OP_ID);
} else {
this.pattern = null;
this.zappyPoints = null;
this.points = null;
this.isAncient = false;
}
}

View file

@ -155,7 +155,7 @@ public class HexBlocks {
new BlockAkashicRecord(akashicWoodyHard().lightLevel(bs -> 15)));
public static final BlockAkashicBookshelf AKASHIC_BOOKSHELF = blockItem("akashic_bookshelf",
new BlockAkashicBookshelf(akashicWoodyHard()
.lightLevel(bs -> (bs.getValue(BlockAkashicBookshelf.HAS_BOOKS)) ? 4 : 0)));
.lightLevel(bs -> (bs.getValue(BlockAkashicBookshelf.HAS_BOOKS)) ? 15 : 0)));
public static final BlockAkashicLigature AKASHIC_LIGATURE = blockItem("akashic_connector",
new BlockAkashicLigature(akashicWoodyHard().lightLevel(bs -> 4)));

View file

@ -3,6 +3,7 @@ package at.petrak.hexcasting.common.lib;
import at.petrak.hexcasting.common.command.BrainsweepCommand;
import at.petrak.hexcasting.common.command.ListPerWorldPatternsCommand;
import at.petrak.hexcasting.common.command.RecalcPatternsCommand;
import at.petrak.hexcasting.common.command.PatternTexturesCommand;
import com.mojang.brigadier.CommandDispatcher;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
@ -14,6 +15,7 @@ public class HexCommands {
BrainsweepCommand.add(mainCmd);
ListPerWorldPatternsCommand.add(mainCmd);
RecalcPatternsCommand.add(mainCmd);
PatternTexturesCommand.add(mainCmd);
dispatcher.register(mainCmd);
}