요약:
- Supabase, React, WebGazer.js, Motion One, anime.js, Stable Audio로 구축
- Supabase 실시간 존재 및 브로드캐스트 활용(데이터베이스 테이블이 전혀 사용되지 않음!)
- GitHub 저장소
- 웹사이트
- 데모영상
또 다른 Supabase Launch Week Hackathon과 Gaze into the Abyss라는 또 다른 실험 프로젝트가 있었는데, 이는 결국 가장 단순하면서도 복잡한 프로젝트 중 하나가 되었습니다. 운 좋게도 저는 최근에 Cursor를 꽤 즐기고 있어서 이 과정을 완료하는 데 도움의 손길을 많이 받았습니다! 또한 저는 마음 속으로 한 가지 질문을 확인하고 싶었습니다. 데이터베이스 테이블 없이 Supabase의 실시간 기능을 단지 사용할 수 있습니까? (어쩌면 다소 뻔한) 대답은 다음과 같습니다. 예, 그렇습니다(사랑합니다, Realtime 팀 ♥️). 그럼 구현에 대해 좀 더 자세히 살펴보겠습니다.
아이디어프로젝트 빌드
또한 시각적인 것만을 원하지 않았습니다. 오디오도 함께 사용하면 더 나은 경험이 될 것입니다. 그래서 참가자들이 마이크에 대고 이야기를 하면 다른 사람들이 쓸데없는 속삭임이나 바람소리로 들을 수 있으면 좋겠다는 생각이 들었습니다. 그러나 이것은 매우 어려운 일이었고 WebAudio와 WebRTC를 잘 연결할 수 없었기 때문에 완전히 중단하기로 결정했습니다. 코드베이스에는 로컬 마이크를 듣고 현재 사용자에 대해 "바람 소리"를 트리거하는 남은 구성 요소가 있습니다. 앞으로 추가할 내용이 있을까요?
실시간 회의실
이 JoinRoom은 useEffect 후크 내부에 있으며 룸 구성 요소가 마운트될 때 호출됩니다. 이 기능을 작업하면서 발견한 한 가지 주의 사항은 currentPresences 매개변수가 조인 이벤트를 사용할 수 있음에도 불구하고 해당 값을 포함하지 않는다는 것입니다. 구현상의 버그인지 아니면 의도한 대로 작동하는지 잘 모르겠습니다. 따라서 사용자가 참여할 때마다 방에 있는 참가자 수를 가져오기 위해 수동 room.presenceState 가져오기를 수행해야 합니다.
참가자 수를 확인하여 현재 방을 탈퇴하고 다른 방에 참여를 시도하거나 현재 방을 진행합니다. 동기화가 너무 늦기 때문에 참여 이벤트에서 이 작업을 수행합니다(이벤트 참여 또는 탈퇴 후에 트리거됨).
브라우저에서 많은 탭을 열어 이 구현을 테스트했는데 모든 것이 훌륭해 보였습니다!
그 후 마우스 위치 업데이트로 솔루션을 디버깅하고 싶었고 채널에서 너무 많은 메시지를 보내는 몇 가지 문제에 빠르게 직면했습니다! 해결책: 통화 속도를 조절하세요.
/** * Creates a throttled version of a function that can only be called at most once * in the specified time period. */ function createThrottledFunction<T extends (...args: unknown[]) => unknown>( functionToThrottle: T, waitTimeMs: number ): (...args: Parameters<T>) => void { let isWaitingToExecute = false return function throttledFunction(...args: Parameters<T>) { if (!isWaitingToExecute) { functionToThrottle.apply(this, args) isWaitingToExecute = true setTimeout(() => { isWaitingToExecute = false }, waitTimeMs) } } }
커서가 작은 스로틀 기능 생성기를 생각해냈고 이를 시선 추적 방송에 다음과 같이 사용했습니다.
const throttledBroadcast = createThrottledFunction((data: EyeTrackingData) => { if (currentChannel) { currentChannel.send({ type: 'broadcast', event: 'eye_tracking', payload: data }) } }, THROTTLE_MS) throttledBroadcast({ userId: userId.current, isBlinking: isCurrentlyBlinking, gazeX, gazeY })
많은 도움이 되었습니다! 또한 초기 버전에서는 시선 추적 메시지가 현재 상태와 함께 전송되었지만 브로드캐스트는 초당 더 많은 메시지를 허용하므로 대신 구현을 전환했습니다. 카메라는 항상 모든 것을 기록하므로 시선 추적에서는 특히 중요합니다.
얼마 전 처음 이 프로젝트에 대한 아이디어를 얻었을 때 WebGazer.js를 접한 적이 있었습니다. 매우 흥미로운 프로젝트이며 놀라울 정도로 잘 작동합니다!
전체 시선 추적 기능은 useEffect 후크의 하나의 기능으로 수행됩니다.
window.webgazer .setGazeListener(async (data: any) => { if (data == null || !currentChannel || !ctxRef.current) return try { // Get normalized gaze coordinates const gazeX = data.x / windowSize.width const gazeY = data.y / windowSize.height // Get video element const videoElement = document.getElementById('webgazerVideoFeed') as HTMLVideoElement if (!videoElement) { console.error('WebGazer video element not found') return } // Set canvas size to match video imageCanvasRef.current.width = videoElement.videoWidth imageCanvasRef.current.height = videoElement.videoHeight // Draw current frame to canvas ctxRef.current?.drawImage(videoElement, 0, 0) // Get eye patches const tracker = window.webgazer.getTracker() const patches = await tracker.getEyePatches( videoElement, imageCanvasRef.current, videoElement.videoWidth, videoElement.videoHeight ) if (!patches?.right?.patch?.data || !patches?.left?.patch?.data) { console.error('No eye patches detected') return } // Calculate brightness for each eye const calculateBrightness = (imageData: ImageData) => { let total = 0 for (let i = 0; i < imageData.data.length; i += 16) { // Convert RGB to grayscale const r = imageData.data[i] const g = imageData.data[i + 1] const b = imageData.data[i + 2] total += (r + g + b) / 3 } return total / (imageData.width * imageData.height / 4) } const rightEyeBrightness = calculateBrightness(patches.right.patch) const leftEyeBrightness = calculateBrightness(patches.left.patch) const avgBrightness = (rightEyeBrightness + leftEyeBrightness) / 2 // Update rolling average if (brightnessSamples.current.length >= SAMPLES_SIZE) { brightnessSamples.current.shift() // Remove oldest sample } brightnessSamples.current.push(avgBrightness) // Calculate dynamic threshold from rolling average const rollingAverage = brightnessSamples.current.reduce((a, b) => a + b, 0) / brightnessSamples.current.length const dynamicThreshold = rollingAverage * THRESHOLD_MULTIPLIER // Detect blink using dynamic threshold const blinkDetected = avgBrightness > dynamicThreshold // Debounce blink detection to avoid rapid changes if (blinkDetected !== isCurrentlyBlinking) { const now = Date.now() if (now - lastBlinkTime > 100) { // Minimum time between blink state changes isCurrentlyBlinking = blinkDetected lastBlinkTime = now } } // Use throttled broadcast instead of direct send throttledBroadcast({ userId: userId.current, isBlinking: isCurrentlyBlinking, gazeX, gazeY }) } catch (error) { console.error('Error processing gaze data:', error) } })
사용자가 보고 있는 위치의 정보를 얻는 것은 간단하며 화면에서 마우스 위치를 얻는 것과 같습니다. 하지만 눈 깜박임 감지를 (멋진) 기능으로 추가하고 싶었는데, 이 기능을 사용하려면 몇 가지 어려움을 겪어야 했습니다.
WebGazer 및 깜박임 감지에 대한 정보를 Google에 검색하면 초기 구현의 일부 잔재를 볼 수 있습니다. 소스에도 주석 처리된 코드가 있는 것처럼 말이죠. 불행하게도 이러한 종류의 기능은 라이브러리에 존재하지 않습니다. 수동으로 수행해야 합니다.
많은 시행착오 끝에 Cursor와 저는 안대 데이터에서 픽셀과 밝기 수준을 계산하여 사용자가 깜박이는 시점을 판단하는 솔루션을 생각해낼 수 있었습니다. 또한 (적어도 나에게는) 조명에 따라 깜박일 때 웹캠이 항상 인식하지 못한다는 것을 알았기 때문에 일부 동적 조명 조정 기능도 있습니다. 나에게는 사진/방이 밝을수록 효과가 더 나빴고, 조명이 어두울수록 효과가 더 좋았습니다(계산해 보세요).
눈 추적 기능을 디버깅하는 동안(WebGazer에는 보고 있는 위치를 시각화하기 위해 화면에 빨간색 점을 표시하는 매우 멋진 .setPredictionPoints 호출이 있음) 보정하지 않으면 추적이 그다지 정확하지 않다는 것을 발견했습니다. 프로젝트에서 룸에 참여하기 전에 수행해야 할 작업은 다음과 같습니다.
/** * Creates a throttled version of a function that can only be called at most once * in the specified time period. */ function createThrottledFunction<T extends (...args: unknown[]) => unknown>( functionToThrottle: T, waitTimeMs: number ): (...args: Parameters<T>) => void { let isWaitingToExecute = false return function throttledFunction(...args: Parameters<T>) { if (!isWaitingToExecute) { functionToThrottle.apply(this, args) isWaitingToExecute = true setTimeout(() => { isWaitingToExecute = false }, waitTimeMs) } } }
이것을 실제로 보는 것은 매우 멋진 경험이었습니다! 주변 선에도 동일한 접근 방식을 적용하고 커서에게 중앙을 향해 "축소"하도록 지시했습니다. 거의 한 번에 완료되었습니다!
그러면 방 전체가 큰 눈처럼 보이도록 셀이 정렬된 간단한 CSS 그리드 내부에 눈이 렌더링됩니다.
const throttledBroadcast = createThrottledFunction((data: EyeTrackingData) => { if (currentChannel) { currentChannel.send({ type: 'broadcast', event: 'eye_tracking', payload: data }) } }, THROTTLE_MS) throttledBroadcast({ userId: userId.current, isBlinking: isCurrentlyBlinking, gazeX, gazeY })
그런 다음 멋진 소개 화면과 배경 음악을 틀면 프로젝트가 시작됩니다!
오디오는 이런 작업을 할 때 항상 경험을 향상시키기 때문에 사용자가 "심연에 들어갈" 때 배경 음악을 생성하기 위해 Stable Audio를 사용했습니다. 제가 음악에 사용한 프롬프트는 다음과 같습니다.
분위기, 오싹함, 배경 음악, 속삭이는 소리, 바람, 느린 템포, 으스스함, 심연
또한 단순한 검은색 화면이 조금 지루하다고 생각하여 배경에 애니메이션 SVG 필터를 추가했습니다. 또한 멋진 페이딩 효과를 주기 위해 화면 중앙에 어둡고 흐린 원을 추가했습니다. 아마도 SVG 필터를 사용하여 이 작업을 수행할 수 있었지만 이에 너무 많은 시간을 소비하고 싶지 않았습니다. 그런 다음 좀 더 움직임을 주기 위해 배경을 축을 중심으로 회전시켰습니다. 때로는 SVG 필터를 사용하여 애니메이션을 수행하는 것이 다소 불안정하므로 대신 이 방법으로 수행하기로 결정했습니다.
<div> <h2> 결론 </h2> <p>여기까지 왔습니다. Supabase의 실시간 기능을 사용하여 스타일화된 시선 추적을 구현하는 방법을 상당히 간단하게 살펴보겠습니다. 개인적으로 나는 이것이 매우 흥미로운 실험이라고 생각했고 작업하는 동안 많은 문제가 발생하지 않았습니다. 그리고 놀랍게도 프로젝트 제출 전 어젯밤에 밤새도록 일할 필요가 없었습니다!</p> <p>프로젝트 결과나 데모 영상을 자유롭게 확인해 보세요. 여러 사람이 동시에 사용하는 경우 몇 가지 문제가 있을 수 있지만(제대로 하려면 여러 장치와 웹 캠이 필요하므로 테스트하기가 매우 어렵습니다), 그게 해커톤 프로젝트의 방식인 것 같나요? 그리고 테스트해 보면 눈이 보이면 인터넷 어딘가에서 다른 사람이 당신을 지켜보고 있다는 것을 기억하세요!</p>
위 내용은 Supabase 및 WebGazer.js를 사용하여 실시간 시선 추적 경험 구축의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!