public UpdateCheckJob(VoxelBlock block, VoxelTree control, byte detailLevel) { this.block = block; this.control = control; this.detailLevel = detailLevel; control.addUpdateCheckJob(); }
protected void apply(Application app, VoxelBlock block, Index pos) { Index cornerChild = pos.getChild(); for(byte c = 0; c<VoxelBlock.CHILD_COUNT; ++c) { // TODO: use min and max to reduce number of values considered Index childPos = cornerChild.getNeighbor(c); Action action = checkMutation(app, childPos); // check if voxel is outside of modifier area if (!action.modify) continue; // check if voxel is inside of masked area Action maskAction = checkMasks(app.tree, childPos); if (!maskAction.modify) continue; // recurse or set full voxel if (childPos.depth < app.tree.maximumDetail && (maskAction.doTraverse || action.doTraverse)) apply(app, block.expand(childPos.xLocal, childPos.yLocal, childPos.zLocal), childPos); else block.children[childPos.xLocal, childPos.yLocal, childPos.zLocal] = mutate(app, childPos, action, block.children[childPos.xLocal, childPos.yLocal, childPos.zLocal].toVoxel()); // update meshes if appropriate if (childPos.depth == app.tree.maximumDetail - VoxelRenderer.VOXEL_COUNT_POWER) { UpdateCheckJob job = new UpdateCheckJob(block, app.tree, childPos.depth); job.setOffset((byte)childPos.x, (byte)childPos.y, (byte)childPos.z); job.setForce(true); app.jobs.Add(job); } } }
public virtual void initialize() { // float startTime = Time.realtimeSinceStartup; // setup lookup tables, etc. sizes = new float[maxDetail + 1]; float s = baseSize; for (int i = 0; i <= maxDetail; ++i) { sizes[i] = s; s /= 2; } updateCheckJobs = 0; // initialize voxels head = new VoxelBlock(); // genData(0); // float endTime = Time.realtimeSinceStartup; // print("Voxel Gen time: " + (endTime - startTime)); // print("Average Voxel Opacity: " + head.averageOpacity()); // print("Total Voxel Blocks: " + VoxelHolder.blockCount); // print("Total Tree Count: " + treeCount); // print("Renderer Count: " + VoxelRenderer.rendCount); // print("Duplicate Triangle Count: " + VoxelRenderer.duplicateTriangleCount); }
public void set(byte detailLevel, int x, int y, int z, Voxel value, Tree control) { if (detailLevel > 0) { short factor = (short)(1 << (detailLevel - CHILD_COUNT_POWER)); byte xi = (byte)(x / factor); byte yi = (byte)(y / factor); byte zi = (byte)(z / factor); if (detailLevel == CHILD_COUNT_POWER) { children[xi, yi, zi] = value; } else { if (children[xi, yi, zi].GetType() == typeof(Voxel)) { if (children[xi, yi, zi].Equals(value)) { ++skippedSubdivisions; return; } children[xi, yi, zi] = new VoxelBlock((Voxel)children[xi, yi, zi]); } ((VoxelBlock)children[xi, yi, zi]).set((byte)(detailLevel - CHILD_COUNT_POWER), x - xi * factor, y - yi * factor, z - zi * factor, value, control); } } else { set(value); } }
public void updateAll(int x, int y, int z, byte detailLevel, VoxelTree control, bool force = false) { // check if this is a high enough detail level. If not, call the childrens' update methods if (!isRenderSize(control.sizes[detailLevel], control) && (!isRenderLod(x, y, z, control.sizes[detailLevel], control))) { for (byte xi = 0; xi < CHILD_DIMENSION; ++xi) { for (byte yi = 0; yi < CHILD_DIMENSION; ++yi) { for (byte zi = 0; zi < CHILD_DIMENSION; ++zi) { //VoxelUpdateInfo childInfo = new VoxelUpdateInfo(info, xi, yi, zi); if (children[xi, yi, zi].GetType() == typeof(Voxel)) { //if (!childInfo.isSolid()) children[xi, yi, zi] = new VoxelBlock((Voxel)children[xi, yi, zi]); //else //continue; } UpdateCheckJob job = new UpdateCheckJob((VoxelBlock)children[xi, yi, zi], control, (byte)(detailLevel + 1)); job.setOffset((byte)(x * CHILD_DIMENSION + xi), (byte)(y * CHILD_DIMENSION + yi), (byte)(z * CHILD_DIMENSION + zi)); control.enqueueCheck(job); } } } if (renderer != null) { //GameObject.Destroy(renderer.ob); //lock (myControl) { // myControl.enqueueJob(new DropRendererJob(renderer)); // renderer = null; //} renderer.old = true; } return; } // check if we already have a mesh if (renderer == null) { //clearSubRenderers(); renderer = new VoxelRenderer(new VoxelIndex(x, y, z, detailLevel), control); //info.renderers[1, 1, 1] = renderer; } else { renderer.old = false; if (!force) { return; } } // We should generate a mesh GenMeshJob updateJob = new GenMeshJob(this, control, detailLevel); updateJob.setOffset(x, y, z); control.enqueueUpdate(updateJob); }
public VoxelBlock expand(uint x, uint y, uint z) { if (children[x, y, z].GetType() == typeof(Voxel)) { children[x, y, z] = new VoxelBlock((Voxel)children[x, y, z]); } return((VoxelBlock)children[x, y, z]); }
public void wipe() { clearRenderers(); if (head != null) { head = null; } dirty = true; }
public override void execute() { //MonoBehaviour.print("APPLYING!"); Vector3 pos = rend.position / rend.size; if (VoxelBlock.isRenderLod(pos.x, pos.y, pos.z, rend.size, rend.control) || VoxelBlock.isRenderSize(rend.size, rend.control)) { rend.applyMesh(detailLevel, x, y, z); } }
protected void traverse(VoxelUpdateInfo info, byte detailLevel) { int factor = 1 << (detailLevel - VoxelBlock.CHILD_COUNT_POWER); byte xiMin = (byte)Mathf.Max(minX / factor - info.x * VoxelBlock.CHILD_DIMENSION, 0f); byte xiMax = (byte)Mathf.Min((maxX + 1) / factor - info.x * VoxelBlock.CHILD_DIMENSION, VoxelBlock.CHILD_DIMENSION - 1f); byte yiMin = (byte)Mathf.Max(minY / factor - info.y * VoxelBlock.CHILD_DIMENSION, 0f); byte yiMax = (byte)Mathf.Min((maxY + 1) / factor - info.y * VoxelBlock.CHILD_DIMENSION, VoxelBlock.CHILD_DIMENSION - 1f); byte ziMin = (byte)Mathf.Max(minZ / factor - info.z * VoxelBlock.CHILD_DIMENSION, 0f); byte ziMax = (byte)Mathf.Min((maxZ + 1) / factor - info.z * VoxelBlock.CHILD_DIMENSION, VoxelBlock.CHILD_DIMENSION - 1f); VoxelBlock block = (VoxelBlock)info.blocks[1, 1, 1]; uint scale = (uint)(1 << (VoxelBlock.CHILD_COUNT_POWER * (detailLevel - 1))); for (byte yi = yiMin; yi <= yiMax; ++yi) { if ((info.y * VoxelBlock.CHILD_DIMENSION + yi) < maskMinY / scale || (info.y * VoxelBlock.CHILD_DIMENSION + yi) > maskMaxY / scale + 1) { continue; } for (byte xi = xiMin; xi <= xiMax; ++xi) { for (byte zi = ziMin; zi <= ziMax; ++zi) { if (detailLevel <= VoxelBlock.CHILD_COUNT_POWER) { block.children[xi, yi, zi] = modifyVoxel(block.children[xi, yi, zi], info.x * VoxelBlock.CHILD_DIMENSION + xi, info.y * VoxelBlock.CHILD_DIMENSION + yi, info.z * VoxelBlock.CHILD_DIMENSION + zi); } else { if (block.children[xi, yi, zi].GetType() == typeof(Voxel)) { block.children[xi, yi, zi] = new VoxelBlock((Voxel)block.children[xi, yi, zi]); } traverse(new VoxelUpdateInfo(info, xi, yi, zi), (byte)(detailLevel - VoxelBlock.CHILD_COUNT_POWER)); } } } } if (updateMesh && info != null && (VoxelBlock.isRenderSize(info.size, control) || VoxelBlock.isRenderLod(info.x, info.y, info.z, info.size, control))) { //block.clearSubRenderers(control); block.updateAll(info.x, info.y, info.z, info.detailLevel, control, true); } }
public void set(byte detailLevel, int x, int y, int z, Voxel value, Tree control) { if (detailLevel > 0) { short factor = (short)(1 << (detailLevel - CHILD_COUNT_POWER)); byte xi = (byte)(x / factor); byte yi = (byte)(y / factor); byte zi = (byte)(z / factor); if (detailLevel == CHILD_COUNT_POWER) { children[xi, yi, zi] = value; } else { if (children[xi, yi, zi].GetType() == typeof(Voxel)) { if (children[xi, yi, zi].Equals(value)) { ++skippedSubdivisions; return; } children[xi, yi, zi] = new VoxelBlock((Voxel)children[xi, yi, zi]); } ((VoxelBlock)children[xi, yi, zi]).set((byte)(detailLevel - CHILD_COUNT_POWER), x - xi * factor, y - yi * factor, z - zi * factor, value, control); } } else set(value); }
public void OnAfterDeserialize() { // clearRenderers(); //print("Deserializing"); lock (this) { if (voxelData.Length > 0) { MemoryStream stream = new MemoryStream(voxelData); BinaryReader reader = new BinaryReader(stream); head = (VoxelBlock)VoxelHolder.deserialize(reader); stream.Close(); } // relink renderers //relinkRenderers(); enqueueJob(new LinkRenderersJob(this)); } }
public bool import(string fileName) { clearRenderers(); Stream stream = File.OpenRead(fileName); BinaryReader reader = new BinaryReader(stream); ulong fileFormatVersion = reader.ReadUInt64(); if (fileFormatVersion != FILE_FORMAT_VERSION) { stream.Close(); print("Wrong voxel file format version: " + fileFormatVersion + ", should be " + FILE_FORMAT_VERSION); return(false); } else { head = (VoxelBlock)VoxelHolder.deserialize(reader); dirty = true; stream.Close(); return(true); } }
public GenMeshJob(VoxelBlock block, VoxelTree control, byte detailLevel) { this.block = block; this.control = control; this.detailLevel = detailLevel; }
public void updateAll(uint x, uint y, uint z, byte detailLevel, Tree control, bool force = false) { // check if this is a high enough detail level. If not, call the childrens' update methods VoxelRenderer renderer = control.getRenderer(new Index(detailLevel, x, y, z)); if (!isRenderSize(control.sizes[detailLevel], control) && (!isRenderLod(x, y, z, control.sizes[detailLevel], control))) { for (byte xi = 0; xi < CHILD_DIMENSION; ++xi) { for (byte yi = 0; yi < CHILD_DIMENSION; ++yi) { for (byte zi = 0; zi < CHILD_DIMENSION; ++zi) { //VoxelUpdateInfo childInfo = new VoxelUpdateInfo(info, xi, yi, zi); if (children[xi, yi, zi].GetType() == typeof(Voxel)) { //if (!childInfo.isSolid()) children[xi, yi, zi] = new VoxelBlock((Voxel)children[xi, yi, zi]); //else //continue; } UpdateCheckJob job = new UpdateCheckJob((VoxelBlock)children[xi, yi, zi], control, (byte)(detailLevel + 1)); job.setOffset((byte)(x * CHILD_DIMENSION + xi), (byte)(y * CHILD_DIMENSION + yi), (byte)(z * CHILD_DIMENSION + zi)); control.enqueueCheck(job); } } } if (renderer != null) { //GameObject.Destroy(renderer.ob); //lock (myControl) { // myControl.enqueueJob(new DropRendererJob(renderer)); // renderer = null; //} renderer.old = true; } return; } // check if we already have a mesh if (renderer == null) { //clearSubRenderers(); renderer = new VoxelRenderer(new Index(detailLevel, x, y, z), control); //info.renderers[1, 1, 1] = renderer; } else { renderer.old = false; if (!force) return; } // We should generate a mesh GenMeshJob updateJob = new GenMeshJob(this, control, detailLevel); updateJob.setOffset(x, y, z); control.enqueueUpdate(updateJob); }
public void setToHeightmap(byte detailLevel, int x, int y, int z, ref float[,] map, byte[,] mats, OcTree control) { if (detailLevel <= CHILD_COUNT_POWER) { for (int xi = 0; xi < CHILD_DIMENSION; ++xi) { for (int zi = 0; zi < CHILD_DIMENSION; ++zi) { for (int yi = 0; yi < CHILD_DIMENSION; ++yi) { if (yi + y >= map[x + xi, z + zi]) break; else if (yi + y >= map[x + xi, z + zi] - 1) { if (mats[x + xi, z + zi] == byte.MaxValue) children[xi, yi, zi] = Voxel.empty; else children[xi, yi, zi] = new Voxel(mats[x + xi, z + zi], (byte)((map[x + xi, z + zi] - yi - y) * byte.MaxValue)); } else { if (mats[x + xi, z + zi] == byte.MaxValue) children[xi, yi, zi] = Voxel.empty; else children[xi, yi, zi] = new Voxel(mats[x + xi, z + zi], byte.MaxValue); } } } } } else { int multiplier = (1 << (detailLevel - CHILD_COUNT_POWER)); for (int xi = 0; xi < CHILD_DIMENSION; ++xi) { for (int zi = 0; zi < CHILD_DIMENSION; ++zi) { int xMax = x + (xi + 1) * multiplier; int zMax = z + (zi + 1) * multiplier; float yMin = float.MaxValue; float yMax = 0; bool multipleMaterials = false; byte material = mats[x, z]; for (int xPos = x + xi * multiplier; xPos < xMax; ++xPos) { for (int zPos = z + zi * multiplier; zPos < zMax; ++zPos) { if (map[xPos, zPos] < yMin) yMin = map[xPos, zPos]; if (map[xPos, zPos] > yMax) yMax = map[xPos, zPos]; if (mats[xPos, zPos] != material) multipleMaterials = true; } } if (multipleMaterials) yMin = 0; int firstUnsolidBlock = Mathf.Min(((int)(yMin - y)) / multiplier, CHILD_DIMENSION); int lastUnsolidBlock = Mathf.Min(((int)(yMax - y)) / multiplier, CHILD_DIMENSION - 1); int yi = 0; for (; yi < firstUnsolidBlock; ++yi) { if (mats[x + xi * multiplier, z + zi * multiplier] == byte.MaxValue) children[xi, yi, zi] = Voxel.empty; else children[xi, yi, zi] = new Voxel(mats[x + xi * multiplier, z + zi * multiplier], byte.MaxValue); } if (lastUnsolidBlock < 0) continue; for (; yi <= lastUnsolidBlock; ++yi) { VoxelBlock newChild = new VoxelBlock(); newChild.setToHeightmap((byte)(detailLevel - CHILD_COUNT_POWER), x + xi * multiplier, y + yi * multiplier, z + zi * multiplier, ref map, mats, control); children[xi, yi, zi] = newChild; } } } } control.dirty = true; }
public void updateAll(uint x, uint y, uint z, byte detailLevel, OcTree control, bool force = false) { // check if this is a high enough detail level. If not, call the childrens' update methods VoxelRenderer renderer = control.getRenderer(new Index(detailLevel, x, y, z)); if (!isRenderSize(control.sizes[detailLevel], control) && (!isRenderLod(x, y, z, control.sizes[detailLevel], control))) { for (byte xi = 0; xi < CHILD_DIMENSION; ++xi) { for (byte yi = 0; yi < CHILD_DIMENSION; ++yi) { for (byte zi = 0; zi < CHILD_DIMENSION; ++zi) { if (children[xi, yi, zi].GetType() == typeof(Voxel)) { children[xi, yi, zi] = new VoxelBlock((Voxel)children[xi, yi, zi]); } UpdateCheckJob job = new UpdateCheckJob((VoxelBlock)children[xi, yi, zi], control, (byte)(detailLevel + 1)); job.setOffset((byte)(x * CHILD_DIMENSION + xi), (byte)(y * CHILD_DIMENSION + yi), (byte)(z * CHILD_DIMENSION + zi)); control.enqueueCheck(job); } } } return; } // check if we already have a mesh if (renderer == null) { renderer = new VoxelRenderer(new Index(detailLevel, x, y, z), control); } else if (!force) { return; } // We should generate a mesh GenMeshJob updateJob = new GenMeshJob(this, control, detailLevel); updateJob.setOffset(x, y, z); control.enqueueUpdate(updateJob); }
public void setToHeightmap(byte detailLevel, int x, int y, int z, ref float[,] map, byte material, OcTree control) { if (detailLevel <= CHILD_COUNT_POWER) { for (int xi = 0; xi < CHILD_DIMENSION; ++xi) { for (int zi = 0; zi < CHILD_DIMENSION; ++zi) { for (int yi = 0; yi < CHILD_DIMENSION; ++yi) { if (yi + y >= map[x + xi, z + zi]) break; else if (material == byte.MaxValue) { children[xi, yi, zi] = Voxel.empty; } else { if (yi + y >= map[x + xi, z + zi] - 1) { byte opacity = (byte)((map[x + xi, z + zi] - yi - y) * byte.MaxValue); if (opacity > control.isoLevel || children[xi, yi, zi].averageOpacity() <= opacity) children[xi, yi, zi] = new Voxel(material, opacity); } else { children[xi, yi, zi] = new Voxel(material, byte.MaxValue); } } } } } } else { int multiplier = (1 << (detailLevel - CHILD_COUNT_POWER)); for (int xi = 0; xi < CHILD_DIMENSION; ++xi) { for (int zi = 0; zi < CHILD_DIMENSION; ++zi) { int xMax = x + (xi + 1) * multiplier; int zMax = z + (zi + 1) * multiplier; float yMin = float.MaxValue; float yMax = 0; for (int xPos = x + xi * multiplier; xPos < xMax; ++xPos) { for (int zPos = z + zi * multiplier; zPos < zMax; ++zPos) { if (map[xPos, zPos] < yMin) yMin = map[xPos, zPos]; if (map[xPos, zPos] > yMax) yMax = map[xPos, zPos]; } } int firstUnsolidBlock = Mathf.Min(((int)(yMin - y)) / multiplier, CHILD_DIMENSION); int lastUnsolidBlock = Mathf.Min(((int)(yMax - y)) / multiplier, CHILD_DIMENSION - 1); int yi = 0; for (; yi < firstUnsolidBlock; ++yi) { if (material == byte.MaxValue) children[xi, yi, zi] = Voxel.empty; else children[xi, yi, zi] = new Voxel(material, byte.MaxValue); } if (lastUnsolidBlock < 0) continue; for (; yi <= lastUnsolidBlock; ++yi) { if (children[xi, yi, zi].GetType() == typeof(Voxel)) children[xi, yi, zi] = new VoxelBlock((Voxel)children[xi, yi, zi]); ((VoxelBlock)children[xi, yi, zi]).setToHeightmap((byte)(detailLevel - CHILD_COUNT_POWER), x + xi * multiplier, y + yi * multiplier, z + zi * multiplier, ref map, material, control); } } } } control.dirty = true; }
public VoxelBlock expand(uint x, uint y, uint z) { if (children[x, y, z].GetType() == typeof(Voxel)) children[x, y, z] = new VoxelBlock((Voxel)children[x, y, z]); return (VoxelBlock)children[x, y, z]; }
public void setToHeightmap(byte detailLevel, int x, int y, int z, ref float[,] map, byte[,] mats, Tree control) { if (detailLevel <= CHILD_COUNT_POWER) { for (int xi = 0; xi < CHILD_DIMENSION; ++xi) { for (int zi = 0; zi < CHILD_DIMENSION; ++zi) { for (int yi = 0; yi < CHILD_DIMENSION; ++yi) { if (yi + y >= map[x + xi, z + zi]) { break; } else if (yi + y >= map[x + xi, z + zi] - 1) { if (mats[x + xi, z + zi] == byte.MaxValue) { children[xi, yi, zi] = Voxel.empty; } else { children[xi, yi, zi] = new Voxel(mats[x + xi, z + zi], (byte)((map[x + xi, z + zi] - yi - y) * byte.MaxValue)); } } else { if (mats[x + xi, z + zi] == byte.MaxValue) { children[xi, yi, zi] = Voxel.empty; } else { children[xi, yi, zi] = new Voxel(mats[x + xi, z + zi], byte.MaxValue); } } } } } } else { int multiplier = (1 << (detailLevel - CHILD_COUNT_POWER)); for (int xi = 0; xi < CHILD_DIMENSION; ++xi) { for (int zi = 0; zi < CHILD_DIMENSION; ++zi) { int xMax = x + (xi + 1) * multiplier; int zMax = z + (zi + 1) * multiplier; float yMin = float.MaxValue; float yMax = 0; bool multipleMaterials = false; byte material = mats[x, z]; for (int xPos = x + xi * multiplier; xPos < xMax; ++xPos) { for (int zPos = z + zi * multiplier; zPos < zMax; ++zPos) { if (map[xPos, zPos] < yMin) { yMin = map[xPos, zPos]; } if (map[xPos, zPos] > yMax) { yMax = map[xPos, zPos]; } if (mats[xPos, zPos] != material) { multipleMaterials = true; } } } if (multipleMaterials) { yMin = 0; } int firstUnsolidBlock = Mathf.Min(((int)(yMin - y)) / multiplier, CHILD_DIMENSION); int lastUnsolidBlock = Mathf.Min(((int)(yMax - y)) / multiplier, CHILD_DIMENSION - 1); int yi = 0; for (; yi < firstUnsolidBlock; ++yi) { if (mats[x + xi * multiplier, z + zi * multiplier] == byte.MaxValue) { children[xi, yi, zi] = Voxel.empty; } else { children[xi, yi, zi] = new Voxel(mats[x + xi * multiplier, z + zi * multiplier], byte.MaxValue); } } if (lastUnsolidBlock < 0) { continue; } for (; yi <= lastUnsolidBlock; ++yi) { VoxelBlock newChild = new VoxelBlock(); newChild.setToHeightmap((byte)(detailLevel - CHILD_COUNT_POWER), x + xi * multiplier, y + yi * multiplier, z + zi * multiplier, ref map, mats, control); children[xi, yi, zi] = newChild; } } } } control.dirty = true; }
public void setToHeightmap(byte detailLevel, int x, int y, int z, ref float[,] map, byte material, Tree control) { if (detailLevel <= CHILD_COUNT_POWER) { for (int xi = 0; xi < CHILD_DIMENSION; ++xi) { for (int zi = 0; zi < CHILD_DIMENSION; ++zi) { for (int yi = 0; yi < CHILD_DIMENSION; ++yi) { if (yi + y >= map[x + xi, z + zi]) { break; } else if (material == byte.MaxValue) { children[xi, yi, zi] = Voxel.empty; } else { if (yi + y >= map[x + xi, z + zi] - 1) { byte opacity = (byte)((map[x + xi, z + zi] - yi - y) * byte.MaxValue); if (opacity > control.isoLevel || children[xi, yi, zi].averageOpacity() <= opacity) { children[xi, yi, zi] = new Voxel(material, opacity); } } else { children[xi, yi, zi] = new Voxel(material, byte.MaxValue); } } } } } } else { int multiplier = (1 << (detailLevel - CHILD_COUNT_POWER)); for (int xi = 0; xi < CHILD_DIMENSION; ++xi) { for (int zi = 0; zi < CHILD_DIMENSION; ++zi) { int xMax = x + (xi + 1) * multiplier; int zMax = z + (zi + 1) * multiplier; float yMin = float.MaxValue; float yMax = 0; for (int xPos = x + xi * multiplier; xPos < xMax; ++xPos) { for (int zPos = z + zi * multiplier; zPos < zMax; ++zPos) { if (map[xPos, zPos] < yMin) { yMin = map[xPos, zPos]; } if (map[xPos, zPos] > yMax) { yMax = map[xPos, zPos]; } } } int firstUnsolidBlock = Mathf.Min(((int)(yMin - y)) / multiplier, CHILD_DIMENSION); int lastUnsolidBlock = Mathf.Min(((int)(yMax - y)) / multiplier, CHILD_DIMENSION - 1); int yi = 0; for (; yi < firstUnsolidBlock; ++yi) { if (material == byte.MaxValue) { children[xi, yi, zi] = Voxel.empty; } else { children[xi, yi, zi] = new Voxel(material, byte.MaxValue); } } if (lastUnsolidBlock < 0) { continue; } for (; yi <= lastUnsolidBlock; ++yi) { if (children[xi, yi, zi].GetType() == typeof(Voxel)) { children[xi, yi, zi] = new VoxelBlock((Voxel)children[xi, yi, zi]); } ((VoxelBlock)children[xi, yi, zi]).setToHeightmap((byte)(detailLevel - CHILD_COUNT_POWER), x + xi * multiplier, y + yi * multiplier, z + zi * multiplier, ref map, material, control); } } } } control.dirty = true; }