three-wfc
Version:
A blazing fast Wave Function Collapse engine for three.js, built for real-time 2D, 2.5D, and 3D procedural world generation at scale.
3 lines (2 loc) • 13.9 kB
JavaScript
(function(I,C){typeof exports=="object"&&typeof module<"u"?C(exports,require("three")):typeof define=="function"&&define.amd?define(["exports","three"],C):(I=typeof globalThis<"u"?globalThis:I||self,C(I.ThreeWFC={},I.THREE))})(this,function(I,C){"use strict";var v=Object.defineProperty;var G=(I,C,A)=>C in I?v(I,C,{enumerable:!0,configurable:!0,writable:!0,value:A}):I[C]=A;var c=(I,C,A)=>G(I,typeof C!="symbol"?C+"":C,A);const W=["TOP","🔻 BOTTOM 🔻","LEFT","RIGHT","FRONT","BACK"];function z(g,t,s){return function(e){const i=e%t+s.x,o=Math.floor(e/t)+s.y<<16^i;let n=g+Math.imul(o,2654435769);return n=Math.imul(n^n>>>15,n|1),n^=n+Math.imul(n^n>>>7,n|61),((n^n>>>14)>>>0)/4294967296}}const w=1;class L{constructor(t){c(this,"keys");c(this,"entropy");c(this,"keyToPos");c(this,"count");this.count=0,this.keys=new Uint32Array(t+w),this.entropy=new Float32Array(t+w),this.keyToPos=new Int32Array(t).fill(-1)}get size(){return this.count}isEmpty(){return this.count===0}read(t){const s=this.keyToPos[t];return s!==-1?this.entropy[s]:Number.POSITIVE_INFINITY}push(t,s){const e=this.count+w;this.keys[e]=t,this.entropy[e]=s,this.keyToPos[t]=e,this.count++,this.bubbleUp(e)}put(t,s){this.update(t,s)||this.push(t,s)}update(t,s){const e=this.keyToPos[t];if(e===-1)return!1;const i=this.entropy[e];return this.entropy[e]=s,s<i?this.bubbleUp(e):this.bubbleDown(e),!0}pop(){if(!this.count)return null;const t=this.keys[w];if(this.keyToPos[t]=-1,this.count>1){const s=this.count+w-1,e=this.keys[s];this.keys[w]=e,this.entropy[w]=this.entropy[s],this.keyToPos[e]=w,this.count--,this.bubbleDown(w)}else this.count--;return t}peek(){return this.count!==0&&this.entropy[w]}peekKey(){return this.count!==0&&this.keys[w]}remove(t){const s=this.keyToPos[t];if(s===-1)return!1;this.keyToPos[t]=-1;const e=this.count+w-1;if(s===e)this.count--;else if(this.count>1){const i=this.keys[e],r=this.entropy[e];this.keys[s]=i,this.entropy[s]=r,this.keyToPos[i]=s,this.count--;const o=s>>>1;s>w&&this.entropy[s]<this.entropy[o]?this.bubbleUp(s):this.bubbleDown(s)}else this.count--;return!0}clear(){this.count=0,this.keyToPos.fill(-1)}swap(t,s){const e=this.keys,i=this.entropy,r=this.keyToPos,o=e[t],n=e[s],h=i[t],l=i[s];e[t]=n,e[s]=o,i[t]=l,i[s]=h,r[o]=s,r[n]=t}bubbleUp(t){const s=this.entropy,e=s[t];for(;t>w;){const i=t>>>1;if(s[i]<=e)break;this.swap(t,i),t=i}}bubbleDown(t){const s=this.entropy,e=s[t],i=this.count+w;for(;;){const r=t<<1,o=r+1;let n=-1;if(r<i&&s[r]<e&&(n=r),o<i){const h=s[o],l=n===-1?e:s[n];h<l&&(n=o)}if(n===-1)break;this.swap(t,n),t=n}}}const B=new Uint8Array(256);for(let g=0;g<256;g++)B[g]=(g&1)+B[g>>1];const x=g=>B[g&255]+B[g>>8&255]+B[g>>16&255]+B[g>>24&255];class O{constructor(t,s){c(this,"array");c(this,"count");c(this,"stride");c(this,"maskLength");c(this,"isSingleChunk");c(this,"tiles");c(this,"unionMask");c(this,"outputIndices");c(this,"lastChunkMask");this.tiles=s;const e=s.count;this.count=s.count,this.stride=Math.ceil(e/32),this.maskLength=e,this.isSingleChunk=this.stride===1,this.array=new Uint32Array(t*this.stride);const i=e%32;this.lastChunkMask=i===0?4294967295:(1<<i)-1,this.outputIndices=new Uint16Array(this.maskLength),this.unionMask=new Uint32Array(this.stride)}flag(t,s){if(this.isSingleChunk){this.array[t]|=1<<s;return}const e=t*this.stride,i=s>>>5,r=s&31;this.array[e+i]|=1<<r}collapse(t,s){if(this.isSingleChunk){this.array[t]=1<<s;return}const e=t*this.stride,i=s>>>5,r=s&31,o=e+i;for(let n=0;n<this.stride;n++)this.array[e+n]=0;this.array[o]=1<<r}enableAll(t){if(this.isSingleChunk)this.array[t]=this.lastChunkMask;else{const s=t*this.stride,e=s+this.stride;this.array.fill(4294967295,s,e-1),this.array[e-1]=this.lastChunkMask}}optionsCount(t){if(this.isSingleChunk)return x(this.array[t]&this.lastChunkMask);let s=0;const e=t*this.stride,i=e+this.stride,r=this.array;for(let o=e;o<i-1;o++)s+=x(r[o]);return s+=x(r[i-1]&this.lastChunkMask),s}getMask(t){if(this.isSingleChunk)return this.array.subarray(t,t+1);const s=t*this.stride;return this.array.subarray(s,s+this.stride)}collapsedTile(t){const s=this.array;if(this.isSingleChunk){const e=s[t]&this.lastChunkMask;return e===0?-1:31-Math.clz32(e)}else{const e=t*this.stride,i=e+this.stride;for(let r=e;r<i;r++){let o=s[r];if(r===i-1&&(o&=this.lastChunkMask),o===0)continue;const n=(r-e)*32,h=31-Math.clz32(o);return n+h}}return-1}propagate(t,s,e){const i=this.tiles,r=this.getMask(t),o=this.stride,n=i.count,h=this.unionMask.fill(0);for(let l=0;l<o;l++){const a=r[l];if(a===0)continue;const u=l*32;let d=a;for(;d!==0;){const f=d&-d,p=31-Math.clz32(f);d&=~f;const T=u+p;if(T>=n)continue;const y=i.getEdgeMask(e,T);for(let k=0;k<o;k++)h[k]|=y[k]}}return this.intersect(s,h)?this.optionsCount(s)?!0:null:!1}intersect(t,s){let e=!1;if(this.isSingleChunk){const i=this.array[t],r=s[0]??0,o=i&r,h=this.count%32!==0?this.lastChunkMask:4294967295;this.array[t]=o,e=(o&h)!==(i&h)}else{const i=this.stride,r=t*i,o=this.array;for(let n=0;n<i;++n){const h=r+n,l=o[h],a=s[n]??0,u=l&a,p=n===i-1&&this.count%32!==0?this.lastChunkMask:4294967295;(u&p)!==(l&p)&&(e=!0),o[h]=u}}return e}getIndices(t){const s=this.outputIndices,e=this.array,i=this.maskLength;let r=0;if(this.isSingleChunk){const o=e[t]&this.lastChunkMask;if(o===0)return null;r=this._extractSetBits(o,0,i,s,r)}else{const o=this.stride,n=t*o,h=n+o;for(let l=n;l<h;l++){let a=e[l];if(l===h-1&&(a&=this.lastChunkMask),a===0)continue;const u=(l-n)*32;if(u>=i)break;r=this._extractSetBits(a,u,i,s,r)}}return r===0?null:s.subarray(0,r)}_extractSetBits(t,s,e,i,r){for(;t!==0;){const o=t&-t,n=31-Math.clz32(o),h=s+n;h<e&&(i[r++]=h),t&=~o}return r}}const F=g=>{let t=2166136261;for(let s=0,e=g.length;s<e;s++){const i=`${g[s]}`;t^=i.length,t=Math.imul(t,16777619)>>>0;for(let r=0,o=i.length;r<o;r++)t^=i.charCodeAt(r),t=Math.imul(t,16777619)>>>0}return t};class U{constructor(t,s=!0){c(this,"count",0);c(this,"weight");c(this,"edges");c(this,"tiles");c(this,"initialEntropy",0);for(let i=0,r=t.length;i<r;i++)t.push(...t[i].transformClones());const e=t.length;return this.tiles=t,this.count=e,this.weight=new Float32Array(e),this.edges=Array.from({length:s?4:6},()=>new O(e,this)),this._initialize(),this}_initialize(){const t=this.count,s=this.tiles,e=this.edges,i=e.length,r=new Map,o=(l,a)=>l*i+a;let n=0,h=0;for(let l=0;l<t;l++){const a=s[l].weight;this.weight[l]=a,n+=a,h+=a*Math.log(a);for(let u=0;u<i;u++)r.set(o(l,u),F(s[l].edges[u]))}this.initialEntropy=Math.log(n)-h/n;for(let l=0;l<t;l++)for(let a=0;a<t;a++)for(let u=0;u<i;u++){const d=u^1,f=r.get(o(l,u)),p=r.get(o(a,d));f===p&&e[u].flag(l,a)}}getWeight(t){return this.weight[t]}getEdgeMask(t,s){return this.edges[t].getMask(s)}}class ${constructor(t){c(this,"buffer");c(this,"bitset");c(this,"size");c(this,"tail");this.buffer=new Uint32Array(t),this.bitset=new Uint8Array(t),this.size=0,this.tail=0}push(t){this.bitset[t]||(this.buffer[this.tail++]=t,this.size++,this.bitset[t]=1)}pop(){if(this.size===0)return;const t=this.buffer[--this.tail];return this.size--,this.bitset[t]=0,t}reset(){return this.tail=0,this.size=0,this}}class N{constructor(t,s,e,i,r={x:0,y:0},o=1e-5){c(this,"count");c(this,"tiles");c(this,"collapsed");c(this,"entropyHeap");c(this,"options");c(this,"rows");c(this,"cols");c(this,"seed");c(this,"noise");c(this,"origin");c(this,"stackBuffer");c(this,"random");const n=s*e;this.count=n,this.cols=s,this.rows=e,this.seed=i,this.noise=o,this.origin={...r},this.random=i?z(i,s,this.origin):Math.random,this.tiles=new U(t),this.options=new O(n,this.tiles),this.collapsed=new Int16Array(n),this.entropyHeap=new L(n),this.stackBuffer=new $(n);const h=this.tiles.initialEntropy,{options:l,collapsed:a,entropyHeap:u,random:d}=this;for(let f=0;f<n;f++)a[f]=-1,l.enableAll(f),u.push(f,h+d(f)*o)}get isCompleted(){return this.entropyHeap.isEmpty()}get remainingCells(){return this.entropyHeap.size}collapse(){const t=this.entropyHeap.pop();return t!==null&&this._collapseCell(t)}collapseAll(){for(;!this.entropyHeap.isEmpty();)if(!this.collapse())return!this.entropyHeap.isEmpty();return!0}collapseCell(t){return t<0||t>=this.count?(console.error(`WFC CollapseCell: Invalid index ${t}.`),!1):this.collapsed[t]!==-1?!0:this.options.optionsCount(t)?this._collapseCell(t):(console.error(`WFC Contradiction: Attempting to collapse cell ${t} which already has no options.`),!1)}getCollapsedTile(t){return this.collapsed[t]}cellIndex({x:t,y:s}){return t=this.origin.x+(t+this.cols)%this.cols,s=this.origin.y+(s+this.rows)%this.rows,s*this.cols+t}cellPosition(t,s){const e=t%this.cols,i=Math.floor(t/this.cols);return s.x=(e-this.origin.x+this.cols)%this.cols,s.y=(i-this.origin.y+this.rows)%this.rows,s}offset({x:t,y:s}){const e=Math.round(t),i=Math.round(s);if(e===0&&i===0)return!0;this.origin.x=(this.origin.x+e+this.cols)%this.cols,this.origin.y=(this.origin.y+i+this.rows)%this.rows;const r=this.cols,o=this.rows,n=this.collapsed,h=this.options,l=this.entropyHeap,a=this.tiles.initialEntropy,u=this.noise,d=this.random,f=e>0?(this.cols-e)%this.cols:0,p=Math.abs(e),T=i>0?(this.rows-i)%this.rows:0,y=Math.abs(i),k=new Set;if(p>0)for(let b=0;b<p;b++){const S=(f+b)%r;for(let M=0;M<o;M++){const _=M*r+S;n[_]=-1,h.enableAll(_),l.put(_,a+d(_)*u);const E=(M-1+o)%o;if(Math.abs(E-M)===1){const m=E*r+S;n[m]!==-1&&k.add(m)}const P=(M+1)%o;if(Math.abs(P-M)===1){const m=P*r+S;n[m]!==-1&&k.add(m)}const H=(S-1+r)%r;if(Math.abs(H-S)===1){const m=M*r+H;n[m]!==-1&&k.add(m)}const K=(S+1)%r;if(Math.abs(K-S)===1){const m=M*r+K;n[m]!==-1&&k.add(m)}}}if(y>0)for(let b=0;b<y;b++){const S=(T+b)%o,M=p>0?p:0;for(let _=M;_<r;_++){const E=S*r+_;n[E]=-1,h.enableAll(E),l.put(E,a+d(E)*u)}}for(const b of k)if(!this._propagate(b))return console.error(`WFC Offset: Contradiction during propagation from boundary cell ${b}.`),!1;return!0}_collapseCell(t){const s=this.tiles,e=this.options.getIndices(t),i=e.length;let r=0;for(let h=0;h<i;h++)r+=s.getWeight(e[h]);let o=this.random(t+i)*r,n=0;for(let h=0;h<i;h++){const l=e[h];if(o-=s.getWeight(l),o<=0){n=l;break}}return this.options.collapse(t,n),this.collapsed[t]=n,this.entropyHeap.remove(t),this._propagate(t)?(this.entropyHeap.peek()===0&&this.collapse(),!0):(console.error(`WFC Collapse Failed: Contradiction detected during propagation after collapsing cell ${t} to tile ${n}.`),!1)}_propagate(t){const s=this.stackBuffer.reset(),e=this.options,i=this.cols,r=this.rows,o=this.collapsed;let n=t;for(;n!==void 0;){const h=n%i,l=Math.floor(n/i);for(let a=0;a<4;a++){let u=h,d=l;switch(a){case 0:if(l===0)continue;d--;break;case 1:if(l===r-1)continue;d++;break;case 2:if(h===0)continue;u--;break;default:if(h===i-1)continue;u++;break}const f=d*i+u;if(o[f]!==-1)continue;const p=e.propagate(n,f,a);if(p){this._computeEntropy(f),s.push(f);continue}if(p===null)return console.error(`WFC Propagation Contradiction: Cell "${f}" (Neighbor of "${n}" on the "${W[a^1]}" edge) has no options left after propagation from cell ${n}.`),this.entropyHeap.remove(n),!1}n=s.pop()}return!0}_computeEntropy(t){const s=this.options.getIndices(t),e=s.length;if(e===1){this.entropyHeap.update(t,0);return}let i=0,r=0;const o=this.tiles;for(let h=0;h<e;h++){const l=s[h],a=o.getWeight(l);i+=a,r+=a*Math.log(a)}const n=Math.log(i)-r/i;this.entropyHeap.update(t,n+this.random(t)*this.noise)}}class D{constructor({canvas:t,width:s,height:e,cellSize:i,seed:r,drawGrid:o}){c(this,"canvas");c(this,"width");c(this,"height");c(this,"size");c(this,"drawGrid",!1);c(this,"wfcBuffer");c(this,"seed");c(this,"offset",new C.Vector2);c(this,"tiles",[]);c(this,"ctx");this.canvas=t,this.width=s,this.height=e,this.size=i,this.ctx=this.canvas.getContext("2d"),this.canvas.width=s,this.canvas.height=e,r&&(this.seed=r),this.drawGrid=!!o}addTile(...t){this.tiles.push(...t)}removeTile(...t){t.forEach(s=>{const e=this.tiles.indexOf(s);~e&&this.tiles.splice(e,1)})}clear(){this.tiles.length=0}init(){const t=Math.ceil(this.width/this.size),s=Math.ceil(this.height/this.size);this.canvas.width=t*this.size,this.canvas.height=s*this.size,this.wfcBuffer=new N(this.tiles,t,s,this.seed)}collapseAll(){return this.wfcBuffer?this.wfcBuffer.collapseAll():(console.error("WFC not initialized. Call init() first."),!1)}collapse(){return this.wfcBuffer?this.wfcBuffer.collapse():(console.error("WFC not initialized. Call init() first."),!1)}collapseCell(t){return this.wfcBuffer?this.wfcBuffer.collapseCell(t):(console.error("WFC not initialized. Call init() first."),!1)}draw(){const t=this.ctx,s=this.wfcBuffer,e=s.cols,i=s.rows,r=this.tiles,o=this.size,n=this.offset.x,h=this.offset.y,l=this.drawGrid;t.clearRect(0,0,this.canvas.width,this.canvas.height),t.strokeStyle="#555";for(let a=0;a<i;a++)for(let u=0;u<e;u++){const d=a*e+u,f=u*o+n,p=a*o+h;if(s.collapsed[d]!==-1){const T=s.getCollapsedTile(d),y=T!==-1?r[T]:null;if(y&&y.image){const k=y.image;if(y._rotation!==0||y._reflectX||y._reflectY){t.save();const S=f+o/2,M=p+o/2;t.translate(S,M),y._rotation>0&&t.rotate(y._rotation*Math.PI/2),(y._reflectX||y._reflectY)&&t.scale(y._reflectX?-1:1,y._reflectY?-1:1);const _=o/2;k instanceof C.Color?(t.fillStyle=`#${k.getHexString()}`,t.fillRect(-_,-_,o,o)):t.drawImage(k,-_,-_,o,o),t.restore()}else k instanceof C.Color?(t.fillStyle=`#${k.getHexString()}`,t.fillRect(f,p,o,o)):t.drawImage(k,f,p,o,o);l&&t.strokeRect(f,p,o,o)}else t.fillStyle="magenta",t.fillRect(f,p,o,o),console.error(`Collapsed cell [${u}, ${a}] (index ${d}) has invalid tile index ${T} or missing content.`)}else{t.strokeRect(f,p,o,o);const T=s.options.optionsCount(d),y=s.tiles.count,k=T/y,b=Math.floor(50+k*100);t.fillStyle=`rgba(${b}, ${b}, ${b}, 0.7)`,t.fillRect(f,p,o,o),t.fillStyle="white",t.textAlign="center",t.textBaseline="middle",t.font=`bold ${o*.4}px sans-serif`,t.fillText(T.toString(),f+o/2,p+o/2),t.fillStyle="#cccccc",t.textAlign="left",t.textBaseline="top",t.font=`${o*.2}px sans-serif`,t.fillText(d.toString(),f+2,p+2)}}}}I.WFC2D=D,Object.defineProperty(I,Symbol.toStringTag,{value:"Module"})});
//# sourceMappingURL=three-wfc.umd.cjs.map