我目前有一个使用 p5js 部署在 https://artifical-life.vercel.app/smoothlife/smoothLife 上的 NextJS 项目。
它是https://arxiv.org/pdf/1111.1567.pdf的实现,本质上是在连续域上推广的细胞自动机。目前它的运行速度约为每秒 10 到 14 帧,我想提高这个速度。
这是包含逻辑的源文件:https://github.com/JaylenLuc/ArtificalLife/blob/main/artificial_life/pages/smoothlife/smoothLife.tsx
export default function P5Sketch () {
/********************************************************
* UNIVERSAL LIFE CONSTANTS AND/OR VARS
********************************************************/
const renderRef = useRef(null);
var WIDTH_HEIGHT = 125 //the true number of cells WIDTH_HEIGHT ^ 2
//const HEIGHT = 150
var SIZE = 4
const RGB_MIN_RANGE = 255 //min range
const [strokePolicy, setStrokePolicy] = useState(false)
const [initOption, setInitPolicy] = useState("center")
const [seedUser, _setSeed] = useState(0)
const [colorScheme, _setColorScheme] = useState(0)
const setColorScheme = () => {
_setColorScheme((colorScheme + 1) % 3)
//console.log(colorScheme)
}
/****
* radius checks
* ****/
const ra_DEFAULT = 11
// const ra_DEFAULT = 11
var noLoop = false
const setnoLoop = (loopVal : boolean = !noLoop) => {
noLoop = loopVal
}
const [ra, _setOuterRadius] = useState(ra_DEFAULT)
const [ri, _setInnerRadius] = useState(ra_DEFAULT/3)
var ri_area = Math.PI * (ri*ri)
var ra_area = (Math.PI * (ra*ra)) - (ri_area)
/****
* sigmoid alpha values
* ****/
const alpha_n_DEFAULT = 0.028
const alpha_m_DEFAULT = 0.147
//αn = 0.028, αm = 0.147
const [alpha_n, setAlphaN] = useState(alpha_n_DEFAULT)
const [alpha_m, setAlphaM] = useState(alpha_m_DEFAULT)
/****
* birth and death interval values given by [b1, b2] and [d1, d2]
* ****/
const d1_DEFAULT = 0.267
const d2_DEFAULT = 0.445
const b1_DEFAULT = 0.278
const b2_DEFAULT = 0.365
// const d1_DEFAULT = 0.365
// const d2_DEFAULT = 0.549
// const b1_DEFAULT = 0.257
// const b2_DEFAULT = 0.336
//var d1 = d1_DEFAULT
const [d1, _setd1] = useState(d1_DEFAULT)
const [d2, _setd2] = useState(d2_DEFAULT)
const [b1, _setb1] = useState(b1_DEFAULT)
const [b2, _setb2] = useState(b2_DEFAULT)
/****
* delta time/ time stepping
* ****/
const dt_DEFAULT = 0.25 //time step
const [dt, setDeltaT] = useState(dt_DEFAULT)
const setDefaultParams = () => {
_setd1(d1_DEFAULT)
_setd2(d2_DEFAULT)
_setb1(b1_DEFAULT)
_setb2(b2_DEFAULT)
setRadius(ra_DEFAULT)
}
var cellsArray: number[] = []
const push_cellsAray = (row : number, col : number, val : number ) => {
cellsArray[WIDTH_HEIGHT * row + col] = val;
}
const get_cellsAray = (row : number, col : number ) => {
return cellsArray[WIDTH_HEIGHT * row + col];
}
//randomize the grid
//determine from the randomziex numbers what we render
//One PRESET
// var d1 = 0.365
// var d2 = 0.549
// var b1 = 0.257
// var b2 = 0.336
// var ra = 11 //outer radius
// var ri = ra/3 // inner radius
// var ri_area = Math.PI * (ri*ri)
// var ra_area = (Math.PI * (ra*ra)) - (ri_area)
// const ra_DEFAULT = 11
// var alpha_n = 0.028
// var alpha_m = 0.147
// var dt = 0.05 //time step
/********************************************************
* Event handlers
********************************************************/
const setSeed = (seed : number) => {
_setSeed(seed);
resetGrid();
};
const setRadius = (e : number) =>{
if (e <= 25){
_setOuterRadius(e)
_setInnerRadius(e/3)
}
}
/********************************************************
* GRID FUNCTIONS
********************************************************/
const random_number = (row: number, col : number, seed: number = seedUser) => {
//console.log("random_number func : ", seed)
if (seed > 0){
let random = Math.sin(seed + row * col) * 10000;
// console.log(random - Math.floor(random))
return random - Math.floor(random);
}else{
//console.log("default")
return Math.random();
}
}
const randomizeFullGrid = () => {
// for (let row = 0 ; row < WIDTH_HEIGHT; row ++){
// cellsArray.push([])
// for (let col = 0; col< WIDTH_HEIGHT; col ++){
// cellsArray[row].push( Math.random());
// }
// }
}
const initGridZero = () => {
//this function will likely be called first
for (let row = 0 ; row < WIDTH_HEIGHT; row ++){
for (let col = 0; col< WIDTH_HEIGHT; col ++){
push_cellsAray(row, col, 0)
}
}
}
const randomizeCenterGrid = (centerWidth : number) => {
//setStrokePolicy(false)
let center_grid = (Math.floor(WIDTH_HEIGHT/2))
let center_diff = (Math.floor((WIDTH_HEIGHT * centerWidth)/2))
let center_start = center_grid - center_diff
let center_end = center_grid + center_diff
for (let row = 0 ; row < WIDTH_HEIGHT; row ++){
for (let col = 0; col< WIDTH_HEIGHT; col ++){
if ((col > center_start && col <= center_end) &&
(row > center_start && row <= center_end)) push_cellsAray(row, col, random_number(row,col));
else push_cellsAray(row, col, seedUser != 0? random_number(row,col)/10 : 0);
}
}
}
const resetGrid = () => {
// console.log("in resetGrid: ", ra)
// console.log("in resetGrid: ", ri)
// console.log("in resetGrid: ", ri_area)
if (cellsArray.length > 0){
cellsArray = []
}
setnoLoop(false)
arbitrateMode()
}
const fillGrid = (p : any, myShader : any) => {
//it takes the rand nums in the grid and draws the color based on the numbers in the grid
let xPos = 0;
let yPos = 0;
for (let row = 0; row < WIDTH_HEIGHT; row ++){
xPos += SIZE
for (let col = 0; col< WIDTH_HEIGHT; col ++){
yPos += SIZE
let current_state = get_cellsAray(row, col)
let fill_value = (current_state * RGB_MIN_RANGE);
/*
fill value is the rgb 255 * the decimal which
is a percentage of how far it is from black until white
this can be changed with the fill value determining
only some of the RGB values , play around
*/
//myShader.setUniform('myColor', [fill_value,fill_value,fill_value])
p.fill(fill_value,fill_value,fill_value)
p.circle(yPos, xPos , SIZE);
// p.fill(70,70,70)
// p.circle(yPos, xPos , 5);
}
yPos = 0
}
}
const clamp = function(value : number, lower_b : number, upper_b : number) {
return Math.min(Math.max(value, lower_b), upper_b);
};
const clamp_test = function(value : number, lower_b : number, upper_b : number) {
if (value < lower_b ) return lower_b
else if (value > upper_b ) return upper_b
else return value
};
const generalizeTransitionFunc = ( ) => {
let row = 0
let col = 0
while ( row < WIDTH_HEIGHT){
while(col < WIDTH_HEIGHT){
let m_n : Array<number> = fillingIntegralN_M(row, col)
let new_value = dt * transitionFunc_S(m_n[1], m_n[0]) // [-1,1]
//console.log(new_value)
//smooth time stepping scheme
//console.log("not clamped: ", new_value)
//f(~x, t + dt) = f(~x, t) + dt S[s(n, m)] f(~x, t)
push_cellsAray(row, col, clamp_test(get_cellsAray(row, col) + new_value, 0, 1 ))
col++
}
col = 0
row += 1
}
}
/********************************************************
* INTEGRAL FUNCTIONS
********************************************************/
const emod = (pos : number, size : number ) => {
return ((pos % size) + size ) % size
}
//outer and inner filling
const fillingIntegralN_M = (_row : number = 0, _col : number = 0) => { //return value between 0 -1 normalize
let c_row : number = _row
let c_col : number = _col
let m : number = 0
let n : number = 0
for (let d_row = -(ra - 1) ; d_row < (ra - 1); ++d_row){ //iterate over the outer radius
let real_pos_row = emod(d_row + c_row, WIDTH_HEIGHT)
for (let d_col = -(ra -1); d_col < (ra -1); ++ d_col){
let real_pos_col = emod(d_col + c_col, WIDTH_HEIGHT)
if (d_row*d_row + d_col* d_col <= ri*ri){ //inner
m += get_cellsAray(real_pos_row,real_pos_col)
//M ++
}else if (d_row*d_row + d_col* d_col <= ra*ra) {//outer
n += get_cellsAray(real_pos_row,real_pos_col)
//N ++
}
}
}
m /= ri_area
//m= clamp(m , 0 ,1 )
n /= ra_area
//n = clamp(n , 0 ,1)
return [m,n] // inner, outer
}
/********************************************************
* SIGMOIDS AND THE TRANSITION FUNCTION
********************************************************/
const sigmoid1 = (x : number , a : number, alpha_val : number) => {
return 1/(1 + Math.exp(-(x-a) * 4/alpha_val))
}
const sigmoid2 = (x : number , a : number , b : number) => {
return sigmoid1(x,a, alpha_n) * (1- sigmoid1(x,b, alpha_n))
}
const sigmoidM = (x :number , y : number, m : number) => {
return x * ( 1 - sigmoid1(m, 0.5,alpha_m) ) + (y * sigmoid1(m, 0.5,alpha_m))
}
const transitionFunc_S = (n : number, m : number ) => {
//If the transition function in the discrete time-stepping scheme was sd(n, m) then the smooth one is s(n, m) = 2sd(n, m)−1.
return 2 * sigmoid2(n, sigmoidM(b1,d1,m), sigmoidM(b2,d2,m) ) - 1
}
const arbitrateMode = () => {
ri_area = (Math.PI * (ri*ri))
ra_area = ((Math.PI * (ra*ra)) - (ri_area))
switch(initOption){
case "full":
randomizeFullGrid();
break;
case "center":
randomizeCenterGrid(0.35);
break;
}
}
/******************************************************
********************************************************/
// const vertex = `
// precision highp float;
// uniform mat4 uModelViewMatrix;
// uniform mat4 uProjectionMatrix;
// attribute vec3 aPosition;
// attribute vec2 aTexCoord;
// varying vec2 vTexCoord;
// void main() {
// vTexCoord = aTexCoord;
// vec4 positionVec4 = vec4(aPosition, 1.0);
// gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4;
// }
// `;
// // the fragment shader is called for each pixel
// const fragment = `
// precision highp float;
// uniform vec2 p;
// uniform float r;
// const int I = 500;
// varying vec2 vTexCoord;
// void main() {
// vec2 c = p + gl_FragCoord.xy * r, z = c;
// float n = 0.0;
// for (int i = I; i > 0; i --) {
// if(z.x*z.x+z.y*z.y > 4.0) {
// n = float(i)/float(I);
// break;
// }
// z = vec2(z.x*z.x-z.y*z.y, 2.0*z.x*z.y) + c;
// }
// gl_FragColor = vec4(0.5-cos(n*17.0)/2.0,0.5-cos(n*13.0)/2.0,0.5-cos(n*23.0)/2.0,1.0);
// }`;
useEffect(() => {
const p5 = require("p5");
p5.disableFriendlyErrors = true;
var myShader: any;
var fps ;
const p5instance = new p5((p : any) => {
// p.preload = () => {
// // load each shader file (don't worry, we will come back to these!)
// myShader = p.createShader(vertex, fragment);
// }
p.setup = () => {
//console.log("HERE:", p5.disableFriendlyErrors )
p.createCanvas( WIDTH_HEIGHT * SIZE, WIDTH_HEIGHT * SIZE).parent(renderRef.current);
//p.createGraphics( WIDTH_HEIGHT + 200,WIDTH_HEIGHT + 200)
if (!strokePolicy) p.noStroke()
let color = ""
switch(colorScheme){
case 0 : p.colorMode(p.RGB); break;
case 1 : p.colorMode(p.HSB); break;
case 2: p.colorMode(p.HSL); break;
default: p.colorMode(p.RGB);
}
arbitrateMode()
}
p.draw = () => {
//fps = p.frameRate();
p.background(0,0,0);
//p.shader(myShader)
// console.time('generalizeTransitionFunc')
if (!noLoop){
generalizeTransitionFunc()
// console.timeEnd('generalizeTransitionFunc')
//console.log( "loooping", cellsArray[13][55])
}
fillGrid(p, myShader);
console.log(p.frameRate());
}
})
return () => {
//console.log("cleaning up...");
// comment this out to get 2 canvases and 2 draw() loops
p5instance.remove();
};
}, [ strokePolicy, seedUser, initOption, b1, b2, d1, d2, dt, ra, ri, ri_area, ra_area, noLoop, colorScheme])
//the entropy of the universe is tending to a maximum
return(
<div className={styles.foreground}>
<Sparkles
color="random"
count={80}
minSize={9}
maxSize={14}
overflowPx={80}
fadeOutSpeed={30}
flicker={true}
/>
<meta name="viewport" content="width=device-height"></meta>
<div className={styles.title}>
The Universe moves to an Entropic Maximum
</div>
<section>
<div className= {styles.life_box} ref={renderRef}></div>
</section>
<div className={styles.buttonlayout}>
<ButtonLayout setStrokePolicy = {setStrokePolicy} strokePolicy = {strokePolicy}/>
<BigBangButton resetHandler={resetGrid}></BigBangButton>
<SeedInput setSeed={setSeed} seedUser = {seedUser}/>
</div>
<br></br>
<div className={styles.buttonlayout}>
<ColorButton changeColor = {setColorScheme}/>
<FourDButton setNoLoop = {setnoLoop} noLoop = {noLoop}/>
</div>
<div className={styles.buttonlayout}>
<ParamNav setd1 = {_setd1} d1 = {d1} setd2= {_setd2}
d2 = {d2} setb1={_setb1} b1={b1} setb2={_setb2} b2={b2} setrad={setRadius} rad = {ra} resetSettings={setDefaultParams}/>
</div>
</div>
)
}
关于如何提高帧速率有什么想法吗?
这是高级实现:
目前必须用 0 到 1 之间的数字初始化一个一维数组:
const push_cellsAray = (row : number, col : number, val : number ) => {
cellsArray[WIDTH_HEIGHT * row + col] = val;
}
const randomizeCenterGrid = (centerWidth : number) => {
//setStrokePolicy(false)
let center_grid = (Math.floor(WIDTH_HEIGHT/2))
let center_diff = (Math.floor((WIDTH_HEIGHT * centerWidth)/2))
let center_start = center_grid - center_diff
let center_end = center_grid + center_diff
for (let row = 0 ; row < WIDTH_HEIGHT; row ++){
for (let col = 0; col< WIDTH_HEIGHT; col ++){
if ((col > center_start && col <= center_end) &&
(row > center_start && row <= center_end)) push_cellsAray(row, col, random_number(row,col));
else push_cellsAray(row, col, seedUser != 0? random_number(row,col)/10 : 0);
}
}
}
再次循环遍历数组,检查给定内半径
ri
和外半径 ra
中的所有邻居,并在从 M
获取值
N
和
fillingIntegralN_M
后将转换函数应用于数组的每个值函数获取给定行和列的新值:
const clamp_test = function(value : number, lower_b : number, upper_b : number) {
if (value < lower_b ) return lower_b
else if (value > upper_b ) return upper_b
else return value
};
const sigmoid1 = (x : number , a : number, alpha_val : number) => {
return 1/(1 + Math.exp(-(x-a) * 4/alpha_val))
}
const sigmoid2 = (x : number , a : number , b : number) => {
return sigmoid1(x,a, alpha_n) * (1- sigmoid1(x,b, alpha_n))
}
const sigmoidM = (x :number , y : number, m : number) => {
return x * ( 1 - sigmoid1(m, 0.5,alpha_m) ) + (y * sigmoid1(m, 0.5,alpha_m))
}
const transitionFunc_S = (n : number, m : number ) => {
//If the transition function in the discrete time-stepping scheme was sd(n, m) then the smooth one is s(n, m) = 2sd(n, m)−1.
return 2 * sigmoid2(n, sigmoidM(b1,d1,m), sigmoidM(b2,d2,m) ) - 1
}
const fillingIntegralN_M = (_row : number = 0, _col : number = 0) => { //return value between 0 -1 normalize
let c_row : number = _row
let c_col : number = _col
let m : number = 0
let n : number = 0
for (let d_row = -(ra - 1) ; d_row < (ra - 1); ++d_row){ //iterate over the outer radius
let real_pos_row = emod(d_row + c_row, WIDTH_HEIGHT)
for (let d_col = -(ra -1); d_col < (ra -1); ++ d_col){
let real_pos_col = emod(d_col + c_col, WIDTH_HEIGHT)
if (d_row*d_row + d_col* d_col <= ri*ri){ //inner
m += get_cellsAray(real_pos_row,real_pos_col)
//M ++
}else if (d_row*d_row + d_col* d_col <= ra*ra) {//outer
n += get_cellsAray(real_pos_row,real_pos_col)
//N ++
}
}
}
m /= ri_area
//m= clamp(m , 0 ,1 )
n /= ra_area
//n = clamp(n , 0 ,1)
return [m,n] // inner, outer
}
const generalizeTransitionFunc = ( ) => {
let row = 0
let col = 0
while ( row < WIDTH_HEIGHT){
while(col < WIDTH_HEIGHT){
let m_n : Array<number> = fillingIntegralN_M(row, col)
let new_value = dt * transitionFunc_S(m_n[1], m_n[0]) // [-1,1]
//console.log(new_value)
//smooth time stepping scheme
//console.log("not clamped: ", new_value)
//f(~x, t + dt) = f(~x, t) + dt S[s(n, m)] f(~x, t)
push_cellsAray(row, col, clamp_test(get_cellsAray(row, col) + new_value, 0, 1 ))
col++
}
col = 0
row += 1
}
}
然后根据这些值使用从 0 到 255 标准化的颜色值填充网格:
const fillGrid = (p : any, myShader : any) => {
//it takes the rand nums in the grid and draws the color based on the numbers in the grid
let xPos = 0;
let yPos = 0;
for (let row = 0; row < WIDTH_HEIGHT; row ++){
xPos += SIZE
for (let col = 0; col< WIDTH_HEIGHT; col ++){
yPos += SIZE
let current_state = get_cellsAray(row, col)
let fill_value = (current_state * RGB_MIN_RANGE);
/*
fill value is the rgb 255 * the decimal which
is a percentage of how far it is from black until white
this can be changed with the fill value determining
only some of the RGB values , play around
*/
//myShader.setUniform('myColor', [fill_value,fill_value,fill_value])
p.fill(fill_value,fill_value,fill_value)
p.circle(yPos, xPos , SIZE);
// p.fill(70,70,70)
// p.circle(yPos, xPos , 5);
}
yPos = 0
}
}
它无限期地重复这个过程。
对性能的主要影响来自于增加半径和增加电路板尺寸。
是否可以在不使用着色器的情况下提高性能?
我没有详细查看你的代码,但是你的
fillingIntegralN_M
函数本质上是计算 2D 卷积,不是吗?
对于大的 kernel 尺寸(实际上,即使几个网格单元的半径也可能足够“大”),使用 快速傅里叶变换 (FFT) 会更有效。
基本上,您要做的就是预先计算内部和外部“填充积分”内核的 FFT,即原点内半径和外半径内的单元格值为 1(或 1/M 或 1/ N,如果您想将归一化因子烘焙到内核中作为优化),并且半径之外的所有单元格的值为 0,如论文中所述,对边界处的单元格应用“抗锯齿”。
(这假设您的网格在边缘“环绕”,因此 (width-1, height-1) 处的单元格与 (0, 0) 对角相邻。傅里叶变换自然会对此进行操作这也是你的
emod
函数似乎实现的,所以你在这里应该很好。)
在模拟循环的每次迭代中,您可以对模拟网格进行 FFT,将其与每个内核的 FFT 逐点相乘,并对结果网格进行逆 FFT,以获得包含 m 和 的两个网格每个点的 n 值。然后您只需使用这些 m 和 n 值来更新网格并重复。
(顺便说一句,如果您的更新规则完全是线性,您可以在“傅立叶空间”中进行所有计算,并且偶尔采用逆FFT来向查看器显示网格。但是由于SmoothLife更新规则是显式非线性,您确实需要在“正常空间”和“傅里叶空间”之间来回跳转,其中非线性映射很容易,但卷积很困难,而“傅立叶空间”,其中非线性映射很困难,但卷积很容易。幸运的是,经过良好优化的 FFT 实现使得来回转换非常快。)
您可能不想编写自己的 JavaScript FFT 实现,至少一开始不想 - 这将是一个完全独立的项目,并且您必须花费巨大的工作量才能让它不仅工作正确但与现有优化实现一样快。
幸运的是,谷歌搜索“javascript fft”会出现几个结果,包括fft.js、jsfft和math.js中的math.fft函数。其中,至少最后一个明确声称支持二维 FFT,但如果您使用不支持二维 FFT 的库,则始终可以通过将 1D FFT 应用于每一行来将 1D FFT 转换为 2D FFT。数组,然后转置转换后的数组(即将行转换为列,反之亦然)并再次对行应用 1D FFT。
FWIW,显然 p5.js 也有一个 FFT 模块,但它似乎是为音频处理应用程序设计的,我不确定是否可以使用它来获取任意数值数据的“原始”FFT。 (FFT.analyze函数可能可以做到这一点,但文档对我来说不是很清楚。)它似乎也有一些限制,例如最大输入/输出大小为1024个值,这意味着使用它会将您限制为 1024 x 1024 点网格。