Shader: hiệu ứng ánh sáng

Thử xem shader có hoạt động không

Tổng quan

Dù tôi tạo cho trò chơi một phong cách retro nhất định, tôi vẫn muốn dùng shader.

Trong video bên dưới, bạn thấy tôi đang thử nghiệm với hiệu ứng ánh sáng. Nếu để ý kỹ, tôi đi xuyên qua vùng sáng và người chơi cũng bị ảnh hưởng bởi hiệu ứng.

Bao gồm cả shader chuyển động, ví dụ cho những ngọn đuốc mà chúng ta sẽ cần trong cuộc phiêu lưu này.

Thiết kế

Nếu phải vẽ ra thành sơ đồ thì nó sẽ đại khái như thế này, chỉ tiếc là làm thật thì phức tạp hơn nhiều.

Sơ đồ shader

Sơ đồ shader

Kinh nghiệm cho thấy trong love2d không thể render nhiều shader trong cùng một vòng lặp vẽ. Điều này là không thể, và tôi chỉ phát hiện ra khi đã có một shader chạy được và muốn bật thêm shader thứ hai.

shader1 = love.graphics.newShader('color-dodge.glsl')
shader2 = love.graphics.newShader('color-dodge.glsl')

function love.draw()
    love.graphics.push("all")
    love.graphics.setCanvas(src)
    love.graphics.clear()

    love.graphics.setShader()
    love.graphics.draw(scene, 0, 0)
    love.graphics.draw(player, playerPos.x, playerPos.y)

    love.graphics.setShader(shader1)
    love.graphics.setShader(shader2)

    love.graphics.setCanvas()
    love.graphics.setShader()
    love.graphics.draw(src, 0, 0)
end

May mắn là có giải pháp cho vấn đề này, đó là render nhiều lượt (multi-pass). Nói đơn giản là lấy kết quả bạn vẽ lên canvas, ghi lại sang một canvas khác rồi mới vẽ shader. Bạn có thể lặp lại việc này cho nhiều shader.

Hàm dưới đây hiện chạy tốt, dù tôi vẫn cần thử nghiệm kỹ hơn.

function Engine3:applyIntermediateShader(shader)
	if not shader then return end

	local src = self.canvas:getCanvas()
	local front = self.canvas:getCanvasFront()

	-- Store current graphics state
	local currentCanvas = love.graphics.getCanvas()
	local currentShader = love.graphics.getShader()

	-- Apply shader: src -> front
	love.graphics.push("all")
	love.graphics.origin()
	love.graphics.setCanvas(front)
	love.graphics.clear()
	love.graphics.setShader(shader:getShader())
	love.graphics.draw(src, 0, 0)
	love.graphics.setShader()
	love.graphics.pop()

	-- Copy result back to main canvas: front -> src
	love.graphics.push("all")
	love.graphics.origin()
	love.graphics.setCanvas(src)
	love.graphics.clear()
	love.graphics.draw(front, 0, 0)
	love.graphics.pop()

	-- Restore graphics state for continued rendering
	love.graphics.setCanvas(currentCanvas)
	love.graphics.setShader(currentShader)
end

Shader Color Dodge

Trong thiết kế bằng Photoshop và Aseprite, tôi dùng bộ lọc color dodge.

Color Dodge trong Photoshop

Color Dodge trong Photoshop

Vì vậy trong trò chơi và trình chỉnh sửa, tôi phải tái tạo nó bằng cách nào đó.

Bộ chọn shader trong trình chỉnh sửa

Bộ chọn shader trong trình chỉnh sửa

Tôi gần như không dùng AI để lập trình trong quá trình phát triển trò chơi. Nhưng đôi khi tôi biết đó là một phần nhỏ mà tôi chưa có kiến thức và có thể dùng làm điểm khởi đầu. Vì vậy tôi đã yêu cầu tạo một shader glsl tên là ‘Color Dodge’ giống với những gì Photoshop làm.

Tất nhiên nó không chạy tốt, vì hệ tọa độ tôi dùng khác, và tôi còn dùng cả co giãn và nhiều thứ khác. Tệp glsl cuối cùng hiện tại như sau.

extern vec2 position;
extern Image image;
extern float scale;
extern float overlayIntensity;
extern vec4 quadRect;

vec3 colorDodge(vec3 base, vec3 blend) {
    return vec3(
        blend.r >= 0.999 ? 1.0 : min(1.0, base.r / max(0.001, 1.0 - blend.r)),
        blend.g >= 0.999 ? 1.0 : min(1.0, base.g / max(0.001, 1.0 - blend.g)),
        blend.b >= 0.999 ? 1.0 : min(1.0, base.b / max(0.001, 1.0 - blend.b))
    );
}

vec4 effect(vec4 color, Image tex, vec2 tc, vec2 sc)
{
    vec4 base = texture(tex, tc);
    vec2 texSize = vec2(textureSize(image, 0));
    vec2 qpos  = quadRect.xy;
    vec2 qsize = quadRect.zw;
    vec2 uv = (sc - position) / (qsize * scale);

    if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) {
        return base;
    }

    uv = (uv * qsize + qpos) / texSize;
    vec4 overlay = texture(image, uv);

    if (overlay.a > 0.0) {
        vec3 enhancedOverlay = min(vec3(1.0), overlay.rgb * overlayIntensity);
        vec3 dodged = colorDodge(base.rgb, enhancedOverlay);
        float enhancedAlpha = min(1.0, overlay.a * overlayIntensity);
        vec3 result = mix(base.rgb, dodged, enhancedAlpha);
        return vec4(result, base.a);
    }

    return base;
}

Trong lớp shader, tôi truyền Frameset và hiệu ứng đã chọn cho shader.

if frameset then
  self:setImage(frameset.resource)
  self:setScale(nil, true)
  self:setPosition(frameset:getFrameX(), frameset:getFrameY(), true)
  if frameset:getQuad() then
    self:setQuad(frameset:getQuad():getViewport())
  end
end

Lời kết

Tôi đã đưa được color dodge của Photoshop vào trò chơi, nhưng vẫn phải thử thêm những thứ khác như bóng đổ và kết hợp hệ thống hạt cho lửa với hiệu ứng shader.

Dù vậy tôi nghĩ việc này là cần thiết để mọi thứ đẹp hơn một chút. May là bộ lọc glsl cũng có trên nhiều trang web, nên tôi không phải tự nghĩ ra tất cả từ đầu.