From a1df2e388e6f618496a642d189ce5e6e2832dd72 Mon Sep 17 00:00:00 2001 From: Per Hultqvist Date: Sun, 10 Nov 2024 14:02:00 +0100 Subject: [PATCH 1/2] New example: text/draw_3d --- examples/text/draw_3d/main.go | 822 ++++++++++++++++++ .../shaders/glsl330/alpha_discard.fs | 19 + 2 files changed, 841 insertions(+) create mode 100644 examples/text/draw_3d/main.go create mode 100644 examples/text/draw_3d/resources/shaders/glsl330/alpha_discard.fs diff --git a/examples/text/draw_3d/main.go b/examples/text/draw_3d/main.go new file mode 100644 index 0000000..f111aa6 --- /dev/null +++ b/examples/text/draw_3d/main.go @@ -0,0 +1,822 @@ +/******************************************************************************************* +* +* raylib [text] example - Draw 3d +* +* NOTE: Draw a 2D text in 3D space, each letter is drawn in a quad (or 2 quads if back face is set) +* where the texture coordinates of each quad map to the texture coordinates of the glyphs +* inside the font texture. +* +* A more efficient approach, i believe, would be to render the text in a render texture and +* map that texture to a plane and render that, or maybe a shader but my method allows more +* flexibility...for example to change position of each letter individually to make some think +* like a wavy text effect. +* +* Special thanks to: +* @Nighten for the DrawTextStyle() code https://github.com/NightenDushi/Raylib_DrawTextStyle +* Chris Camacho (codifies - http://bedroomcoders.co.uk/) for the alpha discard shader +* +* Example originally created with raylib 3.5, last time updated with raylib 4.0 +* +* Example contributed by Vlad Adrian (@demizdor) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2021-2024 Vlad Adrian (@demizdor) +* +********************************************************************************************/ +package main + +import ( + "fmt" + "math" + "path/filepath" + "unicode/utf8" + "unsafe" + + rl "github.com/gen2brain/raylib-go/raylib" +) + +// Globals +const ( + letterBoundarySize = 0.25 + textMaxLayers = 32 + maxTextLength = 64 + screenWidth = 800 + screenHeight = 450 +) + +var letterBoundaryColor = rl.Violet +var showLetterBoundary, showTextBoundary = false, false + +// Data Types definition + +// WaveTextConfig is a configuration structure for waving the text +type WaveTextConfig struct { + waveRange, waveSpeed, waveOffset rl.Vector3 +} + +// Program main entry point +func main() { + // Initialization + rl.SetConfigFlags(rl.FlagMsaa4xHint | rl.FlagVsyncHint) + rl.InitWindow(screenWidth, screenHeight, "raylib [text] example - draw 2D text in 3D") + + spin := true // Spin the camera? + multicolor := false // Multicolor mode + + // Define the camera to look into our 3d world + camera := rl.Camera3D{ + Position: rl.Vector3{ + X: -10.0, + Y: 15.0, + Z: -10.0, + }, // Camera position + Target: rl.Vector3{}, // Camera looking at point + Up: rl.Vector3{Y: 1.0}, // Camera up vector (rotation towards target) + Fovy: 45.0, // Camera field-of-view Y + Projection: rl.CameraPerspective, // Camera projection type + } + + cameraMode := rl.CameraOrbital + + cubePosition := rl.Vector3{Y: 1.0} + cubeSize := rl.Vector3{ + X: 2.0, + Y: 2.0, + Z: 2.0, + } + + // Use the default font + font := rl.GetFontDefault() + var fontSize, fontSpacing, lineSpacing float32 = 8.0, 0.5, -1 + + // Set the text (using markdown!) + text := "Hello ~~World~~ in 3D!" + tbox := rl.Vector3{} + var layers, quads int32 = 1, 0 + var layerDistance float32 = 0.01 + + wcfg := WaveTextConfig{ + waveSpeed: rl.Vector3{ + X: 3, + Y: 3, + Z: 0.5, + }, + waveOffset: rl.Vector3{ + X: 0.35, + Y: 0.35, + Z: 0.35, + }, + waveRange: rl.Vector3{ + X: 0.45, + Y: 0.45, + Z: 0.45, + }, + } + + var time float32 + + // Set up a light and dark color + light, dark := rl.Maroon, rl.Red + + // Load the alpha discard shader + alphaDiscard := rl.LoadShader("", "resources/shaders/glsl330/alpha_discard.fs") + + // Array filled with multiple random colors (when multicolor mode is set) + var multi [textMaxLayers]rl.Color + + rl.DisableCursor() // Limit cursor to relative movement inside the window + + rl.SetTargetFPS(60) // Set our game to run at 60 frames-per-second + + // Main game loop + for !rl.WindowShouldClose() { // Detect window close button or ESC key + // Update + rl.UpdateCamera(&camera, cameraMode) + + // Handle font files dropped + if rl.IsFileDropped() { + droppedFiles := rl.LoadDroppedFiles() + + // NOTE: We only support first ttf file dropped + rl.UnloadFont(font) + if filepath.Ext(droppedFiles[0]) == ".ttf" { + font = rl.LoadFontEx(droppedFiles[0], int32(fontSize), nil, 0) + } else if filepath.Ext(droppedFiles[0]) == ".fnt" { + font = rl.LoadFont(droppedFiles[0]) + fontSize = float32(font.BaseSize) + } + + rl.UnloadDroppedFiles() // Unload file paths from memory + } + + // Handle Events + if rl.IsKeyPressed(rl.KeyF1) { + showLetterBoundary = !showLetterBoundary + } + if rl.IsKeyPressed(rl.KeyF2) { + showTextBoundary = !showTextBoundary + } + if rl.IsKeyPressed(rl.KeyF3) { + // Handle camera change + spin = !spin + // we need to reset the camera when changing modes + camera = rl.Camera3D{ + Target: rl.Vector3{}, // Camera looking at point + Up: rl.Vector3{Y: 1.0}, // Camera up vector (rotation towards target) + Fovy: 45.0, // Camera field-of-view Y + Projection: rl.CameraPerspective, // Camera projection type + } + + if spin { + camera.Position = rl.Vector3{ + X: -10.0, + Y: 15.0, + Z: -10.0, + } // Camera position + cameraMode = rl.CameraOrbital + } else { + camera.Position = rl.Vector3{ + X: 10.0, + Y: 10.0, + Z: -10.0, + } // Camera position + cameraMode = rl.CameraFree + } + } + + // Handle clicking the cube + if rl.IsMouseButtonPressed(rl.MouseButtonLeft) { + // TODO : Missing function, see issue https://github.com/gen2brain/raylib-go/issues/457 + //ray := rl.GetScreenToWorldRay(rl.GetMousePosition(), camera) + ray := rl.GetMouseRay(rl.GetMousePosition(), camera) + + // Check collision between ray and box + v1 := rl.Vector3{ + X: cubePosition.X - cubeSize.X/2, + Y: cubePosition.Y - cubeSize.Y/2, + Z: cubePosition.Z - cubeSize.Z/2, + } + v2 := rl.Vector3{ + X: cubePosition.X + cubeSize.X/2, + Y: cubePosition.Y + cubeSize.Y/2, + Z: cubePosition.Z + cubeSize.Z/2, + } + collision := rl.GetRayCollisionBox(ray, rl.BoundingBox{ + Min: v1, + Max: v2, + }) + + if collision.Hit { + // Generate new random colors + light = generateRandomColor(0.5, 0.78) + dark = generateRandomColor(0.4, 0.58) + } + } + + // Handle text layers changes + if rl.IsKeyPressed(rl.KeyHome) && layers > 1 { + layers-- + } else if rl.IsKeyPressed(rl.KeyEnd) && layers < textMaxLayers { + layers++ + } + + // Handle text changes + if rl.IsKeyPressed(rl.KeyLeft) { + fontSize -= 0.5 + } else if rl.IsKeyPressed(rl.KeyRight) { + fontSize += 0.5 + } else if rl.IsKeyPressed(rl.KeyUp) { + fontSpacing -= 0.1 + } else if rl.IsKeyPressed(rl.KeyDown) { + fontSpacing += 0.1 + } else if rl.IsKeyPressed(rl.KeyPageUp) { + lineSpacing -= 0.5 + } else if rl.IsKeyPressed(rl.KeyPageDown) { + lineSpacing += 0.5 + } else if rl.IsKeyDown(rl.KeyInsert) { + layerDistance -= 0.001 + } else if rl.IsKeyDown(rl.KeyDelete) { + layerDistance += 0.001 + } else if rl.IsKeyPressed(rl.KeyTab) { + multicolor = !multicolor // Enable /disable multicolor mode + + if multicolor { + // Fill color array with random colors + for i := 0; i < textMaxLayers; i++ { + multi[i] = generateRandomColor(0.5, 0.8) + multi[i].A = uint8(rl.GetRandomValue(0, 255)) + } + } + } + + // Handle text input + ch := rl.GetCharPressed() + if rl.IsKeyPressed(rl.KeyBackspace) { + // Remove last char + text = text[:len(text)-1] + } else if rl.IsKeyPressed(rl.KeyEnter) { + // handle newline + text += "\n" + } else if ch != 0 && len(text) < maxTextLength { + // append only printable chars + text += string(ch) + } + + // Measure 3D text so we can center it + tbox = measureTextWave3D(font, text, fontSize, fontSpacing, lineSpacing) + + quads = 0 // Reset quad counter + time += rl.GetFrameTime() // Update timer needed by `drawTextWave3D()` + + // Draw + rl.BeginDrawing() + + rl.ClearBackground(rl.RayWhite) + + rl.BeginMode3D(camera) + rl.DrawCubeV(cubePosition, cubeSize, dark) + rl.DrawCubeWires(cubePosition, 2.1, 2.1, 2.1, light) + + rl.DrawGrid(10, 2.0) + + // Use a shader to handle the depth buffer issue with transparent textures + // NOTE: more info at https://bedroomcoders.co.uk/raylib-billboards-advanced-use/ + rl.BeginShaderMode(alphaDiscard) + + // Draw the 3D text above the red cube + rl.PushMatrix() + rl.Rotatef(90.0, 1.0, 0.0, 0.0) + rl.Rotatef(90.0, 0.0, 0.0, -1.0) + + for i := int32(0); i < layers; i++ { + clr := light + if multicolor { + clr = multi[i] + } + + vec := rl.Vector3{ + X: -tbox.X / 2.0, + Y: layerDistance * float32(i), + Z: -4.5, + } + drawTextWave3D(font, text, vec, fontSize, fontSpacing, lineSpacing, true, &wcfg, time, clr) + } + + // Draw the text boundary if set + if showTextBoundary { + rl.DrawCubeWiresV(rl.Vector3{Z: -4.5 + tbox.Z/2}, tbox, dark) + } + rl.PopMatrix() + + // Don't draw the letter boundaries for the 3D text below + slb := showLetterBoundary + showLetterBoundary = false + + // Draw 3D options (use default font) + rl.PushMatrix() + rl.Rotatef(180.0, 0.0, 1.0, 0.0) + opt := fmt.Sprintf("< SIZE: %2.1f >", fontSize) + quads += int32(len(opt)) + m := measureText3D(rl.GetFontDefault(), opt, 8.0, 1.0, 0.0) + pos := rl.Vector3{ + X: -m.X / 2.0, + Y: 0.01, + Z: 2.0, + } + drawText3D(rl.GetFontDefault(), opt, pos, 8.0, 1.0, 0.0, false, rl.Blue) + pos.Z += 0.5 + m.Z + + opt = fmt.Sprintf("< SPACING: %2.1f >", fontSpacing) + quads += int32(len(opt)) + m = measureText3D(rl.GetFontDefault(), opt, 8.0, 1.0, 0.0) + pos.X = -m.X / 2.0 + drawText3D(rl.GetFontDefault(), opt, pos, 8.0, 1.0, 0.0, false, rl.Blue) + pos.Z += 0.5 + m.Z + + opt = fmt.Sprintf("< LINE: %2.1f >", lineSpacing) + quads += int32(len(opt)) + m = measureText3D(rl.GetFontDefault(), opt, 8.0, 1.0, 0.0) + pos.X = -m.X / 2.0 + drawText3D(rl.GetFontDefault(), opt, pos, 8.0, 1.0, 0.0, false, rl.Blue) + pos.Z += 1.0 + m.Z + + lbox := "OFF" + if slb { + lbox = "ON" + } + opt = fmt.Sprintf("< LBOX: %3s >", lbox) + quads += int32(len(opt)) + m = measureText3D(rl.GetFontDefault(), opt, 8.0, 1.0, 0.0) + pos.X = -m.X / 2.0 + drawText3D(rl.GetFontDefault(), opt, pos, 8.0, 1.0, 0.0, false, rl.Red) + pos.Z += 0.5 + m.Z + + tb := "OFF" + if showTextBoundary { + tb = "ON" + } + opt = fmt.Sprintf("< TBOX: %3s >", tb) + quads += int32(len(opt)) + m = measureText3D(rl.GetFontDefault(), opt, 8.0, 1.0, 0.0) + pos.X = -m.X / 2.0 + drawText3D(rl.GetFontDefault(), opt, pos, 8.0, 1.0, 0.0, false, rl.Red) + pos.Z += 0.5 + m.Z + + opt = fmt.Sprintf("< LAYER DISTANCE: %.3f >", layerDistance) + quads += int32(len(opt)) + m = measureText3D(rl.GetFontDefault(), opt, 8.0, 1.0, 0.0) + pos.X = -m.X / 2.0 + drawText3D(rl.GetFontDefault(), opt, pos, 8.0, 1.0, 0.0, false, rl.DarkPurple) + rl.PopMatrix() + + // Draw 3D info text (use default font) + opt = "All the text displayed here is in 3D" + quads += 36 + m = measureText3D(rl.GetFontDefault(), opt, 10.0, 0.5, 0.0) + pos = rl.Vector3{ + X: -m.X / 2.0, + Y: 0.01, + Z: 2.0, + } + drawText3D(rl.GetFontDefault(), opt, pos, 10.0, 0.5, 0.0, false, rl.DarkBlue) + pos.Z += 1.5 + m.Z + + opt = "press [Left]/[Right] to change the font size" + quads += 44 + m = measureText3D(rl.GetFontDefault(), opt, 6.0, 0.5, 0.0) + pos.X = -m.X / 2.0 + drawText3D(rl.GetFontDefault(), opt, pos, 6.0, 0.5, 0.0, false, rl.DarkBlue) + pos.Z += 0.5 + m.Z + + opt = "press [Up]/[Down] to change the font spacing" + quads += 44 + m = measureText3D(rl.GetFontDefault(), opt, 6.0, 0.5, 0.0) + pos.X = -m.X / 2.0 + drawText3D(rl.GetFontDefault(), opt, pos, 6.0, 0.5, 0.0, false, rl.DarkBlue) + pos.Z += 0.5 + m.Z + + opt = "press [PgUp]/[PgDown] to change the line spacing" + quads += 48 + m = measureText3D(rl.GetFontDefault(), opt, 6.0, 0.5, 0.0) + pos.X = -m.X / 2.0 + drawText3D(rl.GetFontDefault(), opt, pos, 6.0, 0.5, 0.0, false, rl.DarkBlue) + pos.Z += 0.5 + m.Z + + opt = "press [F1] to toggle the letter boundary" + quads += 39 + m = measureText3D(rl.GetFontDefault(), opt, 6.0, 0.5, 0.0) + pos.X = -m.X / 2.0 + drawText3D(rl.GetFontDefault(), opt, pos, 6.0, 0.5, 0.0, false, rl.DarkBlue) + pos.Z += 0.5 + m.Z + + opt = "press [F2] to toggle the text boundary" + quads += 37 + m = measureText3D(rl.GetFontDefault(), opt, 6.0, 0.5, 0.0) + pos.X = -m.X / 2.0 + drawText3D(rl.GetFontDefault(), opt, pos, 6.0, 0.5, 0.0, false, rl.DarkBlue) + + showLetterBoundary = slb + rl.EndShaderMode() + + rl.EndMode3D() + + // Draw 2D info text & stats + msg := `Drag & drop a font file to change the font! +Type something, see what happens! + +Press [F3] to toggle the camera` + rl.DrawText(msg, 10, 35, 10, rl.Black) + + cam := "FREE" + if spin { + cam = "ORBITAL" + } + quads += int32(len(text)) * 2 * layers + tmp := fmt.Sprintf("%2d layer(s) | %s camera | %4d quads (%4d verts)", layers, cam, quads, quads*4) + width := rl.MeasureText(tmp, 10) + rl.DrawText(tmp, screenWidth-20-width, 10, 10, rl.DarkGreen) + + tmp = "[Home]/[End] to add/remove 3D text layers" + width = rl.MeasureText(tmp, 10) + rl.DrawText(tmp, screenWidth-20-width, 25, 10, rl.DarkGray) + + tmp = "[Insert]/[Delete] to increase/decrease distance between layers" + width = rl.MeasureText(tmp, 10) + rl.DrawText(tmp, screenWidth-20-width, 40, 10, rl.DarkGray) + + tmp = "click the [CUBE] for a random color" + width = rl.MeasureText(tmp, 10) + rl.DrawText(tmp, screenWidth-20-width, 55, 10, rl.DarkGray) + + tmp = "[Tab] to toggle multicolor mode" + width = rl.MeasureText(tmp, 10) + rl.DrawText(tmp, screenWidth-20-width, 70, 10, rl.DarkGray) + + rl.DrawFPS(10, 10) + + rl.EndDrawing() + } + + // De-Initialization + rl.UnloadFont(font) + rl.CloseWindow() // Close window and OpenGL context + +} + +// Module Functions Definitions + +// drawTextCodepoint3D draws codepoint at specified position in 3D space +func drawTextCodepoint3D(font rl.Font, codepoint rune, position rl.Vector3, fontSize float32, backface bool, + tint rl.Color) { + // Character index position in sprite font + // NOTE: In case a codepoint is not available in the font, index returned points to '?' + index := rl.GetGlyphIndex(font, codepoint) + scale := fontSize / float32(font.BaseSize) + + // Character destination rectangle on screen + // NOTE: We consider charsPadding on drawing + glyphs := unsafe.Slice(font.Chars, font.CharsCount) + position.X += float32(glyphs[index].OffsetX-font.CharsPadding) / float32(font.BaseSize) * scale + position.Z += float32(glyphs[index].OffsetY-font.CharsPadding) / float32(font.BaseSize) * scale + + // Character source rectangle from font texture atlas + // NOTE: We consider chars padding when drawing, it could be required for outline/glow shader effects + recs := unsafe.Slice(font.Recs, font.CharsCount) + srcRec := rl.Rectangle{ + X: recs[index].X - float32(font.CharsPadding), + Y: recs[index].Y - float32(font.CharsPadding), + Width: recs[index].Width + 2.0*float32(font.CharsPadding), + Height: recs[index].Height + 2.0*float32(font.CharsPadding), + } + + width := (recs[index].Width + 2.0*float32(font.CharsPadding)) / float32(font.BaseSize) * scale + height := (recs[index].Height + 2.0*float32(font.CharsPadding)) / float32(font.BaseSize) * scale + + if font.Texture.ID > 0 { + var x, y, z float32 + + // normalized texture coordinates of the glyph inside the font texture (0.0f -> 1.0f) + tx := srcRec.X / float32(font.Texture.Width) + ty := srcRec.Y / float32(font.Texture.Height) + tw := (srcRec.X + srcRec.Width) / float32(font.Texture.Width) + th := (srcRec.Y + srcRec.Height) / float32(font.Texture.Height) + + if showLetterBoundary { + pos := rl.Vector3{ + X: position.X + width/2, + Y: position.Y, + Z: position.Z + height/2, + } + size := rl.Vector3{ + X: width, + Y: letterBoundarySize, + Z: height, + } + rl.DrawCubeWiresV(pos, size, letterBoundaryColor) + } + + var limit int32 = 4 + if backface { + limit = 8 + } + rl.CheckRenderBatchLimit(limit) + rl.SetTexture(font.Texture.ID) + + rl.PushMatrix() + rl.Translatef(position.X, position.Y, position.Z) + + rl.Begin(rl.Quads) + rl.Color4ub(tint.R, tint.G, tint.B, tint.A) + + // Front Face + rl.Normal3f(0.0, 1.0, 0.0) // Normal Pointing Up + rl.TexCoord2f(tx, ty) + rl.Vertex3f(x, y, z) // Top Left Of The Texture and Quad + rl.TexCoord2f(tx, th) + rl.Vertex3f(x, y, z+height) // Bottom Left Of The Texture and Quad + rl.TexCoord2f(tw, th) + rl.Vertex3f(x+width, y, z+height) // Bottom Right Of The Texture and Quad + rl.TexCoord2f(tw, ty) + rl.Vertex3f(x+width, y, z) // Top Right Of The Texture and Quad + + if backface { + // Back Face + rl.Normal3f(0.0, -1.0, 0.0) // Normal Pointing Down + rl.TexCoord2f(tx, ty) + rl.Vertex3f(x, y, z) // Top Right Of The Texture and Quad + rl.TexCoord2f(tw, ty) + rl.Vertex3f(x+width, y, z) // Top Left Of The Texture and Quad + rl.TexCoord2f(tw, th) + rl.Vertex3f(x+width, y, z+height) // Bottom Left Of The Texture and Quad + rl.TexCoord2f(tx, th) + rl.Vertex3f(x, y, z+height) // Bottom Right Of The Texture and Quad + } + rl.End() + rl.PopMatrix() + rl.SetTexture(0) + } +} + +// drawText3D draws a 2D text in 3D space +func drawText3D(font rl.Font, text string, position rl.Vector3, fontSize, fontSpacing, + lineSpacing float32, backface bool, tint rl.Color) { + length := int32(len(text)) // Total length in bytes of the text, + // scanned by codepoints in loop + + // TextOffsetY : Offset between lines (on line break '\n') + // TextOffsetX : Offset X to next character to draw + var textOffsetY, textOffsetX float32 + + scale := fontSize / float32(font.BaseSize) + for i := int32(0); i < length; { + + // Get next codepoint from byte string and glyph index in font + codepoint, codepointByteCount := getCodepoint(text, i) + index := rl.GetGlyphIndex(font, codepoint) + + // NOTE: Normally we exit the decoding sequence as soon as a bad byte is found (and return 0x3f) + // but we need to draw all the bad bytes using the '?' symbol moving one byte + if codepoint == 0x3f { + codepointByteCount = 1 + } + + if codepoint == 0 || codepoint == '\n' { + // NOTE: Fixed line spacing of 1.5 line-height + // TODO: Support custom line spacing defined by user + textOffsetY += scale + lineSpacing/float32(font.BaseSize)*scale + textOffsetX = 0.0 + } else { + if (codepoint != ' ') && (codepoint != '\t') { + vec := rl.Vector3{ + X: position.X + textOffsetX, + Y: position.Y, + Z: position.Z + textOffsetY, + } + drawTextCodepoint3D(font, codepoint, vec, fontSize, backface, tint) + } + + textOffsetX += getTextWidth(font, index, fontSpacing, scale) + } + + i += codepointByteCount // Move text bytes counter to next codepoint + } +} + +// measureText3D measures a text in 3D. For some reason `MeasureTextEx()` +// just doesn't seem to work, so I had to use this instead. +func measureText3D(font rl.Font, text string, fontSize, fontSpacing, lineSpacing float32) rl.Vector3 { + length := int32(len(text)) + var tempLen, lenCounter int32 // Used to count longer text line num chars + + var ( + tempTextWidth float32 // Used to count longer text line width + scale = fontSize / float32(font.BaseSize) + textHeight = scale + textWidth float32 + ) + + var ( + letter rune // Current character + index int32 // Index position in sprite font + ) + + for i := int32(0); i < length; i++ { + lenCounter++ + + var next int32 + letter, next = getCodepoint(text, i) + index = rl.GetGlyphIndex(font, letter) + + // NOTE: normally we exit the decoding sequence as soon as a bad byte is found (and return 0x3f) + // but we need to draw all the bad bytes using the '?' symbol so to not skip any we set next = 1 + if letter == 0x3f { + next = 1 + } + i += next - 1 + + if letter != 0 && letter != '\n' { + textWidth += getTextWidth(font, index, fontSpacing, scale) + } else { + if tempTextWidth < textWidth { + tempTextWidth = textWidth + } + lenCounter = 0 + textWidth = 0.0 + textHeight += scale + lineSpacing/float32(font.BaseSize)*scale + } + + if tempLen < lenCounter { + tempLen = lenCounter + } + } + + if tempTextWidth < textWidth { + tempTextWidth = textWidth + } + + // Adds chars spacing to measure + vec := rl.Vector3{ + X: tempTextWidth + float32(tempLen-1)*fontSpacing/float32(font.BaseSize)*scale, + Y: 0.25, + Z: textHeight, + } + return vec +} + +// drawTextWave3D draws a 2D text in 3D space and wave the parts that start with `~~` and end with `~~`. +// This is a modified version of the original code by @Nighten found here https://github.com/NightenDushi/Raylib_DrawTextStyle +func drawTextWave3D(font rl.Font, text string, position rl.Vector3, fontSize, fontSpacing, + lineSpacing float32, backface bool, config *WaveTextConfig, time float32, tint rl.Color) { + length := int32(len(text)) // Total length in bytes of the text, scanned by codepoints in loop + + // TextOffsetY : Offset between lines (on line break '\n') + // TextOffsetX : Offset X to next character to draw + var textOffsetY, textOffsetX float32 + var wave bool + + scale := fontSize / float32(font.BaseSize) + + for i, k := int32(0), int32(0); i < length; k++ { + // Get next codepoint from byte string and glyph index in font + codepoint, codepointByteCount := getCodepoint(text, i) + index := rl.GetGlyphIndex(font, codepoint) + + // NOTE: Normally we exit the decoding sequence as soon as a bad byte is found (and return 0x3f) + // but we need to draw all the bad bytes using the '?' symbol moving one byte + if codepoint == 0x3f { + codepointByteCount = 1 + } + + if codepoint == 0 || codepoint == '\n' { + // NOTE: Fixed line spacing of 1.5 line-height + // TODO: Support custom line spacing defined by user + textOffsetY += scale + lineSpacing/float32(font.BaseSize)*scale + textOffsetX = 0.0 + k = 0 + } else if codepoint == '~' { + var r rune + r, codepointByteCount = getCodepoint(text, i+1) + if r == '~' { + codepointByteCount += 1 + wave = !wave + } + } else { + if (codepoint != ' ') && (codepoint != '\t') { + pos := position + if wave { // Apply the wave effect + kk := float32(k) + pos.X += sin(time*config.waveSpeed.X-kk*config.waveOffset.X) * config.waveRange.X + pos.Y += sin(time*config.waveSpeed.Y-kk*config.waveOffset.Y) * config.waveRange.Y + pos.Z += sin(time*config.waveSpeed.Z-kk*config.waveOffset.Z) * config.waveRange.Z + } + + vec := rl.Vector3{ + X: pos.X + textOffsetX, + Y: pos.Y, + Z: pos.Z + textOffsetY, + } + drawTextCodepoint3D(font, codepoint, vec, fontSize, backface, tint) + } + + textOffsetX += getTextWidth(font, index, fontSpacing, scale) + } + + i += codepointByteCount // Move text bytes counter to next codepoint + } +} + +// measureTextWave3D measures a text in 3D ignoring the `~~` chars. +func measureTextWave3D(font rl.Font, text string, fontSize, fontSpacing, lineSpacing float32) rl.Vector3 { + length := int32(len(text)) + var tempLen, lenCounter int32 // Used to count longer text line num chars + + var ( + tempTextWidth float32 = 0.0 // Used to count longer text line width + scale = fontSize / float32(font.BaseSize) + textHeight = scale + textWidth float32 = 0.0 + ) + + var letter, index int32 // Current character and Index position in sprite font + + for i := int32(0); i < length; i++ { + lenCounter++ + var next int32 + + letter, next = getCodepoint(text, i) + index = rl.GetGlyphIndex(font, letter) + + // NOTE: normally we exit the decoding sequence as soon as a bad byte is found (and return 0x3f) + // but we need to draw all the bad bytes using the '?' symbol so to not skip any we set next = 1 + if letter == 0x3f { + next = 1 + } + i += next - 1 + + if letter != 0 && letter != '\n' { + r, _ := getCodepoint(text, i+1) + if letter == '~' && r == '~' { + i++ + } else { + textWidth += getTextWidth(font, index, fontSpacing, scale) + } + } else { + if tempTextWidth < textWidth { + tempTextWidth = textWidth + } + lenCounter = 0 + textWidth = 0.0 + textHeight += scale + lineSpacing/float32(font.BaseSize)*scale + } + + if tempLen < lenCounter { + tempLen = lenCounter + } + } + + if tempTextWidth < textWidth { + tempTextWidth = textWidth + } + + vec := rl.Vector3{ + X: tempTextWidth + float32(tempLen-1)*fontSpacing/float32(font.BaseSize)*scale, // Adds chars spacing to measure + Y: 0.25, + Z: textHeight, + } + + return vec +} + +// generateRandomColor generates a nice color with a random hue +func generateRandomColor(s, v float32) rl.Color { + const Phi = float64(0.618033988749895) // Golden ratio conjugate + h := float64(rl.GetRandomValue(0, 360)) + h = math.Mod(h+h*Phi, 360.0) + return rl.ColorFromHSV(float32(h), s, v) +} + +// getCodepoint returns the rune starting at index, plus the length of that rune in bytes +func getCodepoint(s string, index int32) (rune, int32) { + if index == int32(len(s)) { + return 0, 0 + } + r := []rune(s[index:]) + return r[0], int32(utf8.RuneLen(r[0])) +} + +// sin is just a convenience function to avoid a bunch of type conversions +func sin(value float32) float32 { + return float32(math.Sin(float64(value))) +} + +func getTextWidth(font rl.Font, index int32, spacing, scale float32) float32 { + glyphs := unsafe.Slice(font.Chars, font.CharsCount) + if glyphs[index].AdvanceX == 0 { + recs := unsafe.Slice(font.Recs, font.CharsCount) + return (recs[index].Width + spacing) / float32(font.BaseSize) * scale + } else { + return (float32(glyphs[index].AdvanceX) + spacing) / float32(font.BaseSize) * scale + } +} diff --git a/examples/text/draw_3d/resources/shaders/glsl330/alpha_discard.fs b/examples/text/draw_3d/resources/shaders/glsl330/alpha_discard.fs new file mode 100644 index 0000000..d2134a6 --- /dev/null +++ b/examples/text/draw_3d/resources/shaders/glsl330/alpha_discard.fs @@ -0,0 +1,19 @@ +#version 330 + +// Input vertex attributes (from vertex shader) +in vec2 fragTexCoord; +in vec4 fragColor; + +// Input uniform values +uniform sampler2D texture0; +uniform vec4 colDiffuse; + +// Output fragment color +out vec4 finalColor; + +void main() +{ + vec4 texelColor = texture(texture0, fragTexCoord); + if (texelColor.a == 0.0) discard; + finalColor = texelColor * fragColor * colDiffuse; +} From 6de5b09c62905db7d1db0a0bd5d013ac9cc8857b Mon Sep 17 00:00:00 2001 From: Per Hultqvist Date: Sun, 10 Nov 2024 18:24:52 +0100 Subject: [PATCH 2/2] Small path fix --- .../draw_3d/{resources/shaders/glsl330 => }/alpha_discard.fs | 0 examples/text/draw_3d/main.go | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename examples/text/draw_3d/{resources/shaders/glsl330 => }/alpha_discard.fs (100%) diff --git a/examples/text/draw_3d/resources/shaders/glsl330/alpha_discard.fs b/examples/text/draw_3d/alpha_discard.fs similarity index 100% rename from examples/text/draw_3d/resources/shaders/glsl330/alpha_discard.fs rename to examples/text/draw_3d/alpha_discard.fs diff --git a/examples/text/draw_3d/main.go b/examples/text/draw_3d/main.go index f111aa6..82f4c81 100644 --- a/examples/text/draw_3d/main.go +++ b/examples/text/draw_3d/main.go @@ -121,7 +121,7 @@ func main() { light, dark := rl.Maroon, rl.Red // Load the alpha discard shader - alphaDiscard := rl.LoadShader("", "resources/shaders/glsl330/alpha_discard.fs") + alphaDiscard := rl.LoadShader("", "alpha_discard.fs") // Array filled with multiple random colors (when multicolor mode is set) var multi [textMaxLayers]rl.Color