fix: bug
This commit is contained in:
116
src/App.tsx
116
src/App.tsx
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import * as THREE from "three";
|
||||
import Preview from "./components/Preview";
|
||||
import { generateEffect } from "./engine/aiMock";
|
||||
import { buildShader } from "./engine/shaderBuilder";
|
||||
@@ -40,6 +41,8 @@ function makeEffect(templateName: string, serial: number): EffectItem {
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const channelIndices = [0, 1, 2, 3] as const;
|
||||
const channelKeys = ["iChannel0", "iChannel1", "iChannel2", "iChannel3"] as const;
|
||||
const [effects, setEffects] = useState<EffectItem[]>([makeEffect(templates[0].name, 1)]);
|
||||
const [activeEffectId, setActiveEffectId] = useState<string>("");
|
||||
const [prompt, setPrompt] = useState<string>("hot fire");
|
||||
@@ -52,6 +55,14 @@ export default function App() {
|
||||
}, [effects, activeEffectId]);
|
||||
|
||||
const [uniforms, setUniforms] = useState(() => createUniforms(effects[0].params));
|
||||
const [channelTextures, setChannelTextures] = useState<Array<THREE.Texture | null>>([
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
]);
|
||||
const [channelFileNames, setChannelFileNames] = useState<string[]>(["None", "None", "None", "None"]);
|
||||
const channelTexturesRef = useRef<Array<THREE.Texture | null>>([null, null, null, null]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeEffectId && effects[0]) {
|
||||
@@ -72,6 +83,29 @@ export default function App() {
|
||||
updateUniformValues(uniforms, isPlaying ? activeEffect.params : { ...activeEffect.params, speed: 0 });
|
||||
}, [uniforms, activeEffect, isPlaying]);
|
||||
|
||||
useEffect(() => {
|
||||
channelTexturesRef.current = channelTextures;
|
||||
channelIndices.forEach((idx) => {
|
||||
const channelKey = channelKeys[idx];
|
||||
const texture = channelTextures[idx];
|
||||
uniforms[channelKey].value = texture;
|
||||
if (texture && texture.image) {
|
||||
const image = texture.image as { width?: number; height?: number };
|
||||
const width = image.width ?? 1;
|
||||
const height = image.height ?? 1;
|
||||
uniforms.iChannelResolution.value[idx].set(width, height, 1);
|
||||
} else {
|
||||
uniforms.iChannelResolution.value[idx].set(1, 1, 1);
|
||||
}
|
||||
});
|
||||
}, [channelTextures, uniforms]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
channelTexturesRef.current.forEach((texture) => texture?.dispose());
|
||||
};
|
||||
}, []);
|
||||
|
||||
const updateActiveEffect = (updater: (current: EffectItem) => EffectItem) => {
|
||||
setEffects((prev) => prev.map((effect) => (effect.id === activeEffect.id ? updater(effect) : effect)));
|
||||
};
|
||||
@@ -167,6 +201,51 @@ export default function App() {
|
||||
previewWrapRef.current?.requestFullscreen?.();
|
||||
};
|
||||
|
||||
const onUploadChannelTexture = (channelIndex: number, file: File) => {
|
||||
const objectUrl = URL.createObjectURL(file);
|
||||
const loader = new THREE.TextureLoader();
|
||||
loader.load(
|
||||
objectUrl,
|
||||
(texture) => {
|
||||
texture.needsUpdate = true;
|
||||
setChannelTextures((prev) => {
|
||||
const next = [...prev];
|
||||
next[channelIndex]?.dispose();
|
||||
next[channelIndex] = texture;
|
||||
return next;
|
||||
});
|
||||
setChannelFileNames((prev) => {
|
||||
const next = [...prev];
|
||||
next[channelIndex] = file.name;
|
||||
return next;
|
||||
});
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
},
|
||||
undefined,
|
||||
() => {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const onClearChannelTexture = (channelIndex: number) => {
|
||||
setChannelTextures((prev) => {
|
||||
const next = [...prev];
|
||||
next[channelIndex]?.dispose();
|
||||
next[channelIndex] = null;
|
||||
return next;
|
||||
});
|
||||
setChannelFileNames((prev) => {
|
||||
const next = [...prev];
|
||||
next[channelIndex] = "None";
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const onSampleRateChange = (value: number) => {
|
||||
uniforms.iSampleRate.value = Number.isFinite(value) && value > 0 ? value : 44100;
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="app-shell">
|
||||
<header className="topbar">
|
||||
@@ -305,6 +384,43 @@ export default function App() {
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
|
||||
<label className="field">
|
||||
<span>iSampleRate</span>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
value={uniforms.iSampleRate.value}
|
||||
onChange={(e) => onSampleRateChange(Number(e.target.value))}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className="channel-panel">
|
||||
<h4>Shadertoy Channels</h4>
|
||||
{channelIndices.map((channelIndex) => (
|
||||
<div key={channelIndex} className="channel-row">
|
||||
<span>{`iChannel${channelIndex}`}</span>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
onUploadChannelTexture(channelIndex, file);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<small>{channelFileNames[channelIndex]}</small>
|
||||
<button
|
||||
type="button"
|
||||
className="small secondary"
|
||||
onClick={() => onClearChannelTexture(channelIndex)}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@@ -29,3 +29,32 @@ void main() {
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
export function buildRuntimeFragmentShader(source: string): string {
|
||||
const normalizedSource = source
|
||||
.replace(
|
||||
/uniform\s+samplerXX\s+iChannel0\.\.3\s*;/g,
|
||||
"uniform sampler2D iChannel0;\nuniform sampler2D iChannel1;\nuniform sampler2D iChannel2;\nuniform sampler2D iChannel3;"
|
||||
)
|
||||
.replace(
|
||||
/uniform\s+samplerXX\s+iChannel0\s*\.\.\s*3\s*;/g,
|
||||
"uniform sampler2D iChannel0;\nuniform sampler2D iChannel1;\nuniform sampler2D iChannel2;\nuniform sampler2D iChannel3;"
|
||||
);
|
||||
|
||||
const hasMainImage = /\bvoid\s+mainImage\s*\(/.test(normalizedSource);
|
||||
const hasMain = /\bvoid\s+main\s*\(/.test(normalizedSource);
|
||||
|
||||
if (!hasMainImage || hasMain) {
|
||||
return normalizedSource;
|
||||
}
|
||||
|
||||
return `
|
||||
${normalizedSource}
|
||||
|
||||
void main() {
|
||||
vec4 color = vec4(0.0);
|
||||
mainImage(color, gl_FragCoord.xy);
|
||||
gl_FragColor = color;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,20 @@ export type UniformState = {
|
||||
u_speed: { value: number };
|
||||
u_scale: { value: number };
|
||||
u_intensity: { value: number };
|
||||
iResolution: { value: THREE.Vector3 };
|
||||
iTime: { value: number };
|
||||
iTimeDelta: { value: number };
|
||||
iFrameRate: { value: number };
|
||||
iFrame: { value: number };
|
||||
iChannelTime: { value: number[] };
|
||||
iChannelResolution: { value: THREE.Vector3[] };
|
||||
iMouse: { value: THREE.Vector4 };
|
||||
iDate: { value: THREE.Vector4 };
|
||||
iSampleRate: { value: number };
|
||||
iChannel0: { value: THREE.Texture | null };
|
||||
iChannel1: { value: THREE.Texture | null };
|
||||
iChannel2: { value: THREE.Texture | null };
|
||||
iChannel3: { value: THREE.Texture | null };
|
||||
};
|
||||
|
||||
export function createUniforms(params: EffectParams): UniformState {
|
||||
@@ -13,7 +27,28 @@ export function createUniforms(params: EffectParams): UniformState {
|
||||
u_time: { value: 0 },
|
||||
u_speed: { value: params.speed },
|
||||
u_scale: { value: params.scale },
|
||||
u_intensity: { value: params.intensity }
|
||||
u_intensity: { value: params.intensity },
|
||||
iResolution: { value: new THREE.Vector3(1, 1, 1) },
|
||||
iTime: { value: 0 },
|
||||
iTimeDelta: { value: 0 },
|
||||
iFrameRate: { value: 60 },
|
||||
iFrame: { value: 0 },
|
||||
iChannelTime: { value: [0, 0, 0, 0] },
|
||||
iChannelResolution: {
|
||||
value: [
|
||||
new THREE.Vector3(1, 1, 1),
|
||||
new THREE.Vector3(1, 1, 1),
|
||||
new THREE.Vector3(1, 1, 1),
|
||||
new THREE.Vector3(1, 1, 1)
|
||||
]
|
||||
},
|
||||
iMouse: { value: new THREE.Vector4(0, 0, 0, 0) },
|
||||
iDate: { value: new THREE.Vector4(1970, 1, 1, 0) },
|
||||
iSampleRate: { value: 44100 },
|
||||
iChannel0: { value: null },
|
||||
iChannel1: { value: null },
|
||||
iChannel2: { value: null },
|
||||
iChannel3: { value: null }
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import * as THREE from "three";
|
||||
import type { UniformState } from "./uniformManager";
|
||||
import { buildRuntimeFragmentShader } from "./shaderBuilder";
|
||||
|
||||
type UseThreeProps = {
|
||||
vertexShader: string;
|
||||
@@ -18,6 +19,7 @@ export function useThree({ vertexShader, fragmentShader, uniforms }: UseThreePro
|
||||
return;
|
||||
}
|
||||
|
||||
const runtimeFragmentShader = buildRuntimeFragmentShader(fragmentShader);
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 10);
|
||||
camera.position.z = 1;
|
||||
@@ -31,7 +33,7 @@ export function useThree({ vertexShader, fragmentShader, uniforms }: UseThreePro
|
||||
const geometry = new THREE.PlaneGeometry(2, 2);
|
||||
const material = new THREE.ShaderMaterial({
|
||||
vertexShader,
|
||||
fragmentShader,
|
||||
fragmentShader: runtimeFragmentShader,
|
||||
transparent: true,
|
||||
uniforms
|
||||
});
|
||||
@@ -42,25 +44,106 @@ export function useThree({ vertexShader, fragmentShader, uniforms }: UseThreePro
|
||||
|
||||
let rafId = 0;
|
||||
const start = performance.now();
|
||||
let prevFrame = start;
|
||||
let frame = 0;
|
||||
let isPointerDown = false;
|
||||
|
||||
const fallbackTexture = new THREE.DataTexture(new Uint8Array([0, 0, 0, 255]), 1, 1);
|
||||
fallbackTexture.needsUpdate = true;
|
||||
|
||||
uniforms.iChannel0.value = uniforms.iChannel0.value ?? fallbackTexture;
|
||||
uniforms.iChannel1.value = uniforms.iChannel1.value ?? fallbackTexture;
|
||||
uniforms.iChannel2.value = uniforms.iChannel2.value ?? fallbackTexture;
|
||||
uniforms.iChannel3.value = uniforms.iChannel3.value ?? fallbackTexture;
|
||||
|
||||
const updateResolution = () => {
|
||||
const width = mount.clientWidth;
|
||||
const height = mount.clientHeight;
|
||||
uniforms.iResolution.value.set(width, height, 1);
|
||||
uniforms.iChannelResolution.value.forEach((v) => v.set(1, 1, 1));
|
||||
};
|
||||
|
||||
const toShaderMouse = (event: PointerEvent): [number, number] => {
|
||||
const rect = renderer.domElement.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left;
|
||||
const y = rect.height - (event.clientY - rect.top);
|
||||
return [x, y];
|
||||
};
|
||||
|
||||
const onPointerDown = (event: PointerEvent) => {
|
||||
isPointerDown = true;
|
||||
const [x, y] = toShaderMouse(event);
|
||||
uniforms.iMouse.value.set(x, y, x, y);
|
||||
};
|
||||
|
||||
const onPointerMove = (event: PointerEvent) => {
|
||||
const [x, y] = toShaderMouse(event);
|
||||
if (isPointerDown) {
|
||||
uniforms.iMouse.value.x = x;
|
||||
uniforms.iMouse.value.y = y;
|
||||
} else {
|
||||
uniforms.iMouse.value.x = x;
|
||||
uniforms.iMouse.value.y = y;
|
||||
}
|
||||
};
|
||||
|
||||
const onPointerUp = () => {
|
||||
isPointerDown = false;
|
||||
uniforms.iMouse.value.z = 0;
|
||||
uniforms.iMouse.value.w = 0;
|
||||
};
|
||||
|
||||
const animate = () => {
|
||||
uniforms.u_time.value = (performance.now() - start) / 1000;
|
||||
const now = performance.now();
|
||||
const elapsed = (now - start) / 1000;
|
||||
const delta = Math.max((now - prevFrame) / 1000, 0.0001);
|
||||
prevFrame = now;
|
||||
frame += 1;
|
||||
const wallClock = new Date();
|
||||
const timeOfDay =
|
||||
wallClock.getHours() * 3600 +
|
||||
wallClock.getMinutes() * 60 +
|
||||
wallClock.getSeconds() +
|
||||
wallClock.getMilliseconds() / 1000;
|
||||
|
||||
uniforms.u_time.value = elapsed;
|
||||
uniforms.iTime.value = elapsed;
|
||||
uniforms.iTimeDelta.value = delta;
|
||||
uniforms.iFrame.value = frame;
|
||||
uniforms.iFrameRate.value = 1 / delta;
|
||||
uniforms.iChannelTime.value = [elapsed, elapsed, elapsed, elapsed];
|
||||
uniforms.iDate.value.set(
|
||||
wallClock.getFullYear(),
|
||||
wallClock.getMonth() + 1,
|
||||
wallClock.getDate(),
|
||||
timeOfDay
|
||||
);
|
||||
|
||||
renderer.render(scene, camera);
|
||||
rafId = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
const onResize = () => {
|
||||
renderer.setSize(mount.clientWidth, mount.clientHeight);
|
||||
updateResolution();
|
||||
};
|
||||
|
||||
window.addEventListener("resize", onResize);
|
||||
renderer.domElement.addEventListener("pointerdown", onPointerDown);
|
||||
renderer.domElement.addEventListener("pointermove", onPointerMove);
|
||||
window.addEventListener("pointerup", onPointerUp);
|
||||
updateResolution();
|
||||
animate();
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(rafId);
|
||||
window.removeEventListener("resize", onResize);
|
||||
renderer.domElement.removeEventListener("pointerdown", onPointerDown);
|
||||
renderer.domElement.removeEventListener("pointermove", onPointerMove);
|
||||
window.removeEventListener("pointerup", onPointerUp);
|
||||
geometry.dispose();
|
||||
material.dispose();
|
||||
fallbackTexture.dispose();
|
||||
renderer.dispose();
|
||||
mount.innerHTML = "";
|
||||
};
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
precision mediump float;
|
||||
|
||||
attribute vec3 position;
|
||||
attribute vec2 uv;
|
||||
|
||||
varying vec2 v_uv;
|
||||
|
||||
void main() {
|
||||
|
||||
@@ -204,6 +204,39 @@ textarea {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.channel-panel {
|
||||
margin-top: 6px;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.channel-panel h4 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #c5d9f5;
|
||||
}
|
||||
|
||||
.channel-row {
|
||||
display: grid;
|
||||
grid-template-columns: 78px 1fr auto auto;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.channel-row span {
|
||||
color: #a6bddf;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.channel-row small {
|
||||
color: #8ea6ca;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.content-grid {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
Reference in New Issue
Block a user