0%

jwt-go中我记录了手动封装一个认证中间件,然后今天在看go-admin后台管理系统源码的时候发现作者用到了一个第三方库gin-jwt实现了同样的功能,今天就一起来看一下吧!

gin-jwt能做什么?

  • gin-jwt是基于jwt-go实现的一个认证中间件,同时也是专为Gin框架实现的。

  • gin-jwt提供了额外的函数功能:给登录接口生成token令牌,以及用于刷新token的函数。

源码分析

整个框架就一个auth_jwt.go文件,共747行代码。我们从头往下看:

先定义了一个返回值为map类型的接口
type MapClaims map[string]interface{}

接着就是一个结构体GinJWTMiddleware,还记得我们在使用jwt-go的时候,也会定义一个结构体,里面存放了关于生成token的一些信息,包括:token发放时间、是谁发放的token、主题等,这里也一样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
type GinJWTMiddleware struct {
// Realm name to display to the user. Required.
Realm string

// 算法类型: HS256, HS384, HS512默认是 HS256.
SigningAlgorithm string

// 加密密钥.
Key []byte

// 刷新Token有效期.默认一个小时
MaxRefresh time.Duration

// 认证回调函数
Authenticator func(c *gin.Context) (interface{}, error)

// 通过认证后的处理函数
Authorizator func(data interface{}, c *gin.Context) bool


// 认证失败的回调函数
Unauthorized func(*gin.Context, int, string)


//登录响应函数
LoginResponse func(*gin.Context, int, string, time.Time)

// 超时响应函数
LogoutResponse func(*gin.Context, int)

// 刷新响应函数
RefreshResponse func(*gin.Context, int, string, time.Time)
}

函数(只列了几个目前见到的)

登录返回token

1
2
3
4
5
6
// LoginHandler can be used by clients to get a jwt token.
// Payload needs to be json in the form of {"username": "USERNAME", "password": "PASSWORD"}.
// Reply will be of the form {"token": "TOKEN"}.
func (mw *GinJWTMiddleware) LoginHandler(c *gin.Context) {
...
}

刷新token

1
2
3
4
5
6
7
8
9
10
11
12
// RefreshHandler can be used to refresh a token. The token still needs to be valid on refresh.
// Shall be put under an endpoint that is using the GinJWTMiddleware.
// Reply will be of the form {"token": "TOKEN"}.
func (mw *GinJWTMiddleware) RefreshHandler(c *gin.Context) {
tokenString, expire, err := mw.RefreshToken(c)
if err != nil {
mw.unauthorized(c, http.StatusUnauthorized, mw.HTTPStatusMessageFunc(err, c))
return
}

mw.RefreshResponse(c, http.StatusOK, tokenString, expire)
}

验证API时需要用到,例如检测权限

1
2
3
4
5
6
// MiddlewareFunc makes GinJWTMiddleware implement the Middleware interface.
func (mw *GinJWTMiddleware) MiddlewareFunc() gin.HandlerFunc {
return func(c *gin.Context) {
mw.middlewareImpl(c)
}
}

示例

测试官方demo

源码地址:https://github.com/samtake/gin-jwt-demo

1
2
3
[GIN] 2020/04/18 - 18:29:51 | 200 |    1.502204ms |             ::1 | POST     "/login"
[GIN] 2020/04/18 - 18:30:50 | 401 | 112.029µs | ::1 | GET "/auth/hello"
[GIN] 2020/04/18 - 18:32:55 | 200 | 346.218µs | ::1 | GET "/auth/hello"

直接请求401

1
2
3
4
{
"code": 401,
"message": "cookie token is empty"
}

登录返回token

1
2
3
4
5
{
"code": 200,
"expire": "2020-04-18T19:29:51+08:00",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODcyMDkzOTEsImlkIjoiYWRtaW4iLCJvcmlnX2lhdCI6MTU4NzIwNTc5MX0.o55urrL-qhUqpqEKK8k8KF8yBqrgZo_EtKmuOW1bHnc"
}

携带token请求

1
2
3
4
5
{
"text": "Hello World.",
"userID": "admin",
"userName": "admin"
}

go-admin中的使用

这里也就记录别人在项目中是怎么使用的,以及有没有可以学习的地方。

auth.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package middleware

import (
config2 "go-admin/config"
"go-admin/handler"
jwt "go-admin/pkg/jwtauth"
"time"
)

func AuthInit() (*jwt.GinJWTMiddleware, error) {
return jwt.New(&jwt.GinJWTMiddleware{
Realm: "test zone",
Key: []byte("secret key"),
Timeout: time.Hour,
MaxRefresh: time.Hour,
IdentityKey: config2.ApplicationConfig.JwtSecret,
PayloadFunc: handler.PayloadFunc,
IdentityHandler: handler.IdentityHandler,
Authenticator: handler.Authenticator,
Authorizator: handler.Authorizator,
Unauthorized: handler.Unauthorized,
TokenLookup: "header: Authorization, query: token, cookie: jwt",
TokenHeadName: "Bearer",
TimeFunc: time.Now,
})

}

permission.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package middleware

import (
"github.com/gin-gonic/gin"
"go-admin/pkg"
mycasbin "go-admin/pkg/casbin"
"go-admin/pkg/jwtauth"
_ "go-admin/pkg/jwtauth"
"log"
"net/http"
)

//权限检查中间件
func AuthCheckRole() gin.HandlerFunc {
return func(c *gin.Context) {
data, _ := c.Get("JWT_PAYLOAD")
v := data.(jwtauth.MapClaims)
e, err := mycasbin.Casbin()
pkg.HasError(err,"",500)
//检查权限
res, err := e.Enforce(v["rolekey"], c.Request.URL.Path, c.Request.Method)
log.Println("----------------", v["rolekey"], c.Request.URL.Path, c.Request.Method)

pkg.HasError(err,"",500)

if res {
c.Next()
} else {
c.JSON(http.StatusOK, gin.H{
"status": 401,
"msg": "对不起,您没有该接口访问权限,请联系管理员",
})
c.Abort()
return
}
}
}

router.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func InitRouter() *gin.Engine {

// the jwt middleware
authMiddleware, err := middleware.AuthInit()//初始化

if err != nil {
_ = fmt.Errorf("JWT Error", err.Error())
}

r.POST("/login", authMiddleware.LoginHandler) //框架自带

// Refresh time can be longer than token timeout
r.GET("/refresh_token", authMiddleware.RefreshHandler)//框架自带


apiv1 := r.Group("/api/v1")
{
apiv1.GET("/monitor/server", monitor.ServerInfo)
...
}


auth := r.Group("/api/v1")
auth.Use(authMiddleware.MiddlewareFunc()).Use(middleware.AuthCheckRole())//检测权限
{
auth.GET("/deptList", system.GetDeptList)
...
}
}

写到最后发现这里有篇博客写的挺好的,可以看看~
https://blog.firerain.me/article/18

.end

前言

今天在用一些工具处理文件的时候,批量手动操作真会累死人,时间成本不划算,想着怎么用脚本做成自动化,思路大概是每个脚本处理不同的功能,也就没必要将所有功能写到一个脚本了,担心万一某个模块出了问题影响整个流程。然后其中一个功能需要调用某个网页的接口处理,想了好久,请教了下一些前辈,给出的建议就是破解接口,这时候我立刻想到了Go…大概分三步:

  • shell调用工具拿到处理好的数据
  • 再由Go实现对接口的数据请求…
  • 请求回来接口之后再交由shell调用工具做最后的处理,从而完成整个模块的工作。

这里记住一点:shell脚本无非就做成参数形式。

API

os/exec包可用于从Go触发任何OS命令。 相同的可用于触发.sh文件。

具体函数接口参考官方文档package exec

上代码

首先,在同一目录中创建一个test.sh文件

1
2
3
4
#!/bin/bash

your_name="start test ..."
echo $your_name

执行chmod +x test.sh,不然会因为没有执行权限报错:

1
Execute Shell:./test.sh failed with error:exit status 126%

调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
"os/exec"
)

func main() {
fmt.Println("1")

command := `./test.sh .`
cmd := exec.Command("/bin/bash", "-c", command)

output, err := cmd.Output()
if err != nil {
fmt.Printf("Execute Shell:%s failed with error:%s", command, err.Error())
return
}
fmt.Printf("Execute Shell:%s finished with output:\n%s", command, string(output))
}

输出

1
2
3
1
Execute Shell:./test.sh . finished with output:
start test ...

https://golangbyexample.com//

概述

E-R图也称实体-联系图(Entity Relationship Diagram),也就是概念模型,可以用来做数据库设计。今天先记录下该怎么看E-R图,至于软件的操作下次补充记录~

作图工具在Mac端可以用dbeaver,原因呢主要是免费吧。

然后说说今天我为什么怎么就扯到了E-R图呢?是我在撸一个golang项目的时候,发觉对后台数据库不知道怎么设计,然后就查了下资料,如何设计也算说的很明了,对应的关系就是用E-R图来画的。

如何看图

ER01.png

上面👆概念数据模型中包含了:权限、角色、用户、文章、栏目、评论和两个桥表,其中1对1或1对多等关系在模型中体现。其中:

  • PKprimary key
  • 箭头代表的是关联关系
  • 虚线代表的是备注说明

具体举例(红框部分):
ER02.png

  • 用户角色表(cms_user_role) 同时属于用户表(cms_user)和角色表(cms_role)
  • 说白了,就是一个用户会有多个角色

用例01.png
读者可以进行阅读、注册、登录等操作。登录状态下又可以进行评论与阅读操作。

用例02.png
小编是作者身份,可以对自己的文章进行管理。

用例03.png
超级管理员拥有系统最大权限(或者所有权限),可以进行栏目管理、文章管理(文章管理隐式的包含了拥有阅读权限)、权限管理、角色管理、评论审核等操作。可对文章进行审核,权限管理下可对角色进行管理。

gorm的实现

gorm文档中,我们可以找到对模型建立关系的的文档:https://gorm.io/zh_CN/docs/belongs_to.html

依旧以cms_user_role cms_user cms_role 这三个表为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//在gorm中,模型的结构以驼峰命名方式书写,在数据表会自动转成对应下划线

//CmsUserRole .
type CmsUserRole struct {
gorm.Model
ID string `gorm:"type:varchar(100);not null"json:"id"`
UserID string `gorm:"type:varchar(100);not null"json:"user_id"`
RoleID string `gorm:"type:varchar(100);not null"json:"role_id"`
}

//CmsUser .
type CmsUser struct {
gorm.Model
ID string `gorm:"type:varchar(100);not null"json:"id"`
UserName string `gorm:"type:varchar(100);not null"json:"username"`
Password string `gorm:"type:varchar(100);not null"json:"password"`
CmsUserRole CmsUserRole `gorm:"foreignkey:CmsUserRoleRefer"` // 将 CmsUserRoleRefer 作为外键
CmsUserRoleRefer uint
}

//CmsRole .
type CmsRole struct {
gorm.Model
ID string `gorm:"type:varchar(100);not null"json:"id"`
Name string `gorm:"type:varchar(100);not null"json:"name"`
CmsUserRole CmsUserRole `gorm:"foreignkey:CmsUserRoleRefer"` // 将 CmsUserRoleRefer 作为外键
CmsUserRoleRefer uint
}

到这,你也会发觉用户角色表其实是个中间件。

资料:

【管理系统课程设计】美少女手把手教你后台管理
在mac上有没有类似PowerDesigner的软件?
第1节:7分钟上手ER图
通俗数据库设计(2)E-R图
如何使用 MySQL workbench 生成 EER 图?
E-R图专栏
visio-数据库模型图

https://github.com/spf13/viper

在项目中创建一个config文件夹,新建一个application.yml文件

1
2
3
4
5
6
7
8
9
10
server:
port: 8099
datasource:
driverName: mysql
host: 127.0.0.1
port: 3306
database: gin_vue_bs
username: root
password: 123456
charset: utf8

初始化配置文件,要在初始化数据库之前调用

1
2
3
4
5
6
7
8
9
10
11
//InitConfig 初始化配置文件.
func InitConfig() {
workDir, _ := os.Getwd() //工程项目目录
viper.SetConfigName("application") //配置文件名
viper.SetConfigType("yml") //配置文件类型
viper.AddConfigPath(workDir + "/config") //配置文件目录
err := viper.ReadInConfig()
if err != nil {
panic(err)
}
}

调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//InitDB .
func InitDB() *gorm.DB {
driverName := viper.GetString("datasource.driverName")
host := viper.GetString("datasource.host")
port := viper.GetString("datasource.port")
database := viper.GetString("datasource.database")
username := viper.GetString("datasource.username")
passwoed := viper.GetString("datasource.password")
charset := "utf8"
args := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=%s&parseTime=true",
username,
passwoed,
host,
port,
database,
charset)

db, err := gorm.Open(driverName, args)
if err != nil {
panic("failed to connect database,err" + err.Error())
}

//创建数据表
db.AutoMigrate(&model.User{})

DB = db
return db
}

dto

dto数据传输对象(Data Transfer Object),是一种设计模式之间传输数据的软件应用系统。数据传输目标往往是数据访问对象从数据库中检索数据。数据传输对象与数据交互对象或数据访问对象之间的差异是一个以不具有任何行为除了存储和检索的数据(访问和存取器)。

在golang中目前我遇到是用于控制返回的字段,如:在User中由多个字段,我希望只返回NameTelephone

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package dto

import (
"gin_vue_bs/model"
)

//UserDto .
type UserDto struct {
Name string `json:"name"`
Telephone string `json:"telephone"`
}

//ToUserDto .
func ToUserDto(user model.User) UserDto {
return UserDto{
Name: user.Name,
Telephone: user.Telephone,
}
}
1
2
3
4
5
6
type User struct {
gorm.Model
Name string `gorm:"type:varchar(20);not null"`
Telephone string `gorm:"varchar(110;not null;unique"`
Password string `gorm:"size:255;not null"`
}
1
ctx.JSON(http.StatusOK, gin.H{"code": 200, "data": gin.H{"user": dto.ToUserDto(user.(model.User))}})

dao

dao就叫它数据访问接口吧。

举一个例子先,例如我们比较常见的 Service 层和 DAO 层的操作,Service 处理完逻辑之后,交给 DAO 层进行持久化,或者需要调用 DAO 层从持久化中获取一些必要的数据;在测试的时候,我们很多时候不希望真的持久化或者从持久化中获取数据,那么就会对 DAO 层进行一些 Mock。

还有深刻的理解,相关资料:

http://www.imooc.com/article/33236
https://www.cnblogs.com/makor/p/ut-in-go-lang.html

github

Documentation

引入包

1
go get "github.com/dgrijalva/jwt-go"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package common

import (
"gin_vue_bs/model"
"time"

"github.com/dgrijalva/jwt-go"
)

//定义 jwt 加密密钥
var jwtKey = []byte("a_secret_crect")

//Claims .
type Claims struct {
UserID uint
jwt.StandardClaims
}

//ReleaseToken 登录成功则调用该方法发放token.
func ReleaseToken(user model.User) (string, error) {
expirationTime := time.Now().Add(7 * 24 * time.Hour) //token的过期时间7天
claims := &Claims{
UserID: user.ID,
StandardClaims: jwt.StandardClaims{
ExpiresAt: expirationTime.Unix(),
IssuedAt: time.Now().Unix(), //token发放时间
Issuer: "samtake", //是谁发放的token
Subject: "user token", //主题
},
}

token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(jwtKey)

if err != nil {
return "", err
}

return tokenString, nil
}

返回

1
2
3
4
5
6
7
{
"code": "200",
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySUQiOjMsImV4cCI6MTU4NzA1MjA0MiwiaWF0IjoxNTg2NDQ3MjQyLCJpc3MiOiJzYW10YWtlIiwic3ViIjoidXNlciB0b2tlbiJ9.6a50w6SfsQH7XZBW7zu3_FEGFcrWHRAh2DFYTh9hbCo"
},
"msg": "登录成功"
}

所返回的token由三部分组成:协议头(token的加密协议)、 源码claims储存的信息 、(前面两部分+jwtKey)的哈希值:

1
2
3
4
5
6
7
➜  blogs echo eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9  | base64 -D                                                                                                                                                         
{"alg":"HS256","typ":"JWT"}%
➜ blogs echo eyJVc2VySUQiOjMsImV4cCI6MTU4NzA1MjA0MiwiaWF0IjoxNTg2NDQ3MjQyLCJpc3MiOiJzYW10YWtlIiwic3ViIjoidXNlciB0b2tlbiJ9 | base64 -D
{"UserID":3,"exp":1587052042,"iat":1586447242,"iss":"samtake","sub":"user token"}%
➜ blogs echo 6a50w6SfsQH7XZBW7zu3_FEGFcrWHRAh2DFYTh9hbCo | base64 -D
?tä???]?V?;??Q??!?1XNa%
➜ blogs

https://gorm.io

概述

ORM分别代表:
|O|R|M|
|:—-|:—|:—-|
|Object|Relational|Mapping|
|对象|关系|映射|
|程序中的对象/实例|关系型数据库|-|
|例如Go中的结构体实例|例如MySQL|-|

MySQL与 Go的对应关系:
数据表 <-> 结构体
数据行 <-> 结构体实例
字段 <-> 结构体字段

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
package main

import (
"fmt"
"math/rand"
"net/http"
"time"

"github.com/e421083458/gorm" //https://gorm.io
"github.com/gin-gonic/gin"
_ "github.com/go-sql-driver/mysql"
)

type User struct {
gorm.Model
Name string `gorm:"type:varchar(20);not null"`
Telephone string `gorm:"varchar(110;not null;unique"`
Password string `gorm:"size:255;not null"`
}

func main() {

db := InitDb()
defer db.Close()

r := gin.New()
r.Use(gin.Logger())

r.Use(gin.Recovery())

r.POST("/api/auth/register", func(ctx *gin.Context) {
//获取参数
name := ctx.PostForm("name")
telephone := ctx.PostForm("telephone")
password := ctx.PostForm("password")

//验证数据
if len(telephone) != 11 {
ctx.JSON(http.StatusServiceUnavailable,
gin.H{"code": 422, "msg": "手机号必须为11为"})
}

if len(password) < 6 {
ctx.JSON(http.StatusUnprocessableEntity,
gin.H{"code": 422, "msg": "密码不能少于6位"})
}

//名称如果没有传,则返回随机字符串
if len(name) == 0 {
name = RandomString(10)
}
//判断手机号是否存在
if isTelephoneExist(db, telephone) {
ctx.JSON(http.StatusUnprocessableEntity,
gin.H{"code": 422, "msg": "用户已经存在"})
return
}

//创建用户
newUser := User{
Name: name,
Telephone: telephone,
Password: password,
}
db.Create(&newUser)

//返回结果
ctx.JSON(200, gin.H{
"msg": "注册成功",
})
})

panic(r.Run(":8099"))
}

func isTelephoneExist(db *gorm.DB, telephone string) bool {
var user User
db.Where("telephone = ?", telephone).First(&user)
if user.ID != 0 {
return true
}

return false
}

//返回随机字符串
func RandomString(n int) string {
var letters = []byte("iasdhjklfhascvxnjklasdfhjkasdfklasdfhnjklasdfjklasdfjklfasdsdfjk")
result := make([]byte, n)

rand.Seed(time.Now().Unix())

for i := range result {
result[i] = letters[rand.Intn(len(letters))]
}

return string(result)
}

func InitDb() *gorm.DB {
driverName := "mysql"
host := "127.0.0.1"
port := "3306"
database := "gin_vue_bs"
username := "root"
passwoed := "123456"
charset := "utf8"
args := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=%s&parseTime=true",
username,
passwoed,
host,
port,
database,
charset)

db, err := gorm.Open(driverName, args)
if err != nil {
panic("failed to connect database,err" + err.Error())
}

//创建数据表
db.AutoMigrate(&User{})

return db
}

gorm01.png

gorm02.png

#

OpenGL分为两个部分:CPU上C++编写,GPU采用GLSL语言编写,后者就是着色器的编写方式。

基本数据类型

类型 描述
float ieee 32 位浮点数
double ieee 64 位浮点数
int 有符号二进制补码的 32 位整数
uint 无符号的32位整数
bool 布尔值

变量初始化

所有变量都必须在声明的同时进行初始化

1
2
3
4
int i,nums = 1500;
float force,g = -9.8;
bool falling = true;
double pi = 3.145555LF;

构造函数

所有其他的数值转换都需要提供显示的转换构造函数

1
2
float f = 10.0;
int ten = int(f);

聚合类型

GLSL的向量与矩阵类型

基本数据类型 2D向量 3D向量 矩阵类型
float vec2 vec3 ..
double dvec2 dvec3 ..
int ivec2 ivec3 ..
uint uvec2 uvec3 -
bool bvec2 bvec3 -

类型声明的变量的初始化:

1
vec3 velocity = vec3(0.0, 2.0, 3.0);

类型之间的等价转换:

1
ivec3 steps = ivec3(velocity);

向量之间的转换:

1
2
3
4
5
6
7
8
# 减短
vec4 color;
vec3 RGB = vec3(color);//只取了color的前面三个分量R、G、B

# 加长
vec3 white = vec3(1.0);//white = (1.0, 1.0, 1.0)

vec4 translate = vec4(white, 0.5);

注意⚠️:传入的数据将首先填充列,再填充行。

访问向量和矩阵中的元素

向量中的各个分量可以通过名称访问:

1
2
float red = color.r;
float v_y = velocity.y;

或者通过一个从0开始的索引:

1
2
float red = color[0];
float v_y = velocity[1];

向量分量的访问符

分量访问符 符号描述
(X,Y,Z,W) 与位置相关的分量
(r,g,b,a) 与颜色相关的分量
(s,t,p,q) 与纹理坐标相关的分量
1
2
3
vec3 luminance = color.rrr; # 输入颜色的红色分量来设置一个亮度值

color = colot.abgr; # 反转color的每个分量

结构体

1
2
3
4
5
6
7
struct Partical {
float lifetime;
vec3 position;
vec3 velocity;
}

Partical p = Partical(1.0, pos, vel); # pos、 vel均为vec3类型

数组

数组的声明

1
2
3
float coffee[3];        #有三个float元素的数组
float[3] coffee; #同上
int indices; #为定义维数

静态初始化一个数组的值

1
float coffee[3]  = float[3](2.00, 3.00, 5.23);

操作一个数组中的所有值

1
2
3
for(int i = 0; i<coffee.length(); i++){
coffee[i] *= 2.0;
}

获取列数

1
2
3
mat3x4 m;
int c = m.length(); # m包含的列数为3
int r = m[0].length(); # 第0个列向量中分量的个数为4

存储限制符

类型修饰符 描述
const 将一个变量定义为只读形式。如果它初始化时用的是一个编译时变量,那么它本身也会成为编译时常量
in 设置这个变量为着色器阶段的输入变量
out 设置这个变量为着色器阶段的输出变量
uniform 设置这个变量为用户应用程序传递给着色器的数据,它对于给定的图元而言是一个常量
buffer 设置应用程序共享的一块可读写的内存。这块内存也作为着色器中的存储缓存使用
shared 设置变量是本地工作组中共享的。它只能用于计算着色器中

获取uniform变量的索引并且设置具体值

1
2
3
4
GLint timeLoc;                                      #着色器中的uniform变量time的索引
GLfloat timeValue; #程序运行的时间
timeLoc = glGetUniformLocation(program, "time") #返回着色器中nuiform变量time对应的索引值
glUniformlf(timeLoc, timeValue); #设置uniform变量timeLoc的值

算术操作符

优先级 操作符 可用类型 描述
1 () - 成组的操作
2
[]

f()

.

++、–

[数组、矩阵、向量]

函数

结构体

算术类型

数组的下标

函数的调用、构造函数

访问结构体的域变量/方法

后置递增/递减
3
++、–

+、-

~

!

算术类型

算术类型

整型

布尔型

前置递增/递减






4 *、/、%



5 +、-



6 <<、>>



7 <、>、<=、>=



8 ==、!=



9 &



10 ^



11 ` `

12 &&



13 ^^



14 ` `
15 a?b:c



16
==

+=、-=

*=、/=

%=、<<=、>>=

`&=、^=、
=`

任意

算术类型

算术类型

整型

整型
17 , 任意 操作符序列

操作符重载

流控制

语句 描述
break 终止循环体的运行,并且继续执行循环体外的内容
continue 终止循环体内当前迭代过程的执行,跳转到代码块开始部分并继续执行下一次迭代的内容
return 从当前子例程返回,可以待会一个函数返回值
discard 丢弃当前的片元,终止着色器的执行。discard语句只在片元着色器中有效

函数

1
2
3
4
5
float HornerEval(float coffee[10], float x);

float HornerEval(float coffee[10], float x){
...+
}

计算的不变性

GLSL无法保证在不同的着色器中,两个完全相同的计算式会得到完全一样的结果。为了解决这个问题,需要用到invariant或者precise关键字。

invariant

将一个内置的输出变量声明为invariant,也可以声明一个用户自定义的变量为invariant

1
2
invariant gl_Position;
invariant centroid out vec3 Color

在调试过程中,可能需要将着色器中的所有可变量都设置为invariant。可以通过顶点着色器的预编译命令pragma来完成:

1
#pragma STDGL invariant(all)

precise

precise限制符可以设置任何计算中的变量或者函数的返回值。它的作用是增加计算的可复用性

着色器的预处理器

预处理命令

控制常量与宏的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define
#undef
```

代码的条件编译


```bash
#if
#ifdef
#ifndef
#else
#elif
#endif

强制比那一起将text文字内容插入到着色器的信息日志中

1
#error text

控制编译器的选项

1
#pragma options

设置编译器支持特定GLSL扩展功能

1
#extension options

设置当前使用的GLSL版本名称

1
#version number

设置诊断行号

1
#line options

宏定义

1
#define NUM_ELEMENTS 10

取消之前定义过的宏

1
#undef LPos

数据快接口

着色器与应用程序之间,或者着色器个阶段之间共享的变量可以组织为变量块的形式。

1
2
3
4
uniform b {     //限定符可以为:uniform、in、out、buffer
vec4 v1; //块中的变量列表
vec1 v2; //
}; //访问匿名块成员时使用v1、v2

或者

1
2
3
4
uniform b {     //
vec4 v1; //
vec1 v2; //
}name; ////访问匿名块成员时使用name.v1、name.v2

着色器的编译

创建着色器对象且通过链接生成可执行着色器程序的流程:(两部分)

(第一部分)对于每个着色器都会:

  • 创建一个着色器对象。
  • 将着色器源代码编译为对象。
  • 验证着色器的比那一是否成功。

(第二部分)将多个着色器对象链接为一个着色器程序:

  • 创建一个着色器程序。
  • 将着色器对象关联到着色器程序。
  • 链接着色器程序。
  • 判断着色器的链接过程是否成功完成。
  • 使用着色器来处理顶点和片元。

着色器子程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//第一步
subroutine vec4 LightFunc(vec4);

//第二步
subroutine (LightFunc) vec4 ambient(vec3 n)
{
return Materials.ambient;
}

subroutine (LightFunc) vec4 diffuse(vec3 n)
{
return Materials.diffuse * max(dot (normalize(n), LightVec.xyz), 0.0);
}

//第三步
subroutine uniform LightFunc materialShader;

从这篇开始,我们就跟着课本源码边调试边熟悉OpenGL函数的使用了。里面涉及的C++语法后面会在《C++重温》这里作补充。拿到课本源码,我们需要通过Cmake编译出Mac上运行的Xcode程序。

01-keypress.cpp

程序概括

程序先执行init函数void KeyPressExample::Initialize(const char * title)再执行display函数void KeyPressExample::Display(bool auto_redraw).

按下键盘会调用void KeyPressExample::OnKey(int key, int scancode, int action, int mods)

程序窗口缩放void KeyPressExample::Resize(int width, int height)

init函数说明

glGenVertexArrays( NumVAOs, VAOs );
分配NumVAOs个未使用的对象名到数组VAOs中,用作顶点数组对象。再看下其数组定义:GLuint VAOs[NumVAOs];

glBindVertexArray( VAOs[Triangles] );
VAOs[Triangles]

  • 输入的变量array非0,且为glGenVertexArrays所分配时,激活顶点数组对象,并直接影响对象中所保存的顶点数组状态。
  • array为0,OpenGL不再使用之前绑定的顶点数组。
  • array不是glGenVertexArrays所分配的,或者它已经被glDeleteVertexArray函数释放了,这时会产生一个无效错误。

glGenBuffers( NumBuffers, Buffers );
分配NumBuffers个当前未使用的缓存对象名称,并保存到Buffers数组中。

glBindBuffer( GL_ARRAY_BUFFER, Buffers[ArrayBuffer] );

  • 如果绑定到一个已经创建的缓存对象,那么他将成为当前GL_ARRAY_BUFFER中被激活的缓存对象。
  • 如果绑定的Buffers[ArrayBuffer]值为0,那么OpenGL将不再对当前GL_ARRAY_BUFFER使用任何缓存对象。

glBufferData( GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW );
将数据载入缓存对象。

glVertexAttribPointer( vPosition, 2, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(0) );
将输入顶点着色器的数据 关联到 一个顶点属性的数组。

glEnableVertexAttribArray( vPosition );
启用顶点属性数组。

display函数说明

glClear( GL_COLOR_BUFFER_BIT );
清屏。

glBindVertexArray( VAOs[Triangles] );

glDrawArrays( GL_TRIANGLES, 0, NumVertices );
开始绘制。

效果

程序默认效果
keypress1.png

按下键盘字母M的效果
keypress2.png

详细源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
//////////////////////////////////////////////////////////////////////////////
//
// Triangles.cpp
//
//////////////////////////////////////////////////////////////////////////////

#include <vermilion.h>

#include "vgl.h"
#include "vapp.h"
#include "LoadShaders.h"

enum VAO_IDs { Triangles, NumVAOs };
enum Buffer_IDs { ArrayBuffer, NumBuffers };
enum Attrib_IDs { vPosition = 0 };

GLuint VAOs[NumVAOs];
GLuint Buffers[NumBuffers];

const GLuint NumVertices = 6;

BEGIN_APP_DECLARATION(KeyPressExample)
virtual void Initialize(const char * title);
virtual void Display(bool auto_redraw);
virtual void Finalize(void);
virtual void Resize(int width, int height);
void OnKey(int key, int scancode, int action, int mods);
END_APP_DECLARATION()

DEFINE_APP(KeyPressExample, "Key Press Example")
//----------------------------------------------------------------------------
//
// init
//

void KeyPressExample::Initialize(const char * title)
{
//typedef class VermilionApplication base
base::Initialize(title);

glGenVertexArrays( NumVAOs, VAOs );
glBindVertexArray( VAOs[Triangles] );

GLfloat vertices[NumVertices][2] = {
{ -0.90f, -0.90f }, { 0.85f, -0.90f }, { -0.90f, 0.85f }, // Triangle 1
{ 0.90f, -0.85f }, { 0.90f, 0.90f }, { -0.85f, 0.90f } // Triangle 2
};

glGenBuffers( NumBuffers, Buffers );
glBindBuffer( GL_ARRAY_BUFFER, Buffers[ArrayBuffer] );
glBufferData( GL_ARRAY_BUFFER, sizeof(vertices),
vertices, GL_STATIC_DRAW );

ShaderInfo shaders[] = {
{ GL_VERTEX_SHADER, "../media/shaders/keypress/keypress.vert" },
{ GL_FRAGMENT_SHADER, "../media/shaders/keypress/keypress.frag" },
{ GL_NONE, NULL }
};

GLuint program = LoadShaders( shaders );
glUseProgram( program );

glVertexAttribPointer( vPosition, 2, GL_FLOAT,
GL_FALSE, 0, BUFFER_OFFSET(0) );
glEnableVertexAttribArray( vPosition );
}

void KeyPressExample::OnKey(int key, int scancode, int action, int mods)
{
if (action == GLFW_PRESS)
{
switch (key)
{
case GLFW_KEY_M:
{
static GLenum mode = GL_FILL;

mode = ( mode == GL_FILL ? GL_LINE : GL_FILL );
glPolygonMode( GL_FRONT_AND_BACK, mode );
}
return;
}
}

base::OnKey(key, scancode, action, mods);
}

//----------------------------------------------------------------------------
//
// display
//

void KeyPressExample::Display(bool auto_redraw)
{
glClear( GL_COLOR_BUFFER_BIT );

glBindVertexArray( VAOs[Triangles] );
// GL_TRIANGLES并没有指定是画三角形的边沿还是内部也画,
glDrawArrays( GL_TRIANGLES, 0, NumVertices );

base::Display(auto_redraw);
}

void KeyPressExample::Resize(int width, int height)
{
glViewport(0, 0, width, height);
}

void KeyPressExample::Finalize(void)
{

}